720 lines
32 KiB
TypeScript
720 lines
32 KiB
TypeScript
'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>
|
||
);
|
||
}
|