Add entry facets and links

Surface alias/origin facets, SSR facets on entries page,
fix facet query ambiguity, and document clickable links.

Signed-off-by: codex@lucy.xalior.com
This commit is contained in:
2026-01-10 19:21:46 +00:00
parent 964b48abf1
commit 5130a72641
4 changed files with 84 additions and 9 deletions

View File

@@ -25,17 +25,26 @@ type Paged<T> = {
total: number;
};
type EntryFacets = {
genres: { id: number; name: string; count: number }[];
languages: { id: string; name: string; count: number }[];
machinetypes: { id: number; name: string; count: number }[];
flags: { hasAliases: number; hasOrigins: number };
};
export default function EntriesExplorer({
initial,
initialGenres,
initialLanguages,
initialMachines,
initialFacets,
initialUrlState,
}: {
initial?: Paged<Item>;
initialGenres?: { id: number; name: string }[];
initialLanguages?: { id: string; name: string }[];
initialMachines?: { id: number; name: string }[];
initialFacets?: EntryFacets | null;
initialUrlState?: {
q: string;
page: number;
@@ -65,6 +74,7 @@ export default function EntriesExplorer({
);
const [sort, setSort] = useState<"title" | "id_desc">(initialUrlState?.sort ?? "id_desc");
const [scope, setScope] = useState<SearchScope>(initialUrlState?.scope ?? "title");
const [facets, setFacets] = useState<EntryFacets | null>(initialFacets ?? null);
const pageSize = 20;
const totalPages = useMemo(() => (data ? Math.max(1, Math.ceil(data.total / data.pageSize)) : 1), [data]);
@@ -82,7 +92,7 @@ export default function EntriesExplorer({
router.replace(qs ? `${pathname}?${qs}` : pathname);
}
async function fetchData(query: string, p: number) {
async function fetchData(query: string, p: number, withFacets: boolean) {
setLoading(true);
try {
const params = new URLSearchParams();
@@ -94,10 +104,14 @@ export default function EntriesExplorer({
if (machinetypeId !== "") params.set("machinetypeId", String(machinetypeId));
if (sort) params.set("sort", sort);
if (scope !== "title") params.set("scope", scope);
if (withFacets) params.set("facets", "true");
const res = await fetch(`/api/zxdb/search?${params.toString()}`);
if (!res.ok) throw new Error(`Failed: ${res.status}`);
const json: Paged<Item> = await res.json();
const json = await res.json();
setData(json);
if (withFacets && json.facets) {
setFacets(json.facets as EntryFacets);
}
} catch (e) {
console.error(e);
setData({ items: [], page: 1, pageSize, total: 0 });
@@ -133,7 +147,7 @@ export default function EntriesExplorer({
return;
}
updateUrl(page);
fetchData(q, page);
fetchData(q, page, true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [page, genreId, languageId, machinetypeId, sort, scope]);
@@ -159,7 +173,7 @@ export default function EntriesExplorer({
e.preventDefault();
setPage(1);
updateUrl(1);
fetchData(q, 1);
fetchData(q, 1, true);
}
const prevHref = useMemo(() => {
@@ -251,6 +265,32 @@ export default function EntriesExplorer({
)}
</form>
{facets && (
<div className="mt-3">
<div className="d-flex flex-wrap gap-2 align-items-center">
<span className="text-secondary small">Facets</span>
<button
type="button"
className={`btn btn-sm ${scope === "title_aliases" ? "btn-primary" : "btn-outline-secondary"}`}
onClick={() => { setScope("title_aliases"); setPage(1); }}
disabled={facets.flags.hasAliases === 0}
title="Show results that match aliases"
>
Has aliases ({facets.flags.hasAliases})
</button>
<button
type="button"
className={`btn btn-sm ${scope === "title_aliases_origins" ? "btn-primary" : "btn-outline-secondary"}`}
onClick={() => { setScope("title_aliases_origins"); setPage(1); }}
disabled={facets.flags.hasOrigins === 0}
title="Show results that match origins"
>
Has origins ({facets.flags.hasOrigins})
</button>
</div>
</div>
)}
<div className="mt-3">
{data && data.items.length === 0 && !loading && (
<div className="alert alert-warning">No results.</div>