Skip to content

Commit 46c38b7

Browse files
committed
add query param
1 parent 539e702 commit 46c38b7

1 file changed

Lines changed: 175 additions & 24 deletions

File tree

src/pages/Library.tsx

Lines changed: 175 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ export default function Library() {
8181
const CONTROL_HEIGHT_CLASS = "min-h-11 sm:min-h-9";
8282
const INPUT_CONTROL_HEIGHT_CLASS =
8383
"[&_input]:min-h-11 sm:[&_input]:min-h-9";
84+
85+
const urlInitializedRef = useRef(false);
8486
const [search, setSearch] = useState("");
8587
// Initialize sort/order from localStorage if retention is enabled
8688
const [sortBy, setSortBy] = useState(() => {
@@ -184,11 +186,10 @@ export default function Library() {
184186
// Convert selected game type FilterItems to GamevaultGameTypeEnum values for API
185187
const gameTypeValues = useMemo(() => {
186188
return selectedGameTypes
187-
.map((item) => {
188-
const found = GAME_TYPES.find((t) => t.label === item.name);
189-
return found?.value;
190-
})
191-
.filter((v): v is GamevaultGameTypeEnum => v !== undefined);
189+
.map((item) => String(item.id))
190+
.filter((v): v is GamevaultGameTypeEnum =>
191+
Object.values(GamevaultGameTypeEnum).includes(v as GamevaultGameTypeEnum),
192+
);
192193
}, [selectedGameTypes]);
193194

194195
// Memoize array values to prevent unnecessary re-renders
@@ -237,32 +238,182 @@ export default function Library() {
237238
}
238239
}, [sortBy, order]);
239240

240-
// Sync bookmark filter in URL search params for shareable links
241-
useEffect(() => {
242-
const url = new URL(window.location.href);
243-
if (bookmarkFilter !== "all") {
244-
url.searchParams.set("bookmarked", bookmarkFilter);
245-
} else {
246-
url.searchParams.delete("bookmarked");
247-
}
248-
// We intentionally do not push to history each keystroke of search for cleanliness
249-
window.history.replaceState({}, "", url.toString());
250-
}, [bookmarkFilter]);
241+
const getParamValues = useCallback((params: URLSearchParams, key: string) => {
242+
const repeated = params.getAll(key).filter(Boolean);
243+
if (repeated.length > 0) return repeated;
244+
const single = params.get(key);
245+
if (!single) return [];
246+
return single
247+
.split(",")
248+
.map((v) => v.trim())
249+
.filter(Boolean);
250+
}, []);
251+
252+
const setParamValues = useCallback(
253+
(params: URLSearchParams, key: string, values: string[]) => {
254+
params.delete(key);
255+
values.forEach((v) => params.append(key, v));
256+
},
257+
[],
258+
);
259+
260+
const isProgressState = useCallback(
261+
(value: string): value is ProgressStateEnum =>
262+
Object.values(ProgressStateEnum).includes(value as ProgressStateEnum),
263+
[],
264+
);
265+
266+
const isGameType = useCallback(
267+
(value: string): value is GamevaultGameTypeEnum =>
268+
Object.values(GamevaultGameTypeEnum).includes(value as GamevaultGameTypeEnum),
269+
[],
270+
);
271+
272+
const isEarlyAccess = useCallback(
273+
(value: string): value is EarlyAccessFilter =>
274+
value === "all" || value === "true" || value === "false",
275+
[],
276+
);
277+
278+
const isBookmark = useCallback(
279+
(value: string): value is BookmarkFilter | "1" =>
280+
value === "all" || value === "mine" || value === "others" || value === "1",
281+
[],
282+
);
251283

252284
// Initialize from URL (first render)
253285
useEffect(() => {
286+
if (typeof window === "undefined") return;
287+
254288
const params = new URL(window.location.href).searchParams;
289+
290+
const q = params.get("q");
291+
if (q) setSearch(q);
292+
293+
const sort = params.get("sort");
294+
if (sort && SORT_BY.some((o) => o.value === sort)) setSortBy(sort);
295+
296+
const ord = params.get("order");
297+
if (ord === "ASC" || ord === "DESC") setOrder(ord);
298+
255299
const bookmarked = params.get("bookmarked");
256-
if (
257-
bookmarked === "mine" ||
258-
bookmarked === "others" ||
259-
bookmarked === "1"
260-
) {
261-
setBookmarkFilter(
262-
bookmarked === "1" ? "mine" : (bookmarked as BookmarkFilter),
300+
if (bookmarked && isBookmark(bookmarked)) {
301+
setBookmarkFilter(bookmarked === "1" ? "mine" : bookmarked);
302+
}
303+
304+
const types = getParamValues(params, "types").filter(isGameType);
305+
if (types.length > 0) {
306+
setSelectedGameTypes(
307+
types
308+
.map((t): FilterItem | null => {
309+
const match = GAME_TYPES.find((gt) => gt.value === t);
310+
return match ? { id: t, name: match.label } : null;
311+
})
312+
.filter((x): x is FilterItem => x !== null),
263313
);
264314
}
265-
}, []);
315+
316+
const tags = getParamValues(params, "tags");
317+
if (tags.length > 0) setSelectedTags(tags.map((t) => ({ id: t, name: t })));
318+
319+
const genres = getParamValues(params, "genres");
320+
if (genres.length > 0)
321+
setSelectedGenres(genres.map((g) => ({ id: g, name: g })));
322+
323+
const developers = getParamValues(params, "developers");
324+
if (developers.length > 0)
325+
setSelectedDevelopers(developers.map((d) => ({ id: d, name: d })));
326+
327+
const publishers = getParamValues(params, "publishers");
328+
if (publishers.length > 0)
329+
setSelectedPublishers(publishers.map((p) => ({ id: p, name: p })));
330+
331+
const state = params.get("state");
332+
if (state && isProgressState(state)) setSelectedGameState(state);
333+
334+
const after = params.get("releasedAfter");
335+
if (after) setReleaseDateFrom(after);
336+
337+
const before = params.get("releasedBefore");
338+
if (before) setReleaseDateTo(before);
339+
340+
const ea = params.get("earlyAccess");
341+
if (ea && isEarlyAccess(ea)) setEarlyAccess(ea);
342+
343+
urlInitializedRef.current = true;
344+
}, [getParamValues, isBookmark, isEarlyAccess, isGameType, isProgressState]);
345+
346+
// Sync all filters into URL search params for shareable links
347+
useEffect(() => {
348+
if (typeof window === "undefined") return;
349+
if (!urlInitializedRef.current) return;
350+
351+
const url = new URL(window.location.href);
352+
const params = url.searchParams;
353+
354+
if (search.trim().length > 0) params.set("q", search.trim());
355+
else params.delete("q");
356+
357+
if (sortBy !== "sort_title") params.set("sort", sortBy);
358+
else params.delete("sort");
359+
360+
if (order !== "ASC") params.set("order", order);
361+
else params.delete("order");
362+
363+
if (bookmarkFilter !== "all") params.set("bookmarked", bookmarkFilter);
364+
else params.delete("bookmarked");
365+
366+
setParamValues(
367+
params,
368+
"types",
369+
selectedGameTypes
370+
.map((i) => String(i.id))
371+
.filter((v): v is GamevaultGameTypeEnum => isGameType(v)),
372+
);
373+
setParamValues(params, "tags", selectedTags.map((t) => t.name));
374+
setParamValues(params, "genres", selectedGenres.map((g) => g.name));
375+
setParamValues(
376+
params,
377+
"developers",
378+
selectedDevelopers.map((d) => d.name),
379+
);
380+
setParamValues(
381+
params,
382+
"publishers",
383+
selectedPublishers.map((p) => p.name),
384+
);
385+
386+
if (selectedGameState) params.set("state", selectedGameState);
387+
else params.delete("state");
388+
389+
if (releaseDateFrom) params.set("releasedAfter", releaseDateFrom);
390+
else params.delete("releasedAfter");
391+
392+
if (releaseDateTo) params.set("releasedBefore", releaseDateTo);
393+
else params.delete("releasedBefore");
394+
395+
if (earlyAccess !== "all") params.set("earlyAccess", earlyAccess);
396+
else params.delete("earlyAccess");
397+
398+
// We intentionally do not push to history each keystroke of search for cleanliness
399+
window.history.replaceState({}, "", url.toString());
400+
}, [
401+
bookmarkFilter,
402+
earlyAccess,
403+
isGameType,
404+
order,
405+
releaseDateFrom,
406+
releaseDateTo,
407+
search,
408+
selectedDevelopers,
409+
selectedGameState,
410+
selectedGameTypes,
411+
selectedGenres,
412+
selectedPublishers,
413+
selectedTags,
414+
setParamValues,
415+
sortBy,
416+
]);
266417

267418
const sentinelRef = useRef<HTMLDivElement | null>(null);
268419
useEffect(() => {

0 commit comments

Comments
 (0)