fix: Improved badges next to callsign (Cache, QRZ)
This commit is contained in:
+57
-29
@@ -590,7 +590,7 @@ export default function App() {
|
||||
// Contest session: load once, then persist on every change (merge + save).
|
||||
useEffect(() => {
|
||||
GetUIPref('opslog.contest').then((s) => {
|
||||
if (s) { try { setContest({ ...CONTEST_DEFAULT, ...JSON.parse(s) }); } catch {} }
|
||||
if (s) { try { const c = { ...CONTEST_DEFAULT, ...JSON.parse(s) }; setContest(c); if (c.active) setContestTabEnabled(true); } catch {} }
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
const updateContest = (patch: Partial<ContestSession>) => {
|
||||
@@ -774,6 +774,9 @@ export default function App() {
|
||||
// NET Control tab — enabled from Tools (persisted; once on it's a tab like Cluster).
|
||||
const [netEnabled, setNetEnabled] = useState(() => localStorage.getItem('opslog.netEnabled') === '1');
|
||||
useEffect(() => { localStorage.setItem('opslog.netEnabled', netEnabled ? '1' : '0'); }, [netEnabled]);
|
||||
// Contest tab is hidden until enabled from Tools → Contest mode.
|
||||
const [contestTabEnabled, setContestTabEnabled] = useState(() => localStorage.getItem('opslog.contestTab') === '1');
|
||||
useEffect(() => { localStorage.setItem('opslog.contestTab', contestTabEnabled ? '1' : '0'); }, [contestTabEnabled]);
|
||||
|
||||
const [dvkEnabled, setDvkEnabled] = useState(false);
|
||||
const [dvkMsgs, setDvkMsgs] = useState<DVKMsg[]>([]);
|
||||
@@ -2325,6 +2328,7 @@ export default function App() {
|
||||
{ type: 'item', label: (cwEnabled ? '✓ ' : '') + t('tools.cwDecoder'), action: 'tools.cwdecoder' },
|
||||
{ type: 'separator' },
|
||||
{ type: 'item', label: (netEnabled ? '✓ ' : '') + t('tools.net'), action: 'tools.net' },
|
||||
{ type: 'item', label: (contestTabEnabled ? '✓ ' : '') + t('tools.contest'), action: 'tools.contest' },
|
||||
{ type: 'item', label: t('tools.alerts'), action: 'tools.alerts' },
|
||||
{ type: 'separator' },
|
||||
{ type: 'item', label: t('tools.duplicates'), action: 'tools.duplicates', disabled: total === 0 },
|
||||
@@ -2337,7 +2341,7 @@ export default function App() {
|
||||
{ name: 'help', label: t('menu.help'), items: [
|
||||
{ type: 'item', label: t('help.about'), action: 'help.about' },
|
||||
]},
|
||||
], [total, selectedId, selectedIds, ctyRefreshing, refsDownloading, exporting, wkEnabled, dvkEnabled, cwEnabled, netEnabled, t]);
|
||||
], [total, selectedId, selectedIds, ctyRefreshing, refsDownloading, exporting, wkEnabled, dvkEnabled, cwEnabled, netEnabled, contestTabEnabled, t]);
|
||||
|
||||
function handleMenu(action: string) {
|
||||
switch (action) {
|
||||
@@ -2357,6 +2361,7 @@ export default function App() {
|
||||
case 'tools.dvk': setDvkEnabled((v) => !v); break;
|
||||
case 'tools.cwdecoder': toggleCwDecoder(); break;
|
||||
case 'tools.net': setNetEnabled((v) => { const nv = !v; if (nv) setActiveTab('net'); return nv; }); break;
|
||||
case 'tools.contest': setContestTabEnabled((v) => { const nv = !v; if (nv) setActiveTab('contest'); else setActiveTab((tb) => (tb === 'contest' ? 'recent' : tb)); return nv; }); break;
|
||||
case 'tools.alerts': setAlertsOpen(true); break;
|
||||
case 'tools.duplicates': setShowDuplicates(true); break;
|
||||
case 'tools.refreshCty': refreshCtyDat(); break;
|
||||
@@ -2470,14 +2475,37 @@ export default function App() {
|
||||
<div className="flex flex-col w-44">
|
||||
<Label className="flex items-center gap-2 h-3.5" style={{ marginBottom: 6 }}>
|
||||
{t('field.callsign')}
|
||||
{lookupBusy && <Badge variant="secondary" className="text-[9px] py-0 px-1.5 normal-case font-medium tracking-wider"><Loader2 className="size-2.5 mr-1 animate-spin" />Looking up…</Badge>}
|
||||
{!lookupBusy && lookupResult && (
|
||||
<Badge className="bg-emerald-100 text-emerald-700 hover:bg-emerald-100 text-[9px] py-0 px-1.5 normal-case font-medium tracking-wider" variant="outline">
|
||||
<CheckCircle2 className="size-2.5 mr-1" />{lookupResult.source}
|
||||
</Badge>
|
||||
{lookupBusy && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-muted/70 px-1.5 py-[1px] text-[9px] font-medium text-muted-foreground ring-1 ring-inset ring-border/60">
|
||||
<Loader2 className="size-2.5 animate-spin" />…
|
||||
</span>
|
||||
)}
|
||||
{!lookupBusy && lookupResult && (() => {
|
||||
const src = (lookupResult.source || '').toLowerCase();
|
||||
const tone = src.includes('cache') ? 'amber' : src.includes('qrz') ? 'emerald' : src.includes('ham') ? 'sky' : 'slate';
|
||||
const ring = { amber: 'bg-amber-500/12 text-amber-600 ring-amber-500/30', emerald: 'bg-emerald-500/12 text-emerald-600 ring-emerald-500/30', sky: 'bg-sky-500/12 text-sky-600 ring-sky-500/30', slate: 'bg-slate-500/12 text-slate-500 ring-slate-500/30' }[tone];
|
||||
const dot = { amber: 'bg-amber-500', emerald: 'bg-emerald-500', sky: 'bg-sky-500', slate: 'bg-slate-400' }[tone];
|
||||
return (
|
||||
<span className={cn('inline-flex items-center gap-1 rounded-full px-1.5 py-[1px] text-[9px] font-semibold uppercase tracking-wide ring-1 ring-inset', ring)}>
|
||||
<span className={cn('size-1.5 rounded-full', dot)} />{lookupResult.source}
|
||||
</span>
|
||||
);
|
||||
})()}
|
||||
{!lookupBusy && !lookupResult && lookupError && (
|
||||
<Badge variant="destructive" className="text-[9px] py-0 px-1.5 normal-case font-medium tracking-wider">{lookupError}</Badge>
|
||||
<span className="inline-flex items-center rounded-full bg-rose-500/12 px-1.5 py-[1px] text-[9px] font-semibold text-rose-600 ring-1 ring-inset ring-rose-500/30">{lookupError}</span>
|
||||
)}
|
||||
{callsign.trim() && (
|
||||
<button
|
||||
type="button"
|
||||
title={t('qrz.openTitle', { call: callsign.trim().toUpperCase() })}
|
||||
onClick={() => {
|
||||
const c = callsign.trim().toUpperCase().split('/').map(encodeURIComponent).join('/');
|
||||
if (c) OpenExternalURL(`https://www.qrz.com/db/${c}`).catch((e) => setError(String(e?.message ?? e)));
|
||||
}}
|
||||
className="ml-auto shrink-0 inline-flex items-center gap-0.5 text-[9px] font-semibold normal-case tracking-wider text-primary hover:underline"
|
||||
>
|
||||
QRZ ↗
|
||||
</button>
|
||||
)}
|
||||
</Label>
|
||||
<div className="relative">
|
||||
@@ -3733,10 +3761,22 @@ export default function App() {
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="awards">{t('tab.awards')}</TabsTrigger>
|
||||
<TabsTrigger value="bandmap">{t('tab.bandmap')}</TabsTrigger>
|
||||
<TabsTrigger value="contest" className={cn('gap-1.5', contest.active && 'text-amber-600 data-[state=active]:text-amber-600')}>
|
||||
{t('tab.contest')}
|
||||
{contest.active && <span className="inline-block size-1.5 rounded-full bg-amber-500 animate-pulse" />}
|
||||
</TabsTrigger>
|
||||
{contestTabEnabled && (
|
||||
<TabsTrigger value="contest" className={cn('gap-1.5', contest.active && 'text-amber-600 data-[state=active]:text-amber-600')}>
|
||||
{t('tab.contest')}
|
||||
{contest.active && <span className="inline-block size-1.5 rounded-full bg-amber-500 animate-pulse" />}
|
||||
<span
|
||||
role="button"
|
||||
aria-label="Close Contest"
|
||||
title={t('dup.close')}
|
||||
className="inline-flex items-center justify-center size-4 rounded hover:bg-foreground/10 text-muted-foreground hover:text-foreground"
|
||||
onPointerDown={(e) => { e.stopPropagation(); }}
|
||||
onClick={(e) => { e.stopPropagation(); setContestTabEnabled(false); setActiveTab((tb) => (tb === 'contest' ? 'recent' : tb)); }}
|
||||
>
|
||||
<X className="size-3" />
|
||||
</span>
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{netEnabled && (
|
||||
<TabsTrigger value="net" className="gap-1.5">
|
||||
{t('tab.net')}
|
||||
@@ -3754,20 +3794,6 @@ export default function App() {
|
||||
)}
|
||||
{catState.backend === 'flex' && <TabsTrigger value="flex">FlexRadio</TabsTrigger>}
|
||||
{catState.backend === 'icom' && <TabsTrigger value="icom">Icom</TabsTrigger>}
|
||||
{/* Not a tab — QRZ blocks embedding, so this opens the call's
|
||||
QRZ.com page in the system browser. Styled like a trigger. */}
|
||||
<button
|
||||
type="button"
|
||||
disabled={!callsign.trim()}
|
||||
title={callsign.trim() ? `Open ${callsign.trim().toUpperCase()} on QRZ.com` : 'Enter a callsign first'}
|
||||
onClick={() => {
|
||||
const c = callsign.trim().toUpperCase().split('/').map(encodeURIComponent).join('/');
|
||||
if (c) OpenExternalURL(`https://www.qrz.com/db/${c}`).catch((e) => setError(String(e?.message ?? e)));
|
||||
}}
|
||||
className="inline-flex items-center justify-center gap-1 whitespace-nowrap px-3 py-1.5 text-xs font-medium text-muted-foreground border-b-2 border-transparent transition-all hover:text-foreground disabled:pointer-events-none disabled:opacity-50 -mb-px"
|
||||
>
|
||||
QRZ.com ↗
|
||||
</button>
|
||||
{qslTabOpen && (
|
||||
<TabsTrigger value="qsl" className="gap-1.5">
|
||||
QSL Manager
|
||||
@@ -4062,9 +4088,11 @@ export default function App() {
|
||||
<AwardsPanel onEditQSO={openEdit} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="contest" className="flex-1 min-h-0 p-0">
|
||||
<ContestPanel session={contest} onChange={updateContest} />
|
||||
</TabsContent>
|
||||
{contestTabEnabled && (
|
||||
<TabsContent value="contest" className="flex-1 min-h-0 p-0">
|
||||
<ContestPanel session={contest} onChange={updateContest} />
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{/* FlexRadio SmartSDR-style control panel — only present when the CAT
|
||||
backend is a FlexRadio. */}
|
||||
|
||||
Reference in New Issue
Block a user