fix(watchlist): remove on a single click, like the bell and trophy beside it

The two-click arm goes: removing an entry is cheap to undo (type it again), so
it does not earn a confirmation its neighbours do not have. The bin reddens on
hover and the green line confirms the removal.
This commit is contained in:
2026-08-29 01:36:06 +02:00
parent 9350af9589
commit 107c6fccf6
2 changed files with 8 additions and 19 deletions
+6 -17
View File
@@ -94,7 +94,7 @@ export function WatchlistTab({ spots, spotStatus, onSpotSelect, onSpotClick }: P
setNotice(isError ? '' : msg); setNotice(isError ? '' : msg);
noticeTimer.current = window.setTimeout(() => { setError(''); setNotice(''); }, 4000) as unknown as number; noticeTimer.current = window.setTimeout(() => { setError(''); setNotice(''); }, 4000) as unknown as number;
}; };
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>>({});
@@ -186,22 +186,11 @@ export function WatchlistTab({ spots, spotStatus, onSpotSelect, onSpotClick }: P
} catch (e: any) { flash(String(e?.message ?? e), true); } } catch (e: any) { flash(String(e?.message ?? e), true); }
}; };
// Armed through a REF, checked synchronously: the state variable is captured // One click, like the bell and the trophy beside it: removing a watchlist
// at render time, so a quick double-click read the pre-arm value twice and // entry is cheap to undo (type it again), so it does not earn a confirmation
// both clicks merely armed — the bin turned red and nothing was ever removed. // the other two buttons do not have.
const removeArmRef = useRef('');
const remove = async (call: string) => { const remove = async (call: string) => {
if (removeArmRef.current !== call) { try { await WatchlistRemove(call); flash(t('wl.removed', { call }), false); await refresh(); }
removeArmRef.current = call;
setRemoveArm(call);
window.setTimeout(() => {
if (removeArmRef.current === call) { removeArmRef.current = ''; setRemoveArm(''); }
}, 3000);
return;
}
removeArmRef.current = '';
setRemoveArm('');
try { await WatchlistRemove(call); await refresh(); }
catch (e: any) { flash(String(e?.message ?? e), true); } catch (e: any) { flash(String(e?.message ?? e), true); }
}; };
@@ -376,7 +365,7 @@ export function WatchlistTab({ spots, spotStatus, onSpotSelect, onSpotClick }: P
</button> </button>
<button type="button" title={t('wl.remove')} <button type="button" title={t('wl.remove')}
onClick={() => void remove(e.callsign)} onClick={() => void remove(e.callsign)}
className={cn('p-1 rounded hover:bg-muted', removeArm === e.callsign ? 'text-danger' : 'text-muted-foreground/40')}> className="p-1 rounded hover:bg-muted text-muted-foreground/40 hover:text-danger">
<Trash2 className="size-3.5" /> <Trash2 className="size-3.5" />
</button> </button>
</div> </div>
+2 -2
View File
@@ -49,7 +49,7 @@ const en: Dict = {
'wl.nNeeded': '{n} needed', 'wl.nToday': '{n} today', 'wl.allWorked': 'All worked', 'wl.workedToday': 'Worked today', 'wl.nNeeded': '{n} needed', 'wl.nToday': '{n} today', 'wl.allWorked': 'All worked', 'wl.workedToday': 'Worked today',
'wl.totalSpots': '{n} spots', 'wl.toggleContest': 'Contest entry: judged per UTC day', 'wl.totalSpots': '{n} spots', 'wl.toggleContest': 'Contest entry: judged per UTC day',
'wl.notifyOn': 'Alert me when this station is spotted', 'wl.notifyOff': 'Stop alerting for this station', 'wl.notifyOn': 'Alert me when this station is spotted', 'wl.notifyOff': 'Stop alerting for this station',
'wl.remove': 'Remove (click twice)', 'wl.noSpots': 'No live spots', 'wl.remove': 'Remove', 'wl.removed': '{call} removed from the watchlist.', 'wl.noSpots': 'No live spots',
'wl.newDxcc': 'NEW DXCC', 'wl.worked': 'Worked', 'wl.needed': 'Needed!', 'wl.todayOk': 'Today ✓', 'wl.workToday': 'Work today!', 'wl.newDxcc': 'NEW DXCC', 'wl.worked': 'Worked', 'wl.needed': 'Needed!', 'wl.todayOk': 'Today ✓', 'wl.workToday': 'Work today!',
'wl.spotTip': 'Click: fill the callsign · double-click: tune and work', 'wl.spotTip': 'Click: fill the callsign · double-click: tune and work',
'tools.net': 'NET Control', 'tools.alerts': 'Alert management…', 'tools.contest': 'Contest mode', 'tools.net': 'NET Control', 'tools.alerts': 'Alert management…', 'tools.contest': 'Contest mode',
@@ -573,7 +573,7 @@ const fr: Dict = {
'wl.nNeeded': '{n} manquants', 'wl.nToday': "{n} aujourd'hui", 'wl.allWorked': 'Tout contacté', 'wl.workedToday': "Contacté aujourd'hui", 'wl.nNeeded': '{n} manquants', 'wl.nToday': "{n} aujourd'hui", 'wl.allWorked': 'Tout contacté', 'wl.workedToday': "Contacté aujourd'hui",
'wl.totalSpots': '{n} spots', 'wl.toggleContest': 'Entrée contest : jugée par jour UTC', 'wl.totalSpots': '{n} spots', 'wl.toggleContest': 'Entrée contest : jugée par jour UTC',
'wl.notifyOn': "M'alerter quand cette station est spottée", 'wl.notifyOff': "Ne plus alerter pour cette station", 'wl.notifyOn': "M'alerter quand cette station est spottée", 'wl.notifyOff': "Ne plus alerter pour cette station",
'wl.remove': 'Supprimer (cliquer deux fois)', 'wl.noSpots': 'Aucun spot en cours', 'wl.remove': 'Supprimer', 'wl.removed': '{call} retiré de la watchlist.', 'wl.noSpots': 'Aucun spot en cours',
'wl.newDxcc': 'NOUV DXCC', 'wl.worked': 'Contacté', 'wl.needed': 'Manquant !', 'wl.todayOk': "Auj. ✓", 'wl.workToday': "À faire auj. !", 'wl.newDxcc': 'NOUV DXCC', 'wl.worked': 'Contacté', 'wl.needed': 'Manquant !', 'wl.todayOk': "Auj. ✓", 'wl.workToday': "À faire auj. !",
'wl.spotTip': "Clic : remplir l'indicatif · double-clic : régler la radio et travailler", 'wl.spotTip': "Clic : remplir l'indicatif · double-clic : régler la radio et travailler",
'tools.net': 'Contrôle de NET', 'tools.alerts': 'Gestion des alertes…', 'tools.contest': 'Mode contest', 'tools.net': 'Contrôle de NET', 'tools.alerts': 'Gestion des alertes…', 'tools.contest': 'Mode contest',