feat: status bar added
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Satellite, Loader2 } from 'lucide-react';
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
export interface RecentSpotQSO {
|
||||
callsign: string;
|
||||
freqKHz: number;
|
||||
mode: string;
|
||||
band?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
// Pre-fill values: callsign from the QSO entry (or last logged), the
|
||||
// current TX freq in kHz, and the current mode (goes into the comment).
|
||||
defaultCall: string;
|
||||
defaultFreqKHz: number;
|
||||
defaultMode: string;
|
||||
// Master cluster name, shown so the user knows where the spot goes.
|
||||
targetName?: string;
|
||||
recent: RecentSpotQSO[];
|
||||
onSend: (call: string, freqKHz: number, comment: string) => Promise<void>;
|
||||
}
|
||||
|
||||
// SendSpotModal — Log4OM-style "Send Spot" window. Announces a DX spot on
|
||||
// the master cluster: callsign + frequency (kHz) + a free message (defaults
|
||||
// to the mode). A "Latest QSOs" list lets the operator one-click a recent
|
||||
// contact into the form.
|
||||
export function SendSpotModal({ open, onClose, defaultCall, defaultFreqKHz, defaultMode, targetName, recent, onSend }: Props) {
|
||||
const [call, setCall] = useState('');
|
||||
const [freqKHz, setFreqKHz] = useState('');
|
||||
const [message, setMessage] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [ok, setOk] = useState(false);
|
||||
const callRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// (Re)initialise the form each time the dialog opens.
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setCall((defaultCall || '').toUpperCase());
|
||||
setFreqKHz(defaultFreqKHz > 0 ? trimKHz(defaultFreqKHz) : '');
|
||||
setMessage(defaultMode || '');
|
||||
setError('');
|
||||
setOk(false);
|
||||
// Focus the freq if the call is already known, else the call.
|
||||
setTimeout(() => callRef.current?.focus(), 50);
|
||||
}, [open, defaultCall, defaultFreqKHz, defaultMode]);
|
||||
|
||||
async function send() {
|
||||
const c = call.trim().toUpperCase();
|
||||
const f = parseFloat(freqKHz);
|
||||
if (!c) { setError('Callsign required'); return; }
|
||||
if (!f || f <= 0) { setError('Frequency (kHz) required'); return; }
|
||||
setBusy(true);
|
||||
setError('');
|
||||
try {
|
||||
await onSend(c, f, message.trim());
|
||||
setOk(true);
|
||||
// Brief success flash, then close.
|
||||
setTimeout(() => { setOk(false); onClose(); }, 700);
|
||||
} catch (e: any) {
|
||||
setError(String(e?.message ?? e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
function pick(q: RecentSpotQSO) {
|
||||
setCall(q.callsign.toUpperCase());
|
||||
if (q.freqKHz > 0) setFreqKHz(trimKHz(q.freqKHz));
|
||||
if (q.mode) setMessage(q.mode);
|
||||
setError('');
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(o) => { if (!o) onClose(); }}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Satellite className="size-4 text-primary" /> Send DX Spot
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="px-5 py-3 space-y-3">
|
||||
<div className="flex gap-3">
|
||||
<div className="flex flex-col flex-1">
|
||||
<Label className="mb-1">Callsign</Label>
|
||||
<Input
|
||||
ref={callRef}
|
||||
className="font-mono uppercase font-bold"
|
||||
value={call}
|
||||
onChange={(e) => setCall(e.target.value.toUpperCase())}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); send(); } }}
|
||||
placeholder="DX call"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col w-32">
|
||||
<Label className="mb-1">Frequency (kHz)</Label>
|
||||
<Input
|
||||
className="font-mono"
|
||||
value={freqKHz}
|
||||
onChange={(e) => setFreqKHz(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); send(); } }}
|
||||
placeholder="14205"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<Label className="mb-1">Message</Label>
|
||||
<Input
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); send(); } }}
|
||||
placeholder="e.g. CW · TNX QSO"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{recent.length > 0 && (
|
||||
<div>
|
||||
<Label className="mb-1 block">Latest QSOs</Label>
|
||||
<div className="max-h-40 overflow-y-auto rounded-md border border-border divide-y divide-border/60">
|
||||
{recent.map((q, i) => (
|
||||
<button
|
||||
key={`${q.callsign}-${i}`}
|
||||
type="button"
|
||||
onClick={() => pick(q)}
|
||||
className="flex w-full items-center gap-2 px-2 py-1 text-left text-xs hover:bg-accent/40"
|
||||
>
|
||||
<span className="font-mono font-bold w-24 truncate">{q.callsign}</span>
|
||||
<span className="font-mono text-muted-foreground w-20 text-right">{q.freqKHz > 0 ? trimKHz(q.freqKHz) : '—'}</span>
|
||||
<span className="text-muted-foreground">{q.mode || ''}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && <div className="text-xs text-rose-600">{error}</div>}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<span className="text-[11px] text-muted-foreground mr-auto self-center">
|
||||
{ok ? 'Spot sent ✓' : targetName ? `→ ${targetName}` : 'Master cluster'}
|
||||
</span>
|
||||
<Button variant="outline" onClick={onClose} disabled={busy}>Cancel</Button>
|
||||
<Button onClick={send} disabled={busy}>
|
||||
{busy ? <Loader2 className="size-3.5 animate-spin" /> : <Satellite className="size-3.5" />}
|
||||
{busy ? 'Sending…' : 'Send spot'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
// trimKHz formats a kHz value without a trailing ".0" (14205) but keeps
|
||||
// sub-kHz precision when present (10138.7).
|
||||
function trimKHz(khz: number): string {
|
||||
return String(Math.round(khz * 10) / 10).replace(/\.0$/, '');
|
||||
}
|
||||
Reference in New Issue
Block a user