Files
QR-master/src/app/(app)/bulk-creation/page.tsx
2026-01-01 16:17:14 +01:00

720 lines
32 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import React, { useState, useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
import Papa from 'papaparse';
import ExcelJS from 'exceljs';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { Select } from '@/components/ui/Select';
import { QRCodeSVG } from 'qrcode.react';
import { showToast } from '@/components/ui/Toast';
import { useTranslation } from '@/hooks/useTranslation';
import { useCsrf } from '@/hooks/useCsrf';
import JSZip from 'jszip';
import { saveAs } from 'file-saver';
interface BulkQRData {
title: string;
content: string;
}
interface GeneratedQR {
title: string;
content: string; // Original URL
svg: string; // SVG markup
}
export default function BulkCreationPage() {
const { t } = useTranslation();
const { fetchWithCsrf } = useCsrf();
const [step, setStep] = useState<'upload' | 'preview' | 'complete'>('upload');
const [data, setData] = useState<BulkQRData[]>([]);
const [mapping, setMapping] = useState<Record<string, string>>({});
const [loading, setLoading] = useState(false);
const [generatedQRs, setGeneratedQRs] = useState<GeneratedQR[]>([]);
const [userPlan, setUserPlan] = useState<string>('FREE');
// Check user plan on mount
React.useEffect(() => {
const checkPlan = async () => {
try {
const response = await fetch('/api/user/plan');
if (response.ok) {
const data = await response.json();
setUserPlan(data.plan || 'FREE');
}
} catch (error) {
console.error('Error checking plan:', error);
}
};
checkPlan();
}, []);
const onDrop = useCallback((acceptedFiles: File[]) => {
const file = acceptedFiles[0];
if (!file) return;
const reader = new FileReader();
if (file.name.endsWith('.csv')) {
reader.onload = (e) => {
const text = e.target?.result as string;
const result = Papa.parse(text, { header: true });
processData(result.data);
};
reader.readAsText(file);
} else if (file.name.endsWith('.xlsx') || file.name.endsWith('.xls')) {
reader.onload = async (e) => {
const buffer = e.target?.result as ArrayBuffer;
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.load(buffer);
const worksheet = workbook.worksheets[0];
const jsonData: any[] = [];
// Get headers from first row
const headers: string[] = [];
const firstRow = worksheet.getRow(1);
firstRow.eachCell((cell, colNumber) => {
headers[colNumber - 1] = cell.value?.toString() || '';
});
// Convert rows to objects
worksheet.eachRow((row, rowNumber) => {
if (rowNumber === 1) return; // Skip header row
const rowData: any = {};
row.eachCell((cell, colNumber) => {
const header = headers[colNumber - 1];
if (header) {
rowData[header] = cell.value;
}
});
jsonData.push(rowData);
});
processData(jsonData);
};
reader.readAsArrayBuffer(file);
}
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: {
'text/csv': ['.csv'],
'application/vnd.ms-excel': ['.xls'],
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'],
},
maxFiles: 1,
});
const processData = (rawData: any[]) => {
// Limit to 1000 rows
const limitedData = rawData.slice(0, 1000);
// Auto-detect columns
if (limitedData.length > 0) {
const columns = Object.keys(limitedData[0]);
const autoMapping: Record<string, string> = {};
columns.forEach((col) => {
const lowerCol = col.toLowerCase();
if (lowerCol.includes('title') || lowerCol.includes('name') || lowerCol === 'test') {
autoMapping.title = col;
} else if (lowerCol.includes('content') || lowerCol.includes('url') || lowerCol.includes('data') || lowerCol.includes('link')) {
autoMapping.content = col;
}
});
// If no title column found, use first column
if (!autoMapping.title && columns.length > 0) {
autoMapping.title = columns[0];
}
// If no content column found, use second column
if (!autoMapping.content && columns.length > 1) {
autoMapping.content = columns[1];
}
setMapping(autoMapping);
}
setData(limitedData);
setStep('preview');
};
const generateStaticQRCodes = async () => {
setLoading(true);
try {
const qrCodes: GeneratedQR[] = [];
// Generate all QR codes client-side (Static QR Codes)
for (const row of data) {
const title = row[mapping.title as keyof typeof row] || 'Untitled';
const content = row[mapping.content as keyof typeof row] || 'https://example.com';
// Create a temporary div to render QR code
const tempDiv = document.createElement('div');
tempDiv.style.display = 'none';
document.body.appendChild(tempDiv);
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', '300');
svg.setAttribute('height', '300');
tempDiv.appendChild(svg);
// Use qrcode library to generate SVG
const QRCode = require('qrcode');
const qrSvg = await QRCode.toString(content, {
type: 'svg',
width: 300,
margin: 2,
color: {
dark: '#000000',
light: '#FFFFFF',
},
});
qrCodes.push({
title: String(title),
content: String(content), // Store the original URL
svg: qrSvg,
});
document.body.removeChild(tempDiv);
}
setGeneratedQRs(qrCodes);
setStep('complete');
showToast(`Successfully generated ${qrCodes.length} static QR codes!`, 'success');
} catch (error) {
console.error('QR generation error:', error);
showToast('Failed to generate QR codes', 'error');
} finally {
setLoading(false);
}
};
const downloadAllQRCodes = async () => {
const zip = new JSZip();
generatedQRs.forEach((qr, index) => {
const fileName = `${qr.title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}_${index + 1}.svg`;
zip.file(fileName, qr.svg);
});
const blob = await zip.generateAsync({ type: 'blob' });
saveAs(blob, 'qr-codes-bulk.zip');
showToast('Download started!', 'success');
};
const saveQRCodesToDatabase = async () => {
setLoading(true);
try {
const qrCodesToSave = generatedQRs.map((qr) => ({
title: qr.title,
isStatic: true, // This tells the API it's a static QR code
contentType: 'URL',
content: { url: qr.content }, // Content needs to be an object with url property
status: 'ACTIVE',
}));
// Save each QR code to the database
const savePromises = qrCodesToSave.map((qr) =>
fetchWithCsrf('/api/qrs', {
method: 'POST',
body: JSON.stringify(qr),
})
);
const results = await Promise.all(savePromises);
const failedCount = results.filter((r) => !r.ok).length;
if (failedCount === 0) {
showToast(`Successfully saved ${qrCodesToSave.length} QR codes!`, 'success');
// Redirect to dashboard after 1 second
setTimeout(() => {
window.location.href = '/dashboard';
}, 1000);
} else {
showToast(`Saved ${qrCodesToSave.length - failedCount} QR codes, ${failedCount} failed`, 'warning');
}
} catch (error) {
console.error('Error saving QR codes:', error);
showToast('Failed to save QR codes', 'error');
} finally {
setLoading(false);
}
};
const downloadTemplate = () => {
const template = [
{ title: 'Product Page', content: 'https://example.com/product' },
{ title: 'Landing Page', content: 'https://example.com/landing' },
{ title: 'Contact Form', content: 'https://example.com/contact' },
{ title: 'About Us', content: 'https://example.com/about' },
{ title: 'Pricing Page', content: 'https://example.com/pricing' },
{ title: 'FAQ Page', content: 'https://example.com/faq' },
{ title: 'Blog Article', content: 'https://example.com/blog/article-1' },
{ title: 'Support Portal', content: 'https://example.com/support' },
{ title: 'Download Page', content: 'https://example.com/download' },
{ title: 'Social Media', content: 'https://instagram.com/yourcompany' },
{ title: 'YouTube Video', content: 'https://youtube.com/watch?v=example' },
];
const csv = Papa.unparse(template);
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'bulk-qr-template.csv';
a.click();
URL.revokeObjectURL(url);
};
// Show upgrade prompt if not Business plan
if (userPlan !== 'BUSINESS') {
return (
<div className="max-w-4xl mx-auto">
<Card className="mt-12">
<CardContent className="p-12 text-center">
<div className="w-20 h-20 bg-warning-100 rounded-full flex items-center justify-center mx-auto mb-6">
<svg className="w-10 h-10 text-warning-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">Business Plan Required</h2>
<p className="text-gray-600 mb-8 max-w-md mx-auto">
Bulk QR code creation is exclusively available for Business plan subscribers.
Upgrade now to generate up to 1,000 static QR codes at once.
</p>
<div className="flex justify-center space-x-4">
<Button variant="outline" onClick={() => window.location.href = '/dashboard'}>
Back to Dashboard
</Button>
<Button onClick={() => window.location.href = '/pricing'}>
Upgrade to Business
</Button>
</div>
</CardContent>
</Card>
</div>
);
}
return (
<div className="max-w-6xl mx-auto">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">{t('bulk.title')}</h1>
<p className="text-gray-600 mt-2">{t('bulk.subtitle')}</p>
</div>
{/* Template Warning Banner */}
<Card className="mb-6 bg-warning-50 border-warning-200">
<CardContent className="p-4">
<div className="flex items-start space-x-3">
<svg className="w-6 h-6 text-warning-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div>
<h3 className="font-semibold text-warning-900 mb-1">Please Follow the Template Format</h3>
<p className="text-sm text-warning-800">
Download the template below and follow the format exactly. Your CSV must include columns for <strong>title</strong> and <strong>content</strong> (URL).
</p>
</div>
</div>
</CardContent>
</Card>
{/* Info Banner */}
<Card className="mb-6 bg-blue-50 border-blue-200">
<CardContent className="p-4">
<div className="flex items-start space-x-3">
<svg className="w-6 h-6 text-blue-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<h3 className="font-semibold text-blue-900 mb-1">Static QR Codes Only</h3>
<p className="text-sm text-blue-800">
Bulk creation generates <strong>static QR codes</strong> that cannot be edited after creation.
These QR codes do not include tracking or analytics. Perfect for print materials and offline use.
</p>
</div>
</div>
</CardContent>
</Card>
{/* Progress Steps */}
<div className="mb-8">
<div className="flex items-center justify-between">
<div className={`flex items-center ${step === 'upload' ? 'text-primary-600' : 'text-gray-400'}`}>
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${step === 'upload' ? 'bg-primary-600 text-white' : 'bg-gray-200'
}`}>
1
</div>
<span className="ml-3 font-medium">Upload File</span>
</div>
<div className="flex-1 h-0.5 bg-gray-200 mx-4">
<div className={`h-full bg-primary-600 transition-all ${step === 'preview' || step === 'complete' ? 'w-full' : 'w-0'
}`} />
</div>
<div className={`flex items-center ${step === 'preview' || step === 'complete' ? 'text-primary-600' : 'text-gray-400'
}`}>
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${step === 'preview' || step === 'complete' ? 'bg-primary-600 text-white' : 'bg-gray-200'
}`}>
2
</div>
<span className="ml-3 font-medium">Preview & Map</span>
</div>
<div className="flex-1 h-0.5 bg-gray-200 mx-4">
<div className={`h-full bg-primary-600 transition-all ${step === 'complete' ? 'w-full' : 'w-0'
}`} />
</div>
<div className={`flex items-center ${step === 'complete' ? 'text-primary-600' : 'text-gray-400'}`}>
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${step === 'complete' ? 'bg-primary-600 text-white' : 'bg-gray-200'
}`}>
3
</div>
<span className="ml-3 font-medium">Download</span>
</div>
</div>
</div>
{/* Upload Step */}
{step === 'upload' && (
<Card>
<CardContent className="p-8">
<div className="text-center mb-6">
<Button variant="outline" onClick={downloadTemplate}>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
Download Template
</Button>
</div>
<div
{...getRootProps()}
className={`border-2 border-dashed rounded-lg p-12 text-center cursor-pointer transition-colors ${isDragActive ? 'border-primary-500 bg-primary-50' : 'border-gray-300 hover:border-gray-400'
}`}
>
<input {...getInputProps()} />
<svg className="w-16 h-16 mx-auto mb-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<p className="text-lg font-medium text-gray-900 mb-2">
{isDragActive ? 'Drop the file here' : 'Drag & drop your file here'}
</p>
<p className="text-sm text-gray-500 mb-4">or click to browse</p>
<p className="text-xs text-gray-400">Supports CSV, XLS, XLSX (max 1,000 rows)</p>
</div>
<div className="mt-8 grid md:grid-cols-3 gap-4">
<Card>
<CardContent className="p-4">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<div>
<p className="font-medium text-gray-900">Simple Format</p>
<p className="text-sm text-gray-500">Just title & URL</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z" />
</svg>
</div>
<div>
<p className="font-medium text-gray-900">Static QR Codes</p>
<p className="text-sm text-gray-500">No tracking included</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
</div>
<div>
<p className="font-medium text-gray-900">Instant Download</p>
<p className="text-sm text-gray-500">Get ZIP with all SVGs</p>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Supported QR Code Types Section */}
<div className="mt-8">
<Card>
<CardHeader>
<CardTitle className="text-lg">📋 Supported QR Code Types</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<p className="text-gray-600 mb-6">
This bulk generator creates <strong>static QR codes</strong> for multiple content types. Choose the format that matches your needs:
</p>
<div className="space-y-4">
<div className="border-l-4 border-blue-500 pl-4">
<p className="font-semibold text-gray-900 mb-1">🌐 URL - Website Links</p>
<p className="text-sm text-gray-600 mb-1">Format: <code className="bg-gray-100 px-2 py-1 rounded text-xs">https://example.com</code></p>
<p className="text-xs text-gray-500">Example: Product Page,https://example.com/product</p>
</div>
<div className="border-l-4 border-purple-500 pl-4">
<p className="font-semibold text-gray-900 mb-1">👤 VCARD - Contact Cards</p>
<p className="text-sm text-gray-600 mb-1">Format: <code className="bg-gray-100 px-2 py-1 rounded text-xs">FirstName,LastName,Email,Phone,Organization,Title</code></p>
<p className="text-xs text-gray-500">Example: John Doe,"John,Doe,john@example.com,+1234567890,Company,CEO"</p>
</div>
<div className="border-l-4 border-green-500 pl-4">
<p className="font-semibold text-gray-900 mb-1">📍 GEO - Locations</p>
<p className="text-sm text-gray-600 mb-1">Format: <code className="bg-gray-100 px-2 py-1 rounded text-xs">latitude,longitude,label</code></p>
<p className="text-xs text-gray-500">Example: Office Location,"37.7749,-122.4194,Main Office"</p>
</div>
<div className="border-l-4 border-pink-500 pl-4">
<p className="font-semibold text-gray-900 mb-1">📞 PHONE - Phone Numbers</p>
<p className="text-sm text-gray-600 mb-1">Format: <code className="bg-gray-100 px-2 py-1 rounded text-xs">+1234567890</code></p>
<p className="text-xs text-gray-500">Example: Support Hotline,+1234567890</p>
</div>
<div className="border-l-4 border-yellow-500 pl-4">
<p className="font-semibold text-gray-900 mb-1">📝 TEXT - Plain Text</p>
<p className="text-sm text-gray-600 mb-1">Format: <code className="bg-gray-100 px-2 py-1 rounded text-xs">Any text content</code></p>
<p className="text-xs text-gray-500">Example: Serial Number,SN-12345-ABCDE</p>
</div>
</div>
<div className="bg-gradient-to-r from-blue-50 to-purple-50 rounded-lg p-6 mt-6">
<h4 className="font-semibold text-gray-900 mb-3">📥 CSV File Format:</h4>
<p className="text-sm text-gray-600 mb-3">
Your file needs <strong>two columns</strong>: <code className="bg-white px-2 py-1 rounded">title</code> and <code className="bg-white px-2 py-1 rounded">content</code>
</p>
<div className="bg-white rounded-lg p-4 shadow-sm overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b-2 border-gray-300">
<th className="text-left py-2 px-3 font-semibold text-gray-700">title</th>
<th className="text-left py-2 px-3 font-semibold text-gray-700">content</th>
</tr>
</thead>
<tbody className="font-mono text-xs">
<tr className="border-b border-gray-200">
<td className="py-2 px-3">Product Page</td>
<td className="py-2 px-3">https://example.com/product</td>
</tr>
<tr className="border-b border-gray-200">
<td className="py-2 px-3">John Doe</td>
<td className="py-2 px-3">John,Doe,john@example.com,+1234567890,Company,CEO</td>
</tr>
<tr className="border-b border-gray-200">
<td className="py-2 px-3">Office Location</td>
<td className="py-2 px-3">37.7749,-122.4194,Main Office</td>
</tr>
<tr className="border-b border-gray-200">
<td className="py-2 px-3">Support Hotline</td>
<td className="py-2 px-3">+1234567890</td>
</tr>
<tr>
<td className="py-2 px-3">Serial Number</td>
<td className="py-2 px-3">SN-12345-ABCDE</td>
</tr>
</tbody>
</table>
</div>
</div>
<div className="border-l-4 border-yellow-500 pl-4">
<p className="font-semibold text-gray-900 mb-1"> Important Notes</p>
<ul className="text-sm text-gray-600 space-y-1">
<li> <strong>Static QR codes</strong> - Cannot be edited after creation</li>
<li> <strong>No tracking or analytics</strong> - Scans are not tracked</li>
<li> <strong>Maximum 1,000 QR codes</strong> per upload</li>
<li> <strong>Download as ZIP</strong> or save to your dashboard</li>
<li> <strong>All QR types supported</strong> - URLs, vCards, locations, phone numbers, and text</li>
</ul>
</div>
<div className="mt-4 bg-blue-50 border border-blue-200 rounded-lg p-4">
<p className="text-sm text-blue-900">
<strong>💡 Tip:</strong> Download the template above to see examples of all 5 QR code types with 11 ready-to-use examples!
</p>
</div>
</div>
</CardContent>
</Card>
</div>
</CardContent>
</Card>
)}
{/* Preview Step */}
{step === 'preview' && (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Preview & Map Columns</CardTitle>
<Badge variant="info">{data.length} rows detected</Badge>
</div>
</CardHeader>
<CardContent>
<div className="mb-6 grid md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Title Column</label>
<Select
value={mapping.title || ''}
onChange={(e) => setMapping({ ...mapping, title: e.target.value })}
options={Object.keys(data[0] || {}).map((col) => ({ value: col, label: col }))}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Content/URL Column</label>
<Select
value={mapping.content || ''}
onChange={(e) => setMapping({ ...mapping, content: e.target.value })}
options={Object.keys(data[0] || {}).map((col) => ({ value: col, label: col }))}
/>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b">
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700">Preview</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700">Title</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700">Content</th>
</tr>
</thead>
<tbody>
{data.slice(0, 5).map((row: any, index) => (
<tr key={index} className="border-b">
<td className="py-3 px-4">
<QRCodeSVG
value={row[mapping.content] || 'https://example.com'}
size={40}
/>
</td>
<td className="py-3 px-4 text-sm text-gray-900">
{row[mapping.title] || 'Untitled'}
</td>
<td className="py-3 px-4 text-sm text-gray-900">
{(row[mapping.content] || '').substring(0, 50)}...
</td>
</tr>
))}
</tbody>
</table>
</div>
{data.length > 5 && (
<p className="text-sm text-gray-500 mt-4 text-center">
Showing 5 of {data.length} rows
</p>
)}
<div className="flex justify-between mt-6">
<Button variant="outline" onClick={() => setStep('upload')}>
Back
</Button>
<Button onClick={generateStaticQRCodes} loading={loading}>
Generate {data.length} Static QR Codes
</Button>
</div>
</CardContent>
</Card>
)}
{/* Complete Step */}
{step === 'complete' && (
<Card>
<CardContent className="p-12 text-center">
<div className="w-20 h-20 bg-success-100 rounded-full flex items-center justify-center mx-auto mb-6">
<svg className="w-10 h-10 text-success-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">Generation Complete!</h2>
<p className="text-gray-600 mb-8">
Successfully generated {generatedQRs.length} static QR codes
</p>
<div className="mb-8">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-8 max-w-6xl mx-auto">
{generatedQRs.slice(0, 8).map((qr, index) => (
<div key={index} className="bg-white border border-gray-200 rounded-lg p-6 flex flex-col items-center shadow-sm hover:shadow-md transition-shadow">
<div className="w-full flex items-center justify-center mb-4" style={{ height: '160px' }}>
<div
dangerouslySetInnerHTML={{ __html: qr.svg }}
className="qr-code-container"
style={{ maxWidth: '160px', maxHeight: '160px' }}
/>
</div>
<p className="text-sm text-gray-900 font-medium text-center break-words w-full">{qr.title}</p>
</div>
))}
</div>
</div>
<style jsx>{`
.qr-code-container :global(svg) {
width: 100% !important;
height: 100% !important;
max-width: 160px !important;
max-height: 160px !important;
}
`}</style>
<div className="flex justify-center space-x-4">
<Button variant="outline" onClick={() => {
setStep('upload');
setData([]);
setMapping({});
setGeneratedQRs([]);
}}>
Create More
</Button>
<Button variant="outline" onClick={downloadAllQRCodes}>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
Download All as ZIP
</Button>
<Button onClick={saveQRCodesToDatabase} loading={loading}>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" />
</svg>
Save QR Codes
</Button>
</div>
</CardContent>
</Card>
)}
</div>
);
}