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>
This commit is contained in:
@@ -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,96 +469,82 @@ export function CollectionView(props: CollectionProps) {
|
||||
) : null}
|
||||
</header>
|
||||
|
||||
{/* 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={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>
|
||||
{/* Filter + sort bar */}
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<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>
|
||||
<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>
|
||||
</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} />
|
||||
{/* Grid */}
|
||||
<div className={cn('grid gap-x-6 gap-y-10', colClass[columns])}>
|
||||
{loading
|
||||
? Array.from({ length: limit }).map((_, i) => (
|
||||
<Skeleton key={i} className="aspect-[4/5] w-full" />
|
||||
))
|
||||
: products.map((p: any) => <ProductCard key={p.id} product={p} />)}
|
||||
</div>
|
||||
|
||||
{!loading && products.length === 0 && (
|
||||
<div className="mt-16 text-center text-sm text-muted-foreground">
|
||||
No products found in this collection.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-10">
|
||||
{/* Desktop sidebar */}
|
||||
<div className="hidden md:block">
|
||||
<Sidebar props={props} active={active} onChange={patchActive} />
|
||||
{hasNextPage && !loading && (
|
||||
<div className="mt-12 flex justify-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={fetchMore}
|
||||
className="rounded-md border border-border px-8 py-3 text-sm font-medium hover:bg-muted"
|
||||
>
|
||||
Load more
|
||||
</button>
|
||||
</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">
|
||||
{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">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* Grid */}
|
||||
<div className={cn('grid gap-x-6 gap-y-10', colClass[columns])}>
|
||||
{loading
|
||||
? Array.from({ length: limit }).map((_, i) => (
|
||||
<Skeleton key={i} className="aspect-[4/5] w-full" />
|
||||
))
|
||||
: products.map((p: any) => <ProductCard key={p.id} product={p} />)}
|
||||
</div>
|
||||
|
||||
{!loading && products.length === 0 && (
|
||||
<div className="mt-16 text-center text-sm text-muted-foreground">
|
||||
No products found in this collection.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasNextPage && !loading && (
|
||||
<div className="mt-12 flex justify-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={fetchMore}
|
||||
className="rounded-md border border-border px-8 py-3 text-sm font-medium hover:bg-muted"
|
||||
>
|
||||
Load more
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user