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:
+40
-73
@@ -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. */}
|
||||
|
||||
Reference in New Issue
Block a user