diff --git a/src/app/components/SearchAssembly64.tsx b/src/app/components/SearchAssembly64.tsx index daa7140..d0c564b 100644 --- a/src/app/components/SearchAssembly64.tsx +++ b/src/app/components/SearchAssembly64.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useRef, useState } from 'react'; -import { Search, Loader2, HardDrive, Download, ChevronRight, Trophy, Calendar, Users, RefreshCw, HelpCircle } from 'lucide-react'; +import { Search, Loader2, HardDrive, Download, ChevronRight, ChevronDown, Trophy, Calendar, Users, RefreshCw, HelpCircle } from 'lucide-react'; import { toast } from 'sonner'; import { humanFileSize, basename, joinPath, putFileContents } from '../webdav'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog'; @@ -61,14 +61,34 @@ interface CategoryMapping { [k: string]: unknown; } -interface Preset { +interface PresetValue { + aqlKey: string; name?: string; - title?: string; - query?: string; - aql?: string; + id?: number; [k: string]: unknown; } +interface PresetGroup { + type: string; // 'repo' | 'category' | 'subcat' | 'rating' | 'type' | 'date' | 'latest' | 'sort' | 'order' + description: string; // Human-readable group label shown on the chip + values: PresetValue[]; +} + +// Each preset group maps to a search-token prefix. 'sort' and 'order' are not +// filters (they're result-ordering directives) so they get an empty prefix +// marker and the renderer skips them. +const PRESET_PREFIX: Record = { + repo: 'repo', + category: 'category', + subcat: 'subcat', + rating: 'rating', + type: 'type', + date: 'year', + latest: 'added', + sort: null, + order: null, +}; + // ─── Props ──────────────────────────────────────────────────────────────────── interface SearchAssembly64Props { @@ -143,7 +163,8 @@ export default function SearchAssembly64({ config, setConfig, onClose: _onClose const [categoryFilter, setCategoryFilter] = useState(() => _store.categoryFilter); const [categories, setCategories] = useState([]); - const [presets, setPresets] = useState([]); + const [presets, setPresets] = useState([]); + const [activePreset, setActivePreset] = useState(null); const [selectedItem, setSelectedItem] = useState(null); const [entries, setEntries] = useState(null); @@ -204,10 +225,21 @@ export default function SearchAssembly64({ config, setConfig, onClose: _onClose }; const handleSearch = () => doSearch(query, categoryFilter, 0); - const handleLoadMore = () => doSearch(query, categoryFilter, offset, true); - const applyPreset = (p: Preset) => { - const q = (p.query ?? p.aql ?? '') as string; - setQuery(q); + + // Build an AQL token for a preset value and append/replace it in the query. + // Tokens that already contain a colon (e.g. 'subcat:c64comdemos') are + // inserted verbatim; raw values get the group's prefix. + const applyPreset = (group: PresetGroup, value: PresetValue) => { + const prefix = PRESET_PREFIX[group.type]; + setActivePreset(null); + if (prefix === null || prefix === undefined) return; // sort/order: not a filter + // Some aqlKeys are self-describing ("subcat:c64comdemos"); use them + // verbatim. Otherwise prepend the group's prefix. + const token = value.aqlKey.includes(':') ? value.aqlKey : `${prefix}:${value.aqlKey}`; + const trimmed = query.trim(); + const next = trimmed ? `${trimmed} ${token}` : token; + setQuery(next); + doSearch(next); doSearch(q, categoryFilter, 0); }; @@ -218,8 +250,13 @@ export default function SearchAssembly64({ config, setConfig, onClose: _onClose setEntries(null); setLoadingEntries(true); try { - const data = await leetJson(`/search/entries/${item.id}/${item.category}`); - setEntries(data); + // The /search/entries endpoint now returns { contentEntry: [...] } + // (matching the API's ContentEntryContainerV2 schema), not a bare array. + const data = await leetJson( + `/search/entries/${item.id}/${item.category}`, + ); + const list = Array.isArray(data) ? data : (data.contentEntry ?? []); + setEntries(list); } catch (e: any) { toast.error(`Failed to load entries: ${e?.message ?? e}`); setEntries([]); @@ -316,18 +353,18 @@ export default function SearchAssembly64({ config, setConfig, onClose: _onClose {/* Presets */} {presets.length > 0 && (
- {presets.map((p, i) => { - const label = (p.name ?? p.title ?? `Preset ${i + 1}`) as string; - return ( + {presets + .filter(group => PRESET_PREFIX[group.type] !== null) // hide sort/order for now + .map((group, i) => ( - ); - })} + ))}
)} @@ -618,6 +655,33 @@ export default function SearchAssembly64({ config, setConfig, onClose: _onClose + + {/* Preset values dialog */} + !open && setActivePreset(null)}> + + + {activePreset?.description ?? 'Preset'} + Tap a value to add it to your search. + +
+ {activePreset?.values.map((v, i) => { + const label = v.name ?? v.aqlKey; + const prefix = PRESET_PREFIX[activePreset.type]; + const token = v.aqlKey.includes(':') ? v.aqlKey : `${prefix}:${v.aqlKey}`; + return ( + + ); + })} +
+
+
); }