This commit is contained in:
2026-06-06 11:59:32 +02:00
parent 176cc0e62b
commit f91f9ff3b8
13 changed files with 866 additions and 90 deletions
+42 -7
View File
@@ -13,6 +13,9 @@ type Meta = { code: string; count: number; can_update: boolean };
// are computed, never manually picked, so they don't belong in this picker.
const COMPUTED_FIELDS = new Set(['dxcc', 'cqz', 'ituz', 'prefix', 'callsign', 'state', 'cont', 'country', 'grid']);
// If DXCC-filtered auto-results exceed this, require the user to type instead.
const AUTO_SHOW_MAX = 100;
interface Props {
dxcc?: number;
// Semicolon-delimited "AWARD@REF" entries, e.g. "POTA@FR-11553;IOTA@EU-064"
@@ -26,7 +29,11 @@ export function AwardRefSelector({ dxcc, value, onChange }: Props) {
const [awardCode, setAwardCode] = useState('POTA');
const [q, setQ] = useState('');
const [results, setResults] = useState<AwardRef[]>([]);
// autoResults: loaded immediately when award/dxcc changes (empty query, DXCC-filtered).
// Shown when q is short and count ≤ AUTO_SHOW_MAX (e.g. 5 IOTA refs for France).
const [autoResults, setAutoResults] = useState<AwardRef[]>([]);
// searchResults: loaded when user types 2+ chars.
const [searchResults, setSearchResults] = useState<AwardRef[]>([]);
const [busy, setBusy] = useState(false);
const [selectedRef, setSelectedRef] = useState<AwardRef | null>(null);
const [selectedEntry, setSelectedEntry] = useState<string | null>(null);
@@ -65,20 +72,34 @@ export function AwardRefSelector({ dxcc, value, onChange }: Props) {
if (!awards.find((a) => a.code === awardCode)) setAwardCode(awards[0].code);
}, [awards, awardCode]);
// Auto-load DXCC-filtered refs on award/dxcc change with empty query.
// Fetches AUTO_SHOW_MAX+1 so we can distinguish "all results shown" from "too many".
useEffect(() => {
if (q.length < 2) { setResults([]); return; }
setAutoResults([]);
if (!dxcc) return;
SearchAwardReferences(awardCode, '', dxcc, AUTO_SHOW_MAX + 1)
.then((r) => setAutoResults((r ?? []) as any))
.catch(() => {});
}, [awardCode, dxcc]);
// Typed search (2+ chars).
useEffect(() => {
if (q.length < 2) { setSearchResults([]); return; }
const t = window.setTimeout(async () => {
setBusy(true);
try {
// References are always scoped to the contacted DXCC entity.
const r = await SearchAwardReferences(awardCode, q, dxcc ?? 0, 50);
setResults((r ?? []) as any);
} catch { setResults([]); }
setSearchResults((r ?? []) as any);
} catch { setSearchResults([]); }
finally { setBusy(false); }
}, 200);
return () => window.clearTimeout(t);
}, [awardCode, q, dxcc]);
const tooManyAuto = autoResults.length > AUTO_SHOW_MAX;
// When q is empty/short: show auto-results (if ≤ AUTO_SHOW_MAX); when typing: show search results.
const results: AwardRef[] = q.length >= 2 ? searchResults : (tooManyAuto ? [] : autoResults);
function addRef(ref: AwardRef) {
const entry = `${awardCode}@${ref.code}`;
if (!entries.includes(entry)) {
@@ -99,7 +120,7 @@ export function AwardRefSelector({ dxcc, value, onChange }: Props) {
<Select
value={awardCode}
onValueChange={(v) => { setAwardCode(v); setSelectedRef(null); setQ(''); setResults([]); }}
onValueChange={(v) => { setAwardCode(v); setSelectedRef(null); setQ(''); setSearchResults([]); }}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue />
@@ -191,11 +212,25 @@ export function AwardRefSelector({ dxcc, value, onChange }: Props) {
<Loader2 className="size-3 animate-spin" />Searching
</div>
)}
{!busy && q.length < 2 && (
{/* No callsign yet */}
{!busy && !dxcc && q.length < 2 && (
<div className="px-2 py-1.5 text-[11px] text-muted-foreground leading-snug">
Enter a callsign, or type to search.
</div>
)}
{/* DXCC known but too many auto-results → require typed search */}
{!busy && !!dxcc && q.length < 2 && tooManyAuto && (
<div className="px-2 py-1.5 text-[11px] text-muted-foreground leading-snug">
Type 2+ chars to search
</div>
)}
{/* DXCC known, auto-results loaded, none found */}
{!busy && !!dxcc && q.length < 2 && !tooManyAuto && autoResults.length === 0 && (
<div className="px-2 py-1.5 text-[11px] text-muted-foreground leading-snug">
No references for this entity.
</div>
)}
{/* Typed search, no results */}
{!busy && q.length >= 2 && results.length === 0 && (
<div className="px-2 py-2 text-[11px] text-muted-foreground leading-snug">
No results.