feat: Added worked before in Net Control

This commit is contained in:
2026-07-07 22:39:17 +02:00
parent b9b005ea36
commit 86cbe94cae
8 changed files with 208 additions and 46 deletions
+45 -28
View File
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
AlertCircle, Antenna, CheckCircle2, Clock, Compass, Database, Ear, Eraser, Hash, Loader2, Lock,
AlertCircle, Antenna, Bell, CheckCircle2, Clock, Compass, Database, Ear, Eraser, Hash, Loader2, Lock,
Maximize2, Minimize2, Mic, MessageSquare, Pencil, RadioTower, RefreshCw, Satellite, Send, Settings, SlidersHorizontal, Square, Trash2, Unlock, X, Zap,
} from 'lucide-react';
@@ -798,6 +798,7 @@ export default function App() {
// ~10 min and is clickable to tune the rig to its freq/mode.
type RecentAlert = { id: number; rule: string; call: string; band: string; mode: string; freq_hz: number; country: string; comment: string; at: number };
const [recentAlerts, setRecentAlerts] = useState<RecentAlert[]>([]);
const [alertsPanelOpen, setAlertsPanelOpen] = useState(false); // LED dropdown
const ALERT_TTL_MS = 10 * 60 * 1000;
useEffect(() => {
const id = window.setInterval(() => {
@@ -2722,6 +2723,48 @@ export default function App() {
<Input value={grid} placeholder="JN05" className="font-mono" onChange={(e) => { setGrid(e.target.value); markEdited('grid'); }} />
</div>
);
// A discreet spot-alert LED (bell) that fills the gap at the right of the Grid
// row instead of the intrusive floating cards. It lights + shows a count when
// alerts are pending; click it to see the last 3 (each clickable to tune).
const hasAlerts = recentAlerts.length > 0;
const alertLedBlock = (
<div className="relative self-end mb-0.5 shrink-0">
<button type="button" onClick={() => setAlertsPanelOpen((o) => !o)}
title={hasAlerts ? t('alert.pending', { n: recentAlerts.length }) : t('alert.noneShort')}
className={cn('relative inline-flex size-8 items-center justify-center rounded-full border transition-colors',
hasAlerts ? 'border-warning bg-warning/15 text-warning' : 'border-border bg-card text-muted-foreground hover:bg-muted')}>
<Bell className="size-4" />
{hasAlerts && <span className="absolute -right-1 -top-1 inline-flex size-2.5 rounded-full bg-warning animate-ping" />}
{hasAlerts && <span className="absolute -right-1 -top-1 inline-flex size-2.5 rounded-full bg-warning" />}
</button>
{alertsPanelOpen && (
<>
<div className="fixed inset-0 z-40" onClick={() => setAlertsPanelOpen(false)} />
<div className="absolute right-0 top-9 z-50 w-72 rounded-lg border border-border bg-card shadow-xl overflow-hidden">
<div className="flex items-center justify-between px-3 py-1.5 border-b border-border/60 bg-muted/30">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted-foreground">{t('alert.recent')}</span>
{hasAlerts && <button type="button" className="text-[11px] text-muted-foreground hover:text-foreground" onClick={() => setRecentAlerts([])}>{t('alert.clear')}</button>}
</div>
{!hasAlerts ? (
<div className="px-3 py-4 text-center text-xs text-muted-foreground">{t('alert.noneShort')}</div>
) : recentAlerts.map((a) => (
<button key={a.id} type="button" title={t('alert.tuneHint')}
onClick={() => { handleSpotClick({ comment: a.comment, freq_hz: a.freq_hz, band: a.band, dx_call: a.call }); setAlertsPanelOpen(false); }}
className="block w-full text-left px-3 py-2 border-b border-border/30 last:border-0 hover:bg-warning/10 transition-colors">
<div className="flex items-center gap-1.5">
<span className="font-bold text-sm truncate">{a.call}</span>
{a.freq_hz > 0 && <span className="ml-auto shrink-0 text-[11px] font-mono tabular-nums text-warning">{(a.freq_hz / 1e6).toFixed(3)}</span>}
</div>
<div className="text-[11px] text-muted-foreground truncate">
{a.rule}{a.band ? ` · ${a.band}` : ''}{a.mode ? ` · ${a.mode}` : ''}{a.country ? ` · ${a.country}` : ''}
</div>
</button>
))}
</div>
</>
)}
</div>
);
// Compact-strip Country (stacked label) + a narrow Comment.
const countryBlockSm = (
<div className="flex flex-col w-36">
@@ -3613,6 +3656,7 @@ export default function App() {
</div>
{qthBlock}
{gridBlock}
{alertLedBlock}
</div>
{/* Row 3: tight left detail column (Band/Mode/Country) and
@@ -4391,33 +4435,6 @@ export default function App() {
<AlertsModal onClose={() => setAlertsOpen(false)} bands={bands} modes={modes} countries={countries} />
)}
{/* Persistent spot-alert tray (top-right). Each card stays ~10 min; click it
to tune the rig to its freq/mode + fill the callsign; × dismisses it. */}
{recentAlerts.length > 0 && (
<div className="fixed top-16 right-3 z-[60] flex flex-col gap-2 w-72 max-w-[85vw]">
{recentAlerts.map((a) => (
<div key={a.id} className="flex items-stretch rounded-lg border border-warning/50 bg-card shadow-lg overflow-hidden">
<button type="button"
title={t('alert.tuneHint')}
onClick={() => handleSpotClick({ comment: a.comment, freq_hz: a.freq_hz, band: a.band, dx_call: a.call })}
className="flex-1 min-w-0 text-left px-3 py-2 hover:bg-warning/10 transition-colors">
<div className="flex items-center gap-1.5">
<span>🔔</span>
<span className="font-bold text-sm truncate">{a.call}</span>
{a.freq_hz > 0 && <span className="ml-auto shrink-0 text-[11px] font-mono tabular-nums text-warning">{(a.freq_hz / 1e6).toFixed(3)}</span>}
</div>
<div className="text-[11px] text-muted-foreground truncate">
{a.rule}{a.band ? ` · ${a.band}` : ''}{a.mode ? ` · ${a.mode}` : ''}{a.country ? ` · ${a.country}` : ''}
</div>
</button>
<button type="button" title={t('alert.dismiss')}
onClick={() => setRecentAlerts((prev) => prev.filter((x) => x.id !== a.id))}
className="shrink-0 px-2 text-muted-foreground hover:text-foreground hover:bg-muted transition-colors">×</button>
</div>
))}
</div>
)}
{showDuplicates && (
<DuplicatesModal onClose={() => setShowDuplicates(false)} onDeleted={() => refresh()} />
)}