diff --git a/frontend/src/components/filters/FilterBar.tsx b/frontend/src/components/filters/FilterBar.tsx new file mode 100644 index 0000000..d2aeb90 --- /dev/null +++ b/frontend/src/components/filters/FilterBar.tsx @@ -0,0 +1,114 @@ +import { useMemo, useState } from 'react'; +import Drawer from 'antd/es/drawer'; +import Button from 'antd/es/button'; +import Grid from 'antd/es/grid'; +import { FilterOutlined } from '@ant-design/icons'; +import type { Category, Tag as ItemTag } from '../../api'; +import type { ItemFilters } from '../../filters'; +import type { FilterContext, FilterDimension } from './dimension'; +import FilterChips from './FilterChips'; + +type Props = Readonly<{ + /** Render order. A screen composes the filters it offers. */ + dimensions: FilterDimension[]; + filters: ItemFilters; + onChange: (next: ItemFilters) => void; + onClear: () => void; + categories: Category[]; + tags: ItemTag[]; + priceRange: { min_cents: number; max_cents: number } | null; + resultCount: number; +}>; + +const sectionHeading: React.CSSProperties = { + margin: '0 0 8px', + fontSize: 12, + letterSpacing: '.06em', + textTransform: 'uppercase', + opacity: 0.65 +}; + +/** + * Filtering, for any screen that does it. + * + * The screen says which dimensions it offers; this owns everything around them + * — the always-visible controls, the Filters button and its tally, the chip + * row, and the drawer. Before #188 the drawer was shared and this was written + * twice, which is how the admin's tally drifted from the storefront's. + * + * The tally is the number of chips rather than a second count of the same + * thing, so the button and the chip row cannot disagree. + */ +export default function FilterBar({ + dimensions, + filters, + onChange, + onClear, + categories, + tags, + priceRange, + resultCount +}: Props) { + const [drawerOpen, setDrawerOpen] = useState(false); + const screens = Grid.useBreakpoint(); + + const context: FilterContext = useMemo( + () => ({ filters, onChange, categories, tags, priceRange }), + [filters, onChange, categories, tags, priceRange] + ); + + // Derived, not stored, and derived without rendering anything — the drawer + // unmounts its sections when closed, so anything that needed them mounted + // would lose the chips exactly when they matter. + const chips = dimensions.flatMap((dimension) => dimension.chips(context)); + + const barDimensions = dimensions.filter((dimension) => dimension.placement === 'bar'); + const drawerDimensions = dimensions.filter((dimension) => dimension.placement === 'drawer'); + + return ( + <> + {barDimensions.map((dimension) => ( +