"use client"; import { useEffect, useRef, useState, useImperativeHandle, forwardRef } from "react"; import { Upload, X, Smartphone } from "lucide-react"; // qr-code-styling is browser-only; import dynamically inside effects. export type QrStyle = { dot_shape: string; corner_square_shape: string; corner_dot_shape: string; fg_color: string; bg_color: string; transparent_bg: boolean; gradient: boolean; gradient_color: string; logo_url: string; // data URL (preview) — uploaded separately if persisted logo_size: number; error_correction: string; quiet_zone: number; }; export const defaultStyle: QrStyle = { dot_shape: "square", corner_square_shape: "square", corner_dot_shape: "square", fg_color: "#0b1f4d", bg_color: "#ffffff", transparent_bg: false, gradient: false, gradient_color: "#2563eb", logo_url: "", logo_size: 0.4, error_correction: "M", quiet_zone: 4, }; const DOT_TYPES = ["square", "rounded", "dots", "classy", "classy-rounded", "extra-rounded"]; const CORNER_SQUARE = ["square", "dot", "extra-rounded"]; const CORNER_DOT = ["square", "dot"]; const EC = ["L", "M", "Q", "H"]; export type QrDesignerHandle = { getRawData: (fmt: "png" | "svg") => Promise; getDataUrl: (fmt: "png" | "svg") => Promise; }; const QrDesigner = forwardRef void; data: string; // the verify URL the QR encodes }>(function QrDesigner({ value, onChange, data }, ref) { const previewRef = useRef(null); const qrRef = useRef(null); const [tab, setTab] = useState<"qr" | "preview">("qr"); const set = (k: K, v: QrStyle[K]) => onChange({ ...value, [k]: v }); function buildOptions() { const dotsColor = value.gradient ? undefined : value.fg_color; const gradient = value.gradient ? { type: "linear" as const, rotation: 0.785, colorStops: [ { offset: 0, color: value.fg_color }, { offset: 1, color: value.gradient_color }, ], } : undefined; return { width: 220, height: 220, type: "canvas" as const, data: data || "https://verifypack.example", margin: value.quiet_zone * 4, qrOptions: { errorCorrectionLevel: value.error_correction as any }, image: value.logo_url || undefined, imageOptions: { crossOrigin: "anonymous", margin: 4, imageSize: value.logo_size }, dotsOptions: { type: value.dot_shape as any, color: dotsColor, gradient }, cornersSquareOptions: { type: value.corner_square_shape as any, color: value.fg_color }, cornersDotOptions: { type: value.corner_dot_shape as any, color: value.fg_color }, backgroundOptions: { color: value.transparent_bg ? "transparent" : value.bg_color, }, }; } // init once useEffect(() => { let mounted = true; (async () => { const QRCodeStyling = (await import("qr-code-styling")).default; if (!mounted) return; qrRef.current = new QRCodeStyling(buildOptions()); if (previewRef.current) { previewRef.current.innerHTML = ""; qrRef.current.append(previewRef.current); } })(); return () => { mounted = false; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // live update on any style/data change useEffect(() => { if (qrRef.current) qrRef.current.update(buildOptions()); // eslint-disable-next-line react-hooks/exhaustive-deps }, [value, data]); useImperativeHandle(ref, () => ({ async getRawData(fmt) { if (!qrRef.current) return null; return (await qrRef.current.getRawData(fmt)) as Blob; }, async getDataUrl(fmt) { const blob = await this.getRawData(fmt); if (!blob) return ""; return await new Promise((res) => { const r = new FileReader(); r.onloadend = () => res(r.result as string); r.readAsDataURL(blob); }); }, })); function onLogo(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file) return; if (file.size > 1024 * 1024) { alert("Logo must be under 1 MB"); return; } const reader = new FileReader(); reader.onload = () => { onChange({ ...value, logo_url: reader.result as string, error_correction: "H" }); }; reader.readAsDataURL(file); } return (
{/* Controls */}
{DOT_TYPES.map((d) => ( set("dot_shape", d)} label={d} /> ))}
set("fg_color", v)} /> set("bg_color", v)} disabled={value.transparent_bg} />
set("transparent_bg", !value.transparent_bg)} /> set("gradient", !value.gradient)} />
{value.gradient && (
set("gradient_color", v)} />
)}
Frame around corner
{CORNER_SQUARE.map((c) => ( set("corner_square_shape", c)} label={c} /> ))}
Corner dots
{CORNER_DOT.map((c) => ( set("corner_dot_shape", c)} label={c} /> ))}
{value.logo_url ? (
logo
) : ( )} {value.logo_url && ( )}
Error correction
{EC.map((e) => ( ))}
{/* Live preview (phone mockup) */}
{tab === "qr" ? (
) : (
Consumer verification page preview appears here after publish.
)}
); }); export default QrDesigner; function Panel({ title, subtitle, children }: any) { return (

{title}

{subtitle &&

{subtitle}

}
{children}
); } function Swatch({ active, onClick, label }: any) { return ( ); } function Color({ label, value, onChange, disabled }: { label: string; value: string; onChange: (v: string) => void; disabled?: boolean; }) { return ( ); } function Toggle({ label, on, onClick }: any) { return ( ); }