'use client'; import React from 'react'; import Barcode from 'react-barcode'; 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 { getWwwOrigin } from '@/lib/hosts'; 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; const NS = 'http://www.w3.org/2000/svg'; const widthAttr = cloned.getAttribute('width'); const heightAttr = cloned.getAttribute('height'); const width = widthAttr ? parseFloat(widthAttr) : 200; const height = heightAttr ? parseFloat(heightAttr) : 100; const extraHeight = 18; cloned.setAttribute('height', String(height + extraHeight)); const viewBox = cloned.getAttribute('viewBox'); if (viewBox) { const parts = viewBox.split(/\s+/); if (parts.length === 4) { cloned.setAttribute( 'viewBox', `${parts[0]} ${parts[1]} ${parts[2]} ${parseFloat(parts[3]) + extraHeight}` ); } } const text = document.createElementNS(NS, 'text'); text.setAttribute('x', String(width / 2)); text.setAttribute('y', String(height + 12)); text.setAttribute('text-anchor', 'middle'); text.setAttribute('font-size', '9'); text.setAttribute('font-family', 'Arial, Helvetica, sans-serif'); text.setAttribute('fill', '#666666'); text.textContent = caption; cloned.appendChild(text); return new XMLSerializer().serializeToString(cloned); } interface QRCodeCardProps { qr: { id: string; title: string; type: 'STATIC' | 'DYNAMIC'; contentType: string; content?: any; slug: string; createdAt: string; scans?: number; style?: any; }; onEdit: (id: string) => void; onDelete: (id: string) => void; } export const QRCodeCard: React.FC = ({ 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 // // Must be the WWW origin, never the app origin: this value gets encoded into the QR // code the user downloads and prints. /r/ is served by the marketing host. const baseUrl = getWwwOrigin(); // Get the QR URL based on type let qrUrl = ''; // SIMPLE FIX: For STATIC QR codes, ALWAYS use the direct content if (qr.type === 'STATIC') { // Extract the actual URL/content based on contentType if (qr.contentType === 'URL' && qr.content?.url) { qrUrl = qr.content.url; } else if (qr.contentType === 'PHONE' && qr.content?.phone) { qrUrl = `tel:${qr.content.phone}`; } else if (qr.contentType === 'VCARD') { // VCARD content needs to be formatted properly qrUrl = `BEGIN:VCARD VERSION:3.0 FN:${qr.content.firstName || ''} ${qr.content.lastName || ''} N:${qr.content.lastName || ''};${qr.content.firstName || ''};;; ${qr.content.organization ? `ORG:${qr.content.organization}` : ''} ${qr.content.title ? `TITLE:${qr.content.title}` : ''} ${qr.content.email ? `EMAIL:${qr.content.email}` : ''} ${qr.content.phone ? `TEL:${qr.content.phone}` : ''} END:VCARD`; } else if (qr.contentType === 'GEO' && qr.content) { const lat = qr.content.latitude || 0; const lon = qr.content.longitude || 0; const label = qr.content.label ? `?q=${encodeURIComponent(qr.content.label)}` : ''; qrUrl = `geo:${lat},${lon}${label}`; } else if (qr.contentType === 'TEXT' && qr.content?.text) { qrUrl = qr.content.text; } else if (qr.content?.qrContent) { // Fallback to qrContent if it exists qrUrl = qr.content.qrContent; } else { // Last resort fallback qrUrl = `${baseUrl}/r/${qr.slug}`; } console.log(`STATIC QR [${qr.title}]: ${qrUrl}`); } else { // DYNAMIC QR codes always use redirect for tracking qrUrl = `${baseUrl}/r/${qr.slug}`; console.log(`DYNAMIC QR [${qr.title}]: ${qrUrl}`); } const downloadQR = async (format: 'png' | 'svg') => { // Use the tight download wrapper const container = document.querySelector(`#qr-download-${qr.id}`); if (!container) return; // Dynamic import of html-to-image const { toPng } = await import('html-to-image'); try { if (format === 'png') { const dataUrl = await toPng(container as HTMLElement, { cacheBust: true, 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(); } 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 if (qr.style?.frameType && qr.style.frameType !== 'none') { // Frame exists - use PNG for full capture const dataUrl = await toPng(container as HTMLElement, { cacheBust: true, pixelRatio: 3, backgroundColor: '#ffffff' }); 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}`); const svg = svgContainer?.querySelector('svg'); if (svg) { let svgData = qr.contentType === 'BARCODE' ? addBarcodeCaptionToSvg(svg as SVGElement, 'Scan: iPhone -> Barcode Scanner App | Android -> Google Lens') : new XMLSerializer().serializeToString(svg); if (qr.style?.cornerStyle === 'rounded') { const width = svg.getAttribute('width') || '96'; const height = svg.getAttribute('height') || '96'; const borderRadius = 10; svgData = ` ${svgData} `; } 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(); } } } } catch (err) { console.error('Download failed:', err); } }; return (

{qr.title}

{qr.type}
} > window.location.href = `/qr/${qr.id}`}>View Details downloadQR('png')}>Download PNG downloadQR('svg')}>Download SVG {qr.type === 'DYNAMIC' && ( onEdit(qr.id)}>Edit )} onDelete(qr.id)} className="text-red-600"> Delete
{/* Download wrapper - tightly wraps content */}
{/* Frame Label */} {qr.style?.frameType && qr.style.frameType !== 'none' && (
{qr.style.frameType === 'scanme' ? 'Scan Me' : qr.style.frameType === 'website' ? 'Website' : qr.style.frameType === 'visit' ? 'Visit' : qr.style.frameType === 'callme' ? 'Call Me' : qr.style.frameType === 'call' ? 'Call' : qr.style.frameType === 'findus' ? 'Find Us' : qr.style.frameType === 'navigate' ? 'Navigate' : qr.style.frameType === 'contact' ? 'Contact' : qr.style.frameType === 'save' ? 'Save' : qr.style.frameType === 'textme' ? 'Text Me' : qr.style.frameType === 'message' ? 'Message' : qr.style.frameType === 'chatme' ? 'Chat Me' : qr.style.frameType === 'whatsapp' ? 'WhatsApp' : qr.style.frameType === 'read' ? 'Read' : qr.style.frameType === 'info' ? 'Info' : qr.style.frameType.charAt(0).toUpperCase() + qr.style.frameType.slice(1)}
)}
{qr.contentType === 'BARCODE' ? (

Scan: iPhone โ†’ Barcode Scanner App ยท Android โ†’ Google Lens

) : ( /* 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. */ )}
Type: {qr.contentType}
{qr.type === 'DYNAMIC' && (
Scans: {qr.scans || 0}
)}
Created: {formatDate(qr.createdAt)}
{qr.type === 'DYNAMIC' && (

๐Ÿ“Š Dynamic QR: Tracks scans via {baseUrl}/r/{qr.slug}

)} {/* Feedback Button - only for FEEDBACK type */} {qr.contentType === 'FEEDBACK' && ( )}
); };