This commit is contained in:
2026-07-27 20:47:36 +02:00
parent 90dfedf098
commit ab63d4b916
9 changed files with 1199 additions and 547 deletions

View File

@@ -1,16 +1,16 @@
'use client';
import React from 'react';
import { QRCodeSVG } from 'qrcode.react';
import Barcode from 'react-barcode';
import { Card, CardContent } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
import { Dropdown, DropdownItem } from '@/components/ui/Dropdown';
import { formatDate } from '@/lib/utils';
import {
ONBOARDING_DOWNLOAD_COMPLETE_EVENT,
ONBOARDING_DOWNLOAD_COMPLETE_KEY,
} from '@/lib/revops';
import StyledQRCode from '@/components/generator/StyledQRCode';
import { Card, CardContent } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
import { Dropdown, DropdownItem } from '@/components/ui/Dropdown';
import { formatDate } from '@/lib/utils';
import {
ONBOARDING_DOWNLOAD_COMPLETE_EVENT,
ONBOARDING_DOWNLOAD_COMPLETE_KEY,
} from '@/lib/revops';
function addBarcodeCaptionToSvg(svgElement: SVGElement, caption: string): string {
const cloned = svgElement.cloneNode(true) as SVGElement;
@@ -63,21 +63,21 @@ interface QRCodeCardProps {
onDelete: (id: string) => void;
}
export const QRCodeCard: React.FC<QRCodeCardProps> = ({
qr,
onEdit,
onDelete,
}) => {
const markDownloadComplete = () => {
if (typeof window === 'undefined') {
return;
}
localStorage.setItem(ONBOARDING_DOWNLOAD_COMPLETE_KEY, '1');
window.dispatchEvent(new CustomEvent(ONBOARDING_DOWNLOAD_COMPLETE_EVENT));
};
// For dynamic QR codes, use the redirect URL for tracking
export const QRCodeCard: React.FC<QRCodeCardProps> = ({
qr,
onEdit,
onDelete,
}) => {
const markDownloadComplete = () => {
if (typeof window === 'undefined') {
return;
}
localStorage.setItem(ONBOARDING_DOWNLOAD_COMPLETE_KEY, '1');
window.dispatchEvent(new CustomEvent(ONBOARDING_DOWNLOAD_COMPLETE_EVENT));
};
// For dynamic QR codes, use the redirect URL for tracking
// For static QR codes, use the direct URL from content
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || (typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3050');
@@ -138,11 +138,11 @@ END:VCARD`;
pixelRatio: 3,
backgroundColor: '#ffffff' // White background for clean export
});
const link = document.createElement('a');
link.download = `${qr.title.replace(/\s+/g, '-').toLowerCase()}.png`;
link.href = dataUrl;
link.click();
markDownloadComplete();
const link = document.createElement('a');
link.download = `${qr.title.replace(/\s+/g, '-').toLowerCase()}.png`;
link.href = dataUrl;
link.click();
markDownloadComplete();
} else {
// For SVG, if no frame, export just the QR code SVG for vector quality
// If frame exists, use toPng as fallback since HTML-to-SVG is complex
@@ -153,11 +153,11 @@ END:VCARD`;
pixelRatio: 3,
backgroundColor: '#ffffff'
});
const link = document.createElement('a');
link.download = `${qr.title.replace(/\s+/g, '-').toLowerCase()}.png`;
link.href = dataUrl;
link.click();
markDownloadComplete();
const link = document.createElement('a');
link.download = `${qr.title.replace(/\s+/g, '-').toLowerCase()}.png`;
link.href = dataUrl;
link.click();
markDownloadComplete();
} else {
// No frame - export clean SVG from the svg wrapper
const svgContainer = document.querySelector(`#qr-svg-${qr.id}`);
@@ -185,12 +185,12 @@ END:VCARD`;
const blob = new Blob([svgData], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${qr.title.replace(/\s+/g, '-').toLowerCase()}.svg`;
a.click();
URL.revokeObjectURL(url);
markDownloadComplete();
const a = document.createElement('a');
a.href = url;
a.download = `${qr.title.replace(/\s+/g, '-').toLowerCase()}.svg`;
a.click();
URL.revokeObjectURL(url);
markDownloadComplete();
}
}
}
@@ -200,16 +200,16 @@ END:VCARD`;
};
return (
<Card hover className="rounded-[24px] border-slate-200 p-0 shadow-none">
<CardContent className="p-4">
<Card hover className="rounded-[24px] border-slate-200 p-0 shadow-none">
<CardContent className="p-4">
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<h3 className="font-semibold text-gray-900 mb-1">{qr.title}</h3>
<div className="flex items-center space-x-2">
<Badge className={qr.type === 'DYNAMIC' ? 'bg-primary-600 text-white' : 'bg-slate-100 text-slate-700'}>
{qr.type}
</Badge>
</div>
<Badge className={qr.type === 'DYNAMIC' ? 'bg-primary-600 text-white' : 'bg-slate-100 text-slate-700'}>
{qr.type}
</Badge>
</div>
</div>
<Dropdown
@@ -234,12 +234,12 @@ END:VCARD`;
</Dropdown>
</div>
<div className="mb-3 flex flex-col items-center justify-center rounded-[20px] bg-slate-50 p-4">
{/* Download wrapper - tightly wraps content */}
<div
id={`qr-download-${qr.id}`}
className="inline-flex flex-col items-center rounded-[20px] border border-slate-100 bg-white p-4"
>
<div className="mb-3 flex flex-col items-center justify-center rounded-[20px] bg-slate-50 p-4">
{/* Download wrapper - tightly wraps content */}
<div
id={`qr-download-${qr.id}`}
className="inline-flex flex-col items-center rounded-[20px] border border-slate-100 bg-white p-4"
>
{/* Frame Label */}
{qr.style?.frameType && qr.style.frameType !== 'none' && (
<div
@@ -284,18 +284,31 @@ END:VCARD`;
</p>
</div>
) : (
<QRCodeSVG
/* Same renderer as the detail page and the bulk export.
qrcode.react only knows two colours, so a code saved with a
module shape, gradient or logo showed up here as a plain
black grid - the design looked lost even when it was stored. */
<StyledQRCode
value={qrUrl}
size={96}
fgColor={qr.style?.foregroundColor || '#000000'}
bgColor={qr.style?.backgroundColor || '#FFFFFF'}
level="H"
imageSettings={qr.style?.imageSettings ? {
src: qr.style.imageSettings.src,
height: qr.style.imageSettings.height * (96 / 200),
width: qr.style.imageSettings.width * (96 / 200),
excavate: qr.style.imageSettings.excavate,
} : undefined}
moduleShape={qr.style?.moduleShape || 'square'}
eyeFrameShape={qr.style?.eyeFrameShape || 'square'}
eyeBallShape={qr.style?.eyeBallShape || 'square'}
gradient={
qr.style?.gradientMode && qr.style.gradientMode !== 'none'
? {
type: qr.style.gradientMode,
from: qr.style.foregroundColor || '#000000',
to: qr.style.gradientTo || '#000000',
}
: null
}
errorCorrection="H"
logoUrl={qr.style?.imageSettings?.src}
logoScale={(qr.style?.imageSettings?.width ?? 24) / 200}
margin={0}
/>
)}
</div>
@@ -318,7 +331,7 @@ END:VCARD`;
<span className="text-gray-900">{formatDate(qr.createdAt)}</span>
</div>
{qr.type === 'DYNAMIC' && (
<div className="border-t border-slate-100 pt-2">
<div className="border-t border-slate-100 pt-2">
<p className="text-xs text-gray-500">
📊 Dynamic QR: Tracks scans via {baseUrl}/r/{qr.slug}
</p>
@@ -337,4 +350,4 @@ END:VCARD`;
</CardContent>
</Card>
);
};
};