feat: add QR code generator tool

Add a sixth tool with live SVG preview, customizable foreground/background
colors, error correction level, margin control, and export as PNG (256–2048px)
or SVG. URL params enable shareable state. All processing runs client-side
via the qrcode package.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 00:58:57 +01:00
parent 695ba434e2
commit f917891a31
13 changed files with 776 additions and 16 deletions

View File

@@ -0,0 +1,132 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from '@/components/ui/empty';
import { Copy, Share2, Image as ImageIcon, FileCode, QrCode } from 'lucide-react';
import type { ExportSize } from '@/types/qrcode';
interface QRPreviewProps {
svgString: string;
isGenerating: boolean;
exportSize: ExportSize;
onExportSizeChange: (size: ExportSize) => void;
onCopyImage: () => void;
onShare: () => void;
onDownloadPng: () => void;
onDownloadSvg: () => void;
}
export function QRPreview({
svgString,
isGenerating,
exportSize,
onExportSizeChange,
onCopyImage,
onShare,
onDownloadPng,
onDownloadSvg,
}: QRPreviewProps) {
return (
<Card className="h-full flex flex-col">
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
<CardTitle>Preview</CardTitle>
<div className="flex items-center gap-1.5 flex-wrap">
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="xs" onClick={onCopyImage} disabled={!svgString}>
<Copy className="h-3 w-3 mr-1" />
Copy
</Button>
</TooltipTrigger>
<TooltipContent>Copy image to clipboard</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="xs" onClick={onShare} disabled={!svgString}>
<Share2 className="h-3 w-3 mr-1" />
Share
</Button>
</TooltipTrigger>
<TooltipContent>Copy shareable URL</TooltipContent>
</Tooltip>
<div className="flex items-center gap-1">
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="xs" onClick={onDownloadPng} disabled={!svgString}>
<ImageIcon className="h-3 w-3 mr-1" />
PNG
</Button>
</TooltipTrigger>
<TooltipContent>Download as PNG</TooltipContent>
</Tooltip>
<ToggleGroup
type="single"
value={String(exportSize)}
onValueChange={(v) => v && onExportSizeChange(Number(v) as ExportSize)}
variant="outline"
size="sm"
>
<ToggleGroupItem value="256" className="h-6 px-1.5 min-w-0 text-[10px]">256</ToggleGroupItem>
<ToggleGroupItem value="512" className="h-6 px-1.5 min-w-0 text-[10px]">512</ToggleGroupItem>
<ToggleGroupItem value="1024" className="h-6 px-1.5 min-w-0 text-[10px]">1k</ToggleGroupItem>
<ToggleGroupItem value="2048" className="h-6 px-1.5 min-w-0 text-[10px]">2k</ToggleGroupItem>
</ToggleGroup>
</div>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="xs" onClick={onDownloadSvg} disabled={!svgString}>
<FileCode className="h-3 w-3 mr-1" />
SVG
</Button>
</TooltipTrigger>
<TooltipContent>Download as SVG</TooltipContent>
</Tooltip>
</div>
</CardHeader>
<CardContent className="flex-1 flex flex-col">
<div className="flex-1 min-h-[200px] rounded-lg p-4 flex items-center justify-center"
style={{
backgroundImage: 'repeating-conic-gradient(hsl(var(--muted)) 0% 25%, transparent 0% 50%)',
backgroundSize: '16px 16px',
}}
>
{isGenerating ? (
<Skeleton className="h-[200px] w-[200px]" />
) : svgString ? (
<div
className="w-full max-w-[400px] aspect-square [&>svg]:w-full [&>svg]:h-full"
dangerouslySetInnerHTML={{ __html: svgString }}
/>
) : (
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<QrCode />
</EmptyMedia>
<EmptyTitle>Enter text to generate a QR code</EmptyTitle>
<EmptyDescription>Type text or a URL in the input field above</EmptyDescription>
</EmptyHeader>
</Empty>
)}
</div>
</CardContent>
</Card>
);
}