From a9a6579a9aa31650ff8fffa486be356dd6d709df Mon Sep 17 00:00:00 2001 From: rouggy Date: Sat, 29 Aug 2026 01:30:10 +0200 Subject: [PATCH] feat(watchlist): confirm an add, and both messages clear themselves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A green line under the toolbar says what was added (and as what — the contest variant names the per-UTC-day rule); the already-exists error keeps its red line. Both fade after four seconds: a stale 'added' from ten minutes ago reads as a fresh one. --- frontend/src/components/WatchlistTab.tsx | 20 +++++++++++++++++--- frontend/src/lib/i18n.tsx | 4 ++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/WatchlistTab.tsx b/frontend/src/components/WatchlistTab.tsx index b8259e5..b706d5d 100644 --- a/frontend/src/components/WatchlistTab.tsx +++ b/frontend/src/components/WatchlistTab.tsx @@ -84,6 +84,16 @@ export function WatchlistTab({ spots, spotStatus, onSpotSelect, onSpotClick }: P return m === modeFilter; }; const [error, setError] = useState(''); + // A quiet confirmation under the toolbar; both messages clear themselves — + // a stale "added" from ten minutes ago reads as a fresh one. + const [notice, setNotice] = useState(''); + const noticeTimer = useRef(undefined); + const flash = (msg: string, isError: boolean) => { + if (noticeTimer.current) window.clearTimeout(noticeTimer.current); + setError(isError ? msg : ''); + setNotice(isError ? '' : msg); + noticeTimer.current = window.setTimeout(() => { setError(''); setNotice(''); }, 4000) as unknown as number; + }; const [removeArm, setRemoveArm] = useState(''); // worked answer per "call|band|modeclass|contest" key. const [worked, setWorked] = useState>({}); @@ -168,9 +178,12 @@ export function WatchlistTab({ spots, spotStatus, onSpotSelect, onSpotClick }: P const add = async () => { const c = addCall.trim().toUpperCase(); if (!c) return; - setError(''); - try { await WatchlistAdd(c, addContest); setAddCall(''); await refresh(); } - catch (e: any) { setError(String(e?.message ?? e)); } + try { + await WatchlistAdd(c, addContest); + setAddCall(''); + flash(t(addContest ? 'wl.addedContest' : 'wl.added', { call: c }), false); + await refresh(); + } catch (e: any) { flash(String(e?.message ?? e), true); } }; const remove = async (call: string) => { @@ -296,6 +309,7 @@ export function WatchlistTab({ spots, spotStatus, onSpotSelect, onSpotClick }: P {error &&
{error}
} + {notice &&
{notice}
} {/* cards */}
diff --git a/frontend/src/lib/i18n.tsx b/frontend/src/lib/i18n.tsx index 6ceaa81..890c626 100644 --- a/frontend/src/lib/i18n.tsx +++ b/frontend/src/lib/i18n.tsx @@ -39,7 +39,7 @@ const en: Dict = { 'cwd.tipOnIdle': 'CW decoder — on, idle until CW mode · click to disable', 'cwd.tipOff': 'CW decoder · click to enable (decodes RX audio in CW mode)', 'tools.watchlist': 'Watchlist…', 'tab.watchlist': 'Watchlist', - 'wl.addPh': 'Callsign or VK9*', 'wl.add': 'Add', 'wl.contest': 'Contest', 'wl.addAsContest': 'as contest', 'wl.cTotal': 'Watchlist:', 'wl.cActive': 'Active:', 'wl.cNeeded': 'Needed:', 'wl.allModes': 'All modes', 'wl.modeFilter': 'Only show spots in this mode', 'wl.patternPh': 'Auto contest (e.g. WWA)', 'wl.patternHint': 'Auto-add as contest: any spotted callsign CONTAINING this text joins the watchlist as a contest entry by itself. Empty = off; collected entries stay.', + 'wl.addPh': 'Callsign or VK9*', 'wl.added': '{call} added to the watchlist.', 'wl.addedContest': '{call} added as a contest entry (judged per UTC day).', 'wl.add': 'Add', 'wl.contest': 'Contest', 'wl.addAsContest': 'as contest', 'wl.cTotal': 'Watchlist:', 'wl.cActive': 'Active:', 'wl.cNeeded': 'Needed:', 'wl.allModes': 'All modes', 'wl.modeFilter': 'Only show spots in this mode', 'wl.patternPh': 'Auto contest (e.g. WWA)', 'wl.patternHint': 'Auto-add as contest: any spotted callsign CONTAINING this text joins the watchlist as a contest entry by itself. Empty = off; collected entries stay.', 'wl.contestHint': 'Contest station: worked/needed is judged against the current UTC day — at 00:00 UTC every slot can be worked again.', 'wl.searchPh': 'Search…', 'wl.famAll': 'All', 'wl.famNormal': 'DX', 'wl.famContest': 'Contest', 'wl.activeOnly': 'Active', 'wl.neededOnly': 'Needed', @@ -563,7 +563,7 @@ const fr: Dict = { 'cwd.tipOnIdle': 'Décodeur CW — actif, en veille hors mode CW · clic pour désactiver', 'cwd.tipOff': 'Décodeur CW · clic pour activer (décode l’audio RX en mode CW)', 'tools.watchlist': 'Watchlist…', 'tab.watchlist': 'Watchlist', - 'wl.addPh': 'Indicatif ou VK9*', 'wl.add': 'Ajouter', 'wl.contest': 'Contest', 'wl.addAsContest': 'comme contest', 'wl.cTotal': 'Watchlist :', 'wl.cActive': 'Actives :', 'wl.cNeeded': 'Manquantes :', 'wl.allModes': 'Tous les modes', 'wl.modeFilter': 'Ne montrer que les spots de ce mode', 'wl.patternPh': 'Auto contest (ex. WWA)', 'wl.patternHint': "Ajout auto comme contest : tout indicatif spotté CONTENANT ce texte rejoint la watchlist en entrée contest tout seul. Vide = désactivé ; les entrées déjà collectées restent.", + 'wl.addPh': 'Indicatif ou VK9*', 'wl.added': '{call} ajouté à la watchlist.', 'wl.addedContest': '{call} ajouté en entrée contest (jugée par jour UTC).', 'wl.add': 'Ajouter', 'wl.contest': 'Contest', 'wl.addAsContest': 'comme contest', 'wl.cTotal': 'Watchlist :', 'wl.cActive': 'Actives :', 'wl.cNeeded': 'Manquantes :', 'wl.allModes': 'Tous les modes', 'wl.modeFilter': 'Ne montrer que les spots de ce mode', 'wl.patternPh': 'Auto contest (ex. WWA)', 'wl.patternHint': "Ajout auto comme contest : tout indicatif spotté CONTENANT ce texte rejoint la watchlist en entrée contest tout seul. Vide = désactivé ; les entrées déjà collectées restent.", 'wl.contestHint': "Station contest : contacté/manquant est jugé sur la journée UTC courante — à 00:00 UTC chaque créneau redevient à faire.", 'wl.searchPh': 'Chercher…', 'wl.famAll': 'Tous', 'wl.famNormal': 'DX', 'wl.famContest': 'Contest', 'wl.activeOnly': 'Actifs', 'wl.neededOnly': 'Manquants',