// screens.jsx — Login, Upload (with idle/dragover/uploading/success), History, Admin. // ── Login ─────────────────────────────────────────────────────────────────── function Login({ onAuthed }) { const [un, setUn] = React.useState(''); const [pw, setPw] = React.useState(''); const [err, setErr] = React.useState(''); const unRef = React.useRef(null); React.useEffect(() => { unRef.current?.focus(); }, []); const submit = async (e) => { e.preventDefault(); const r = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: un.trim().toLowerCase(), passphrase: pw }), }); if (r.ok) { onAuthed(await r.json()); } else { const body = await r.json().catch(() => ({})); setErr(body.error || "Those details don't match. Have another go."); } }; return (
); } // ── Drop zone (the projection screen) ─────────────────────────────────────── // One element drives idle / dragover / uploading / success states via a // data-state attribute. The visual difference is the screen's brightness + // the inner content; surface chrome stays put. function DropZone({ state, file, progress, scanPhase, readyAt, onPickFile, onDragOver, onDragLeave, onDrop, onReset }) { const fileInputRef = React.useRef(null); const pickHandler = () => fileInputRef.current?.click(); return (
File upload · ready

{state === 'success' ? 'Received.' : state === 'uploading' ? 'Receiving your file' : state === 'dragover' ? 'Release to receive' : state === 'rejected' ? 'File stopped.' : state === 'error' ? "That didn't go through." : state === 'quota' ? 'Upload limit reached.' : 'Upload your file here.'}

{state === 'success' ? ( <>It's in the library now. {readyAt && <>It'll be playable in about five minutes — by {readyAt}.} ) : state === 'uploading' ? ( <>Sit tight. We're moving it across, scanning it, and tucking it into the library. You don't need to wait on this screen. ) : state === 'dragover' ? ( <>Let it go. We'll do the rest. ) : state === 'rejected' ? ( <>Something on that file tripped the scanner, so we stopped it from being published. Hunter's been notified — he'll take a look. You haven't done anything wrong. ) : state === 'error' ? ( <>Connection dropped before we finished. Give it another go when you're ready. ) : state === 'quota' ? ( <>You've used your share of the library. Ask Hunter and he'll clear the counter — your files are all still safe. ) : ( <>Drag a file from your USB stick or desktop onto the screen below. One at a time is plenty. )}

); } function IdleContent({ dragover, onPick }) { return (
{dragover ? 'Drop to receive' : 'Drop your file onto this screen'}
{dragover ? '\u00a0' : 'or'}
{!dragover && ( )}
); } function UploadingContent({ file, progress, scanPhase }) { const mb = (file?.size || 0) / (1024 * 1024); const sentMb = (mb * progress) / 100; return (
{file?.name || 'film.mkv'}
{mb.toFixed(0)} MB ·{' '} {scanPhase === 'scanning' ? <>scanning with ClamAV… : scanPhase === 'finalizing' ? <>moving into the library… : <>{Math.min(100, Math.round(progress))}% received · {sentMb.toFixed(0)} MB of {mb.toFixed(0)} MB}
= 100} active={progress < 100} />
); } function Phase({ label, done, active }) { return ( {label} ); } const PhaseDash = () =>