refactor(filters): compose the storefront from dimensions and delete the old components (#188)

Replaces App.tsx's hand-written Segmented/Filters-button/ActiveFilterChips
region with a single FilterBar composed from five dimensions, and removes
the FilterDrawer and ActiveFilterChips components along with the
activeFilterCount and hasActiveFilters helpers they were the only callers
of. Catalogue now receives a filtered boolean computed the same way
FilterBar computes its own chip tally, rather than the ItemFilters object
it only ever used for that one check.
This commit is contained in:
2026-08-25 15:39:18 -05:00
parent 2f0da9afe1
commit 147e280f88
4 changed files with 40 additions and 502 deletions
+40 -73
View File
@@ -10,25 +10,21 @@ import theme from 'antd/es/theme';
import Badge from 'antd/es/badge';
import Empty from 'antd/es/empty';
import Alert from 'antd/es/alert';
import Segmented from 'antd/es/segmented';
import { ShoppingCartOutlined, FilterOutlined } from '@ant-design/icons';
import { ShoppingCartOutlined } from '@ant-design/icons';
import { Link, useLocation, useSearchParams } from 'react-router-dom';
import { Item } from './api';
import { useCatalogue } from './useCatalogue';
import ItemCard from './components/ItemCard';
import BrandMark from './components/BrandMark';
import FilterDrawer from './components/FilterDrawer';
import ActiveFilterChips from './components/ActiveFilterChips';
import FilterBar from './components/filters/FilterBar';
import {
ItemFilters,
SaleState,
STOREFRONT_SALE_STATUSES,
activeFilterCount,
filtersFromSearchParams,
filtersToSearchParams,
hasActiveFilters,
saleStateFromStatuses
} from './filters';
availabilityDimension,
categoryDimension,
favoritesDimension,
priceDimension,
tagDimension
} from './components/filters/standardDimensions';
import { ItemFilters, filtersFromSearchParams, filtersToSearchParams } from './filters';
import AuthPromptModal from './customer/AuthPromptModal';
import { useThemeMode } from './theme/ThemeContext';
import { useCustomerAuth } from './customer/CustomerAuthContext';
@@ -44,7 +40,7 @@ type CatalogueProps = Readonly<{
failed: boolean;
loading: boolean;
items: Item[];
filters: ItemFilters;
filtered: boolean;
needsFavoritesAuth: boolean;
onRetry: () => void;
onSignIn: () => void;
@@ -61,7 +57,7 @@ function Catalogue({
failed,
loading,
items,
filters,
filtered,
needsFavoritesAuth,
onRetry,
onSignIn,
@@ -93,7 +89,6 @@ function Catalogue({
if (!loading && !items.length) {
// Distinguished so "no items match these filters" never reads as an empty
// shop, and so the way out is offered only when there is one.
const filtered = hasActiveFilters(filters);
return (
<Empty description={filtered ? 'No items match these filters' : 'No items yet — check back soon'}>
{filtered ? <Button onClick={onClearFilters}>Clear filters</Button> : null}
@@ -112,8 +107,18 @@ function Catalogue({
);
}
// Availability first and always visible, because it is the coarsest cut and
// worth seeing without opening anything. Favorites next, so someone who came
// for their favorites does not scroll past the catalogue controls.
const STOREFRONT_DIMENSIONS = [
availabilityDimension,
favoritesDimension,
categoryDimension,
tagDimension,
priceDimension
];
export default function App() {
const [drawerOpen, setDrawerOpen] = useState(false);
const [authModalOpen, setAuthModalOpen] = useState(false);
const [searchParams, setSearchParams] = useSearchParams();
const location = useLocation();
@@ -143,7 +148,17 @@ export default function App() {
setSearchParams(new URLSearchParams(), { replace: true });
}, [setSearchParams]);
const activeCount = activeFilterCount(filters);
// The parent needs to know whether anything is filtering — for the empty
// state's wording — but has no chip row of its own to count. Computed the
// same way FilterBar computes its own tally, from the same dimensions.
const filterContext = {
filters,
onChange: applyFilters,
categories: options?.categories ?? [],
tags: options?.tags ?? [],
priceRange: options?.priceRange ?? null
};
const filtered = STOREFRONT_DIMENSIONS.some((dimension) => dimension.chips(filterContext).length > 0);
return (
<Layout style={{ minHeight: '100vh' }}>
@@ -191,50 +206,15 @@ export default function App() {
</Header>
<Content style={{ padding: 24 }}>
<div className="filter-bar">
{/* In the bar rather than inside the drawer, deliberately. The default
now hides sold pieces, so a customer who never opens the drawer
would otherwise have no way to know sold items exist — and on a
one-of-a-kind catalogue the sold pieces are part of the story. */}
<Segmented
aria-label="Filter by availability"
// The fallback matches the server's: the favorites view defaults to
// everything, so the control must not claim Not Sold while sold
// favorites are on screen.
value={saleStateFromStatuses(
filters.status,
STOREFRONT_SALE_STATUSES,
filters.favoritesOnly ? 'all' : 'not-sold'
)}
onChange={(value) => {
const state = value as SaleState;
applyFilters({
...filters,
// Not Sold is the default, so it is stored as "no preference"
// rather than as an explicit list. That keeps it out of the URL
// and out of the Filters (N) count, where it would otherwise
// show as an active filter nobody chose.
status: state === 'not-sold' ? null : STOREFRONT_SALE_STATUSES[state]
});
}}
options={[
{ label: 'Not sold', value: 'not-sold' },
{ label: 'Sold', value: 'sold' },
{ label: 'All', value: 'all' }
]}
/>
<Button
icon={<FilterOutlined />}
onClick={() => setDrawerOpen(true)}
type={activeCount ? 'primary' : 'default'}
>
Filters{activeCount ? ` (${activeCount})` : ''}
</Button>
<ActiveFilterChips
categories={options?.categories ?? []}
tags={options?.tags ?? []}
<FilterBar
dimensions={STOREFRONT_DIMENSIONS}
filters={filters}
onChange={applyFilters}
onClear={clearFilters}
categories={options?.categories ?? []}
tags={options?.tags ?? []}
priceRange={options?.priceRange ?? null}
resultCount={items.length}
/>
</div>
@@ -265,7 +245,7 @@ export default function App() {
failed={failed}
loading={loading}
items={items}
filters={filters}
filtered={filtered}
needsFavoritesAuth={needsFavoritesAuth}
onRetry={retry}
onSignIn={openAuthModal}
@@ -278,19 +258,6 @@ export default function App() {
<Link to="/privacy">Privacy Policy</Link>
</Footer>
<FilterDrawer
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
categories={options?.categories ?? []}
tags={options?.tags ?? []}
priceRange={options?.priceRange ?? null}
filters={filters}
onChange={applyFilters}
onClear={clearFilters}
resultCount={items.length}
showFavorites
/>
{/* The same prompt the heart button and Add to Cart use. Signing in
resolves the gate above, and the filter then applies on its own — the
customer never has to set it a second time. */}