This commit is contained in:
2026-06-05 22:35:28 +02:00
parent 88623f55df
commit 51d3a734e8
21 changed files with 2613 additions and 153 deletions
@@ -0,0 +1,85 @@
import { useEffect, useRef, useState } from 'react';
import { X, Loader2 } from 'lucide-react';
import { SearchAwardReferences } from '../../wailsjs/go/main/App';
type Ref = { code: string; name: string; dxcc: number; group: string; subgrp: string };
interface Props {
code: string; // award code, e.g. "POTA"
label: string; // display label
dxcc?: number; // contacted-station DXCC; used when "this country" is on
countryOnly: boolean;
value: string; // currently assigned reference
onChange: (ref: string) => void;
}
// AwardRefPicker — type-ahead search over an award's reference list (POTA parks,
// SOTA summits, …), optionally restricted to the contacted DXCC. Picking a
// result assigns it to the QSO.
export function AwardRefPicker({ code, label, dxcc, countryOnly, value, onChange }: Props) {
const [q, setQ] = useState('');
const [results, setResults] = useState<Ref[]>([]);
const [open, setOpen] = useState(false);
const [busy, setBusy] = useState(false);
const boxRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!open) return;
const t = window.setTimeout(async () => {
setBusy(true);
try {
const r = await SearchAwardReferences(code, q, countryOnly ? (dxcc ?? 0) : 0, 30);
setResults((r ?? []) as any);
} catch { setResults([]); }
finally { setBusy(false); }
}, 200);
return () => window.clearTimeout(t);
}, [q, open, code, dxcc, countryOnly]);
useEffect(() => {
if (!open) return;
const close = (e: MouseEvent) => { if (boxRef.current && !boxRef.current.contains(e.target as Node)) setOpen(false); };
window.addEventListener('mousedown', close);
return () => window.removeEventListener('mousedown', close);
}, [open]);
return (
<div className="grid grid-cols-[60px_1fr] items-center gap-2">
<label className="text-xs font-semibold text-muted-foreground">{label}</label>
<div className="relative" ref={boxRef}>
{value ? (
<div className="flex items-center gap-1.5 h-7 px-2 rounded-md border border-emerald-300 bg-emerald-50 text-emerald-800 text-xs">
<span className="font-mono font-semibold">{value}</span>
<button className="ml-auto hover:text-emerald-950" onClick={() => onChange('')} title="Remove"><X className="size-3.5" /></button>
</div>
) : (
<input
className="h-7 w-full rounded-md border border-input bg-background px-2 text-xs focus:outline-none focus:ring-1 focus:ring-ring"
placeholder={`Search ${label}`}
value={q}
onFocus={() => setOpen(true)}
onChange={(e) => { setQ(e.target.value); setOpen(true); }}
/>
)}
{open && !value && (
<div className="absolute z-50 mt-1 w-[320px] max-h-64 overflow-auto rounded-md border border-border bg-popover shadow-lg text-xs">
{busy && <div className="px-3 py-2 text-muted-foreground flex items-center gap-2"><Loader2 className="size-3 animate-spin" /> Searching</div>}
{!busy && results.length === 0 && (
<div className="px-3 py-2 text-muted-foreground">No match{countryOnly && dxcc ? ' for this DXCC' : ''}.</div>
)}
{results.map((r) => (
<button
key={r.code}
className="flex w-full items-baseline gap-2 px-3 py-1.5 text-left hover:bg-accent/50"
onClick={() => { onChange(r.code); setOpen(false); setQ(''); }}
>
<span className="font-mono font-semibold shrink-0">{r.code}</span>
<span className="text-muted-foreground truncate">{r.name}</span>
</button>
))}
</div>
)}
</div>
</div>
);
}