From 4882c9f2fafee9cba76ef8cb79fcb3938fb746fc Mon Sep 17 00:00:00 2001 From: Preston Date: Mon, 21 Sep 2026 22:58:07 -0400 Subject: [PATCH] Change how QR code system works --- app/page.tsx | 51 ++++++--- components/match-submission-qr.tsx | 91 +++++++++++++++ components/offline-readiness.tsx | 3 +- components/qr-relay.tsx | 178 ++--------------------------- lib/qr-relay.ts | 6 +- tests/qr-relay.test.ts | 48 ++++++++ 6 files changed, 184 insertions(+), 193 deletions(-) create mode 100644 components/match-submission-qr.tsx create mode 100644 tests/qr-relay.test.ts diff --git a/app/page.tsx b/app/page.tsx index e1aa058..37c2667 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -53,6 +53,7 @@ import { TeamTrendChart, type TeamTrend } from '@/components/team-trend-chart'; import { ScoutingOperations } from '@/components/scouting-operations'; import { OfflineReadiness } from '@/components/offline-readiness'; import { QrRelay } from '@/components/qr-relay'; +import { MatchSubmissionQr } from '@/components/match-submission-qr'; import { getCachedValue, getDraft, @@ -61,6 +62,7 @@ import { saveCachedValue, saveDraft, synchronizePendingMutations, + type PendingMutation, } from '@/lib/offline-db'; import { observedPoints, type ScoutingPayload } from '@/lib/scouting-metrics'; import { @@ -432,6 +434,8 @@ export default function Home() { const [submissionStatus, setSubmissionStatus] = useState< 'draft' | 'queued' | 'synchronized' | 'rejected' >('draft'); + const [submittedMatchMutation, setSubmittedMatchMutation] = + useState(null); const eventTeams = eventPack ? [ ...new Set( @@ -893,7 +897,7 @@ export default function Home() { } setSaveError(''); try { - await queueMutation({ + const mutation: PendingMutation = { id: scoutEntryMutationId( eventPack.event.key, currentMatch.key, @@ -914,7 +918,9 @@ export default function Home() { schemaVersion: 1, ...currentPayload, }, - }); + }; + await queueMutation(mutation); + setSubmittedMatchMutation(mutation); setQueuedCount((await getPendingMutations()).length); setSaved(true); setSubmissionStatus('queued'); @@ -1082,6 +1088,7 @@ export default function Home() { setSelectedStation(station); setSaved(false); setSubmissionStatus('draft'); + setSubmittedMatchMutation(null); navigate('Scout'); } @@ -1779,11 +1786,7 @@ export default function Home() { online={online} onRefresh={() => loadEventPack(false, true)} /> - + Event coverage @@ -2073,18 +2076,28 @@ export default function Home() { {(submissionStatus === 'queued' || submissionStatus === 'synchronized') && ( - + <> + {submittedMatchMutation && eventPack && ( + + )} + + )} diff --git a/components/match-submission-qr.tsx b/components/match-submission-qr.tsx new file mode 100644 index 0000000..cce9e0a --- /dev/null +++ b/components/match-submission-qr.tsx @@ -0,0 +1,91 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import Image from 'next/image'; +import QRCode from 'qrcode'; +import { QrCode } from 'lucide-react'; +import { Badge } from '@/components/ui/badge'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import type { PendingMutation } from '@/lib/offline-db'; +import { + createRelayFrames, + ensureRelayDeviceRegistered, +} from '@/lib/qr-relay'; + +export function MatchSubmissionQr({ + mutation, + organizationId, + eventKey, + online, +}: { + mutation: PendingMutation; + organizationId: string; + eventKey: string; + online: boolean; +}) { + const [image, setImage] = useState(''); + const [message, setMessage] = useState('Preparing match handoff code…'); + + useEffect(() => { + let cancelled = false; + setImage(''); + setMessage('Preparing match handoff code…'); + void ensureRelayDeviceRegistered(online) + .then((device) => + createRelayFrames(device, organizationId, eventKey, [mutation]), + ) + .then((frames) => { + if (frames.length !== 1) + throw new Error( + 'This match contains too much data for one QR code. Shorten the notes and save again.', + ); + return QRCode.toDataURL(frames[0], { + width: 420, + margin: 1, + errorCorrectionLevel: 'M', + }); + }) + .then((dataUrl) => { + if (cancelled) return; + setImage(dataUrl); + setMessage( + 'Have a signed-in device with data service scan this code. You can also sync normally when service returns.', + ); + }) + .catch((error: unknown) => { + if (cancelled) return; + setMessage( + error instanceof Error + ? error.message + : 'Could not create the match handoff code.', + ); + }); + return () => { + cancelled = true; + }; + }, [eventKey, mutation, online, organizationId]); + + return ( + + + + Match handoff + + One match + + + {image && ( + QR code containing this match scouting submission + )} +

{message}

+
+
+ ); +} diff --git a/components/offline-readiness.tsx b/components/offline-readiness.tsx index dd24e9a..73416f8 100644 --- a/components/offline-readiness.tsx +++ b/components/offline-readiness.tsx @@ -201,7 +201,8 @@ export function OfflineReadiness({ ) : ( )} - {relayReady ? 'Registered' : 'Missing'} QR relay + {relayReady ? 'Registered' : 'Missing'} match QR + handoff key diff --git a/components/qr-relay.tsx b/components/qr-relay.tsx index 8e17900..71fcabe 100644 --- a/components/qr-relay.tsx +++ b/components/qr-relay.tsx @@ -1,107 +1,27 @@ 'use client'; import { useCallback, useEffect, useRef, useState } from 'react'; -import Image from 'next/image'; -import QRCode from 'qrcode'; -import { - Camera, - ChevronLeft, - ChevronRight, - Copy, - QrCode, - RadioTower, -} from 'lucide-react'; +import { Camera, QrCode } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { getPendingMutations } from '@/lib/offline-db'; import { assembleRelayEnvelope, - createRelayFrames, - ensureRelayDeviceRegistered, parseRelayFrame, } from '@/lib/qr-relay'; -export function QrRelay({ - organizationId, - eventKey, - online, -}: { - organizationId: string; - eventKey: string; - online: boolean; -}) { - const [mode, setMode] = useState<'send' | 'receive'>('send'); - const [frames, setFrames] = useState([]); - const [frameIndex, setFrameIndex] = useState(0); - const [qrImage, setQrImage] = useState(''); +export function QrRelay({ online }: { online: boolean }) { const [message, setMessage] = useState(''); const [progress, setProgress] = useState({ received: 0, total: 0 }); const [manualFrame, setManualFrame] = useState(''); - const [busy, setBusy] = useState(false); const videoRef = useRef(null); const controlsRef = useRef<{ stop(): void } | null>(null); const transferRef = useRef(''); const chunksRef = useRef(new Map()); const uploadingRef = useRef(false); - useEffect(() => { - if (!frames.length) return; - void QRCode.toDataURL(frames[frameIndex], { - width: 360, - margin: 1, - errorCorrectionLevel: 'M', - }).then(setQrImage); - }, [frameIndex, frames]); - - useEffect(() => { - if (frames.length < 2) return; - const timer = window.setInterval( - () => setFrameIndex((index) => (index + 1) % frames.length), - 1100, - ); - return () => window.clearInterval(timer); - }, [frames]); - useEffect(() => () => controlsRef.current?.stop(), []); - async function buildTransfer() { - setBusy(true); - setMessage(''); - try { - const [device, pending] = await Promise.all([ - ensureRelayDeviceRegistered(online), - getPendingMutations(), - ]); - const scoutEntries = pending.filter( - (item) => item.entity === 'scoutEntry', - ); - if (!scoutEntries.length) - throw new Error( - 'There are no queued match submissions on this device.', - ); - const nextFrames = await createRelayFrames( - device, - organizationId, - eventKey, - scoutEntries, - ); - setFrames(nextFrames); - setFrameIndex(0); - setMessage( - `${Math.min(24, scoutEntries.length)} submission${scoutEntries.length === 1 ? '' : 's'} ready in ${nextFrames.length} QR frame${nextFrames.length === 1 ? '' : 's'}. Keep this screen open until the other device confirms upload.`, - ); - } catch (error) { - setMessage( - error instanceof Error - ? error.message - : 'Could not create the QR transfer.', - ); - } finally { - setBusy(false); - } - } - const acceptFrame = useCallback( async (text: string) => { if (uploadingRef.current) return; @@ -186,95 +106,12 @@ export function QrRelay({ - QR data relay + Scan a scout match - Offline sender → online receiver + Online receiving device -
- - -
- {mode === 'send' ? ( - <> - {!frames.length ? ( - - ) : ( -
- {qrImage && ( - {`QR - )} -
- - - Frame {frameIndex + 1} of {frames.length} - - -
- -
- )} - - ) : ( - <> + <>