From 906f2934fbcc7782ba1e14acaeb2c6bba27033cd Mon Sep 17 00:00:00 2001 From: Rami Bitar Date: Sat, 9 May 2026 15:20:50 -0400 Subject: [PATCH] Move collection and search filters into a Sheet drawer Replace the always-visible sidebar and inline mobile filter panel with a single Filters button that opens a left-side Sheet, with Clear (outline) and Search (default) actions in the footer for consistency across pages. Co-Authored-By: Claude Opus 4.7 --- components/commerce/collection.tsx | 220 +++++++++------------ components/commerce/search-products.tsx | 251 ++++++++++-------------- 2 files changed, 201 insertions(+), 270 deletions(-) diff --git a/components/commerce/collection.tsx b/components/commerce/collection.tsx index faaba36..2051e64 100644 --- a/components/commerce/collection.tsx +++ b/components/commerce/collection.tsx @@ -1,6 +1,6 @@ import { useState, useCallback } from 'react'; import { useParams } from 'react-router'; -import { ChevronDown, SlidersHorizontal, X } from 'lucide-react'; +import { ChevronDown, SlidersHorizontal } from 'lucide-react'; import type { ShopifyCollection } from '@reacteditor/field-shopify'; import { useCollectionProducts, @@ -11,6 +11,8 @@ import { ProductCard } from './product-card'; import { Typography } from '@/components/Typography'; import { Skeleton } from '@/components/ui/skeleton'; import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select'; +import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger, SheetFooter } from '@/components/ui/sheet'; +import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; type FilterOption = { label: string }; @@ -151,48 +153,8 @@ function Sidebar({ onChange({ [key]: arr.includes(value) ? arr.filter((v) => v !== value) : [...arr, value] }); } - const hasActiveFilters = - active.availability || - active.productTypes.length > 0 || - active.vendors.length > 0 || - active.tags.length > 0 || - active.colors.length > 0 || - active.styles.length > 0 || - active.sizes.length > 0 || - active.materials.length > 0 || - active.minPrice !== '' || - active.maxPrice !== '' || - Object.values(active.metafieldValues).some((arr) => arr.length > 0); - return ( - + ); } @@ -400,7 +362,7 @@ export function CollectionView(props: CollectionProps) { const [sort, setSort] = useState(defaultSort); const [reverse, setReverse] = useState(false); - const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false); + const [filtersOpen, setFiltersOpen] = useState(false); const [active, setActive] = useState({ availability: false, productTypes: [], @@ -419,6 +381,22 @@ export function CollectionView(props: CollectionProps) { setActive((prev) => ({ ...prev, ...patch })); }, []); + const clearAll = useCallback(() => { + setActive({ + availability: false, + productTypes: [], + vendors: [], + tags: [], + colors: [], + styles: [], + sizes: [], + materials: [], + minPrice: '', + maxPrice: '', + metafieldValues: {}, + }); + }, []); + const productFilters = buildProductFilters(active); const handleSortChange = (value: string) => { @@ -491,96 +469,82 @@ export function CollectionView(props: CollectionProps) { ) : null} - {/* Mobile filter toggle */} -
- - + {/* Filter + sort bar */} +
+ + + + + + + Filters + +
+ +
+ + + + +
+
+ +
+

+ {loading ? 'Loading…' : `${products.length} product${products.length === 1 ? '' : 's'}`} +

+ +
- {/* Mobile filter panel */} - {mobileFiltersOpen && ( -
- + {/* Grid */} +
+ {loading + ? Array.from({ length: limit }).map((_, i) => ( + + )) + : products.map((p: any) => )} +
+ + {!loading && products.length === 0 && ( +
+ No products found in this collection.
)} -
- {/* Desktop sidebar */} -
- + {hasNextPage && !loading && ( +
+
- - {/* Product area */} -
- {/* Sort + count bar */} -
-

- {loading ? 'Loading…' : `${products.length} product${products.length === 1 ? '' : 's'}`} -

-
- -
-
- - {/* Grid */} -
- {loading - ? Array.from({ length: limit }).map((_, i) => ( - - )) - : products.map((p: any) => )} -
- - {!loading && products.length === 0 && ( -
- No products found in this collection. -
- )} - - {hasNextPage && !loading && ( -
- -
- )} -
-
+ )}
); diff --git a/components/commerce/search-products.tsx b/components/commerce/search-products.tsx index ec92872..b0c856b 100644 --- a/components/commerce/search-products.tsx +++ b/components/commerce/search-products.tsx @@ -1,11 +1,13 @@ import { useState, useEffect, useCallback } from 'react'; import { useSearchParams } from 'react-router'; -import { ChevronDown, SlidersHorizontal, X } from 'lucide-react'; +import { ChevronDown, SlidersHorizontal } from 'lucide-react'; import { useShopifySearch, type SearchFilters, type SortOption } from '@/hooks/use-shopify-search'; import { ProductCard } from './product-card'; import { Skeleton } from '@/components/ui/skeleton'; import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select'; +import { Button } from '@/components/ui/button'; +import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger, SheetFooter } from '@/components/ui/sheet'; import { cn } from '@/lib/utils'; type FilterOption = { label: string }; @@ -146,48 +148,8 @@ function Sidebar({ onChange({ [key]: arr.includes(value) ? arr.filter((v) => v !== value) : [...arr, value] }); } - const hasActiveFilters = - active.availability || - active.productTypes.length > 0 || - active.vendors.length > 0 || - active.tags.length > 0 || - active.colors.length > 0 || - active.styles.length > 0 || - active.sizes.length > 0 || - active.materials.length > 0 || - active.minPrice !== '' || - active.maxPrice !== '' || - Object.values(active.metafieldValues).some((arr) => arr.length > 0); - return ( - +
); } @@ -360,7 +322,7 @@ export function SearchProductsView(props: SearchProductsProps) { const [query, setQuery] = useState(initialQ); const [inputValue, setInputValue] = useState(initialQ); const [sort, setSort] = useState(props.defaultSort); - const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false); + const [filtersOpen, setFiltersOpen] = useState(false); const [active, setActive] = useState({ availability: false, productTypes: [], @@ -379,6 +341,22 @@ export function SearchProductsView(props: SearchProductsProps) { setActive((prev) => ({ ...prev, ...patch })); }, []); + const clearAll = useCallback(() => { + setActive({ + availability: false, + productTypes: [], + vendors: [], + tags: [], + colors: [], + styles: [], + sizes: [], + materials: [], + minPrice: '', + maxPrice: '', + metafieldValues: {}, + }); + }, []); + const filters: SearchFilters = { q: query, sort, @@ -449,113 +427,102 @@ export function SearchProductsView(props: SearchProductsProps) {
- {/* Mobile filter toggle */} -
+ {/* Search bar (desktop only — mobile lives in header above) */} +
+ setInputValue(e.target.value)} + placeholder="Search products…" + className="flex-1 rounded-md border border-border bg-background px-4 py-2.5 text-sm outline-none focus:border-foreground" + /> - +
+ + {/* Filter + sort bar */} +
+ + + + + + + Filters + +
+ +
+ + + + +
+
+ +
+

+ {loading ? 'Loading…' : `${products.length} product${products.length === 1 ? '' : 's'}`} +

+ +
- {/* Mobile filter panel */} - {mobileFiltersOpen && ( -
- + {error && ( +

+ {error} +

+ )} + + {/* Grid */} +
+ {loading + ? Array.from({ length: props.limit }).map((_, i) => ( + + )) + : products.map((p) => )} +
+ + {!loading && products.length === 0 && !error && ( +
+ No products found.{query ? ` Try a different search term.` : ''}
)} -
- {/* Desktop sidebar */} -
- + {hasNextPage && !loading && ( +
+
- - {/* Product area */} -
- {/* Search bar (desktop only) */} -
- setInputValue(e.target.value)} - placeholder="Search products…" - className="flex-1 rounded-md border border-border bg-background px-4 py-2.5 text-sm outline-none focus:border-foreground" - /> - -
- - {/* Sort + count bar */} -
-

- {loading ? 'Loading…' : `${products.length} product${products.length === 1 ? '' : 's'}`} -

-
- -
-
- - {error && ( -

- {error} -

- )} - - {/* Grid */} -
- {loading - ? Array.from({ length: props.limit }).map((_, i) => ( - - )) - : products.map((p) => )} -
- - {!loading && products.length === 0 && !error && ( -
- No products found.{query ? ` Try a different search term.` : ''} -
- )} - - {hasNextPage && !loading && ( -
- -
- )} -
-
+ )}
);