Files
QR-master/src/components/dashboard/QRCodeCard.tsx
2026-04-17 22:56:49 +02:00

278 lines
12 KiB
TypeScript

'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';
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<QRCodeCardProps> = ({
qr,
onEdit,
onDelete,
}) => {
// 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');
// 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();
} 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();
} 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 = 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 = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
<defs>
<clipPath id="rounded-corners-${qr.id}">
<rect x="0" y="0" width="${width}" height="${height}" rx="${borderRadius}" ry="${borderRadius}"/>
</clipPath>
</defs>
<g clip-path="url(#rounded-corners-${qr.id})">
${svgData}
</g>
</svg>`;
}
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);
}
}
}
} catch (err) {
console.error('Download failed:', err);
}
};
return (
<Card hover>
<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 variant={qr.type === 'DYNAMIC' ? 'info' : 'default'}>
{qr.type}
</Badge>
</div>
</div>
<Dropdown
align="right"
trigger={
<button className="p-1 hover:bg-gray-100 rounded">
<svg className="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" />
</svg>
</button>
}
>
<DropdownItem onClick={() => window.location.href = `/qr/${qr.id}`}>View Details</DropdownItem>
<DropdownItem onClick={() => downloadQR('png')}>Download PNG</DropdownItem>
<DropdownItem onClick={() => downloadQR('svg')}>Download SVG</DropdownItem>
{qr.type === 'DYNAMIC' && (
<DropdownItem onClick={() => onEdit(qr.id)}>Edit</DropdownItem>
)}
<DropdownItem onClick={() => onDelete(qr.id)} className="text-red-600">
Delete
</DropdownItem>
</Dropdown>
</div>
<div className="flex flex-col items-center justify-center bg-gray-50 rounded-lg p-4 mb-3">
{/* Download wrapper - tightly wraps content */}
<div id={`qr-download-${qr.id}`} className="inline-flex flex-col items-center bg-white p-4 rounded-xl">
{/* Frame Label */}
{qr.style?.frameType && qr.style.frameType !== 'none' && (
<div
className="mb-3 px-4 py-1.5 rounded-full font-bold text-xs tracking-widest uppercase shadow-sm text-white"
style={{ backgroundColor: qr.style?.foregroundColor || '#000000' }}
>
{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)}
</div>
)}
<div id={`qr-svg-${qr.id}`} className={qr.style?.cornerStyle === 'rounded' ? 'rounded-lg overflow-hidden' : ''}>
{qr.contentType === 'BARCODE' ? (
<Barcode
key={`${qr.id}-${qr.type === 'STATIC' ? qr.content?.value : qrUrl}-${qr.content?.format}`}
value={qr.type === 'STATIC' ? (qr.content?.value || '123456789') : qrUrl}
format={(qr.content?.format as any) || 'CODE128'}
lineColor={qr.style?.foregroundColor || '#000000'}
background={qr.style?.backgroundColor || '#FFFFFF'}
width={1.5}
height={60}
displayValue={true}
fontSize={10}
/>
) : (
<QRCodeSVG
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}
/>
)}
</div>
</div>
</div>
<div className="space-y-2 text-sm">
<div className="flex items-center justify-between">
<span className="text-gray-500">Type:</span>
<span className="text-gray-900">{qr.contentType}</span>
</div>
{qr.type === 'DYNAMIC' && (
<div className="flex items-center justify-between">
<span className="text-gray-500">Scans:</span>
<span className="text-gray-900">{qr.scans || 0}</span>
</div>
)}
<div className="flex items-center justify-between">
<span className="text-gray-500">Created:</span>
<span className="text-gray-900">{formatDate(qr.createdAt)}</span>
</div>
{qr.type === 'DYNAMIC' && (
<div className="pt-2 border-t">
<p className="text-xs text-gray-500">
📊 Dynamic QR: Tracks scans via {baseUrl}/r/{qr.slug}
</p>
</div>
)}
{/* Feedback Button - only for FEEDBACK type */}
{qr.contentType === 'FEEDBACK' && (
<button
onClick={() => window.location.href = `/qr/${qr.id}/feedback`}
className="w-full mt-3 py-2 px-3 bg-amber-50 hover:bg-amber-100 text-amber-700 rounded-lg text-sm font-medium flex items-center justify-center gap-2 transition-colors"
>
View Customer Feedback
</button>
)}
</div>
</CardContent>
</Card>
);
};