feat(filters): add favorites and status dimensions (#188)

This commit is contained in:
2026-08-25 15:12:35 -05:00
parent 60b76cae82
commit eb5799dd17
2 changed files with 130 additions and 1 deletions
@@ -1,6 +1,7 @@
import TreeSelect from 'antd/es/tree-select';
import Empty from 'antd/es/empty';
import Select from 'antd/es/select';
import Switch from 'antd/es/switch';
import Tag from 'antd/es/tag';
import Slider from 'antd/es/slider';
import InputNumber from 'antd/es/input-number';
@@ -8,6 +9,9 @@ import {
buildCategoryTree,
categoryPath,
formatPriceRange,
ItemStatus,
STATUS_OPTIONS,
statusLabel,
toCategoryTreeData
} from '../../filters';
import type { FilterDimension } from './dimension';
@@ -190,3 +194,78 @@ export const priceDimension: FilterDimension = {
}
]
};
export const favoritesDimension: FilterDimension = {
key: 'favorites',
placement: 'drawer',
heading: 'Favorites',
// Shown to signed-out visitors too: switching it on prompts them to sign in,
// which is how they learn favorites exist. The prompt itself is not this
// dimension's business — useCatalogue reports needsFavoritesAuth and the page
// owns the modal.
render: ({ filters, onChange }) => (
// Deliberately not wrapped in a <label>: antd renders the switch as a
// button, which is labelable, so a wrapping label can forward a click the
// switch already handled and toggle it twice.
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<Switch
checked={filters.favoritesOnly}
onChange={(checked) => onChange({ ...filters, favoritesOnly: checked })}
aria-label="Only my favorites"
/>
<span>Only my favorites</span>
</div>
),
chips: ({ filters, onChange }) =>
filters.favoritesOnly
? [
{
key: 'favorites',
label: 'My favorites',
onRemove: () => onChange({ ...filters, favoritesOnly: false })
}
]
: []
};
export const statusDimension: FilterDimension = {
key: 'status',
placement: 'drawer',
heading: 'Status — any of these',
// The status dimension itself rather than presets over it, which #105's Sold
// / Not sold / All control was. Presets could not express Published or
// Unpublished and could not isolate Reserved. Selecting statuses answers all
// of them: Unpublished is Pending, Published is the other three, and Not sold
// is everything except Sold. See #132.
render: ({ filters, onChange }) => (
<Select
allowClear
mode="multiple"
showSearch
optionFilterProp="label"
placeholder="Any status"
aria-label="Filter by status"
style={{ width: '100%' }}
value={filters.status ?? []}
onChange={(value: ItemStatus[]) =>
onChange({ ...filters, status: value.length ? value : null })
}
options={STATUS_OPTIONS}
/>
),
chips: ({ filters, onChange }) =>
(filters.status ?? []).map((status) => ({
key: `status-${status}`,
label: statusLabel(status),
onRemove: () => {
const rest = (filters.status ?? []).filter((value) => value !== status);
// Null rather than an empty list: emptying it means "no status filter",
// where an empty list would mean "no statuses" and show nothing.
onChange({ ...filters, status: rest.length ? rest : null });
}
}))
};