import { useEffect, useState, useCallback, useMemo } from 'react'; import { Layout, Typography, Switch, Row, Col, Spin, Button, theme, Badge, Empty, Alert } from 'antd'; import { ShoppingCartOutlined, FilterOutlined } from '@ant-design/icons'; import { Link, useLocation, useSearchParams } from 'react-router-dom'; import { Item, FilterOptions, fetchItems, fetchFilterOptions } from './api'; import ItemCard from './components/ItemCard'; import FilterDrawer from './components/FilterDrawer'; import ActiveFilterChips from './components/ActiveFilterChips'; import { ItemFilters, activeFilterCount, filtersFromSearchParams, filtersToSearchParams, hasActiveFilters } from './filters'; import AuthPromptModal from './customer/AuthPromptModal'; import { useThemeMode } from './theme/ThemeContext'; import { useCustomerAuth } from './customer/CustomerAuthContext'; import { useCart } from './cart/CartContext'; const { Header, Content, Footer } = Layout; const { Title } = Typography; // Dragging the price slider fires a change per pixel; without this every one // would become its own request. const FILTER_DEBOUNCE_MS = 250; export default function App() { const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [failed, setFailed] = useState(false); const [options, setOptions] = useState(null); const [drawerOpen, setDrawerOpen] = useState(false); const [authModalOpen, setAuthModalOpen] = useState(false); const [searchParams, setSearchParams] = useSearchParams(); const location = useLocation(); const { mode, toggle } = useThemeMode(); const { customer, loading: authLoading } = 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 filterKey = filtersToSearchParams(filters).toString(); // "Only my favorites" needs to know who is asking. Until the session has // resolved we hold rather than guess: firing the request early would 401 and // show the outage banner to someone who is in fact signed in. const awaitingAuth = filters.favoritesOnly && authLoading; const needsFavoritesAuth = filters.favoritesOnly && !authLoading && !customer; 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 load = useCallback(() => { return fetchItems(filtersFromSearchParams(new URLSearchParams(filterKey))) .then((loaded) => { setItems(loaded); setFailed(false); }) // A failed request must never fall through to the empty state: telling a // customer "no items yet" when the server is broken hides the outage and // reads as an empty shop. .catch(() => setFailed(true)) .finally(() => setLoading(false)); }, [filterKey]); useEffect(() => { if (awaitingAuth) { setLoading(true); return; } // Prompt instead of requesting. The server would answer 401, and rendering // that as "no items match these filters" would tell a signed-out visitor // they have no favorites rather than that we do not know who they are. if (needsFavoritesAuth) { setItems([]); setFailed(false); setLoading(false); setAuthModalOpen(true); return; } setLoading(true); const timer = setTimeout(() => void load(), FILTER_DEBOUNCE_MS); return () => clearTimeout(timer); }, [load, awaitingAuth, needsFavoritesAuth]); useEffect(() => { fetchFilterOptions().then(setOptions).catch(() => setOptions(null)); }, []); // Adding to cart flips an item to reserved, and the filter options' price // bounds shift as inventory changes. const reload = useCallback(() => { void load(); fetchFilterOptions().then(setOptions).catch(() => undefined); }, [load]); const activeCount = activeFilterCount(filters); return (
Redefined Designs
) : ( <> )}
{loading && !items.length && !failed ? : null} {failed ? ( { setLoading(true); void load(); }}>Retry} /> ) : needsFavoritesAuth ? ( ) : !loading && !items.length ? ( {hasActiveFilters(filters) ? : null} ) : ( {items.map(item => ( ))} )}
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)} />
); }