Import source: src/components/BatchModal
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -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<number | null>(null);
|
||||||
|
const [done, setDone] = useState(false);
|
||||||
|
const backdropRef = useRef<HTMLDivElement>(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 (
|
||||||
|
<div
|
||||||
|
ref={backdropRef}
|
||||||
|
onClick={e => { 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)' }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-full max-w-lg rounded-[20px] p-6 flex flex-col gap-5 shadow-2xl"
|
||||||
|
style={{ background: 'var(--bg-card)', border: '1px solid var(--border)' }}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-[18px] font-bold" style={{ color: 'var(--text-primary)' }}>Batch Export</h2>
|
||||||
|
<p className="text-[13px]" style={{ color: 'var(--text-muted)' }}>One URL per line — downloads a ZIP of PNGs</p>
|
||||||
|
</div>
|
||||||
|
<button onClick={onClose} className="w-8 h-8 rounded-full flex items-center justify-center cursor-pointer transition-colors"
|
||||||
|
style={{ background: 'var(--bg-elevated)', color: 'var(--text-secondary)' }}>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||||
|
<path d="M2 2l10 10M12 2L2 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Textarea */}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-[13px] font-medium" style={{ color: 'var(--text-secondary)' }}>
|
||||||
|
URLs <span style={{ color: 'var(--text-muted)' }}>({urls.length} detected)</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={input}
|
||||||
|
onChange={e => { setInput(e.target.value); setDone(false); }}
|
||||||
|
placeholder={"https://example.com\nhttps://github.com\nhttps://openai.com"}
|
||||||
|
rows={8}
|
||||||
|
className="px-4 py-3 rounded-[11px] text-[13px] resize-none outline-none font-mono transition-all"
|
||||||
|
style={{ background: 'var(--bg-input)', border: '1px solid var(--border-sub)', color: 'var(--text-primary)' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress bar */}
|
||||||
|
{progress !== null && (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="h-2 rounded-full overflow-hidden" style={{ background: 'var(--bg-elevated)' }}>
|
||||||
|
<div className="h-full rounded-full transition-all duration-300" style={{ width: `${progress}%`, background: '#7C6FE0' }} />
|
||||||
|
</div>
|
||||||
|
<p className="text-[12px]" style={{ color: 'var(--text-muted)' }}>Generating… {progress}%</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{done && (
|
||||||
|
<p className="text-[13px] font-medium" style={{ color: '#22c55e' }}>✓ ZIP downloaded successfully</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button onClick={onClose} className="flex-1 h-11 rounded-[10px] text-[14px] font-medium cursor-pointer transition-all"
|
||||||
|
style={{ background: 'var(--bg-elevated)', border: '1px solid var(--border-sub)', color: 'var(--text-secondary)' }}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleGenerate}
|
||||||
|
disabled={!urls.length || progress !== null}
|
||||||
|
className="flex-1 h-11 rounded-[10px] text-[14px] font-medium text-white transition-all"
|
||||||
|
style={{
|
||||||
|
background: urls.length && progress === null ? '#7C6FE0' : 'var(--bg-elevated)',
|
||||||
|
color: urls.length && progress === null ? 'white' : 'var(--text-placeholder)',
|
||||||
|
cursor: urls.length && progress === null ? 'pointer' : 'not-allowed',
|
||||||
|
boxShadow: urls.length && progress === null ? '0 4px 16px rgba(124,111,224,0.3)' : 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{progress !== null ? 'Generating…' : `Generate ${urls.length || ''} QR${urls.length !== 1 ? 's' : ''}`}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user