1
- import { useState } from 'react' ;
1
+ import { SyntheticEvent , useMemo , useState } from 'react' ;
2
2
import { Heading , Button , Drawer as DrawerComponent } from '~/components' ;
3
- import { Link , useLocation } from '@remix-run/react' ;
3
+ import { Link , useLocation , useSearchParams } from '@remix-run/react' ;
4
+ import { useDebounce } from 'react-use' ;
5
+
4
6
import type {
5
7
FilterType ,
6
8
Filter ,
@@ -10,7 +12,7 @@ type Props = {
10
12
filters : Filter [ ] ;
11
13
} ;
12
14
13
- export function SortFilter ( { filters} : { filters : Filter [ ] } ) {
15
+ export function SortFilter ( { filters} : Props ) {
14
16
const [ isOpen , setIsOpen ] = useState ( false ) ;
15
17
16
18
return (
@@ -20,7 +22,7 @@ export function SortFilter({filters}: {filters: Filter[]}) {
20
22
Filter and sort
21
23
</ Button >
22
24
</ div >
23
- < Drawer
25
+ < FiltersDrawer
24
26
filters = { filters }
25
27
isOpen = { isOpen }
26
28
onClose = { ( ) => setIsOpen ( false ) }
@@ -29,7 +31,7 @@ export function SortFilter({filters}: {filters: Filter[]}) {
29
31
) ;
30
32
}
31
33
32
- export function Drawer ( {
34
+ export function FiltersDrawer ( {
33
35
isOpen,
34
36
onClose,
35
37
filters = [ ] ,
@@ -38,8 +40,45 @@ export function Drawer({
38
40
onClose : ( ) => void ;
39
41
filters : Filter [ ] ;
40
42
} ) {
43
+ const [ params ] = useSearchParams ( ) ;
41
44
const location = useLocation ( ) ;
42
45
46
+ const filterMarkup = ( filter : Filter , option : Filter [ 'values' ] [ 0 ] ) => {
47
+ switch ( filter . type ) {
48
+ case 'PRICE_RANGE' :
49
+ const min =
50
+ params . has ( 'minPrice' ) && ! isNaN ( Number ( params . get ( 'minPrice' ) ) )
51
+ ? Number ( params . get ( 'minPrice' ) )
52
+ : undefined ;
53
+
54
+ const max =
55
+ params . has ( 'maxPrice' ) && ! isNaN ( Number ( params . get ( 'maxPrice' ) ) )
56
+ ? Number ( params . get ( 'maxPrice' ) )
57
+ : undefined ;
58
+
59
+ return < PriceRangeFilter min = { min } max = { max } /> ;
60
+
61
+ default :
62
+ const to = getFilterLink (
63
+ filter ,
64
+ option . input as string ,
65
+ params ,
66
+ location ,
67
+ ) ;
68
+ return (
69
+ < Link
70
+ className = "focus:underline hover:underline whitespace-nowrap"
71
+ prefetch = "intent"
72
+ onClick = { onClose }
73
+ reloadDocument
74
+ to = { to }
75
+ >
76
+ { option . label }
77
+ </ Link >
78
+ ) ;
79
+ }
80
+ } ;
81
+
43
82
return (
44
83
< DrawerComponent
45
84
open = { isOpen }
@@ -55,29 +94,7 @@ export function Drawer({
55
94
</ Heading >
56
95
< ul key = { filter . id } className = "pb-8" >
57
96
{ filter . values ?. map ( ( option ) => {
58
- const params = new URLSearchParams ( location . search ) ;
59
-
60
- const newParams = filterInputToParams (
61
- filter . type ,
62
- option . input as string ,
63
- params ,
64
- ) ;
65
-
66
- const to = `${ location . pathname } ?${ newParams . toString ( ) } ` ;
67
-
68
- return (
69
- < li key = { option . id } >
70
- < Link
71
- className = "focus:underline hover:underline whitespace-nowrap"
72
- prefetch = "intent"
73
- onClick = { onClose }
74
- reloadDocument
75
- to = { to }
76
- >
77
- { option . label }
78
- </ Link >
79
- </ li >
80
- ) ;
97
+ return < li key = { option . id } > { filterMarkup ( filter , option ) } </ li > ;
81
98
} ) }
82
99
</ ul >
83
100
</ div >
@@ -87,6 +104,86 @@ export function Drawer({
87
104
) ;
88
105
}
89
106
107
+ function getFilterLink (
108
+ filter : Filter ,
109
+ rawInput : string | Record < string , any > ,
110
+ params : URLSearchParams ,
111
+ location : ReturnType < typeof useLocation > ,
112
+ ) {
113
+ const paramsClone = new URLSearchParams ( params ) ;
114
+ const newParams = filterInputToParams ( filter . type , rawInput , paramsClone ) ;
115
+ return `${ location . pathname } ?${ newParams . toString ( ) } ` ;
116
+ }
117
+
118
+ const PRICE_RANGE_FILTER_DEBOUNCE = 500 ;
119
+
120
+ function PriceRangeFilter ( { max, min} : { max ?: number ; min ?: number } ) {
121
+ const location = useLocation ( ) ;
122
+ const params = useMemo (
123
+ ( ) => new URLSearchParams ( location . search ) ,
124
+ [ location . search ] ,
125
+ ) ;
126
+
127
+ const [ minPrice , setMinPrice ] = useState ( min ? String ( min ) : '' ) ;
128
+ const [ maxPrice , setMaxPrice ] = useState ( max ? String ( max ) : '' ) ;
129
+
130
+ useDebounce (
131
+ ( ) => {
132
+ if (
133
+ ( minPrice === '' || minPrice === String ( min ) ) &&
134
+ ( maxPrice === '' || maxPrice === String ( max ) )
135
+ )
136
+ return ;
137
+
138
+ const price : { min ?: string ; max ?: string } = { } ;
139
+ if ( minPrice !== '' ) price . min = minPrice ;
140
+ if ( maxPrice !== '' ) price . max = maxPrice ;
141
+
142
+ const newParams = filterInputToParams ( 'PRICE_RANGE' , { price} , params ) ;
143
+ window . location . href = `${ location . pathname } ?${ newParams . toString ( ) } ` ;
144
+ } ,
145
+ PRICE_RANGE_FILTER_DEBOUNCE ,
146
+ [ minPrice , maxPrice ] ,
147
+ ) ;
148
+
149
+ const onChangeMax = ( event : SyntheticEvent ) => {
150
+ const newMaxPrice = ( event . target as HTMLInputElement ) . value ;
151
+ setMaxPrice ( newMaxPrice ) ;
152
+ } ;
153
+
154
+ const onChangeMin = ( event : SyntheticEvent ) => {
155
+ const newMinPrice = ( event . target as HTMLInputElement ) . value ;
156
+ setMinPrice ( newMinPrice ) ;
157
+ } ;
158
+
159
+ return (
160
+ < div className = "flex" >
161
+ < label className = "mb-4" >
162
+ < span > from</ span >
163
+ < input
164
+ name = "maxPrice"
165
+ className = "text-black"
166
+ type = "text"
167
+ defaultValue = { min }
168
+ placeholder = { '$' }
169
+ onChange = { onChangeMin }
170
+ />
171
+ </ label >
172
+ < label >
173
+ < span > to</ span >
174
+ < input
175
+ name = "minPrice"
176
+ className = "text-black"
177
+ type = "number"
178
+ defaultValue = { max }
179
+ placeholder = { '$' }
180
+ onChange = { onChangeMax }
181
+ />
182
+ </ label >
183
+ </ div >
184
+ ) ;
185
+ }
186
+
90
187
function filterInputToParams (
91
188
type : FilterType ,
92
189
rawInput : string | Record < string , any > ,
@@ -95,8 +192,8 @@ function filterInputToParams(
95
192
const input = typeof rawInput === 'string' ? JSON . parse ( rawInput ) : rawInput ;
96
193
switch ( type ) {
97
194
case 'PRICE_RANGE' :
98
- params . set ( 'minPrice' , input . min ) ;
99
- params . set ( 'maxPrice' , input . max ) ;
195
+ if ( input . price . min ) params . set ( 'minPrice' , input . price . min ) ;
196
+ if ( input . price . max ) params . set ( 'maxPrice' , input . price . max ) ;
100
197
break ;
101
198
case 'LIST' :
102
199
Object . entries ( input ) . forEach ( ( [ key , value ] ) => {
0 commit comments