import { useState, useCallback, useMemo } from 'react'; import Layout from 'antd/es/layout'; import Typography from 'antd/es/typography'; import Switch from 'antd/es/switch'; import Row from 'antd/es/row'; import Col from 'antd/es/col'; import Spin from 'antd/es/spin'; import Button from 'antd/es/button'; 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 { 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 { ItemFilters, SaleState, STOREFRONT_SALE_STATUSES, activeFilterCount, filtersFromSearchParams, filtersToSearchParams, hasActiveFilters, saleStateFromStatuses } from './filters'; import AuthPromptModal from './customer/AuthPromptModal'; import { useThemeMode } from './theme/ThemeContext'; import { useCustomerAuth } from './customer/CustomerAuthContext'; import { useCart } from './cart/CartContext'; import ErrorBoundary from './components/ErrorBoundary'; import ErrorFallback from './components/ErrorFallback'; import DevThrow from './components/DevThrow'; const { Header, Content, Footer } = Layout; const { Title } = Typography; type CatalogueProps = Readonly<{ failed: boolean; loading: boolean; items: Item[]; filters: ItemFilters; needsFavoritesAuth: boolean; onRetry: () => void; onSignIn: () => void; onClearFilters: () => void; onChanged: () => void; }>; // The body of the catalogue: an outage, a sign-in prompt, an empty state, or // the grid. Extracted from App so the four cases read as early returns rather // than a ternary chain, and because a function's cognitive complexity counts // everything nested inside it — leaving this inline is what put App over the // limit. function Catalogue({ failed, loading, items, filters, needsFavoritesAuth, onRetry, onSignIn, onClearFilters, onChanged }: CatalogueProps) { if (failed) { return ( Retry} /> ); } // Prompted rather than requested: see the effect in App that sets this. if (needsFavoritesAuth) { return ( ); } 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 ( {filtered ? : null} ); } return ( {items.map(item => ( ))} ); } export default function App() { const [drawerOpen, setDrawerOpen] = useState(false); const [authModalOpen, setAuthModalOpen] = useState(false); const [searchParams, setSearchParams] = useSearchParams(); const location = useLocation(); const { mode, toggle } = useThemeMode(); const { customer } = useCustomerAuth(); const { items: cartItems } = useCart(); const { token } = theme.useToken(); // The URL is the single source of truth for filter state, so a reload, a // shared link, and the back button all restore the same view. const filters = useMemo(() => filtersFromSearchParams(searchParams), [searchParams]); const openAuthModal = useCallback(() => setAuthModalOpen(true), []); const { items, loading, failed, options, needsFavoritesAuth, reload, retry, filterKey } = useCatalogue(filters, openAuthModal); const applyFilters = useCallback( (next: ItemFilters) => { // replace, not push: dragging a slider shouldn't bury the previous page // under dozens of history entries. setSearchParams(filtersToSearchParams(next), { replace: true }); }, [setSearchParams] ); const clearFilters = useCallback(() => { setSearchParams(new URLSearchParams(), { replace: true }); }, [setSearchParams]); const activeCount = activeFilterCount(filters); return (
{/* Wrapped so the mark and the wordmark travel together — the title keeps its own ellipsis behaviour when the header gets narrow, and the mark is never what gets truncated. */}
Redefined Designs
) : ( <> )}
{/* 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. */} { 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' } ]} />
{loading && !items.length && !failed ? : null} ( window.location.reload()}> Reload } /> )} > {import.meta.env.DEV && }
Privacy Policy
setDrawerOpen(false)} options={options} filters={filters} onChange={applyFilters} onClear={clearFilters} resultCount={items.length} /> {/* 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. */} setAuthModalOpen(false)} onSuccess={() => setAuthModalOpen(false)} />
); }