Add multi-select machine filters
Replace machine dropdowns with multi-select chips and pass machine lists in queries. Signed-off-by: codex@lucy.xalior.com
This commit is contained in:
@@ -52,11 +52,21 @@ export default function EntriesExplorer({
|
||||
page: number;
|
||||
genreId: string | number | "";
|
||||
languageId: string | "";
|
||||
machinetypeId: string | number | "";
|
||||
machinetypeId: string;
|
||||
sort: "title" | "id_desc";
|
||||
scope?: SearchScope;
|
||||
};
|
||||
}) {
|
||||
const preferredMachineIds = [27, 26, 8, 9];
|
||||
const parseMachineIds = (value?: string) => {
|
||||
if (!value) return preferredMachineIds.slice();
|
||||
const ids = value
|
||||
.split(",")
|
||||
.map((id) => Number(id.trim()))
|
||||
.filter((id) => Number.isFinite(id) && id > 0);
|
||||
return ids.length ? ids : preferredMachineIds.slice();
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
@@ -72,17 +82,20 @@ export default function EntriesExplorer({
|
||||
initialUrlState?.genreId === "" ? "" : initialUrlState?.genreId ? Number(initialUrlState.genreId) : ""
|
||||
);
|
||||
const [languageId, setLanguageId] = useState<string | "">(initialUrlState?.languageId ?? "");
|
||||
const [machinetypeId, setMachinetypeId] = useState<number | "">(
|
||||
initialUrlState?.machinetypeId === "" ? "" : initialUrlState?.machinetypeId ? Number(initialUrlState.machinetypeId) : ""
|
||||
);
|
||||
const [machinetypeIds, setMachinetypeIds] = useState<number[]>(parseMachineIds(initialUrlState?.machinetypeId));
|
||||
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 preferredMachineIds = [27, 26, 8, 9];
|
||||
const preferredMachineNames = useMemo(() => {
|
||||
if (!machines.length) return preferredMachineIds.map((id) => `#${id}`);
|
||||
return preferredMachineIds.map((id) => machines.find((m) => m.id === id)?.name ?? `#${id}`);
|
||||
}, [machines]);
|
||||
const orderedMachines = useMemo(() => {
|
||||
const seen = new Set(preferredMachineIds);
|
||||
const preferred = preferredMachineIds.map((id) => machines.find((m) => m.id === id)).filter(Boolean) as { id: number; name: string }[];
|
||||
const rest = machines.filter((m) => !seen.has(m.id));
|
||||
return [...preferred, ...rest];
|
||||
}, [machines]);
|
||||
|
||||
const pageSize = 20;
|
||||
const totalPages = useMemo(() => (data ? Math.max(1, Math.ceil(data.total / data.pageSize)) : 1), [data]);
|
||||
@@ -97,14 +110,14 @@ export default function EntriesExplorer({
|
||||
const name = languages.find((l) => l.id === languageId)?.name ?? languageId;
|
||||
chips.push(`lang: ${name}`);
|
||||
}
|
||||
if (machinetypeId !== "") {
|
||||
const name = machines.find((m) => m.id === Number(machinetypeId))?.name ?? `#${machinetypeId}`;
|
||||
chips.push(`machine: ${name}`);
|
||||
if (machinetypeIds.length > 0) {
|
||||
const names = machinetypeIds.map((id) => machines.find((m) => m.id === id)?.name ?? `#${id}`);
|
||||
chips.push(`machine: ${names.join(", ")}`);
|
||||
}
|
||||
if (scope === "title_aliases") chips.push("scope: titles + aliases");
|
||||
if (scope === "title_aliases_origins") chips.push("scope: titles + aliases + origins");
|
||||
return chips;
|
||||
}, [appliedQ, genreId, languageId, machinetypeId, scope, genres, languages, machines]);
|
||||
}, [appliedQ, genreId, languageId, machinetypeIds, scope, genres, languages, machines]);
|
||||
|
||||
function updateUrl(nextPage = page) {
|
||||
const params = new URLSearchParams();
|
||||
@@ -112,7 +125,7 @@ export default function EntriesExplorer({
|
||||
params.set("page", String(nextPage));
|
||||
if (genreId !== "") params.set("genreId", String(genreId));
|
||||
if (languageId !== "") params.set("languageId", String(languageId));
|
||||
if (machinetypeId !== "") params.set("machinetypeId", String(machinetypeId));
|
||||
if (machinetypeIds.length > 0) params.set("machinetypeId", machinetypeIds.join(","));
|
||||
if (sort) params.set("sort", sort);
|
||||
if (scope !== "title") params.set("scope", scope);
|
||||
const qs = params.toString();
|
||||
@@ -128,7 +141,7 @@ export default function EntriesExplorer({
|
||||
params.set("pageSize", String(pageSize));
|
||||
if (genreId !== "") params.set("genreId", String(genreId));
|
||||
if (languageId !== "") params.set("languageId", String(languageId));
|
||||
if (machinetypeId !== "") params.set("machinetypeId", String(machinetypeId));
|
||||
if (machinetypeIds.length > 0) params.set("machinetypeId", machinetypeIds.join(","));
|
||||
if (sort) params.set("sort", sort);
|
||||
if (scope !== "title") params.set("scope", scope);
|
||||
if (withFacets) params.set("facets", "true");
|
||||
@@ -165,8 +178,7 @@ export default function EntriesExplorer({
|
||||
(initialUrlState?.q ?? "") === appliedQ &&
|
||||
(initialUrlState?.genreId === "" ? "" : Number(initialUrlState?.genreId ?? "")) === (genreId === "" ? "" : Number(genreId)) &&
|
||||
(initialUrlState?.languageId ?? "") === (languageId ?? "") &&
|
||||
(initialUrlState?.machinetypeId === "" ? "" : Number(initialUrlState?.machinetypeId ?? "")) ===
|
||||
(machinetypeId === "" ? "" : Number(machinetypeId)) &&
|
||||
parseMachineIds(initialUrlState?.machinetypeId).join(",") === machinetypeIds.join(",") &&
|
||||
sort === (initialUrlState?.sort ?? "id_desc") &&
|
||||
(initialUrlState?.scope ?? "title") === scope
|
||||
) {
|
||||
@@ -176,7 +188,7 @@ export default function EntriesExplorer({
|
||||
updateUrl(page);
|
||||
fetchData(appliedQ, page, true);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [page, genreId, languageId, machinetypeId, sort, scope, appliedQ]);
|
||||
}, [page, genreId, languageId, machinetypeIds, sort, scope, appliedQ]);
|
||||
|
||||
// Load filter lists on mount only if not provided by server
|
||||
useEffect(() => {
|
||||
@@ -207,7 +219,7 @@ export default function EntriesExplorer({
|
||||
setAppliedQ("");
|
||||
setGenreId("");
|
||||
setLanguageId("");
|
||||
setMachinetypeId("");
|
||||
setMachinetypeIds(preferredMachineIds.slice());
|
||||
setSort("id_desc");
|
||||
setScope("title");
|
||||
setPage(1);
|
||||
@@ -219,11 +231,11 @@ export default function EntriesExplorer({
|
||||
params.set("page", String(Math.max(1, (data?.page ?? 1) - 1)));
|
||||
if (genreId !== "") params.set("genreId", String(genreId));
|
||||
if (languageId !== "") params.set("languageId", String(languageId));
|
||||
if (machinetypeId !== "") params.set("machinetypeId", String(machinetypeId));
|
||||
if (machinetypeIds.length > 0) params.set("machinetypeId", machinetypeIds.join(","));
|
||||
if (sort) params.set("sort", sort);
|
||||
if (scope !== "title") params.set("scope", scope);
|
||||
return `/zxdb/entries?${params.toString()}`;
|
||||
}, [appliedQ, data?.page, genreId, languageId, machinetypeId, sort, scope]);
|
||||
}, [appliedQ, data?.page, genreId, languageId, machinetypeIds, sort, scope]);
|
||||
|
||||
const nextHref = useMemo(() => {
|
||||
const params = new URLSearchParams();
|
||||
@@ -231,11 +243,11 @@ export default function EntriesExplorer({
|
||||
params.set("page", String(Math.max(1, (data?.page ?? 1) + 1)));
|
||||
if (genreId !== "") params.set("genreId", String(genreId));
|
||||
if (languageId !== "") params.set("languageId", String(languageId));
|
||||
if (machinetypeId !== "") params.set("machinetypeId", String(machinetypeId));
|
||||
if (machinetypeIds.length > 0) params.set("machinetypeId", machinetypeIds.join(","));
|
||||
if (sort) params.set("sort", sort);
|
||||
if (scope !== "title") params.set("scope", scope);
|
||||
return `/zxdb/entries?${params.toString()}`;
|
||||
}, [appliedQ, data?.page, genreId, languageId, machinetypeId, sort, scope]);
|
||||
}, [appliedQ, data?.page, genreId, languageId, machinetypeIds, sort, scope]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -303,15 +315,34 @@ export default function EntriesExplorer({
|
||||
</div>
|
||||
<div>
|
||||
<label className="form-label small text-secondary">Machine</label>
|
||||
<select className="form-select" value={machinetypeId} onChange={(e) => { setMachinetypeId(e.target.value === "" ? "" : Number(e.target.value)); setPage(1); }}>
|
||||
<option value="">All machines</option>
|
||||
{machines.map((m) => (
|
||||
<option key={m.id} value={m.id}>{m.name}</option>
|
||||
))}
|
||||
</select>
|
||||
{machinetypeId === "" && (
|
||||
<div className="form-text">Preferred: {preferredMachineNames.join(", ")}</div>
|
||||
)}
|
||||
<div className="d-flex flex-wrap gap-2">
|
||||
{orderedMachines.map((m) => {
|
||||
const active = machinetypeIds.includes(m.id);
|
||||
return (
|
||||
<button
|
||||
key={m.id}
|
||||
type="button"
|
||||
className={`btn btn-sm ${active ? "btn-primary" : "btn-outline-secondary"}`}
|
||||
onClick={() => {
|
||||
setMachinetypeIds((current) => {
|
||||
const next = new Set(current);
|
||||
if (next.has(m.id)) {
|
||||
next.delete(m.id);
|
||||
} else {
|
||||
next.add(m.id);
|
||||
}
|
||||
const order = orderedMachines.map((item) => item.id);
|
||||
return order.filter((id) => next.has(id));
|
||||
});
|
||||
setPage(1);
|
||||
}}
|
||||
>
|
||||
{m.name}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="form-text">Preferred: {preferredMachineNames.join(", ")}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="form-label small text-secondary">Sort</label>
|
||||
|
||||
Reference in New Issue
Block a user