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}
- ))}
-
-
- )}
-
-
-
-
-
-
- {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 && (
+
+
+
| ID |
Title |
@@ -426,13 +402,13 @@ export default function EntriesExplorer({
{it.machinetypeId != null ? (
it.machinetypeName ? (
{it.machinetypeName}
- ) : (
- {it.machinetypeId}
- )
) : (
- -
- )}
-
+ {it.machinetypeId}
+ )
+ ) : (
+ -
+ )}
+
{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..."}
-
-
-
-
-
-
-
-
-
-
-
-
+
+ )}
+ >
{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 (
+
+ );
+}
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 (
+
+ );
+ })}
+
+ );
+}