@@ -6,6 +6,10 @@ import { Heading } from "@tw/heading";
66import { Input } from "@tw/input" ;
77import { Listbox , ListboxLabel , ListboxOption } from "@tw/listbox" ;
88import { useDeferredValue , useEffect , useRef , useState } from "react" ;
9+ import { StarIcon } from "@heroicons/react/24/solid" ;
10+ import { StarIcon as StarOutlineIcon } from "@heroicons/react/24/outline" ;
11+ import Card from "@/components/Card" ;
12+ import { Button } from "@/components/tailwind/button" ;
913import { Badge } from "../components/tailwind/badge" ;
1014
1115const SORT_BY : { label : string ; value : string } [ ] = [
@@ -28,34 +32,12 @@ const LIB_SORT_KEY = 'app_library_sort';
2832const LIB_ORDER_KEY = 'app_library_order' ;
2933
3034export default function Library ( ) {
31- const { serverUrl } = useAuth ( ) ;
35+ const { serverUrl, user } = useAuth ( ) ;
3236 const [ search , setSearch ] = useState ( "" ) ;
33- const [ sortBy , setSortBy ] = useState ( ( ) => {
34- try {
35- if ( localStorage . getItem ( RETAIN_KEY ) === '1' ) {
36- return localStorage . getItem ( LIB_SORT_KEY ) || 'sort_title' ;
37- }
38- } catch { }
39- return 'sort_title' ;
40- } ) ;
41- const [ order , setOrder ] = useState < "ASC" | "DESC" > ( ( ) => {
42- try {
43- if ( localStorage . getItem ( RETAIN_KEY ) === '1' ) {
44- const v = localStorage . getItem ( LIB_ORDER_KEY ) as 'ASC' | 'DESC' | null ;
45- if ( v === 'ASC' || v === 'DESC' ) return v ;
46- }
47- } catch { }
48- return 'ASC' ;
49- } ) ;
50- // Persist when changed if retention enabled
51- useEffect ( ( ) => {
52- try {
53- if ( localStorage . getItem ( RETAIN_KEY ) === '1' ) {
54- localStorage . setItem ( LIB_SORT_KEY , sortBy ) ;
55- localStorage . setItem ( LIB_ORDER_KEY , order ) ;
56- }
57- } catch { }
58- } , [ sortBy , order ] ) ;
37+ const [ sortBy , setSortBy ] = useState ( "sort_title" ) ;
38+ const [ order , setOrder ] = useState < "ASC" | "DESC" > ( "ASC" ) ;
39+ const [ showAdvanced , setShowAdvanced ] = useState ( false ) ;
40+ const [ bookmarkedOnly , setBookmarkedOnly ] = useState ( false ) ;
5941 // Defer search to avoid spamming requests while user types quickly
6042 const deferredSearch = useDeferredValue ( search ) ;
6143 const { count, games, loading, error, loadMore, hasMore, refetch } = useGames (
@@ -64,13 +46,32 @@ export default function Library() {
6446 sortBy,
6547 order,
6648 limit : 50 ,
49+ bookmarkedOnly,
6750 } ,
6851 ) ;
6952
7053 // Reset to first page when filters change (search, sortBy, order)
7154 useEffect ( ( ) => {
7255 refetch ( ) ;
73- } , [ deferredSearch , sortBy , order , refetch ] ) ;
56+ } , [ deferredSearch , sortBy , order , bookmarkedOnly , refetch ] ) ;
57+
58+ // Sync bookmark filter in URL search params for shareable links
59+ useEffect ( ( ) => {
60+ const url = new URL ( window . location . href ) ;
61+ if ( bookmarkedOnly ) {
62+ url . searchParams . set ( "bookmarked" , "1" ) ;
63+ } else {
64+ url . searchParams . delete ( "bookmarked" ) ;
65+ }
66+ // We intentionally do not push to history each keystroke of search for cleanliness
67+ window . history . replaceState ( { } , "" , url . toString ( ) ) ;
68+ } , [ bookmarkedOnly ] ) ;
69+
70+ // Initialize from URL (first render)
71+ useEffect ( ( ) => {
72+ const params = new URL ( window . location . href ) . searchParams ;
73+ if ( params . get ( "bookmarked" ) === "1" ) setBookmarkedOnly ( true ) ;
74+ } , [ ] ) ;
7475
7576 const sentinelRef = useRef < HTMLDivElement | null > ( null ) ;
7677 useEffect ( ( ) => {
@@ -144,7 +145,40 @@ export default function Library() {
144145 ) ) }
145146 </ Listbox >
146147 </ div >
148+ < div className = "flex flex-col w-40" >
149+ < label className = "block text-xs font-medium text-fg-muted mb-1 invisible" >
150+ Advanced Filters
151+ </ label >
152+ < Button
153+ outline
154+ className = "w-full justify-center h-9 text-sm items-center"
155+ onClick = { ( ) => setShowAdvanced ( ( s ) => ! s ) }
156+ >
157+ { showAdvanced ? "Hide Filters" : "Advanced Filters" }
158+ </ Button >
159+ </ div >
147160 </ div >
161+ { showAdvanced && (
162+ < Card title = "Advanced Filters" className = "gap-4 !mb-6" >
163+ < div className = "flex items-center gap-3 flex-wrap text-xs" >
164+ < Button
165+ aria-pressed = { bookmarkedOnly }
166+ disabled = { ! user }
167+ onClick = { ( ) => user && setBookmarkedOnly ( ( v ) => ! v ) }
168+ { ...( bookmarkedOnly ? { color : "yellow" } : { outline : true } ) }
169+ className = "text-sm h-9 px-3 flex items-center gap-1"
170+ >
171+ { bookmarkedOnly ? (
172+ < StarIcon data-slot = "icon" className = "h-4 w-4" />
173+ ) : (
174+ < StarOutlineIcon data-slot = "icon" className = "h-4 w-4" />
175+ ) }
176+ Bookmarked
177+ </ Button >
178+ </ div >
179+ { /* Placeholder for future filters */ }
180+ </ Card >
181+ ) }
148182 < div className = "flex-1 overflow-auto" >
149183 { ! serverUrl && (
150184 < div className = "p-8 text-sm text-fg-muted" >
0 commit comments