Skip to content

Commit 9e318f9

Browse files
Create PriceRangeFilter component
1 parent 428ac49 commit 9e318f9

File tree

2 files changed

+157
-33
lines changed

2 files changed

+157
-33
lines changed

templates/demo-store/app/components/SortFilter.tsx

Lines changed: 127 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import {useState} from 'react';
1+
import {SyntheticEvent, useMemo, useState} from 'react';
22
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+
46
import type {
57
FilterType,
68
Filter,
@@ -10,7 +12,7 @@ type Props = {
1012
filters: Filter[];
1113
};
1214

13-
export function SortFilter({filters}: {filters: Filter[]}) {
15+
export function SortFilter({filters}: Props) {
1416
const [isOpen, setIsOpen] = useState(false);
1517

1618
return (
@@ -20,7 +22,7 @@ export function SortFilter({filters}: {filters: Filter[]}) {
2022
Filter and sort
2123
</Button>
2224
</div>
23-
<Drawer
25+
<FiltersDrawer
2426
filters={filters}
2527
isOpen={isOpen}
2628
onClose={() => setIsOpen(false)}
@@ -29,7 +31,7 @@ export function SortFilter({filters}: {filters: Filter[]}) {
2931
);
3032
}
3133

32-
export function Drawer({
34+
export function FiltersDrawer({
3335
isOpen,
3436
onClose,
3537
filters = [],
@@ -38,8 +40,45 @@ export function Drawer({
3840
onClose: () => void;
3941
filters: Filter[];
4042
}) {
43+
const [params] = useSearchParams();
4144
const location = useLocation();
4245

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+
4382
return (
4483
<DrawerComponent
4584
open={isOpen}
@@ -55,29 +94,7 @@ export function Drawer({
5594
</Heading>
5695
<ul key={filter.id} className="pb-8">
5796
{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>;
8198
})}
8299
</ul>
83100
</div>
@@ -87,6 +104,86 @@ export function Drawer({
87104
);
88105
}
89106

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+
90187
function filterInputToParams(
91188
type: FilterType,
92189
rawInput: string | Record<string, any>,
@@ -95,8 +192,8 @@ function filterInputToParams(
95192
const input = typeof rawInput === 'string' ? JSON.parse(rawInput) : rawInput;
96193
switch (type) {
97194
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);
100197
break;
101198
case 'LIST':
102199
Object.entries(input).forEach(([key, value]) => {

templates/demo-store/app/routes/collections/$collectionHandle.tsx

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,17 @@ import {PRODUCT_CARD_FRAGMENT} from '~/data';
1818

1919
const PAGINATION_SIZE = 48;
2020

21+
type VariantFilterParam = Record<string, string>;
22+
type PriceFiltersQueryParam = Record<'price', {max?: number; min?: number}>;
23+
type VariantOptionFiltersQueryParam = Record<
24+
'variantOption',
25+
{name: string; value: string}
26+
>;
27+
28+
type FiltersQueryParams = Array<
29+
VariantFilterParam | PriceFiltersQueryParam | VariantOptionFiltersQueryParam
30+
>;
31+
2132
export async function loader({
2233
params,
2334
request,
@@ -29,18 +40,34 @@ export async function loader({
2940

3041
const searchParams = new URL(request.url).searchParams;
3142
const knownFilters = ['cursor', 'productVendor', 'productType', 'available'];
43+
const priceFilters = ['minPrice', 'maxPrice'];
3244

33-
const filters: Record<string, {name: string; value: string} | string>[] = [];
45+
const filters: FiltersQueryParams = [];
3446

3547
for (const [key, value] of searchParams.entries()) {
36-
// TODO: Add price min/max to query
3748
if (knownFilters.includes(key)) {
3849
filters.push({[key]: value});
39-
} else {
50+
} else if (!priceFilters.includes(key)) {
4051
filters.push({variantOption: {name: key, value}});
4152
}
4253
}
4354

55+
// Builds min and max price filter since we can't stack them separately into
56+
// the filters array. See price filters limitations:
57+
// https://shopify.dev/custom-storefronts/products-collections/filter-products#limitations
58+
if (searchParams.has('minPrice') || searchParams.has('maxPrice')) {
59+
const price: {min?: number; max?: number} = {};
60+
if (searchParams.has('minPrice')) {
61+
price.min = Number(searchParams.get('minPrice')) || 0;
62+
}
63+
if (searchParams.has('maxPrice')) {
64+
price.max = Number(searchParams.get('maxPrice')) || 0;
65+
}
66+
filters.push({
67+
price,
68+
});
69+
}
70+
4471
const {language, country} = getLocalizationFromLang(params.lang);
4572

4673
const {collection} = await storefront.query<{

0 commit comments

Comments
 (0)