'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([]); const [mapping, setMapping] = useState>({}); const [loading, setLoading] = useState(false); const [generatedQRs, setGeneratedQRs] = useState([]); const [userPlan, setUserPlan] = useState('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 = {}; 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 (

Business Plan Required

Bulk QR code creation is exclusively available for Business plan subscribers. Upgrade now to generate up to 1,000 static QR codes at once.

); } return (

{t('bulk.title')}

{t('bulk.subtitle')}

{/* Template Warning Banner */}

Please Follow the Template Format

Download the template below and follow the format exactly. Your CSV must include columns for title and content (URL).

{/* Info Banner */}

Static QR Codes Only

Bulk creation generates static QR codes that cannot be edited after creation. These QR codes do not include tracking or analytics. Perfect for print materials and offline use.

{/* Progress Steps */}
1
Upload File
2
Preview & Map
3
Download
{/* Upload Step */} {step === 'upload' && (

{isDragActive ? 'Drop the file here' : 'Drag & drop your file here'}

or click to browse

Supports CSV, XLS, XLSX (max 1,000 rows)

Simple Format

Just title & URL

Static QR Codes

No tracking included

Instant Download

Get ZIP with all SVGs

{/* Supported QR Code Types Section */}
📋 Supported QR Code Types

This bulk generator creates static QR codes for multiple content types. Choose the format that matches your needs:

🌐 URL - Website Links

Format: https://example.com

Example: Product Page,https://example.com/product

👤 VCARD - Contact Cards

Format: FirstName,LastName,Email,Phone,Organization,Title

Example: John Doe,"John,Doe,john@example.com,+1234567890,Company,CEO"

📍 GEO - Locations

Format: latitude,longitude,label

Example: Office Location,"37.7749,-122.4194,Main Office"

📞 PHONE - Phone Numbers

Format: +1234567890

Example: Support Hotline,+1234567890

📝 TEXT - Plain Text

Format: Any text content

Example: Serial Number,SN-12345-ABCDE

đŸ“Ĩ CSV File Format:

Your file needs two columns: title and content

title content
Product Page https://example.com/product
John Doe John,Doe,john@example.com,+1234567890,Company,CEO
Office Location 37.7749,-122.4194,Main Office
Support Hotline +1234567890
Serial Number SN-12345-ABCDE

â„šī¸ Important Notes

  • â€ĸ Static QR codes - Cannot be edited after creation
  • â€ĸ No tracking or analytics - Scans are not tracked
  • â€ĸ Maximum 1,000 QR codes per upload
  • â€ĸ Download as ZIP or save to your dashboard
  • â€ĸ All QR types supported - URLs, vCards, locations, phone numbers, and text

💡 Tip: Download the template above to see examples of all 5 QR code types with 11 ready-to-use examples!

)} {/* Preview Step */} {step === 'preview' && (
Preview & Map Columns {data.length} rows detected
setMapping({ ...mapping, content: e.target.value })} options={Object.keys(data[0] || {}).map((col) => ({ value: col, label: col }))} />
{data.slice(0, 5).map((row: any, index) => ( ))}
Preview Title Content
{row[mapping.title] || 'Untitled'} {(row[mapping.content] || '').substring(0, 50)}...
{data.length > 5 && (

Showing 5 of {data.length} rows

)}
)} {/* Complete Step */} {step === 'complete' && (

Generation Complete!

Successfully generated {generatedQRs.length} static QR codes

{generatedQRs.slice(0, 8).map((qr, index) => (

{qr.title}

))}
)}
); }