feat(filters): add FilterBar, one component both screens compose (#188)
This commit is contained in:
@@ -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) => (
|
||||
<div key={dimension.key}>{dimension.render(context)}</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
icon={<FilterOutlined />}
|
||||
onClick={() => setDrawerOpen(true)}
|
||||
type={chips.length ? 'primary' : 'default'}
|
||||
>
|
||||
Filters{chips.length ? ` (${chips.length})` : ''}
|
||||
</Button>
|
||||
|
||||
<FilterChips chips={chips} onClear={onClear} />
|
||||
|
||||
<Drawer
|
||||
title="Filters"
|
||||
placement="right"
|
||||
open={drawerOpen}
|
||||
onClose={() => setDrawerOpen(false)}
|
||||
// Unmounting on close keeps a single copy of controls like "Clear all"
|
||||
// in the document at any time.
|
||||
destroyOnHidden
|
||||
width={screens.md ? 380 : '90%'}
|
||||
footer={
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Button block onClick={onClear}>Clear all</Button>
|
||||
<Button block type="primary" onClick={() => setDrawerOpen(false)}>
|
||||
Show {resultCount} {resultCount === 1 ? 'item' : 'items'}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{drawerDimensions.map((dimension, index) => (
|
||||
<section
|
||||
key={dimension.key}
|
||||
style={index < drawerDimensions.length - 1 ? { marginBottom: 28 } : undefined}
|
||||
>
|
||||
{dimension.heading && <h4 style={sectionHeading}>{dimension.heading}</h4>}
|
||||
{dimension.render(context)}
|
||||
</section>
|
||||
))}
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user