feat: Contest mode added
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ListContests } 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 survives a restart (serial counter included).
|
||||
export type ContestSession = {
|
||||
active: boolean;
|
||||
code: string; // ADIF CONTEST_ID written to each QSO
|
||||
name: string; // display name
|
||||
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
|
||||
};
|
||||
|
||||
export const CONTEST_DEFAULT: ContestSession = {
|
||||
active: false, code: '', name: '', exchange: 'serial',
|
||||
fixedKind: 'CQ Zone', sentFixed: '', nextSerial: 1,
|
||||
};
|
||||
|
||||
const FIXED_KINDS = ['CQ Zone', 'ITU Zone', 'State/Prov', 'Département', 'Grid', 'Name', 'Age', 'Section', 'Custom'];
|
||||
|
||||
// Map the catalogue's default exchange hint onto a fixed-kind label.
|
||||
const KIND_LABEL: Record<string, string> = {
|
||||
'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 };
|
||||
|
||||
export function ContestBar({ session, onChange }: {
|
||||
session: ContestSession;
|
||||
onChange: (patch: Partial<ContestSession>) => void;
|
||||
}) {
|
||||
const [contests, setContests] = useState<Def[]>([]);
|
||||
useEffect(() => { ListContests().then((c) => setContests((c ?? []) as Def[])).catch(() => {}); }, []);
|
||||
|
||||
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' });
|
||||
}
|
||||
};
|
||||
|
||||
if (!session.active) {
|
||||
return (
|
||||
<div className="flex items-center px-3 py-1.5">
|
||||
<button type="button" onClick={() => onChange({ active: true })}
|
||||
className="rounded-md border border-border bg-card px-2.5 py-1 text-xs font-bold text-muted-foreground hover:bg-muted">
|
||||
▶ Contest mode
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const snt = session.exchange === 'serial'
|
||||
? String(session.nextSerial).padStart(3, '0')
|
||||
: (session.sentFixed || '—');
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 border-y-2 border-amber-500 bg-amber-500/10 flex-wrap">
|
||||
<span className="rounded bg-amber-500 px-2 py-0.5 text-[11px] font-black uppercase tracking-wider text-white shrink-0">
|
||||
Contest
|
||||
</span>
|
||||
<select value={session.code} onChange={(e) => pickContest(e.target.value)}
|
||||
className="rounded-md border border-border bg-card px-2 py-1 text-xs max-w-[260px]">
|
||||
<option value="">— pick contest —</option>
|
||||
{contests.map((c) => <option key={c.code} value={c.code}>{c.name}</option>)}
|
||||
</select>
|
||||
{session.code && <span className="text-[11px] font-mono text-muted-foreground shrink-0">{session.code}</span>}
|
||||
|
||||
{/* Serial vs fixed exchange. */}
|
||||
<div className="inline-flex rounded-md border border-border overflow-hidden shrink-0">
|
||||
{(['serial', 'fixed'] as const).map((x) => (
|
||||
<button key={x} type="button" onClick={() => onChange({ exchange: x })}
|
||||
className={cn('px-2 py-1 text-[11px] font-bold border-l border-border first:border-l-0',
|
||||
session.exchange === x ? 'bg-primary text-primary-foreground' : 'bg-card text-muted-foreground hover:bg-muted')}>
|
||||
{x === 'serial' ? 'Serial' : 'Fixed'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{session.exchange === 'serial' ? (
|
||||
<label className="flex items-center gap-1 text-[11px] text-muted-foreground shrink-0">
|
||||
Next#
|
||||
<input type="number" min={1} value={session.nextSerial}
|
||||
onChange={(e) => onChange({ nextSerial: Math.max(1, parseInt(e.target.value || '1', 10)) })}
|
||||
className="w-16 rounded-md border border-border bg-card px-1.5 py-1 text-xs font-mono" />
|
||||
</label>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<select value={session.fixedKind} onChange={(e) => onChange({ fixedKind: e.target.value })}
|
||||
className="rounded-md border border-border bg-card px-2 py-1 text-xs">
|
||||
{FIXED_KINDS.map((k) => <option key={k} value={k}>{k}</option>)}
|
||||
</select>
|
||||
<input value={session.sentFixed} onChange={(e) => onChange({ sentFixed: e.target.value.toUpperCase() })}
|
||||
placeholder="your exch" className="w-24 rounded-md border border-border bg-card px-2 py-1 text-xs font-mono" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span className="text-[11px] text-muted-foreground shrink-0">
|
||||
Snt <b className="font-mono text-foreground">{snt}</b>
|
||||
</span>
|
||||
|
||||
<button type="button" onClick={() => onChange({ active: false })}
|
||||
className="ml-auto rounded-md border border-border bg-card px-2 py-1 text-[11px] text-muted-foreground hover:bg-muted shrink-0">
|
||||
Exit
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -426,7 +426,7 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [], b
|
||||
{flagURL(draft.dxcc) && <img src={flagURL(draft.dxcc)} alt="" className="h-4 rounded-[2px] border border-border/50" referrerPolicy="no-referrer" />}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="w-20 shrink-0">Freq</Label>
|
||||
<Label className="w-20 shrink-0">TX Freq</Label>
|
||||
<Input value={freqKHz} onChange={(e) => setFreqKHz(e.target.value.replace(/\D/g, ''))} className="font-mono w-24" placeholder="kHz" />
|
||||
<Input value={freqHz} onChange={(e) => setFreqHz(e.target.value.replace(/\D/g, ''))} maxLength={3} className="font-mono w-16" placeholder="Hz" />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user