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>

View File

@@ -1,5 +1,5 @@
import EntriesExplorer from "./EntriesExplorer";
import { listGenres, listLanguages, listMachinetypes, searchEntries } from "@/server/repo/zxdb";
import { getEntryFacets, listGenres, listLanguages, listMachinetypes, searchEntries } from "@/server/repo/zxdb";
export const metadata = {
title: "ZXDB Entries",
@@ -20,7 +20,7 @@ export default async function Page({ searchParams }: { searchParams: Promise<{ [
| "title_aliases"
| "title_aliases_origins";
const [initial, genres, langs, machines] = await Promise.all([
const [initial, genres, langs, machines, facets] = await Promise.all([
searchEntries({
page,
pageSize: 20,
@@ -34,6 +34,14 @@ export default async function Page({ searchParams }: { searchParams: Promise<{ [
listGenres(),
listLanguages(),
listMachinetypes(),
getEntryFacets({
q,
sort,
scope,
genreId: genreId ? Number(genreId) : undefined,
languageId: languageId || undefined,
machinetypeId: machinetypeId ? Number(machinetypeId) : undefined,
}),
]);
return (
@@ -42,6 +50,7 @@ export default async function Page({ searchParams }: { searchParams: Promise<{ [
initialGenres={genres}
initialLanguages={langs}
initialMachines={machines}
initialFacets={facets}
initialUrlState={{ q, page, genreId, languageId, machinetypeId, sort, scope }}
/>
);