From 79d161afe1e8016cc0213420e037acc165a31116 Mon Sep 17 00:00:00 2001 From: "D. Rimron-Soutter" Date: Sun, 11 Jan 2026 13:21:19 +0000 Subject: [PATCH] Share explorer sidebar components Introduce reusable explorer layout, sidebar, chips, and multi-select components. Signed-off-by: codex@lucy.xalior.com --- src/app/zxdb/entries/EntriesExplorer.tsx | 267 +++++++++---------- src/app/zxdb/releases/ReleasesExplorer.tsx | 81 +++--- src/components/explorer/ExplorerLayout.tsx | 39 +++ src/components/explorer/FilterChips.tsx | 20 ++ src/components/explorer/FilterSidebar.tsx | 13 + src/components/explorer/MultiSelectChips.tsx | 37 +++ 6 files changed, 262 insertions(+), 195 deletions(-) create mode 100644 src/components/explorer/ExplorerLayout.tsx create mode 100644 src/components/explorer/FilterChips.tsx create mode 100644 src/components/explorer/FilterSidebar.tsx create mode 100644 src/components/explorer/MultiSelectChips.tsx diff --git a/src/app/zxdb/entries/EntriesExplorer.tsx b/src/app/zxdb/entries/EntriesExplorer.tsx index a3fb34a..a651650 100644 --- a/src/app/zxdb/entries/EntriesExplorer.tsx +++ b/src/app/zxdb/entries/EntriesExplorer.tsx @@ -5,6 +5,9 @@ import Link from "next/link"; import EntryLink from "../components/EntryLink"; import { usePathname, useRouter } from "next/navigation"; import ZxdbBreadcrumbs from "@/app/zxdb/components/ZxdbBreadcrumbs"; +import ExplorerLayout from "@/components/explorer/ExplorerLayout"; +import FilterSidebar from "@/components/explorer/FilterSidebar"; +import MultiSelectChips from "@/components/explorer/MultiSelectChips"; type Item = { id: number; @@ -96,6 +99,7 @@ export default function EntriesExplorer({ const rest = machines.filter((m) => !seen.has(m.id)); return [...preferred, ...rest]; }, [machines]); + const machineOptions = useMemo(() => orderedMachines.map((m) => ({ id: m.id, label: m.name })), [orderedMachines]); const pageSize = 20; const totalPages = useMemo(() => (data ? Math.max(1, Math.ceil(data.total / data.pageSize)) : 1), [data]); @@ -258,146 +262,118 @@ export default function EntriesExplorer({ ]} /> -
-
-

Entries

-
- {data ? `${data.total.toLocaleString()} results` : "Loading results..."} -
-
- {activeFilters.length > 0 && ( -
- {activeFilters.map((chip) => ( - {chip} - ))} - -
- )} -
- -
-
-
-
-
+ + +
+ + setQ(e.target.value)} + /> +
+
+ +
+
+ + +
+
+ + +
+
+ + { + setMachinetypeIds((current) => { + const next = new Set(current); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + const order = machineOptions.map((item) => item.id); + return order.filter((value) => next.has(value)); + }); + setPage(1); + }} + /> +
Preferred: {preferredMachineNames.join(", ")}
+
+
+ + +
+
+ + +
+ {facets && (
- - setQ(e.target.value)} - /> -
-
- -
-
- - -
-
- - -
-
- +
Facets
- {orderedMachines.map((m) => { - const active = machinetypeIds.includes(m.id); - return ( - - ); - })} + +
-
Default: {preferredMachineNames.join(", ")}
-
- - -
-
- - -
- {facets && ( -
-
Facets
-
- - -
-
- )} - {loading &&
Loading...
} - -
-
-
- -
- {data && data.items.length === 0 && !loading && ( -
No results.
- )} - {data && data.items.length > 0 && ( -
- - + )} + {loading &&
Loading...
} + + + )} + > + {data && data.items.length === 0 && !loading && ( +
No results.
+ )} + {data && data.items.length > 0 && ( +
+
+ @@ -426,13 +402,13 @@ export default function EntriesExplorer({ {it.machinetypeId != null ? ( it.machinetypeName ? ( {it.machinetypeName} - ) : ( - {it.machinetypeId} - ) ) : ( - - - )} - + {it.machinetypeId} + ) + ) : ( + - + )} +
ID Title {it.languageId ? ( it.languageName ? ( @@ -448,10 +424,9 @@ export default function EntriesExplorer({ ))}
-
- )} -
-
+ + )} +
Page {data?.page ?? 1} / {totalPages} diff --git a/src/app/zxdb/releases/ReleasesExplorer.tsx b/src/app/zxdb/releases/ReleasesExplorer.tsx index 4d35b33..a7981aa 100644 --- a/src/app/zxdb/releases/ReleasesExplorer.tsx +++ b/src/app/zxdb/releases/ReleasesExplorer.tsx @@ -5,6 +5,9 @@ import Link from "next/link"; import EntryLink from "../components/EntryLink"; import { usePathname, useRouter } from "next/navigation"; import ZxdbBreadcrumbs from "@/app/zxdb/components/ZxdbBreadcrumbs"; +import ExplorerLayout from "@/components/explorer/ExplorerLayout"; +import FilterSidebar from "@/components/explorer/FilterSidebar"; +import MultiSelectChips from "@/components/explorer/MultiSelectChips"; type Item = { entryId: number; @@ -98,6 +101,7 @@ export default function ReleasesExplorer({ const rest = machines.filter((m) => !seen.has(m.id)); return [...preferred, ...rest]; }, [machines]); + const machineOptions = useMemo(() => orderedMachines.map((m) => ({ id: m.id, label: m.name })), [orderedMachines]); const pageSize = 20; const totalPages = useMemo(() => (data ? Math.max(1, Math.ceil(data.total / data.pageSize)) : 1), [data]); @@ -258,20 +262,12 @@ export default function ReleasesExplorer({ ]} /> -
-
-

Releases

-
- {data ? `${data.total.toLocaleString()} results` : "Loading results..."} -
-
-
- -
-
-
-
-
+ +
-
- {orderedMachines.map((m) => { - const active = dMachinetypeIds.includes(m.id); - return ( - - ); - })} -
-
Default: {preferredMachineNames.join(", ")}
+ { + setDMachinetypeIds((current) => { + const next = new Set(current); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + const order = machineOptions.map((item) => item.id); + return order.filter((value) => next.has(value)); + }); + setPage(1); + }} + /> +
Preferred: {preferredMachineNames.join(", ")}
@@ -386,11 +372,9 @@ export default function ReleasesExplorer({
{loading &&
Loading...
} -
-
-
- -
+ + )} + > {data && data.items.length === 0 && !loading && (
No results.
)} @@ -438,8 +422,7 @@ export default function ReleasesExplorer({
)} -
-
+
Page {data?.page ?? 1} / {totalPages} diff --git a/src/components/explorer/ExplorerLayout.tsx b/src/components/explorer/ExplorerLayout.tsx new file mode 100644 index 0000000..1f25e10 --- /dev/null +++ b/src/components/explorer/ExplorerLayout.tsx @@ -0,0 +1,39 @@ +import { ReactNode } from "react"; +import FilterChips from "./FilterChips"; + +type ExplorerLayoutProps = { + title: string; + subtitle?: string; + chips?: string[]; + onClearChips?: () => void; + sidebar: ReactNode; + children: ReactNode; +}; + +export default function ExplorerLayout({ + title, + subtitle, + chips = [], + onClearChips, + sidebar, + children, +}: ExplorerLayoutProps) { + return ( +
+
+
+

{title}

+ {subtitle ?
{subtitle}
: null} +
+ {chips.length > 0 ? ( + + ) : null} +
+ +
+
{sidebar}
+
{children}
+
+
+ ); +} diff --git a/src/components/explorer/FilterChips.tsx b/src/components/explorer/FilterChips.tsx new file mode 100644 index 0000000..140f0f3 --- /dev/null +++ b/src/components/explorer/FilterChips.tsx @@ -0,0 +1,20 @@ +type FilterChipsProps = { + chips: string[]; + onClear?: () => void; + clearLabel?: string; +}; + +export default function FilterChips({ chips, onClear, clearLabel = "Clear filters" }: FilterChipsProps) { + return ( +
+ {chips.map((chip) => ( + {chip} + ))} + {onClear ? ( + + ) : null} +
+ ); +} diff --git a/src/components/explorer/FilterSidebar.tsx b/src/components/explorer/FilterSidebar.tsx new file mode 100644 index 0000000..159b574 --- /dev/null +++ b/src/components/explorer/FilterSidebar.tsx @@ -0,0 +1,13 @@ +import { ReactNode } from "react"; + +type FilterSidebarProps = { + children: ReactNode; +}; + +export default function FilterSidebar({ children }: FilterSidebarProps) { + return ( +
+
{children}
+
+ ); +} diff --git a/src/components/explorer/MultiSelectChips.tsx b/src/components/explorer/MultiSelectChips.tsx new file mode 100644 index 0000000..4541a5d --- /dev/null +++ b/src/components/explorer/MultiSelectChips.tsx @@ -0,0 +1,37 @@ +type ChipOption = { + id: T; + label: string; +}; + +type MultiSelectChipsProps = { + options: ChipOption[]; + selected: T[]; + onToggle: (id: T) => void; + size?: "sm" | "md"; +}; + +export default function MultiSelectChips({ + options, + selected, + onToggle, + size = "sm", +}: MultiSelectChipsProps) { + const btnSize = size === "sm" ? "btn-sm" : ""; + return ( +
+ {options.map((option) => { + const active = selected.includes(option.id); + return ( + + ); + })} +
+ ); +}