Compare commits

...

2 Commits

Author SHA1 Message Date
Rami Bitar
572de32e1a Merge staging: filter sheet drawers 2026-05-09 15:23:12 -04:00
Rami Bitar
906f2934fb 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 <noreply@anthropic.com>
2026-05-09 15:20:50 -04:00
2 changed files with 201 additions and 270 deletions

View File

@@ -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 (
<aside className="w-full shrink-0 md:w-52 lg:w-56">
<div className="flex items-center justify-between border-b border-border pb-4">
<span className="text-xs font-semibold uppercase tracking-[0.15em]">Filters</span>
{hasActiveFilters && (
<button
type="button"
onClick={() =>
onChange({
availability: false,
productTypes: [],
vendors: [],
tags: [],
colors: [],
styles: [],
sizes: [],
materials: [],
minPrice: '',
maxPrice: '',
metafieldValues: {},
})
}
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
>
<X size={12} /> Clear all
</button>
)}
</div>
<div className="w-full">
{props.showAvailability === 'yes' && (
<FilterGroup label="Availability">
<Checkbox
@@ -352,7 +314,7 @@ function Sidebar({
</FilterGroup>
);
})}
</aside>
</div>
);
}
@@ -400,7 +362,7 @@ export function CollectionView(props: CollectionProps) {
const [sort, setSort] = useState<CollectionSortKey>(defaultSort);
const [reverse, setReverse] = useState(false);
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false);
const [filtersOpen, setFiltersOpen] = useState(false);
const [active, setActive] = useState<ActiveFilters>({
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,54 +469,42 @@ export function CollectionView(props: CollectionProps) {
) : null}
</header>
{/* Mobile filter toggle */}
<div className="mb-4 flex items-center justify-between md:hidden">
{/* Filter + sort bar */}
<div className="mb-6 flex items-center justify-between">
<Sheet open={filtersOpen} onOpenChange={setFiltersOpen}>
<SheetTrigger asChild>
<button
type="button"
onClick={() => setMobileFiltersOpen((o) => !o)}
className="flex items-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium"
className="flex items-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium hover:bg-muted"
>
<SlidersHorizontal size={14} />
Filters
</button>
<Select value={sortValue} onValueChange={handleSortChange}>
<SelectTrigger className="h-auto px-3 py-2 text-sm">
<SelectValue>
{[...SORT_OPTIONS, { label: 'Price: High to Low', value: 'PRICE_DESC' }].find((o) => o.value === sortValue)?.label}
</SelectValue>
</SelectTrigger>
<SelectContent>
{SORT_OPTIONS.map((o) => (
<SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>
))}
<SelectItem value="PRICE_DESC">Price: High to Low</SelectItem>
</SelectContent>
</Select>
</div>
{/* Mobile filter panel */}
{mobileFiltersOpen && (
<div className="mb-6 rounded-lg border border-border p-4 md:hidden">
</SheetTrigger>
<SheetContent side="left" className="w-full sm:max-w-md">
<SheetHeader>
<SheetTitle>Filters</SheetTitle>
</SheetHeader>
<div className="flex-1 overflow-y-auto px-4">
<Sidebar props={props} active={active} onChange={patchActive} />
</div>
)}
<SheetFooter className="flex-row gap-2 border-t border-border">
<Button variant="outline" className="flex-1" onClick={clearAll}>
Clear
</Button>
<Button className="flex-1" onClick={() => setFiltersOpen(false)}>
Search
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
<div className="flex gap-10">
{/* Desktop sidebar */}
<div className="hidden md:block">
<Sidebar props={props} active={active} onChange={patchActive} />
</div>
{/* Product area */}
<div className="min-w-0 flex-1">
{/* Sort + count bar */}
<div className="mb-6 flex items-center justify-between">
<p className="text-sm text-muted-foreground">
<div className="flex items-center gap-4">
<p className="hidden text-sm text-muted-foreground sm:block">
{loading ? 'Loading…' : `${products.length} product${products.length === 1 ? '' : 's'}`}
</p>
<div className="hidden md:block">
<Select value={sortValue} onValueChange={handleSortChange}>
<SelectTrigger className="h-auto px-3 py-1.5 text-sm">
<SelectTrigger className="h-auto px-3 py-2 text-sm">
<SelectValue>
{[...SORT_OPTIONS, { label: 'Price: High to Low', value: 'PRICE_DESC' }].find((o) => o.value === sortValue)?.label}
</SelectValue>
@@ -580,8 +546,6 @@ export function CollectionView(props: CollectionProps) {
</div>
)}
</div>
</div>
</div>
</section>
);
}

View File

@@ -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 (
<aside className="w-full shrink-0 md:w-52 lg:w-56">
<div className="flex items-center justify-between border-b border-border pb-4">
<span className="text-xs font-semibold uppercase tracking-[0.15em]">Filters</span>
{hasActiveFilters && (
<button
type="button"
onClick={() =>
onChange({
availability: false,
productTypes: [],
vendors: [],
tags: [],
colors: [],
styles: [],
sizes: [],
materials: [],
minPrice: '',
maxPrice: '',
metafieldValues: {},
})
}
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
>
<X size={12} /> Clear all
</button>
)}
</div>
<div className="w-full">
{props.showAvailability === 'yes' && (
<FilterGroup label="Availability">
<Checkbox
@@ -347,7 +309,7 @@ function Sidebar({
</FilterGroup>
);
})}
</aside>
</div>
);
}
@@ -360,7 +322,7 @@ export function SearchProductsView(props: SearchProductsProps) {
const [query, setQuery] = useState(initialQ);
const [inputValue, setInputValue] = useState(initialQ);
const [sort, setSort] = useState<SortOption>(props.defaultSort);
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false);
const [filtersOpen, setFiltersOpen] = useState(false);
const [active, setActive] = useState<ActiveFilters>({
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,44 +427,7 @@ export function SearchProductsView(props: SearchProductsProps) {
</form>
</div>
{/* Mobile filter toggle */}
<div className="mb-4 flex items-center justify-between md:hidden">
<button
type="button"
onClick={() => setMobileFiltersOpen((o) => !o)}
className="flex items-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium"
>
<SlidersHorizontal size={14} />
Filters
</button>
<Select value={sort} onValueChange={(v) => setSort(v as SortOption)}>
<SelectTrigger className="h-auto px-3 py-2 text-sm">
<SelectValue>{SORT_OPTIONS.find((o) => o.value === sort)?.label}</SelectValue>
</SelectTrigger>
<SelectContent>
{SORT_OPTIONS.map((o) => (
<SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Mobile filter panel */}
{mobileFiltersOpen && (
<div className="mb-6 rounded-lg border border-border p-4 md:hidden">
<Sidebar props={props} active={active} onChange={patchActive} />
</div>
)}
<div className="flex gap-10">
{/* Desktop sidebar */}
<div className="hidden md:block">
<Sidebar props={props} active={active} onChange={patchActive} />
</div>
{/* Product area */}
<div className="min-w-0 flex-1">
{/* Search bar (desktop only) */}
{/* Search bar (desktop only — mobile lives in header above) */}
<form onSubmit={handleSearch} className="mb-4 hidden gap-2 md:flex">
<input
type="search"
@@ -503,14 +444,42 @@ export function SearchProductsView(props: SearchProductsProps) {
</button>
</form>
{/* Sort + count bar */}
{/* Filter + sort bar */}
<div className="mb-6 flex items-center justify-between">
<p className="text-sm text-muted-foreground">
<Sheet open={filtersOpen} onOpenChange={setFiltersOpen}>
<SheetTrigger asChild>
<button
type="button"
className="flex items-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium hover:bg-muted"
>
<SlidersHorizontal size={14} />
Filters
</button>
</SheetTrigger>
<SheetContent side="left" className="w-full sm:max-w-md">
<SheetHeader>
<SheetTitle>Filters</SheetTitle>
</SheetHeader>
<div className="flex-1 overflow-y-auto px-4">
<Sidebar props={props} active={active} onChange={patchActive} />
</div>
<SheetFooter className="flex-row gap-2 border-t border-border">
<Button variant="outline" className="flex-1" onClick={clearAll}>
Clear
</Button>
<Button className="flex-1" onClick={() => setFiltersOpen(false)}>
Search
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
<div className="flex items-center gap-4">
<p className="hidden text-sm text-muted-foreground sm:block">
{loading ? 'Loading…' : `${products.length} product${products.length === 1 ? '' : 's'}`}
</p>
<div className="hidden md:block">
<Select value={sort} onValueChange={(v) => setSort(v as SortOption)}>
<SelectTrigger className="h-auto px-3 py-1.5 text-sm">
<SelectTrigger className="h-auto px-3 py-2 text-sm">
<SelectValue>{SORT_OPTIONS.find((o) => o.value === sort)?.label}</SelectValue>
</SelectTrigger>
<SelectContent>
@@ -555,8 +524,6 @@ export function SearchProductsView(props: SearchProductsProps) {
</div>
)}
</div>
</div>
</div>
</section>
);
}