'use client'; import * as React from 'react'; import { QRInput } from './QRInput'; import { QRPreview } from './QRPreview'; import { QROptions } from './QROptions'; import { generateSvg, generateDataUrl } from '@/lib/qrcode/qrcodeService'; import { decodeQRFromUrl, updateQRUrl, getQRShareableUrl } from '@/lib/qrcode/urlSharing'; import { downloadBlob } from '@/lib/media/utils/fileUtils'; import { debounce } from '@/lib/utils/debounce'; import { toast } from 'sonner'; import { cn } from '@/lib/utils/cn'; import { MobileTabs } from '@/components/ui/mobile-tabs'; 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'); const [foregroundColor, setForegroundColor] = React.useState('#000000'); const [backgroundColor, setBackgroundColor] = React.useState('#ffffff'); const [margin, setMargin] = React.useState(4); 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(() => { const urlState = decodeQRFromUrl(); if (urlState) { if (urlState.text !== undefined) setText(urlState.text); if (urlState.errorCorrection) setErrorCorrection(urlState.errorCorrection); if (urlState.foregroundColor) setForegroundColor(urlState.foregroundColor); if (urlState.backgroundColor) setBackgroundColor(urlState.backgroundColor); if (urlState.margin !== undefined) setMargin(urlState.margin); } }, []); // Debounced generation const generate = React.useMemo( () => debounce(async (t: string, ec: ErrorCorrectionLevel, fg: string, bg: string, m: number) => { if (!t) { setSvgString(''); setIsGenerating(false); return; } setIsGenerating(true); try { const svg = await generateSvg(t, ec, fg, bg, m); setSvgString(svg); } catch (error) { console.error('QR generation error:', error); setSvgString(''); toast.error('Failed to generate QR code. Text may be too long.'); } finally { setIsGenerating(false); } }, 200), [], ); React.useEffect(() => { generate(text, errorCorrection, foregroundColor, backgroundColor, margin); updateQRUrl(text, errorCorrection, foregroundColor, backgroundColor, margin); }, [text, errorCorrection, foregroundColor, backgroundColor, margin, generate]); const handleDownloadPng = 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(); downloadBlob(blob, `qrcode-${Date.now()}.png`); } catch { toast.error('Failed to export PNG'); } }; const handleDownloadSvg = () => { if (!svgString) return; const blob = new Blob([svgString], { type: 'image/svg+xml' }); downloadBlob(blob, `qrcode-${Date.now()}.svg`); }; 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 })]); toast.success('Image copied to clipboard!'); } catch { toast.error('Failed to copy image'); } }; 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'); } }; return (
setMobileTab(v as MobileTab)} /> {/* ── Main layout ─────────────────────────────────────── */}
{/* Left: Input + Options */}
{/* Right: Preview */}
); }