diff --git a/components/qrcode/QRCodeGenerator.tsx b/components/qrcode/QRCodeGenerator.tsx index e543589..118fca5 100644 --- a/components/qrcode/QRCodeGenerator.tsx +++ b/components/qrcode/QRCodeGenerator.tsx @@ -9,8 +9,11 @@ import { decodeQRFromUrl, updateQRUrl, getQRShareableUrl } from '@/lib/qrcode/ur import { downloadBlob } from '@/lib/media/utils/fileUtils'; import { debounce } from '@/lib/utils/debounce'; import { toast } from 'sonner'; +import { cn } from '@/lib/utils/cn'; import type { ErrorCorrectionLevel, ExportSize } from '@/types/qrcode'; +type MobileTab = 'configure' | 'preview'; + export function QRCodeGenerator() { const [text, setText] = React.useState('https://kit.pivoine.art'); const [errorCorrection, setErrorCorrection] = React.useState('M'); @@ -20,6 +23,7 @@ export function QRCodeGenerator() { const [exportSize, setExportSize] = React.useState(512); const [svgString, setSvgString] = React.useState(''); const [isGenerating, setIsGenerating] = React.useState(false); + const [mobileTab, setMobileTab] = React.useState('configure'); // Load state from URL on mount React.useEffect(() => { @@ -37,11 +41,7 @@ export function QRCodeGenerator() { const generate = React.useMemo( () => debounce(async (t: string, ec: ErrorCorrectionLevel, fg: string, bg: string, m: number) => { - if (!t) { - setSvgString(''); - setIsGenerating(false); - return; - } + if (!t) { setSvgString(''); setIsGenerating(false); return; } setIsGenerating(true); try { const svg = await generateSvg(t, ec, fg, bg, m); @@ -57,13 +57,11 @@ export function QRCodeGenerator() { [], ); - // Regenerate on changes React.useEffect(() => { generate(text, errorCorrection, foregroundColor, backgroundColor, margin); updateQRUrl(text, errorCorrection, foregroundColor, backgroundColor, margin); }, [text, errorCorrection, foregroundColor, backgroundColor, margin, generate]); - // Export: PNG download const handleDownloadPng = async () => { if (!text) return; try { @@ -71,74 +69,94 @@ export function QRCodeGenerator() { const res = await fetch(dataUrl); const blob = await res.blob(); downloadBlob(blob, `qrcode-${Date.now()}.png`); - } catch { - toast.error('Failed to export PNG'); - } + } catch { toast.error('Failed to export PNG'); } }; - // Export: SVG download const handleDownloadSvg = () => { if (!svgString) return; const blob = new Blob([svgString], { type: 'image/svg+xml' }); downloadBlob(blob, `qrcode-${Date.now()}.svg`); }; - // Copy image to clipboard const handleCopyImage = async () => { if (!text) return; try { const dataUrl = await generateDataUrl(text, errorCorrection, foregroundColor, backgroundColor, margin, exportSize); const res = await fetch(dataUrl); const blob = await res.blob(); - await navigator.clipboard.write([ - new ClipboardItem({ 'image/png': blob }), - ]); + await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]); toast.success('Image copied to clipboard!'); - } catch { - toast.error('Failed to copy image'); - } + } catch { toast.error('Failed to copy image'); } }; - // Share URL const handleShare = async () => { const shareUrl = getQRShareableUrl(text, errorCorrection, foregroundColor, backgroundColor, margin); try { await navigator.clipboard.writeText(shareUrl); toast.success('Shareable URL copied!'); - } catch { - toast.error('Failed to copy URL'); - } + } catch { toast.error('Failed to copy URL'); } }; return ( -
- {/* Left Column - Input and Options */} -
- - +
+ + {/* ── Mobile tab switcher ─────────────────────────────── */} +
+ {(['configure', 'preview'] as MobileTab[]).map((t) => ( + + ))}
- {/* Right Column - Preview */} -
- + {/* ── Main layout ─────────────────────────────────────── */} +
+ + {/* Left: Input + Options */} +
+
+
+ +
+ +
+
+
+ + {/* Right: Preview */} +
+ +
); diff --git a/components/qrcode/QRInput.tsx b/components/qrcode/QRInput.tsx index fdf8edf..5b8587c 100644 --- a/components/qrcode/QRInput.tsx +++ b/components/qrcode/QRInput.tsx @@ -1,34 +1,29 @@ 'use client'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Textarea } from '@/components/ui/textarea'; +const MAX_LENGTH = 2048; interface QRInputProps { value: string; onChange: (value: string) => void; } -const MAX_LENGTH = 2048; - export function QRInput({ value, onChange }: QRInputProps) { return ( - - - Text - - -