diff --git a/frontend/src/components/FacetGroups.jsx b/frontend/src/components/FacetGroups.jsx new file mode 100644 index 0000000..d7fceef --- /dev/null +++ b/frontend/src/components/FacetGroups.jsx @@ -0,0 +1,97 @@ +// FacetGroups.jsx — §22.4a SLICE-3 faceted left-pane filters (§5.1). +// +// Renders one collapsible group per facet field returned by the API, each with +// per-value result counts and multi-select checkboxes. A `tags`-type field gets +// a "filter values…" search box so it stays usable at 30+ values. Selection +// state + the malformed toggle are owned by the parent (Catalog), which +// re-fetches server-side on change. + +import { useState } from 'react' + +// Human label for the built-in state facet; declared fields use their own name +// (or the schema `label` when the API supplies it on `fields`). +function groupLabel(field, fields) { + if (field === 'state') return 'State' + const def = fields?.[field] + return def?.label || field.charAt(0).toUpperCase() + field.slice(1) +} + +function FacetGroup({ field, fields, counts, selected, onToggle, isTags }) { + const [open, setOpen] = useState(true) + const [valueSearch, setValueSearch] = useState('') + const entries = Object.entries(counts).sort((a, b) => b[1] - a[1]) + const needle = valueSearch.trim().toLowerCase() + const shown = isTags && needle + ? entries.filter(([v]) => v.toLowerCase().includes(needle)) + : entries + return ( +
+ + {open && ( +
+ {isTags && entries.length > 8 && ( + setValueSearch(e.target.value)} + /> + )} + {shown.length === 0 && ( +
No values.
+ )} + {shown.map(([value, count]) => ( + + ))} +
+ )} +
+ ) +} + +export default function FacetGroups({ + facets, fields, selections, onToggle, malformedOnly, onToggleMalformed, + onClear, hasActiveFilters, +}) { + // The API returns facets in field order with `state` last; render in that + // order. `selections` is { field: Set }. + const fieldOrder = Object.keys(facets || {}) + // A `tags`-type field renders the value-search box. + const isTags = (field) => fields?.[field]?.type === 'tags' + return ( +
+ {hasActiveFilters && ( + + )} + {fieldOrder.map(field => ( + + ))} + +
+ ) +}