feat(watchlist): confirm an add, and both messages clear themselves
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.
This commit is contained in:
@@ -84,6 +84,16 @@ export function WatchlistTab({ spots, spotStatus, onSpotSelect, onSpotClick }: P
|
|||||||
return m === modeFilter;
|
return m === modeFilter;
|
||||||
};
|
};
|
||||||
const [error, setError] = useState('');
|
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<number | undefined>(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('');
|
const [removeArm, setRemoveArm] = useState('');
|
||||||
// worked answer per "call|band|modeclass|contest" key.
|
// worked answer per "call|band|modeclass|contest" key.
|
||||||
const [worked, setWorked] = useState<Record<string, boolean>>({});
|
const [worked, setWorked] = useState<Record<string, boolean>>({});
|
||||||
@@ -168,9 +178,12 @@ export function WatchlistTab({ spots, spotStatus, onSpotSelect, onSpotClick }: P
|
|||||||
const add = async () => {
|
const add = async () => {
|
||||||
const c = addCall.trim().toUpperCase();
|
const c = addCall.trim().toUpperCase();
|
||||||
if (!c) return;
|
if (!c) return;
|
||||||
setError('');
|
try {
|
||||||
try { await WatchlistAdd(c, addContest); setAddCall(''); await refresh(); }
|
await WatchlistAdd(c, addContest);
|
||||||
catch (e: any) { setError(String(e?.message ?? e)); }
|
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) => {
|
const remove = async (call: string) => {
|
||||||
@@ -296,6 +309,7 @@ export function WatchlistTab({ spots, spotStatus, onSpotSelect, onSpotClick }: P
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{error && <div className="text-xs text-danger px-1">{error}</div>}
|
{error && <div className="text-xs text-danger px-1">{error}</div>}
|
||||||
|
{notice && <div className="text-xs text-success px-1">{notice}</div>}
|
||||||
|
|
||||||
{/* cards */}
|
{/* cards */}
|
||||||
<div className="flex-1 min-h-0 overflow-y-auto space-y-2 pr-1">
|
<div className="flex-1 min-h-0 overflow-y-auto space-y-2 pr-1">
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const en: Dict = {
|
|||||||
'cwd.tipOnIdle': 'CW decoder — on, idle until CW mode · click to disable',
|
'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)',
|
'cwd.tipOff': 'CW decoder · click to enable (decodes RX audio in CW mode)',
|
||||||
'tools.watchlist': 'Watchlist…', 'tab.watchlist': 'Watchlist',
|
'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.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.searchPh': 'Search…', 'wl.famAll': 'All', 'wl.famNormal': 'DX', 'wl.famContest': 'Contest',
|
||||||
'wl.activeOnly': 'Active', 'wl.neededOnly': 'Needed',
|
'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.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)',
|
'cwd.tipOff': 'Décodeur CW · clic pour activer (décode l’audio RX en mode CW)',
|
||||||
'tools.watchlist': 'Watchlist…', 'tab.watchlist': 'Watchlist',
|
'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.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.searchPh': 'Chercher…', 'wl.famAll': 'Tous', 'wl.famNormal': 'DX', 'wl.famContest': 'Contest',
|
||||||
'wl.activeOnly': 'Actifs', 'wl.neededOnly': 'Manquants',
|
'wl.activeOnly': 'Actifs', 'wl.neededOnly': 'Manquants',
|
||||||
|
|||||||
Reference in New Issue
Block a user