feat: Added partial_matches fallback UI to search results — shows muted…
- "frontend/src/api/public-client.ts" - "frontend/src/pages/SearchResults.tsx" - "frontend/src/App.css" GSD-Task: S01/T03
This commit is contained in:
parent
5a484fb27a
commit
c1cdba14f2
3 changed files with 84 additions and 2 deletions
|
|
@ -772,6 +772,34 @@ a.app-footer__repo:hover {
|
|||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* ── Partial match fallback ─────────────────────────────────────────── */
|
||||
|
||||
.partial-match-banner {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.875rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md, 8px);
|
||||
background: var(--color-surface-raised, var(--color-surface));
|
||||
}
|
||||
|
||||
.partial-match-results__header {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.partial-match-results .search-result-card {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export interface SearchResultItem {
|
|||
|
||||
export interface SearchResponse {
|
||||
items: SearchResultItem[];
|
||||
partial_matches: SearchResultItem[];
|
||||
total: number;
|
||||
query: string;
|
||||
fallback_used: boolean;
|
||||
|
|
|
|||
|
|
@ -22,12 +22,14 @@ export default function SearchResults() {
|
|||
useDocumentTitle(q ? `Search: ${q} — Chrysopedia` : "Search — Chrysopedia");
|
||||
|
||||
const [results, setResults] = useState<SearchResultItem[]>([]);
|
||||
const [partialMatches, setPartialMatches] = useState<SearchResultItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const doSearch = useCallback(async (query: string) => {
|
||||
if (!query.trim()) {
|
||||
setResults([]);
|
||||
setPartialMatches([]);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -36,9 +38,11 @@ export default function SearchResults() {
|
|||
try {
|
||||
const res = await searchApi(query.trim());
|
||||
setResults(res.items);
|
||||
setPartialMatches(res.partial_matches ?? []);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Search failed");
|
||||
setResults([]);
|
||||
setPartialMatches([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -69,8 +73,18 @@ export default function SearchResults() {
|
|||
{loading && <div className="loading">Searching…</div>}
|
||||
{error && <div className="loading error-text">Error: {error}</div>}
|
||||
|
||||
{/* No results */}
|
||||
{!loading && !error && q && results.length === 0 && (
|
||||
{/* No exact results — partial match fallback */}
|
||||
{!loading && !error && q && results.length === 0 && partialMatches.length > 0 && (
|
||||
<>
|
||||
<div className="partial-match-banner">
|
||||
<p>No exact matches for all terms in "{q}"</p>
|
||||
</div>
|
||||
<PartialMatchResults items={partialMatches} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* No results at all */}
|
||||
{!loading && !error && q && results.length === 0 && partialMatches.length === 0 && (
|
||||
<div className="empty-state">
|
||||
<p>No results found for "{q}"</p>
|
||||
</div>
|
||||
|
|
@ -155,3 +169,42 @@ function SearchResultCard({ item, staggerIndex }: { item: SearchResultItem; stag
|
|||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function PartialMatchResults({ items }: { items: SearchResultItem[] }) {
|
||||
const techniqueResults = items.filter((r) => r.type === "technique_page");
|
||||
const momentResults = items.filter((r) => r.type === "key_moment");
|
||||
|
||||
return (
|
||||
<div className="partial-match-results">
|
||||
<h3 className="partial-match-results__header">
|
||||
Results matching some of your terms
|
||||
</h3>
|
||||
|
||||
{techniqueResults.length > 0 && (
|
||||
<section className="search-group">
|
||||
<h4 className="search-group__title">
|
||||
Techniques ({techniqueResults.length})
|
||||
</h4>
|
||||
<div className="search-group__list">
|
||||
{techniqueResults.map((item, i) => (
|
||||
<SearchResultCard key={`partial-tp-${item.slug}`} item={item} staggerIndex={i} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{momentResults.length > 0 && (
|
||||
<section className="search-group">
|
||||
<h4 className="search-group__title">
|
||||
Key Moments ({momentResults.length})
|
||||
</h4>
|
||||
<div className="search-group__list">
|
||||
{momentResults.map((item, i) => (
|
||||
<SearchResultCard key={`partial-km-${item.slug}-${i}`} item={item} staggerIndex={i} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue