feat(bandmap,dxped): drop the ctrl+wheel zoom, badge the news

ctrl+wheel is the WINDOW zoom everywhere else in OpsLog, and one gesture
that resizes the whole app over one panel and one band map over another
is a gesture nobody can trust. The + and - buttons keep the zoom, and
the footer hint stops advertising what is gone.

The DX-World headlines now carry a chase badge like the announcements —
but ENTITY-LEVEL only. A headline names no band, and asking the slot
question without one answers 'new band' for every entity ever worked:
the one mistake that costs a QSO. So the news says NEW DXCC or worked,
and the sharper verdicts stay where the bands and modes are known.
This commit is contained in:
2026-08-31 18:57:44 +02:00
parent 4a017f6290
commit 2b6f1ba9d7
7 changed files with 119 additions and 53 deletions
+4 -13
View File
@@ -471,19 +471,10 @@ export function BandMap({ band, spots, spotStatus: spotStatusRaw, currentFreqHz,
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [band, containerH, currentFreqHz, range, lo, hi, pxPerKHz, fitToBand]);
useEffect(() => {
const el = scrollerRef.current;
if (!el) return;
const onWheel = (e: WheelEvent) => {
if (!range) return;
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
changeZoom(e.deltaY > 0 ? -1 : 1);
}
};
el.addEventListener('wheel', onWheel, { passive: false });
return () => el.removeEventListener('wheel', onWheel);
}, [range]);
// No ctrl+wheel zoom here any more: ctrl+wheel is the WINDOW zoom everywhere
// else in OpsLog (View ▸ Zoom in/out), and one gesture that resizes the whole
// app over one panel and one band map over another is a gesture nobody can
// trust. The + / buttons keep the zoom, deliberately and visibly.
// Ctrl+↑ / Ctrl+↓ hop to the next spot above / below the rig frequency and tune
// to it. Higher freq is UP on the map (see freqToY), so ↑ = next higher spot.
@@ -26,6 +26,10 @@ type DXped = {
type News = {
title: string; link: string; pub_date: string; excerpt: string;
creator: string; image_url: string; tag: string; calls?: string[];
// Entity-level only — a headline names no band, so the backend answers
// 'new' or 'worked' and nothing finer.
status_chase?: string;
unconfirmed?: boolean;
};
// One badge per expedition, the strongest verdict winning — the same palette
@@ -180,6 +184,7 @@ export function DXpeditionsPanel() {
)}
{news.map((n, i) => {
const newsCalls = n.calls ?? [];
const newsBadge = CHASE_BADGE[n.status_chase ?? ''];
const newsAllWatched = newsCalls.length > 0 && newsCalls.every((c) => watched.has(c.toUpperCase()));
return (
<article key={`${n.link}-${i}`} className="rounded-md border border-border bg-muted/20 p-2">
@@ -193,6 +198,15 @@ export function DXpeditionsPanel() {
{n.tag && (
<span className="px-1 py-px rounded text-[9px] font-bold uppercase bg-primary/15 text-primary">{n.tag}</span>
)}
{newsBadge && (
<span className="px-1 py-px rounded text-[9px] font-bold uppercase tracking-wide border shrink-0"
title={n.unconfirmed ? t('dec.unconfTip') : undefined}
style={n.unconfirmed
? { color: newsBadge.colour, borderColor: newsBadge.colour, borderStyle: 'dashed', opacity: 0.6 }
: { color: newsBadge.colour, borderColor: newsBadge.colour }}>
{t(newsBadge.label)}
</span>
)}
<button type="button" onClick={() => n.link && BrowserOpenURL(n.link)}
className="text-xs font-medium text-left hover:underline">{n.title}</button>
</div>