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; } // 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(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 ( { if (!o) onClose(); }}> Send DX Spot
setCall(e.target.value.toUpperCase())} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); send(); } }} placeholder="DX call" />
setFreqKHz(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); send(); } }} placeholder="14205" />
setMessage(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); send(); } }} placeholder="e.g. CW · TNX QSO" />
{recent.length > 0 && (
{recent.map((q, i) => ( ))}
)} {error &&
{error}
}
{ok ? 'Spot sent ✓' : targetName ? `→ ${targetName}` : 'Master cluster'}
); } // 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$/, ''); }