/** * Full search results page. * * Reads `q` from URL search params, calls searchApi, groups results by type * (technique_pages first, then key_moments). Shows fallback banner when * keyword search was used. */ import { useCallback, useEffect, useState } from "react"; import { Link, useSearchParams, useNavigate } from "react-router-dom"; import { searchApi, type SearchResultItem } from "../api"; import { catSlug } from "../utils/catSlug"; import SearchAutocomplete from "../components/SearchAutocomplete"; import SortDropdown from "../components/SortDropdown"; import TagList from "../components/TagList"; import { useDocumentTitle } from "../hooks/useDocumentTitle"; import { useSortPreference } from "../hooks/useSortPreference"; const SEARCH_SORT_OPTIONS = [ { value: "relevance", label: "Relevance" }, { value: "newest", label: "Newest" }, { value: "oldest", label: "Oldest" }, { value: "alpha", label: "A–Z" }, { value: "creator", label: "Creator" }, ]; export default function SearchResults() { const [searchParams] = useSearchParams(); const navigate = useNavigate(); const q = searchParams.get("q") ?? ""; const [sort, setSort] = useSortPreference("relevance"); useDocumentTitle(q ? `Search: ${q} — Chrysopedia` : "Search — Chrysopedia"); const [results, setResults] = useState([]); const [partialMatches, setPartialMatches] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const doSearch = useCallback(async (query: string, sortBy: string) => { if (!query.trim()) { setResults([]); setPartialMatches([]); return; } setLoading(true); setError(null); try { const res = await searchApi(query.trim(), undefined, undefined, sortBy); setResults(res.items); setPartialMatches(res.partial_matches ?? []); } catch (err) { setError(err instanceof Error ? err.message : "Search failed"); setResults([]); setPartialMatches([]); } finally { setLoading(false); } }, []); // Search when URL param or sort changes useEffect(() => { if (q) void doSearch(q, sort); }, [q, sort, doSearch]); // Group results by type const techniqueResults = results.filter((r) => r.type === "technique_page"); const sectionResults = results.filter((r) => r.type === "technique_section"); const momentResults = results.filter((r) => r.type === "key_moment"); return (

Search Results

{/* Inline search bar */} navigate(`/search?q=${encodeURIComponent(newQ)}`, { replace: true }) } /> {/* Sort control */} {q && ( )} {/* Status */} {loading &&
Searching…
} {error &&
Error: {error}
} {/* No exact results — partial match fallback */} {!loading && !error && q && results.length === 0 && partialMatches.length > 0 && ( <>

No exact matches for all terms in "{q}"

)} {/* No results at all */} {!loading && !error && q && results.length === 0 && partialMatches.length === 0 && (

No results found for "{q}"

)} {/* Technique pages */} {techniqueResults.length > 0 && (

Techniques ({techniqueResults.length})

{techniqueResults.map((item, i) => ( ))}
)} {/* Technique sections */} {sectionResults.length > 0 && (

Sections ({sectionResults.length})

{sectionResults.map((item, i) => ( ))}
)} {/* Key moments */} {momentResults.length > 0 && (

Key Moments ({momentResults.length})

{momentResults.map((item, i) => ( ))}
)}
); } function getSearchResultLink(item: SearchResultItem): string { if (item.type === "technique_section") { if (item.technique_page_slug && item.section_anchor) { return `/techniques/${item.technique_page_slug}#${item.section_anchor}`; } if (item.technique_page_slug) { return `/techniques/${item.technique_page_slug}`; } return `/search?q=${encodeURIComponent(item.title)}`; } if (item.type === "key_moment") { if (item.technique_page_slug) { return `/techniques/${item.technique_page_slug}#km-${item.slug || item.title}`; } // Graceful fallback — re-search instead of 404 return `/search?q=${encodeURIComponent(item.title)}`; } return `/techniques/${item.slug}`; } function SearchResultCard({ item, staggerIndex }: { item: SearchResultItem; staggerIndex: number }) { const typeLabels: Record = { technique_page: "Technique", key_moment: "Key Moment", technique_section: "Section", }; return (
{item.title} {typeLabels[item.type] ?? item.type}
{item.type === "technique_section" && item.section_heading && (
§ {item.section_heading}
)} {item.match_context && (
{item.match_context}
)} {item.summary && (

{item.summary.length > 200 ? `${item.summary.slice(0, 200)}…` : item.summary}

)}
{item.creator_name && {item.creator_name}} {item.topic_category && ( <> · {item.topic_category} )} {item.topic_tags.length > 0 && ( )}
); } function PartialMatchResults({ items }: { items: SearchResultItem[] }) { const techniqueResults = items.filter((r) => r.type === "technique_page"); const sectionResults = items.filter((r) => r.type === "technique_section"); const momentResults = items.filter((r) => r.type === "key_moment"); return (

Results matching some of your terms

{techniqueResults.length > 0 && (

Techniques ({techniqueResults.length})

{techniqueResults.map((item, i) => ( ))}
)} {sectionResults.length > 0 && (

Sections ({sectionResults.length})

{sectionResults.map((item, i) => ( ))}
)} {momentResults.length > 0 && (

Key Moments ({momentResults.length})

{momentResults.map((item, i) => ( ))}
)}
); }