@@ -5,50 +5,34 @@ import { useTranslation } from 'react-i18next'
55import { useMediaQuery } from 'react-responsive'
66import { Link , useSearchParams } from 'react-router'
77import { Tooltip } from 'react-tooltip'
8+ import { z } from 'zod'
89import { Card } from '@/components/Card'
910import { CardsTable } from '@/components/CardsTable.tsx'
1011import FiltersPanel from '@/components/FiltersPanel'
1112import { Button } from '@/components/ui/button'
1213import { Sheet , SheetContent , SheetHeader , SheetTitle } from '@/components/ui/sheet'
14+ import useSearchState from '@/hooks/use-search-state'
1315import { toast } from '@/hooks/use-toast'
14- import {
15- type CardTypeOption ,
16- cardTypeOptions ,
17- expansionOptions ,
18- type Filters ,
19- type FiltersAll ,
20- getFilteredCards ,
21- ownershipOptions ,
22- sortByOptions ,
23- tradingOptions ,
24- } from '@/lib/filters'
16+ import { getFilteredCards , ownershipOptions , sortByOptions , tradingOptions } from '@/lib/filters'
2517import { getCardNameByLang } from '@/lib/utils.ts'
2618import { useAccount , useProfileDialog } from '@/services/account/useAccount'
27- import { type Card as CardType , type CollectionRow , type Rarity , rarities } from '@/types'
28-
29- const numberParser = ( s : string ) => {
30- const x = Number ( s )
31- return Number . isNaN ( x ) ? undefined : x
32- }
33- const boolParser = ( s : string ) => {
34- return s === 'true' ? true : s === 'false' ? false : undefined
35- }
36-
37- const filterParsers : { [ K in keyof FiltersAll ] : ( s : string ) => Filters [ K ] | undefined } = {
38- search : ( s ) => s ,
39- expansion : ( s ) => ( ( expansionOptions as readonly string [ ] ) . includes ( s ) ? ( s as Filters [ 'expansion' ] ) : undefined ) ,
40- pack : ( s ) => s ,
41- cardType : ( s ) => s . split ( ',' ) . filter ( ( x ) : x is CardTypeOption => ( cardTypeOptions as readonly string [ ] ) . includes ( x ) ) ,
42- rarity : ( s ) => s . split ( ',' ) . filter ( ( x ) : x is Rarity => ( rarities as readonly string [ ] ) . includes ( x ) ) ,
43- ownership : ( s ) => ( ( ownershipOptions as readonly string [ ] ) . includes ( s ) ? ( s as Filters [ 'ownership' ] ) : undefined ) ,
44- trading : ( s ) => ( ( tradingOptions as readonly string [ ] ) . includes ( s ) ? ( s as Filters [ 'trading' ] ) : undefined ) ,
45- sortBy : ( s ) => ( ( sortByOptions as readonly string [ ] ) . includes ( s ) ? ( s as Filters [ 'sortBy' ] ) : undefined ) ,
46- sortDesc : boolParser ,
47- minNumber : numberParser ,
48- maxNumber : numberParser ,
49- deckbuildingMode : boolParser ,
50- allTextSearch : boolParser ,
51- }
19+ import { type Card as CardType , type CollectionRow , cardTypes , expansionIds , rarities } from '@/types'
20+
21+ const schema = z . object ( {
22+ search : z . string ( ) . default ( '' ) ,
23+ expansion : z . union ( [ z . enum ( expansionIds ) , z . literal ( 'all' ) ] ) . default ( 'all' ) ,
24+ pack : z . string ( ) . default ( 'all' ) ,
25+ cardType : z . array ( z . enum ( cardTypes ) ) . default ( [ ] ) ,
26+ rarity : z . array ( z . enum ( rarities ) ) . default ( [ ] ) ,
27+ ownership : z . enum ( ownershipOptions ) . default ( 'all' ) ,
28+ trading : z . enum ( tradingOptions ) . default ( 'all' ) ,
29+ sortBy : z . enum ( sortByOptions ) . default ( 'expansion-newest' ) ,
30+ sortDesc : z . boolean ( ) . default ( false ) ,
31+ minNumber : z . number ( ) . default ( 0 ) ,
32+ maxNumber : z . union ( [ z . number ( ) , z . literal ( '∞' ) ] ) . default ( '∞' ) ,
33+ deckbuildingMode : z . boolean ( ) . default ( false ) ,
34+ allTextSearch : z . boolean ( ) . default ( false ) ,
35+ } )
5236
5337interface Props {
5438 children ?: ReactNode
@@ -57,80 +41,19 @@ interface Props {
5741 share ?: boolean // undefined => disable, false => open settings, true => copy link
5842}
5943
60- const defaultFilters : Filters = {
61- search : '' ,
62- expansion : 'all' ,
63- pack : 'all' ,
64- cardType : [ ] ,
65- rarity : [ ] ,
66- ownership : 'all' ,
67- trading : 'all' ,
68- sortBy : 'expansion-newest' ,
69- sortDesc : false ,
70- minNumber : 0 ,
71- maxNumber : '∞' ,
72- deckbuildingMode : false ,
73- allTextSearch : false ,
74- }
75-
7644export default function CollectionCards ( { children, cards, isPublic, share } : Props ) {
7745 const { t } = useTranslation ( 'pages/collection' )
7846 const isMobile = useMediaQuery ( { query : '(max-width: 767px)' } ) // tailwind "md"
7947
8048 const { setIsProfileDialogOpen } = useProfileDialog ( )
8149 const [ isFiltersSheetOpen , setIsFiltersSheetOpen ] = useState ( false ) // used only on mobile
82- const [ searchParams , setSearchParams ] = useSearchParams ( )
8350 const { data : account } = useAccount ( )
8451
85- const filters = ( ) => {
86- const res : Filters = { ...defaultFilters }
87-
88- const updateFilter = < K extends keyof Filters > ( key : K , value : string ) => {
89- const parsed = filterParsers [ key ] ( value ) as Filters [ K ]
90- if ( parsed !== undefined ) {
91- res [ key ] = parsed
92- }
93- }
94-
95- for ( const key in res ) {
96- const raw = searchParams . get ( key )
97- if ( raw != null ) {
98- updateFilter ( key as keyof Filters , raw )
99- }
100- }
101- return res
102- }
103-
104- const setFilters = ( updates : Partial < Filters > ) => {
105- const params = new URLSearchParams ( searchParams )
106- for ( const key in updates ) {
107- const val = updates [ key as keyof Filters ]
108- if ( val == null || val === 'all' || ( Array . isArray ( val ) && val . length === 0 ) || val === '' ) {
109- params . delete ( key )
110- } else if ( Array . isArray ( val ) ) {
111- params . set ( key , val . join ( ',' ) )
112- } else {
113- params . set ( key , String ( val ) )
114- }
115- setSearchParams ( params )
116- }
117- }
118-
52+ const [ filters , setFilters , activeFilters ] = useSearchState ( schema )
53+ const [ _ , setSearchParams ] = useSearchParams ( )
11954 const clearFilters = ( ) => setSearchParams ( new URLSearchParams ( ) )
12055
121- const activeFilters = ( ) => {
122- let res = 0
123- const currentFilters = filters ( )
124- for ( const key in currentFilters ) {
125- const k = key as keyof FiltersAll
126- if ( currentFilters [ k ] !== defaultFilters [ k ] ) {
127- res ++
128- }
129- }
130- return res
131- }
132-
133- const filteredCards = getFilteredCards ( filters ( ) , cards , account ?. trade_rarity_settings )
56+ const filteredCards = getFilteredCards ( filters , cards , account ?. trade_rarity_settings )
13457
13558 const getTradingMessage = ( ) => {
13659 if ( ! account ) {
@@ -164,10 +87,10 @@ export default function CollectionCards({ children, cards, isPublic, share }: Pr
16487 }
16588
16689 cardValues += `${ t ( 'trade.lookingForCards' ) } :\n`
167- putCards ( getFilteredCards ( { ...filters ( ) , trading : 'wanted' } , cards , account . trade_rarity_settings ) )
90+ putCards ( getFilteredCards ( { ...filters , trading : 'wanted' } , cards , account . trade_rarity_settings ) )
16891
16992 cardValues += `\n\n${ t ( 'trade.forTradeCards' ) } :\n`
170- putCards ( getFilteredCards ( { ...filters ( ) , trading : 'extra' } , cards , account . trade_rarity_settings ) )
93+ putCards ( getFilteredCards ( { ...filters , trading : 'extra' } , cards , account . trade_rarity_settings ) )
17194
17295 return cardValues
17396 }
@@ -215,7 +138,7 @@ export default function CollectionCards({ children, cards, isPublic, share }: Pr
215138 </ >
216139 ) }
217140 </ small >
218- < FiltersPanel className = "flex flex-col gap-y-3" filters = { filters ( ) } setFilters = { setFilters } clearFilters = { clearFilters } />
141+ < FiltersPanel className = "flex flex-col gap-y-3" filters = { filters } setFilters = { setFilters } clearFilters = { clearFilters } />
219142 < div className = "flex flex-col mt-4 gap-2" >
220143 { share !== undefined && (
221144 < Button variant = "outline" onClick = { onShare } >
@@ -264,9 +187,9 @@ export default function CollectionCards({ children, cards, isPublic, share }: Pr
264187 < div className = "h-9 flex overflow-hidden text-center rounded-md text-sm font-medium border shadow-sm border-neutral-700 divide-x divide-neutral-700 [&>*]:cursor-pointer [&>*]:hover:bg-neutral-600 [&>*]:hover:text-neutral-50" >
265188 < button type = "button" className = "flex-1" onClick = { ( ) => setIsFiltersSheetOpen ( true ) } >
266189 Filters
267- { activeFilters ( ) > 0 && ` (${ activeFilters ( ) } )` }
190+ { activeFilters > 0 && ` (${ activeFilters } )` }
268191 </ button >
269- { activeFilters ( ) > 0 && (
192+ { activeFilters > 0 && (
270193 < button type = "button" className = "group px-2" onClick = { clearFilters } >
271194 < Trash2 className = "stroke-neutral-200 group-hover:stroke-neutral-50" />
272195 </ button >
@@ -276,8 +199,8 @@ export default function CollectionCards({ children, cards, isPublic, share }: Pr
276199 { children }
277200 < CardsTable
278201 cards = { filteredCards }
279- groupExpansions = { filters ( ) . sortBy === 'expansion-newest' }
280- render = { ( c ) => < Card card = { c } editable = { ! filters ( ) . deckbuildingMode && ! isPublic } /> }
202+ groupExpansions = { filters . sortBy === 'expansion-newest' }
203+ render = { ( c ) => < Card card = { c } editable = { ! filters . deckbuildingMode && ! isPublic } /> }
281204 />
282205 </ div >
283206 </ div >
0 commit comments