import { useEffect, useState } from 'react'; import { Trophy } from 'lucide-react'; import { ListContests, ContestStats } from '../../wailsjs/go/main/App'; import { cn } from '@/lib/utils'; // ContestSession is the live contest-mode state, persisted as JSON in a UI pref // so a running contest (serial counter included) survives a restart. export type ContestSession = { active: boolean; code: string; // ADIF CONTEST_ID written to each QSO name: string; exchange: 'serial' | 'fixed'; fixedKind: string; // label for the fixed exchange (CQ Zone, Département…) sentFixed: string; // the value YOU send when the exchange is fixed nextSerial: number; // running sent serial number startISO: string; // contest window start (UTC ISO) — only QSOs after count endISO: string; // contest window end (UTC ISO, empty = open) }; export const CONTEST_DEFAULT: ContestSession = { active: false, code: '', name: '', exchange: 'serial', fixedKind: 'CQ Zone', sentFixed: '', nextSerial: 1, startISO: '', endISO: '', }; // Contests run on UTC, so the date/time inputs are treated as UTC. const utcInputToISO = (v: string) => (v ? new Date(v + ':00Z').toISOString() : ''); const isoToUtcInput = (iso: string) => (iso ? new Date(iso).toISOString().slice(0, 16) : ''); const FIXED_KINDS = ['CQ Zone', 'ITU Zone', 'State/Prov', 'Département', 'Grid', 'Name', 'Age', 'Section', 'Custom']; const KIND_LABEL: Record = { 'cq-zone': 'CQ Zone', 'itu-zone': 'ITU Zone', 'state': 'State/Prov', 'dept': 'Département', 'grid': 'Grid', 'name': 'Name', 'class-section': 'Section', }; type Def = { code: string; name: string; exchange: string }; type Stats = { qsos: number; by_band?: { band: string; count: number }[] | null; mult: number; mult_label: string; score: number; last_hour: number }; function Stat({ label, value, accent }: { label: string; value: string | number; accent?: string }) { return (
{label}
{value}
); } export function ContestPanel({ session, onChange }: { session: ContestSession; onChange: (patch: Partial) => void; }) { const [contests, setContests] = useState([]); const [stats, setStats] = useState(null); useEffect(() => { ListContests().then((c) => setContests((c ?? []) as Def[])).catch(() => {}); }, []); // Poll the scoreboard while a contest is running. useEffect(() => { if (!session.active || !session.code) { setStats(null); return; } let alive = true; const load = () => ContestStats( session.code, session.exchange === 'fixed' ? kindKey(session.fixedKind) : 'serial', session.startISO, session.endISO, ).then((s) => { if (alive) setStats(s as Stats); }).catch(() => {}); load(); const id = window.setInterval(load, 4000); return () => { alive = false; window.clearInterval(id); }; }, [session.active, session.code, session.exchange, session.fixedKind, session.startISO, session.endISO]); const pickContest = (code: string) => { const def = contests.find((c) => c.code === code); if (!def) { onChange({ code: '', name: '' }); return; } if (def.exchange === 'serial' || def.exchange === 'rst') { onChange({ code: def.code, name: def.name, exchange: 'serial' }); } else { onChange({ code: def.code, name: def.name, exchange: 'fixed', fixedKind: KIND_LABEL[def.exchange] || 'Custom' }); } }; return (
{/* Setup card. */}
Contest setup {session.active && Live}
{session.code && {session.code}}
{(['serial', 'fixed'] as const).map((x) => ( ))}
{session.exchange === 'serial' ? ( ) : (
onChange({ sentFixed: e.target.value.toUpperCase() })} placeholder="your exchange" className="w-32 rounded-md border border-border bg-card px-2 py-1.5 text-sm font-mono" />
)}
{/* Contest window (UTC). Only QSOs inside it count toward the score and dupe check — so old runs of the same contest aren't included. */}
empty end = open · leave blank to count all
{session.active ? ( ) : ( )} {session.active ? 'The entry form shows Snt/Rcv and a DUPE badge; QSOs are stamped with this contest.' : 'Pick a contest, set the window, then Start.'}
{/* Scoreboard. */} {session.active && (
Scoreboard estimate
{stats && stats.by_band && stats.by_band.length > 0 && (
{stats.by_band.map((r) => ( ))}
Band QSOs
{r.band} {r.count}
)}
)}
); } // kindKey maps a fixed-kind label back to the backend's exchange key for mult // counting. function kindKey(label: string): string { const m: Record = { 'CQ Zone': 'cq-zone', 'ITU Zone': 'itu-zone', 'State/Prov': 'state', 'Département': 'dept', 'Grid': 'grid', 'Name': 'name', 'Section': 'section', 'Age': 'age', }; return m[label] || 'state'; }