fix
This commit is contained in:
+21
-38
@@ -548,11 +548,9 @@ export default function App() {
|
||||
const [clusterServerStatuses, setClusterServerStatuses] = useState<ServerStatus[]>([]);
|
||||
// "Send DX spot" dialog (Log4OM-style) — only reachable when a cluster is up.
|
||||
const [showSpotModal, setShowSpotModal] = useState(false);
|
||||
// "You have been spotted" banner — set when a cluster spot's DX call is our
|
||||
// own station callsign. Ref holds our call for the (one-shot) spot listener.
|
||||
const [selfSpot, setSelfSpot] = useState<{ spotter: string; freqKHz: number; band?: string; comment?: string; at: number } | null>(null);
|
||||
// Holds our station callsign for the (one-shot) cluster spot listener, so a
|
||||
// self-spot can be surfaced in the shared header toast.
|
||||
const myCallRef = useRef('');
|
||||
const selfSpotTimerRef = useRef<number | null>(null);
|
||||
|
||||
// === WinKeyer CW keyer ===
|
||||
const [wkEnabled, setWkEnabled] = useState(false);
|
||||
@@ -567,6 +565,8 @@ export default function App() {
|
||||
// F1-F12 macro shortcuts active only when the keyer is enabled + connected.
|
||||
const wkActiveRef = useRef(false);
|
||||
const wkEscClearsRef = useRef(true);
|
||||
const wkBusyRef = useRef(false); // live "keyer is sending" flag, for the <LOGQSO> wait-then-log
|
||||
useEffect(() => { wkBusyRef.current = wkStatus.busy; }, [wkStatus.busy]);
|
||||
useEffect(() => { wkActiveRef.current = wkEnabled && wkStatus.connected; }, [wkEnabled, wkStatus.connected]);
|
||||
useEffect(() => { wkEscClearsRef.current = wkEscClears; }, [wkEscClears]);
|
||||
|
||||
@@ -1074,13 +1074,13 @@ export default function App() {
|
||||
const next = [sp, ...arr];
|
||||
return next.length > SPOTS_CAP ? next.slice(0, SPOTS_CAP) : next;
|
||||
});
|
||||
// Self-spot: someone spotted OUR callsign on the cluster.
|
||||
// Self-spot: someone spotted OUR callsign — show it in the shared header
|
||||
// toast (same place as the other notifications), not a separate banner.
|
||||
const mine = myCallRef.current;
|
||||
if (mine && (sp.dx_call ?? '').toUpperCase() === mine) {
|
||||
setSelfSpot({ spotter: cleanSpotter(sp.spotter ?? ''), freqKHz: sp.freq_khz, band: sp.band, comment: sp.comment, at: Date.now() });
|
||||
// Auto-hide 3 s after the last self-spot; a new one resets the timer.
|
||||
if (selfSpotTimerRef.current) window.clearTimeout(selfSpotTimerRef.current);
|
||||
selfSpotTimerRef.current = window.setTimeout(() => setSelfSpot(null), 3000);
|
||||
const by = cleanSpotter(sp.spotter ?? '') || '?';
|
||||
const c = (sp.comment ?? '').trim();
|
||||
showToast(`Spotted by ${by}${c ? ` with ${c}` : ''}`);
|
||||
}
|
||||
});
|
||||
return () => { unsubState?.(); unsubSpot?.(); };
|
||||
@@ -1208,12 +1208,18 @@ export default function App() {
|
||||
}
|
||||
return out.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
function wkSend(rawText: string) {
|
||||
async function wkSend(rawText: string) {
|
||||
setWkSent('');
|
||||
WinkeyerSend(resolveCW(rawText)).catch((e) => setError(String(e?.message ?? e)));
|
||||
// <LOGQSO> in a macro (e.g. "73 TU <LOGQSO>") logs the contact after sending.
|
||||
// resolveCW already strips the token from the keyed text (unknown var → "").
|
||||
if (/<LOGQSO>/i.test(rawText)) void save();
|
||||
const doLog = /<LOGQSO>/i.test(rawText); // resolveCW strips the token (unknown var → "")
|
||||
await WinkeyerSend(resolveCW(rawText)).catch((e) => setError(String(e?.message ?? e)));
|
||||
// <LOGQSO> (e.g. "BK 73 TU <LOGQSO>") logs the contact AFTER the keyer has
|
||||
// finished sending — wait for the busy flag to rise then fall, so the QSO
|
||||
// isn't logged (and the form cleared) while the CW is still going out.
|
||||
if (!doLog) return;
|
||||
const sleep = (ms: number) => new Promise((r) => window.setTimeout(r, ms));
|
||||
for (let i = 0; i < 20 && !wkBusyRef.current; i++) await sleep(50); // ≤1s for sending to start
|
||||
for (let i = 0; i < 1200 && wkBusyRef.current; i++) await sleep(50); // ≤60s for it to finish
|
||||
void save();
|
||||
}
|
||||
function wkSendMacro(i: number) { const m = wkMacros[i]; if (m) wkSend(m.text); }
|
||||
wkSendMacroRef.current = wkSendMacro;
|
||||
@@ -1300,16 +1306,10 @@ export default function App() {
|
||||
email: details.email,
|
||||
};
|
||||
applyAwardRefs(payload, details.award_refs ?? '', awardFieldRef.current);
|
||||
const loggedCall = String(payload.callsign ?? '');
|
||||
const loggedDxcc = typeof payload.dxcc === 'number' ? payload.dxcc : 0;
|
||||
await AddQSO(payload);
|
||||
resetEntry();
|
||||
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();
|
||||
// Refresh the Worked-before matrix so the just-logged band/mode flips to
|
||||
// "worked" — resetEntry cleared it, so re-fetch for the logged call (a
|
||||
// live DB query, so it now includes this QSO).
|
||||
if (loggedCall.length >= 3) runWorkedBefore(loggedCall, loggedDxcc);
|
||||
} catch (e: any) {
|
||||
setError(String(e?.message ?? e));
|
||||
} finally { setSaving(false); }
|
||||
@@ -2378,22 +2378,6 @@ export default function App() {
|
||||
)}
|
||||
|
||||
|
||||
{/* "You have been spotted" banner — shows when our own callsign appears
|
||||
in a cluster spot. Floated top-centre (with the other notifications),
|
||||
never shifts the layout; auto-hides 3s after the last self-spot. */}
|
||||
{!compact && selfSpot && (
|
||||
<div className="fixed top-12 left-1/2 -translate-x-1/2 z-[100] flex items-center gap-2 rounded-lg border border-amber-300 bg-amber-100 text-amber-900 px-3.5 py-2 text-xs shadow-lg animate-in fade-in slide-in-from-top-2">
|
||||
<RadioTower className="size-3.5 shrink-0" />
|
||||
<span>
|
||||
Spotted by <strong className="font-mono">{selfSpot.spotter || '?'}</strong>
|
||||
{selfSpot.comment ? <span className="text-amber-800"> with {selfSpot.comment}</span> : null}
|
||||
</span>
|
||||
<div className="flex-1" />
|
||||
<button className="text-amber-700 hover:text-amber-900" title="Dismiss" onClick={() => setSelfSpot(null)}>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ===== ENTRY STRIP =====
|
||||
Enter from any <input> inside the strip logs the QSO. Radix Selects
|
||||
@@ -2588,7 +2572,6 @@ export default function App() {
|
||||
<TabsTrigger value="main">Main</TabsTrigger>
|
||||
<TabsTrigger value="recent">
|
||||
Recent QSOs
|
||||
<Badge variant="secondary" className="ml-1.5 px-1.5 py-0 text-[10px]">{qsos.length}</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="cluster">Cluster</TabsTrigger>
|
||||
<TabsTrigger value="worked">
|
||||
|
||||
@@ -12,6 +12,7 @@ interface Props {
|
||||
currentBand: string;
|
||||
currentMode: string;
|
||||
bands?: string[]; // operator's configured bands; falls back to DEFAULT_BANDS
|
||||
hasCall?: boolean; // a callsign is being entered — only then highlight the "current entry" cell
|
||||
}
|
||||
|
||||
// Compact column label for a band tag: keep the classic V/U for 2m/70cm,
|
||||
@@ -78,7 +79,7 @@ function cellTitle(band: string, cls: string, status: string, current: boolean):
|
||||
return `${band} ${cls}: ${desc}${current ? ' — current entry' : ''}`;
|
||||
}
|
||||
|
||||
export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands }: Props) {
|
||||
export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, hasCall = true }: Props) {
|
||||
// Columns from the operator's configured bands (so the matrix shows only the
|
||||
// bands they actually use), falling back to the built-in default set.
|
||||
const cols = useMemo(
|
||||
@@ -181,7 +182,7 @@ export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands }: Prop
|
||||
</th>
|
||||
{cols.map((b) => {
|
||||
const st = statusMap.get(`${b.tag}|${cls}`) ?? '';
|
||||
const isCurrent = b.tag === currentBand && classCurrent;
|
||||
const isCurrent = hasCall && b.tag === currentBand && classCurrent;
|
||||
return (
|
||||
<td
|
||||
key={b.tag}
|
||||
|
||||
@@ -118,7 +118,7 @@ function Field({ label, span = 1, children }: { label: string; span?: 1 | 2 | 3
|
||||
);
|
||||
}
|
||||
|
||||
export function DetailsPanel({ callsign: _cs, prefix, operatorGrid, remoteGrid, details, onChange, wb, wbBusy, band, mode, bands, tab, onTab, keyerActive }: Props) {
|
||||
export function DetailsPanel({ callsign, prefix, operatorGrid, remoteGrid, details, onChange, wb, wbBusy, band, mode, bands, tab, onTab, keyerActive }: Props) {
|
||||
const [internalOpen, setInternalOpen] = useState<TabName>('stats');
|
||||
const open = tab ?? internalOpen; // controlled when `tab` is provided
|
||||
// Bearing/distance from operator's home grid to the remote station.
|
||||
@@ -182,7 +182,7 @@ export function DetailsPanel({ callsign: _cs, prefix, operatorGrid, remoteGrid,
|
||||
<div className="overflow-y-auto min-h-0">
|
||||
{open === 'stats' && (
|
||||
<div className="px-3 py-2.5">
|
||||
<BandSlotGrid wb={wb} busy={!!wbBusy} currentBand={band} currentMode={mode} bands={bands} />
|
||||
<BandSlotGrid wb={wb} busy={!!wbBusy} currentBand={band} currentMode={mode} bands={bands} hasCall={callsign.trim() !== ''} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user