@@ -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