diff --git a/src/components/BatchModal.tsx b/src/components/BatchModal.tsx new file mode 100644 index 0000000..be04d8f --- /dev/null +++ b/src/components/BatchModal.tsx @@ -0,0 +1,128 @@ +import { useState, useRef } from 'react'; +import QRCodeStyling from 'qr-code-styling'; +import JSZip from 'jszip'; + +interface Props { + isOpen: boolean; + onClose: () => void; +} + +export function BatchModal({ isOpen, onClose }: Props) { + const [input, setInput] = useState(''); + const [progress, setProgress] = useState(null); + const [done, setDone] = useState(false); + const backdropRef = useRef(null); + + const urls = input.split('\n').map(l => l.trim()).filter(Boolean); + + const handleGenerate = async () => { + if (!urls.length) return; + setProgress(0); + setDone(false); + const zip = new JSZip(); + + for (let i = 0; i < urls.length; i++) { + const url = urls[i]; + const qr = new QRCodeStyling({ + width: 512, height: 512, + data: url, + dotsOptions: { color: '#1A1A1A', type: 'square' }, + backgroundOptions: { color: '#ffffff' }, + qrOptions: { errorCorrectionLevel: 'M' }, + }); + const blob = await qr.getRawData('png') as Blob; + const safeName = url.replace(/https?:\/\//, '').replace(/[^a-zA-Z0-9]/g, '_').slice(0, 40); + zip.file(`qrcode-${i + 1}-${safeName}.png`, blob); + setProgress(Math.round(((i + 1) / urls.length) * 100)); + } + + const content = await zip.generateAsync({ type: 'blob' }); + const a = document.createElement('a'); + a.href = URL.createObjectURL(content); + a.download = 'qrcodes-batch.zip'; + a.click(); + URL.revokeObjectURL(a.href); + setDone(true); + setProgress(null); + }; + + if (!isOpen) return null; + + return ( +
{ if (e.target === backdropRef.current) onClose(); }} + className="fixed inset-0 z-50 flex items-center justify-center p-4" + style={{ background: 'rgba(0,0,0,0.6)', backdropFilter: 'blur(4px)' }} + > +
+ {/* Header */} +
+
+

Batch Export

+

One URL per line — downloads a ZIP of PNGs

+
+ +
+ + {/* Textarea */} +
+ +