diff --git a/components/favicon/CodeSnippet.tsx b/components/favicon/CodeSnippet.tsx index 484eab6..e084577 100644 --- a/components/favicon/CodeSnippet.tsx +++ b/components/favicon/CodeSnippet.tsx @@ -2,15 +2,13 @@ import * as React from 'react'; import { Copy, Check } from 'lucide-react'; -import { Button } from '@/components/ui/button'; import { toast } from 'sonner'; interface CodeSnippetProps { code: string; - language?: string; } -export function CodeSnippet({ code, language }: CodeSnippetProps) { +export function CodeSnippet({ code }: CodeSnippetProps) { const [copied, setCopied] = React.useState(false); const handleCopy = () => { @@ -21,18 +19,15 @@ export function CodeSnippet({ code, language }: CodeSnippetProps) { }; return ( -
++ +diff --git a/components/favicon/FaviconFileUpload.tsx b/components/favicon/FaviconFileUpload.tsx index c558f5b..f7d2cb8 100644 --- a/components/favicon/FaviconFileUpload.tsx +++ b/components/favicon/FaviconFileUpload.tsx @@ -1,9 +1,8 @@ 'use client'; import * as React from 'react'; -import { Upload, X, FileImage, HardDrive } from 'lucide-react'; +import { Upload, X, FileImage, HardDrive, Film } from 'lucide-react'; import { cn } from '@/lib/utils/cn'; -import { Button } from '@/components/ui/button'; export interface FaviconFileUploadProps { onFileSelect: (file: File) => void; @@ -26,7 +25,7 @@ export function FaviconFileUpload({ if (selectedFile) { const img = new Image(); img.onload = () => { - setDimensions(`${img.width} × ${img.height}`); + setDimensions(`${img.width}×${img.height}`); URL.revokeObjectURL(img.src); }; img.src = URL.createObjectURL(selectedFile); @@ -35,49 +34,22 @@ export function FaviconFileUpload({ } }, [selectedFile]); - const handleDragEnter = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - if (!disabled) setIsDragging(true); - }; - - const handleDragLeave = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragging(false); - }; - - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - }; - const handleDrop = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); - if (disabled) return; - const files = Array.from(e.dataTransfer.files); - if (files.length > 0 && files[0].type.startsWith('image/')) { - onFileSelect(files[0]); - } + if (files.length > 0 && files[0].type.startsWith('image/')) onFileSelect(files[0]); }; const handleFileInput = (e: React.ChangeEvent{code}) => { const files = Array.from(e.target.files || []); - if (files.length > 0 && files[0].type.startsWith('image/')) { - onFileSelect(files[0]); - } - }; - - const handleClick = () => { - if (!disabled) fileInputRef.current?.click(); + if (files.length > 0 && files[0].type.startsWith('image/')) onFileSelect(files[0]); }; return ( - +{selectedFile ? ( ----+ ++++ +) : (+-+ {selectedFile.name} +
+---- {selectedFile.name} -
- --+-- {dimensions && ( -- {(selectedFile.size / 1024).toFixed(1)} KB - -- )} -- {dimensions} - + ++ {selectedFile.size < 1024 * 1024 + ? `${(selectedFile.size / 1024).toFixed(1)} KB` + : `${(selectedFile.size / (1024 * 1024)).toFixed(1)} MB`} + + {dimensions && ( + + {dimensions} + + )} !disabled && fileInputRef.current?.click()} + onDragEnter={(e) => { e.preventDefault(); if (!disabled) setIsDragging(true); }} + onDragLeave={(e) => { e.preventDefault(); setIsDragging(false); }} + onDragOver={(e) => e.preventDefault()} onDrop={handleDrop} className={cn( - 'border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-all duration-200', - 'hover:border-primary/40 hover:bg-primary/5', - { - 'border-primary bg-primary/10 scale-[0.98]': isDragging, - 'border-border/50': !isDragging, - 'opacity-50 cursor-not-allowed': disabled, - } + 'flex flex-col items-center justify-center rounded-xl border-2 border-dashed transition-all cursor-pointer text-center select-none py-8', + isDragging + ? 'border-primary bg-primary/10 scale-[0.99]' + : 'border-border/35 hover:border-primary/40 hover:bg-primary/5', + disabled && 'opacity-50 cursor-not-allowed pointer-events-none' )} > --)} diff --git a/components/favicon/FaviconGenerator.tsx b/components/favicon/FaviconGenerator.tsx index 96e40df..1d3d06f 100644 --- a/components/favicon/FaviconGenerator.tsx +++ b/components/favicon/FaviconGenerator.tsx @@ -1,24 +1,34 @@ 'use client'; import * as React from 'react'; -import { Download, Loader2, Code2, Globe, Layout } from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Progress } from '@/components/ui/progress'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Download, Loader2, Code2, Globe, Layout, FileImage } from 'lucide-react'; import { FaviconFileUpload } from './FaviconFileUpload'; import { CodeSnippet } from './CodeSnippet'; import { generateFaviconSet } from '@/lib/favicon/faviconService'; import { downloadBlobsAsZip } from '@/lib/media/utils/fileUtils'; import type { FaviconSet, FaviconOptions } from '@/types/favicon'; import { toast } from 'sonner'; +import { cn } from '@/lib/utils/cn'; + +type Tab = 'icons' | 'html' | 'manifest'; +type MobileTab = 'setup' | 'results'; + +const TABS: { value: Tab; label: string; icon: React.ReactNode }[] = [ + { value: 'icons', label: 'Icons', icon:+ +-- Drop icon source here +
+ {isDragging ? 'Drop to upload' : 'Drop icon here or click to browse'}
-- 512x512 PNG or SVG recommended +
+ PNG · SVG · 512×512 recommended
}, + { value: 'html', label: 'HTML', icon: }, + { value: 'manifest', label: 'Manifest', icon: }, +]; + +const actionBtn = + 'flex items-center justify-center gap-1.5 px-3 py-1.5 text-xs glass rounded-md border border-border/30 text-muted-foreground hover:text-primary hover:border-primary/30 hover:bg-primary/10 transition-all disabled:opacity-40 disabled:cursor-not-allowed'; + +const inputCls = + 'w-full bg-transparent border border-border/40 rounded-lg px-3 py-2 text-xs font-mono outline-none focus:border-primary/50 transition-colors text-foreground/80 placeholder:text-muted-foreground/30'; export function FaviconGenerator() { const [sourceFile, setSourceFile] = React.useState (null); const [options, setOptions] = React.useState ({ - name: 'My Awesome App', + name: 'My App', shortName: 'App', backgroundColor: '#ffffff', themeColor: '#3b82f6', @@ -26,22 +36,18 @@ export function FaviconGenerator() { const [isGenerating, setIsGenerating] = React.useState(false); const [progress, setProgress] = React.useState(0); const [result, setResult] = React.useState (null); + const [tab, setTab] = React.useState ('icons'); + const [mobileTab, setMobileTab] = React.useState ('setup'); const handleGenerate = async () => { - if (!sourceFile) { - toast.error('Please upload a source image'); - return; - } - + if (!sourceFile) { toast.error('Please upload a source image'); return; } setIsGenerating(true); setProgress(0); - try { - const resultSet = await generateFaviconSet(sourceFile, options, (p) => { - setProgress(p); - }); + const resultSet = await generateFaviconSet(sourceFile, options, (p) => setProgress(p)); setResult(resultSet); - toast.success('Favicon set generated successfully!'); + setMobileTab('results'); + toast.success('Favicon set generated!'); } catch (error) { console.error(error); toast.error('Failed to generate favicons'); @@ -52,229 +58,249 @@ export function FaviconGenerator() { const handleDownloadAll = async () => { if (!result) return; - - const files = result.icons.map((icon) => ({ - blob: icon.blob!, - filename: icon.name, - })); - - // Add manifest to ZIP + const files = result.icons.map((icon) => ({ blob: icon.blob!, filename: icon.name })); const manifestBlob = new Blob([result.manifest], { type: 'application/json' }); - files.push({ - blob: manifestBlob, - filename: 'site.webmanifest', - }); - + files.push({ blob: manifestBlob, filename: 'site.webmanifest' }); await downloadBlobsAsZip(files, 'favicons.zip'); - toast.success('Downloading favicons ZIP...'); + toast.success('Downloading favicons ZIP…'); }; const handleReset = () => { setSourceFile(null); setResult(null); setProgress(0); + setMobileTab('setup'); }; return ( - - {/* Settings Column */} --- +- -App Details -- -- - setOptions({ ...options, name: e.target.value })} - placeholder="e.g. My Awesome Website" - /> --- - setOptions({ ...options, shortName: e.target.value })} - placeholder="e.g. My App" - /> --Used for mobile home screen labels
--- + {/* ── Mobile tab switcher ─────────────────────────────── */} +- -Theme Colors -- ---- --- setOptions({ ...options, backgroundColor: e.target.value })} - /> - setOptions({ ...options, backgroundColor: e.target.value })} - /> --- --- setOptions({ ...options, themeColor: e.target.value })} - /> - setOptions({ ...options, themeColor: e.target.value })} - /> --+ {(['setup', 'results'] as MobileTab[]).map((t) => ( + + ))} +-- + {/* ── Main layout ─────────────────────────────────────── */} + + + {/* Left: Setup */} ++ + {/* Upload zone */} ++ + Source Image ++ + {/* Right: Results */} +setSourceFile(null)} disabled={isGenerating} /> - +);+ + {/* Tab bar + download button */} +++ + {/* Scrollable content */} ++ {TABS.map(({ value, label, icon }) => ( ++ {result && ( +setTab(value)} + className={cn( + 'flex-1 flex items-center justify-center gap-1.5 py-1.5 rounded-md text-xs font-medium transition-all', + tab === value + ? 'bg-primary text-primary-foreground shadow-sm' + : 'text-muted-foreground hover:text-foreground' + )} + > + {icon}{label} + + ))} ++ + )} ++ ZIP + {isGenerating ? ( +- - {/* Results Column */} -++ ) : result ? ( <> -+ +++ Processing… + {progress}% +++ ++- Generating... {progress}% + {tab === 'icons' && ( + + {result.icons.map((icon) => ( ++ )} + {tab === 'html' && ( +++ ))} ++ {icon.previewUrl ? ( +++ ) : ( +
+ )} + ++{icon.name}
+{icon.width}×{icon.height} · {(icon.size / 1024).toFixed(1)} KB
+++ )} + {tab === 'manifest' && ( ++ +++ Place generated files in your site root or update the href paths. +
++ )} > ) : ( - 'Generate Favicons' + +)} - - {result && ( -+++ ++No assets yet
+Upload an image and generate favicons
+- Reset - - )} - - -- {isGenerating ? ( -- - ) : result ? ( -- ----- Processing... -- {progress}% --- ) : null} +-- -Generated Assets
-- -- Download ZIP - - - - -- -- Icons - - -- HTML - - -- Manifest - - - -- {result?.icons.map((icon) => ( --- - ))} ---- {icon.previewUrl && ( --- )} -
--- {icon.name} -
-- {icon.width}x{icon.height} · {(icon.size / 1024).toFixed(1)} KB -
-- - -- - {result &&-} - --- Place generated files in your site root or update the
-hrefpaths. -- -- - {result &&-} -