feat: Contest mode added
This commit is contained in:
+56
-6
@@ -68,6 +68,7 @@ import { ClusterGrid } from '@/components/ClusterGrid';
|
||||
import { cleanSpotter, inferSpotMode, spotModeCategory, spotStatusKey } from '@/lib/spot';
|
||||
import { WorkedBeforeGrid } from '@/components/WorkedBeforeGrid';
|
||||
import { NetControlPanel } from '@/components/NetControlPanel';
|
||||
import { ContestBar, CONTEST_DEFAULT, type ContestSession } from '@/components/ContestBar';
|
||||
import { AlertsModal } from '@/components/AlertsModal';
|
||||
import { BulkEditModal } from '@/components/BulkEditModal';
|
||||
import { ChatPanel, type ChatMsg, type ChatPresence } from '@/components/ChatPopover';
|
||||
@@ -339,6 +340,11 @@ export default function App() {
|
||||
const [grid, setGrid] = useState('');
|
||||
const [name, setName] = useState('');
|
||||
const [qth, setQth] = useState('');
|
||||
// Contest mode: a picked contest + exchange config; persisted so a running
|
||||
// contest (serial counter included) survives a restart. `rcv` is the received
|
||||
// exchange for the current QSO.
|
||||
const [contest, setContest] = useState<ContestSession>(CONTEST_DEFAULT);
|
||||
const [rcv, setRcv] = useState('');
|
||||
const [country, setCountry] = useState('');
|
||||
const [comment, setComment] = useState('');
|
||||
const [note, setNote] = useState('');
|
||||
@@ -576,6 +582,20 @@ export default function App() {
|
||||
});
|
||||
useEffect(() => { writeUiPref('hamlog.qsoLimit', String(qsoLimit)); }, [qsoLimit]);
|
||||
|
||||
// Contest session: load once, then persist on every change (merge + save).
|
||||
useEffect(() => {
|
||||
GetUIPref('opslog.contest').then((s) => {
|
||||
if (s) { try { setContest({ ...CONTEST_DEFAULT, ...JSON.parse(s) }); } catch {} }
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
const updateContest = (patch: Partial<ContestSession>) => {
|
||||
setContest((c) => {
|
||||
const next = { ...c, ...patch };
|
||||
writeUiPref('opslog.contest', JSON.stringify(next));
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// === DX Cluster live state ===
|
||||
type ClusterSpot = {
|
||||
source_id: number;
|
||||
@@ -1833,7 +1853,23 @@ export default function App() {
|
||||
email: details.email,
|
||||
};
|
||||
applyAwardRefs(payload, details.award_refs ?? '', awardFieldRef.current);
|
||||
// Contest mode overrides the exchange fields: CONTEST_ID, the sent
|
||||
// exchange (running serial or fixed value) and the received exchange.
|
||||
if (contest.active && contest.code) {
|
||||
const sent = contest.exchange === 'serial' ? String(contest.nextSerial) : contest.sentFixed;
|
||||
const sE = exchangeFields(sent);
|
||||
const rE = exchangeFields(rcv.trim());
|
||||
Object.assign(payload, {
|
||||
contest_id: contest.code,
|
||||
stx: sE.num, stx_string: sE.str,
|
||||
srx: rE.num, srx_string: rE.str,
|
||||
});
|
||||
}
|
||||
await AddQSO(payload);
|
||||
// Advance the sent serial only after a successful log.
|
||||
if (contest.active && contest.code && contest.exchange === 'serial') {
|
||||
updateContest({ nextSerial: contest.nextSerial + 1 });
|
||||
}
|
||||
resetEntry(); // clears the call AND the Worked-before matrix
|
||||
callsignRef.current?.focus(); // return focus to the call field, wherever it was (e.g. Name)
|
||||
await refresh();
|
||||
@@ -1849,7 +1885,7 @@ export default function App() {
|
||||
// Discard any in-progress QSO recording (no-op if it was already saved on
|
||||
// log, or if the recorder is off).
|
||||
QSOAudioCancel(); setRecording(false); recordingCallRef.current = "";
|
||||
setCallsign(''); setComment(''); setNote('');
|
||||
setCallsign(''); setComment(''); setNote(''); setRcv('');
|
||||
if (!locks.start) setQsoStartedAt(null);
|
||||
if (!locks.end) setQsoEndedAt(null);
|
||||
resetAutoFill();
|
||||
@@ -2539,7 +2575,22 @@ export default function App() {
|
||||
<Input value={name} onChange={(e) => { setName(e.target.value); markEdited('name'); }} />
|
||||
</div>
|
||||
);
|
||||
const qthBlock = (
|
||||
// In contest mode QTH is replaced by the sent (read-only, auto) and received
|
||||
// exchange fields — the exchange is what matters, not the town.
|
||||
const contestSnt = contest.exchange === 'serial'
|
||||
? String(contest.nextSerial).padStart(3, '0')
|
||||
: (contest.sentFixed || '—');
|
||||
const qthBlock = contest.active ? (
|
||||
<>
|
||||
<div className="flex flex-col w-16 shrink-0"><Label className="mb-1 h-3.5">Snt</Label>
|
||||
<Input readOnly tabIndex={-1} value={contestSnt} className="font-mono bg-muted/40" />
|
||||
</div>
|
||||
<div className="flex flex-col w-24 shrink-0"><Label className="mb-1 h-3.5 text-amber-600 font-bold">Rcv</Label>
|
||||
<Input value={rcv} placeholder="exch" className="font-mono"
|
||||
onChange={(e) => setRcv(e.target.value.toUpperCase())} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col flex-[0.55] min-w-[70px]"><Label className="mb-1 h-3.5">QTH</Label>
|
||||
<Input value={qth} onChange={(e) => { setQth(e.target.value); markEdited('qth'); }} />
|
||||
</div>
|
||||
@@ -2602,7 +2653,7 @@ export default function App() {
|
||||
// CQ/ITU zones moved to the Info (F2) tab (DetailsPanel).
|
||||
const freqBlock = (
|
||||
<div className="flex flex-col w-32">
|
||||
<Label className="mb-1 h-3.5 flex items-center gap-1">{catState.split ? 'TX Freq (MHz)' : 'Freq (MHz)'} <LockBtn k="freq" title="frequency" /></Label>
|
||||
<Label className="mb-1 h-3.5 flex items-center gap-1">TX Freq (MHz) <LockBtn k="freq" title="frequency" /></Label>
|
||||
<Input
|
||||
tabIndex={-1}
|
||||
className="font-mono"
|
||||
@@ -3423,9 +3474,7 @@ export default function App() {
|
||||
<div className="flex flex-col w-[300px] shrink-0"><Label className="mb-1 h-3.5">Name</Label>
|
||||
<Input value={name} onChange={(e) => { setName(e.target.value); markEdited('name'); }} />
|
||||
</div>
|
||||
<div className="flex flex-col flex-1 min-w-[70px]"><Label className="mb-1 h-3.5">QTH</Label>
|
||||
<Input value={qth} onChange={(e) => { setQth(e.target.value); markEdited('qth'); }} />
|
||||
</div>
|
||||
{qthBlock}
|
||||
{gridBlock}
|
||||
</div>
|
||||
|
||||
@@ -3638,6 +3687,7 @@ export default function App() {
|
||||
|
||||
{/* ===== LOWER: tabbed table / cluster / band map ===== */}
|
||||
{compact ? null : <>
|
||||
<ContestBar session={contest} onChange={updateContest} />
|
||||
<div className={cn('grid gap-2.5 p-2.5 flex-1 min-h-0 grid-rows-[minmax(0,1fr)]',
|
||||
showBandMap ? (bandMapSide === 'left' ? 'grid-cols-[300px_1fr]' : 'grid-cols-[1fr_300px]') : 'grid-cols-[1fr]')}>
|
||||
<section className="bg-card border border-border rounded-lg shadow-sm flex flex-col min-h-0 overflow-hidden">
|
||||
|
||||
Reference in New Issue
Block a user