MVP
This commit is contained in:
594
src/app/(app)/bulk-creation/page.tsx
Normal file
594
src/app/(app)/bulk-creation/page.tsx
Normal file
@@ -0,0 +1,594 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import Papa from 'papaparse';
|
||||
import * as XLSX from 'xlsx';
|
||||
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 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 [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 = (e) => {
|
||||
const data = new Uint8Array(e.target?.result as ArrayBuffer);
|
||||
const workbook = XLSX.read(data, { type: 'array' });
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[sheetName];
|
||||
const jsonData = XLSX.utils.sheet_to_json(worksheet);
|
||||
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) =>
|
||||
fetch('/api/qrs', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
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' },
|
||||
];
|
||||
|
||||
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>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user