Add collection filters and sort, out-of-stock options, gallery drag

- Collections: filters and sort via the Storefront API — the products
  connection now takes `filters`/`after` and returns its facets, with
  cursor-based load more on the collection page
- Extract ProductFilters (renamed from SearchFilters) and a shared
  ProductToolbar so search and collections use the same chrome
- PDP: mark option values with no in-stock variant using a diagonal
  strike, computed against the other selected options
- PDP: pointer drag-scrolling for the mobile image carousel, since a
  scroll container doesn't drag with a mouse
- Header/cart: circular icon buttons, tighter spacing, and icon sizes
  set by class (Button's base CSS clamps un-classed svgs to size-4)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jWbNNJLksC1QG8z8845FX
This commit is contained in:
Rami Bitar
2026-08-01 12:27:05 -04:00
co-authored by Claude Opus 5
parent cc901b9ce8
commit 2ef5639a2e
13 changed files with 515 additions and 144 deletions
+12 -56
View File
@@ -3,10 +3,10 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import ProductCard from './product-card';
import SearchFilters from './search-filters';
import ProductFilters from './product-filters';
import ProductToolbar from './product-toolbar';
import { Button } from '@/components/ui/button';
import { Loader } from '@/app/components/ui/loader';
import { RiEqualizerLine, RiArrowDownSLine, RiCheckLine } from '@remixicon/react';
import {
searchProducts,
type SearchFilter,
@@ -42,7 +42,6 @@ const SearchResults: React.FC = () => {
const [hasNextPage, setHasNextPage] = useState(false);
const [sortIndex, setSortIndex] = useState(0);
const [sortOpen, setSortOpen] = useState(false);
const [filtersOpen, setFiltersOpen] = useState(false);
const [activeFilters, setActiveFilters] = useState<string[]>([]);
@@ -126,58 +125,15 @@ const SearchResults: React.FC = () => {
Search
</h1>
{/* Toolbar */}
<div className="mt-6 flex items-center justify-between">
<Button
onClick={() => setFiltersOpen(true)}
variant="ghost"
className="gap-x-2 px-0 font-normal hover:bg-transparent"
>
<RiEqualizerLine size={18} />
Filters
</Button>
<div className="flex items-center gap-x-4">
<span className="text-sm text-muted-foreground tabular-nums">
{totalCount} {totalCount === 1 ? 'Item' : 'Items'}
</span>
<div className="relative">
<Button
onClick={() => setSortOpen((prev) => !prev)}
variant="ghost"
aria-expanded={sortOpen}
className="gap-x-1 px-0 font-normal hover:bg-transparent"
>
Sort
<RiArrowDownSLine size={16} />
</Button>
{sortOpen && (
<>
<div
className="fixed inset-0 z-40"
onClick={() => setSortOpen(false)}
/>
<div className="absolute right-0 top-full z-50 mt-1 w-56 rounded-md border border-border bg-background py-1 shadow-md">
{SORT_OPTIONS.map((option, index) => (
<button
key={option.label}
onClick={() => {
setSortIndex(index);
setSortOpen(false);
}}
className="flex w-full items-center justify-between px-4 py-2 text-left text-sm text-foreground hover:bg-accent"
>
{option.label}
{index === sortIndex && <RiCheckLine size={16} />}
</button>
))}
</div>
</>
)}
</div>
</div>
<div className="mt-6">
<ProductToolbar
totalCount={totalCount}
onOpenFilters={() => setFiltersOpen(true)}
sortOptions={SORT_OPTIONS}
sortIndex={sortIndex}
onSortChange={setSortIndex}
activeFilterCount={activeFilters.length}
/>
</div>
{/* Results */}
@@ -227,7 +183,7 @@ const SearchResults: React.FC = () => {
</div>
</div>
<SearchFilters
<ProductFilters
open={filtersOpen}
onOpenChange={setFiltersOpen}
filters={filters}