feat(SearchCommoServe): add CORS handling and loading state for search errors

This commit is contained in:
Jaime Idolpx 2026-06-15 01:16:18 -04:00
parent c3af4406bf
commit 8957254471
2 changed files with 22 additions and 7 deletions

View File

@ -231,6 +231,7 @@ export default function SearchAssembly64({ config, setConfig, onClose }: SearchA
}; };
const handleSearch = () => doSearch(query, categoryFilter, 0); const handleSearch = () => doSearch(query, categoryFilter, 0);
const handleLoadMore = () => doSearch(query, categoryFilter, offset, true);
// Build an AQL token for a preset value and append/replace it in the query. // 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 // Tokens that already contain a colon (e.g. 'subcat:c64comdemos') are

View File

@ -158,6 +158,7 @@ export default function SearchCommoServe({ config, setConfig, onClose }: SearchC
const [isSearching, setIsSearching] = useState(false); const [isSearching, setIsSearching] = useState(false);
const [isLoadingMore, setIsLoadingMore] = useState(false); const [isLoadingMore, setIsLoadingMore] = useState(false);
const [searchError, setSearchError] = useState<string | null>(null); const [searchError, setSearchError] = useState<string | null>(null);
const [corsBlocked, setCorsBlocked] = useState(false);
const [categoryFilter, setCategoryFilter] = useState<number | null>(() => _store.categoryFilter); const [categoryFilter, setCategoryFilter] = useState<number | null>(() => _store.categoryFilter);
const [categories, setCategories] = useState<CategoryMapping[]>([]); const [categories, setCategories] = useState<CategoryMapping[]>([]);
@ -186,8 +187,9 @@ export default function SearchCommoServe({ config, setConfig, onClose }: SearchC
}, []); }, []);
useEffect(() => { useEffect(() => {
leetJson<CategoryMapping[]>('/search/categories').then(setCategories).catch(() => {}); const isCors = (e: any) => /failed to fetch|networkerror/i.test(e?.message ?? '');
leetJson<PresetGroup[]>('/search/aql/presets').then(setPresets).catch(() => {}); leetJson<CategoryMapping[]>('/search/categories').then(setCategories).catch(e => { if (isCors(e)) setCorsBlocked(true); });
leetJson<PresetGroup[]>('/search/aql/presets').then(setPresets).catch(e => { if (isCors(e)) setCorsBlocked(true); });
}, []); }, []);
const categoryName = useMemo(() => { const categoryName = useMemo(() => {
@ -215,7 +217,8 @@ export default function SearchCommoServe({ config, setConfig, onClose }: SearchC
setHasMore(data.length === PAGE_SIZE); setHasMore(data.length === PAGE_SIZE);
setHasSearched(true); setHasSearched(true);
} catch (e: any) { } catch (e: any) {
setSearchError(e?.message ?? 'Search failed'); if (/failed to fetch|networkerror/i.test(e?.message ?? '')) setCorsBlocked(true);
else setSearchError(e?.message ?? 'Search failed');
} finally { } finally {
setIsSearching(false); setIsSearching(false);
setIsLoadingMore(false); setIsLoadingMore(false);
@ -397,14 +400,25 @@ export default function SearchCommoServe({ config, setConfig, onClose }: SearchC
className="flex-1 overflow-y-auto" className="flex-1 overflow-y-auto"
onScroll={e => { _store.scrollTop = (e.currentTarget as HTMLDivElement).scrollTop; }} onScroll={e => { _store.scrollTop = (e.currentTarget as HTMLDivElement).scrollTop; }}
> >
{isSearching && !hasSearched && ( {corsBlocked && (
<div className="flex flex-col items-center justify-center py-16 px-8 gap-3 text-center">
<span className="text-3xl">🚫</span>
<p className="text-sm font-medium text-neutral-700">CommoServe is not accessible</p>
<p className="text-xs text-neutral-400 leading-relaxed">
The CommoServe server does not allow requests from this browser origin (CORS policy).
This service may only be reachable directly from your Meatloaf device.
</p>
</div>
)}
{!corsBlocked && isSearching && !hasSearched && (
<div className="flex flex-col items-center justify-center py-16 gap-3"> <div className="flex flex-col items-center justify-center py-16 gap-3">
<Loader2 className="w-8 h-8 text-blue-500 animate-spin" /> <Loader2 className="w-8 h-8 text-blue-500 animate-spin" />
<p className="text-sm text-neutral-500">Searching</p> <p className="text-sm text-neutral-500">Searching</p>
</div> </div>
)} )}
{searchError && ( {!corsBlocked && searchError && (
<div className="p-6 text-center"> <div className="p-6 text-center">
<p className="text-sm text-red-500">{searchError}</p> <p className="text-sm text-red-500">{searchError}</p>
<button onClick={handleSearch} className="mt-2 inline-flex items-center gap-1 text-xs text-blue-600"> <button onClick={handleSearch} className="mt-2 inline-flex items-center gap-1 text-xs text-blue-600">
@ -413,7 +427,7 @@ export default function SearchCommoServe({ config, setConfig, onClose }: SearchC
</div> </div>
)} )}
{!searchError && hasSearched && ( {!corsBlocked && !searchError && hasSearched && (
<> <>
{results.length > 0 ? ( {results.length > 0 ? (
<> <>
@ -487,7 +501,7 @@ export default function SearchCommoServe({ config, setConfig, onClose }: SearchC
</> </>
)} )}
{!hasSearched && !isSearching && ( {!corsBlocked && !hasSearched && !isSearching && (
<div className="py-16 text-center px-6"> <div className="py-16 text-center px-6">
<Search className="w-10 h-10 mx-auto mb-3 text-neutral-300" /> <Search className="w-10 h-10 mx-auto mb-3 text-neutral-300" />
<p className="text-sm font-medium text-neutral-600 mb-1">Search the CommoServe database</p> <p className="text-sm font-medium text-neutral-600 mb-1">Search the CommoServe database</p>