ProvidersStep structured filters: tradition, type, verified, online, price
Replace generic ProviderFilter[] chip array with structured filter state: - Service tradition: Autocomplete with type-to-search (20 options) - Funeral type: Chip multi-select (6 types from spec) - Verified providers only: Switch toggle - Online arrangements available: Switch toggle - Price range: Dual-knob slider with editable inputs (matches CoffinsStep) New ProviderFilterValues interface, EMPTY_FILTER_VALUES export. New story: WithActiveFilters (pre-applied Catholic + cremation + $0-2k). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import type { Meta, StoryObj } from '@storybook/react';
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
import { ProvidersStep } from './ProvidersStep';
|
import { ProvidersStep } from './ProvidersStep';
|
||||||
import type { ProviderData, ProviderFilter } from './ProvidersStep';
|
import type { ProviderData, ProviderFilterValues } from './ProvidersStep';
|
||||||
|
import { EMPTY_FILTER_VALUES } from './ProvidersStep';
|
||||||
import { Navigation } from '../../organisms/Navigation';
|
import { Navigation } from '../../organisms/Navigation';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
|
|
||||||
@@ -96,12 +97,6 @@ const mockProviders: ProviderData[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const defaultFilters: ProviderFilter[] = [
|
|
||||||
{ label: 'Funeral Type', active: false },
|
|
||||||
{ label: 'Verified Only', active: false },
|
|
||||||
{ label: 'Under $1,500', active: false },
|
|
||||||
];
|
|
||||||
|
|
||||||
// ─── Meta ────────────────────────────────────────────────────────────────────
|
// ─── Meta ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const meta: Meta<typeof ProvidersStep> = {
|
const meta: Meta<typeof ProvidersStep> = {
|
||||||
@@ -118,11 +113,11 @@ type Story = StoryObj<typeof ProvidersStep>;
|
|||||||
|
|
||||||
// ─── Interactive (default) ──────────────────────────────────────────────────
|
// ─── Interactive (default) ──────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Click-to-navigate — clicking a provider triggers navigation (D-D) */
|
/** Full filter panel — tradition, funeral type, verified, online, price range */
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
render: () => {
|
render: () => {
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
const [filters, setFilters] = useState(defaultFilters);
|
const [filters, setFilters] = useState<ProviderFilterValues>(EMPTY_FILTER_VALUES);
|
||||||
|
|
||||||
const filtered = mockProviders.filter((p) =>
|
const filtered = mockProviders.filter((p) =>
|
||||||
p.location.toLowerCase().includes(query.toLowerCase()),
|
p.location.toLowerCase().includes(query.toLowerCase()),
|
||||||
@@ -134,11 +129,41 @@ export const Default: Story = {
|
|||||||
onSelectProvider={(id) => alert(`Navigate to provider: ${id}`)}
|
onSelectProvider={(id) => alert(`Navigate to provider: ${id}`)}
|
||||||
searchQuery={query}
|
searchQuery={query}
|
||||||
onSearchChange={setQuery}
|
onSearchChange={setQuery}
|
||||||
filters={filters}
|
filterValues={filters}
|
||||||
onFilterToggle={(i) =>
|
onFilterChange={setFilters}
|
||||||
setFilters((prev) => prev.map((f, idx) => (idx === i ? { ...f, active: !f.active } : f)))
|
onBack={() => alert('Back')}
|
||||||
}
|
navigation={nav}
|
||||||
onFilterClear={() => setFilters((prev) => prev.map((f) => ({ ...f, active: false })))}
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── With active filters ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Filters pre-applied — verified only + price cap */
|
||||||
|
export const WithActiveFilters: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
const [filters, setFilters] = useState<ProviderFilterValues>({
|
||||||
|
tradition: 'Catholic',
|
||||||
|
funeralTypes: ['service_and_cremation'],
|
||||||
|
verifiedOnly: true,
|
||||||
|
onlineArrangements: false,
|
||||||
|
priceRange: [0, 2000],
|
||||||
|
});
|
||||||
|
|
||||||
|
const filtered = mockProviders.filter((p) =>
|
||||||
|
p.location.toLowerCase().includes(query.toLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProvidersStep
|
||||||
|
providers={filtered}
|
||||||
|
onSelectProvider={(id) => alert(`Navigate to provider: ${id}`)}
|
||||||
|
searchQuery={query}
|
||||||
|
onSearchChange={setQuery}
|
||||||
|
filterValues={filters}
|
||||||
|
onFilterChange={setFilters}
|
||||||
onBack={() => alert('Back')}
|
onBack={() => alert('Back')}
|
||||||
navigation={nav}
|
navigation={nav}
|
||||||
/>
|
/>
|
||||||
@@ -152,6 +177,7 @@ export const Default: Story = {
|
|||||||
export const PrePlanning: Story = {
|
export const PrePlanning: Story = {
|
||||||
render: () => {
|
render: () => {
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
|
const [filters, setFilters] = useState<ProviderFilterValues>(EMPTY_FILTER_VALUES);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProvidersStep
|
<ProvidersStep
|
||||||
@@ -159,6 +185,8 @@ export const PrePlanning: Story = {
|
|||||||
onSelectProvider={(id) => alert(`Navigate to provider: ${id}`)}
|
onSelectProvider={(id) => alert(`Navigate to provider: ${id}`)}
|
||||||
searchQuery={query}
|
searchQuery={query}
|
||||||
onSearchChange={setQuery}
|
onSearchChange={setQuery}
|
||||||
|
filterValues={filters}
|
||||||
|
onFilterChange={setFilters}
|
||||||
onBack={() => alert('Back')}
|
onBack={() => alert('Back')}
|
||||||
navigation={nav}
|
navigation={nav}
|
||||||
isPrePlanning
|
isPrePlanning
|
||||||
@@ -173,6 +201,7 @@ export const PrePlanning: Story = {
|
|||||||
export const EmptyResults: Story = {
|
export const EmptyResults: Story = {
|
||||||
render: () => {
|
render: () => {
|
||||||
const [query, setQuery] = useState('xyz');
|
const [query, setQuery] = useState('xyz');
|
||||||
|
const [filters, setFilters] = useState<ProviderFilterValues>(EMPTY_FILTER_VALUES);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProvidersStep
|
<ProvidersStep
|
||||||
@@ -180,6 +209,8 @@ export const EmptyResults: Story = {
|
|||||||
onSelectProvider={() => {}}
|
onSelectProvider={() => {}}
|
||||||
searchQuery={query}
|
searchQuery={query}
|
||||||
onSearchChange={setQuery}
|
onSearchChange={setQuery}
|
||||||
|
filterValues={filters}
|
||||||
|
onFilterChange={setFilters}
|
||||||
onBack={() => alert('Back')}
|
onBack={() => alert('Back')}
|
||||||
navigation={nav}
|
navigation={nav}
|
||||||
/>
|
/>
|
||||||
@@ -193,6 +224,7 @@ export const EmptyResults: Story = {
|
|||||||
export const SingleProvider: Story = {
|
export const SingleProvider: Story = {
|
||||||
render: () => {
|
render: () => {
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
|
const [filters, setFilters] = useState<ProviderFilterValues>(EMPTY_FILTER_VALUES);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProvidersStep
|
<ProvidersStep
|
||||||
@@ -200,6 +232,8 @@ export const SingleProvider: Story = {
|
|||||||
onSelectProvider={(id) => alert(`Navigate to provider: ${id}`)}
|
onSelectProvider={(id) => alert(`Navigate to provider: ${id}`)}
|
||||||
searchQuery={query}
|
searchQuery={query}
|
||||||
onSearchChange={setQuery}
|
onSearchChange={setQuery}
|
||||||
|
filterValues={filters}
|
||||||
|
onFilterChange={setFilters}
|
||||||
onBack={() => alert('Back')}
|
onBack={() => alert('Back')}
|
||||||
navigation={nav}
|
navigation={nav}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,15 +2,20 @@ import React from 'react';
|
|||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import InputAdornment from '@mui/material/InputAdornment';
|
import InputAdornment from '@mui/material/InputAdornment';
|
||||||
|
import Autocomplete from '@mui/material/Autocomplete';
|
||||||
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
|
import Slider from '@mui/material/Slider';
|
||||||
import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined';
|
import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined';
|
||||||
import type { SxProps, Theme } from '@mui/material/styles';
|
import type { SxProps, Theme } from '@mui/material/styles';
|
||||||
import { WizardLayout } from '../../templates/WizardLayout';
|
import { WizardLayout } from '../../templates/WizardLayout';
|
||||||
import { ProviderCard } from '../../molecules/ProviderCard';
|
import { ProviderCard } from '../../molecules/ProviderCard';
|
||||||
import { FilterPanel } from '../../molecules/FilterPanel';
|
import { FilterPanel } from '../../molecules/FilterPanel';
|
||||||
import { Chip } from '../../atoms/Chip';
|
import { Chip } from '../../atoms/Chip';
|
||||||
|
import { Switch } from '../../atoms/Switch';
|
||||||
import { Typography } from '../../atoms/Typography';
|
import { Typography } from '../../atoms/Typography';
|
||||||
|
import { Divider } from '../../atoms/Divider';
|
||||||
|
|
||||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
// ──<EFBFBD><EFBFBD> Types ─────────────────<EFBFBD><EFBFBD><EFBFBD>─────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Provider data for display in the list */
|
/** Provider data for display in the list */
|
||||||
export interface ProviderData {
|
export interface ProviderData {
|
||||||
@@ -38,12 +43,26 @@ export interface ProviderData {
|
|||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Filter chip state */
|
/** A funeral type option for the filter */
|
||||||
export interface ProviderFilter {
|
export interface FuneralTypeOption {
|
||||||
/** Filter label */
|
/** Machine-readable value (e.g. "service_and_cremation") */
|
||||||
|
value: string;
|
||||||
|
/** Human-readable label (e.g. "Service & Cremation") */
|
||||||
label: string;
|
label: string;
|
||||||
/** Whether this filter is active */
|
}
|
||||||
active: boolean;
|
|
||||||
|
/** Structured filter state for the providers list */
|
||||||
|
export interface ProviderFilterValues {
|
||||||
|
/** Selected service tradition (null = any) */
|
||||||
|
tradition: string | null;
|
||||||
|
/** Selected funeral type values (empty = all) */
|
||||||
|
funeralTypes: string[];
|
||||||
|
/** Show only verified providers */
|
||||||
|
verifiedOnly: boolean;
|
||||||
|
/** Show only providers offering online arrangements */
|
||||||
|
onlineArrangements: boolean;
|
||||||
|
/** Price range [min, max] */
|
||||||
|
priceRange: [number, number];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Props for the ProvidersStep page component */
|
/** Props for the ProvidersStep page component */
|
||||||
@@ -58,25 +77,77 @@ export interface ProvidersStepProps {
|
|||||||
onSearchChange: (query: string) => void;
|
onSearchChange: (query: string) => void;
|
||||||
/** Callback when search is submitted */
|
/** Callback when search is submitted */
|
||||||
onSearch?: (query: string) => void;
|
onSearch?: (query: string) => void;
|
||||||
/** Filter chips */
|
/** Current filter state */
|
||||||
filters?: ProviderFilter[];
|
filterValues: ProviderFilterValues;
|
||||||
/** Callback when a filter chip is toggled */
|
/** Callback when any filter changes */
|
||||||
onFilterToggle?: (index: number) => void;
|
onFilterChange: (values: ProviderFilterValues) => void;
|
||||||
/** Callback to clear all filters */
|
/** Available service tradition options for the autocomplete */
|
||||||
onFilterClear?: () => void;
|
traditionOptions?: string[];
|
||||||
|
/** Available funeral type options */
|
||||||
|
funeralTypeOptions?: FuneralTypeOption[];
|
||||||
|
/** Minimum price for the slider (default 0) */
|
||||||
|
minPrice?: number;
|
||||||
|
/** Maximum price for the slider (default 15000) */
|
||||||
|
maxPrice?: number;
|
||||||
/** Callback for the Back button */
|
/** Callback for the Back button */
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
/** Map panel content — slot for future map integration */
|
/** Map panel content — slot for future map integration */
|
||||||
mapPanel?: React.ReactNode;
|
mapPanel?: React.ReactNode;
|
||||||
/** Navigation bar — passed through to WizardLayout */
|
/** Navigation bar — passed through to WizardLayout */
|
||||||
navigation?: React.ReactNode;
|
navigation?: React.ReactNode;
|
||||||
|
/** Progress stepper — passed through to WizardLayout */
|
||||||
|
progressStepper?: React.ReactNode;
|
||||||
|
/** Running total — passed through to WizardLayout */
|
||||||
|
runningTotal?: React.ReactNode;
|
||||||
/** Whether this is a pre-planning flow (shows softer copy) */
|
/** Whether this is a pre-planning flow (shows softer copy) */
|
||||||
isPrePlanning?: boolean;
|
isPrePlanning?: boolean;
|
||||||
/** MUI sx prop for the root */
|
/** MUI sx prop for the root */
|
||||||
sx?: SxProps<Theme>;
|
sx?: SxProps<Theme>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Component ───────────────────────────────────────────────────────────────
|
// ─── Defaults ────<E29480><E29480>──────────────────────<EFBFBD><EFBFBD>───────────────────────────────────
|
||||||
|
|
||||||
|
const DEFAULT_TRADITIONS = [
|
||||||
|
'Anglican',
|
||||||
|
"Bahá'í",
|
||||||
|
'Baptist',
|
||||||
|
'Buddhist',
|
||||||
|
'Catholic',
|
||||||
|
'Eastern Orthodox',
|
||||||
|
'Hindu',
|
||||||
|
'Humanist',
|
||||||
|
'Indigenous Australian',
|
||||||
|
'Jewish',
|
||||||
|
'Lutheran',
|
||||||
|
'Methodist',
|
||||||
|
'Muslim',
|
||||||
|
'Non-religious',
|
||||||
|
'Pentecostal',
|
||||||
|
'Presbyterian',
|
||||||
|
'Salvation Army',
|
||||||
|
'Secular',
|
||||||
|
'Sikh',
|
||||||
|
'Uniting Church',
|
||||||
|
];
|
||||||
|
|
||||||
|
const DEFAULT_FUNERAL_TYPES: FuneralTypeOption[] = [
|
||||||
|
{ value: 'service_and_cremation', label: 'Service & Cremation' },
|
||||||
|
{ value: 'service_and_burial', label: 'Service & Burial' },
|
||||||
|
{ value: 'cremation_only', label: 'Cremation Only' },
|
||||||
|
{ value: 'graveside_burial', label: 'Graveside Burial' },
|
||||||
|
{ value: 'water_cremation', label: 'Water Cremation' },
|
||||||
|
{ value: 'burial_only', label: 'Burial Only' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const EMPTY_FILTER_VALUES: ProviderFilterValues = {
|
||||||
|
tradition: null,
|
||||||
|
funeralTypes: [],
|
||||||
|
verifiedOnly: false,
|
||||||
|
onlineArrangements: false,
|
||||||
|
priceRange: [0, 15000],
|
||||||
|
};
|
||||||
|
|
||||||
|
// ──<E29480><E29480><EFBFBD> Component ─────────────────────────────────────────────────────<E29480><E29480><EFBFBD>─────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Step 2 — Provider selection page for the FA arrangement wizard.
|
* Step 2 — Provider selection page for the FA arrangement wizard.
|
||||||
@@ -88,6 +159,10 @@ export interface ProvidersStepProps {
|
|||||||
* Click-to-navigate (D-D): clicking a provider card triggers
|
* Click-to-navigate (D-D): clicking a provider card triggers
|
||||||
* navigation directly — no selection state or Continue button.
|
* navigation directly — no selection state or Continue button.
|
||||||
*
|
*
|
||||||
|
* Filters: service tradition (autocomplete), funeral type (chips),
|
||||||
|
* verified only (switch), online arrangements (switch), price range
|
||||||
|
* (dual-knob slider with editable inputs).
|
||||||
|
*
|
||||||
* Pure presentation component — props in, callbacks out.
|
* Pure presentation component — props in, callbacks out.
|
||||||
*
|
*
|
||||||
* Spec: documentation/steps/steps/02_providers.yaml
|
* Spec: documentation/steps/steps/02_providers.yaml
|
||||||
@@ -98,12 +173,17 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
|
|||||||
searchQuery,
|
searchQuery,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
onSearch,
|
onSearch,
|
||||||
filters,
|
filterValues,
|
||||||
onFilterToggle,
|
onFilterChange,
|
||||||
onFilterClear,
|
traditionOptions = DEFAULT_TRADITIONS,
|
||||||
|
funeralTypeOptions = DEFAULT_FUNERAL_TYPES,
|
||||||
|
minPrice = 0,
|
||||||
|
maxPrice = 15000,
|
||||||
onBack,
|
onBack,
|
||||||
mapPanel,
|
mapPanel,
|
||||||
navigation,
|
navigation,
|
||||||
|
progressStepper,
|
||||||
|
runningTotal,
|
||||||
isPrePlanning = false,
|
isPrePlanning = false,
|
||||||
sx,
|
sx,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -111,10 +191,59 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
|
|||||||
? 'Take your time exploring providers. You can always come back and choose a different one.'
|
? 'Take your time exploring providers. You can always come back and choose a different one.'
|
||||||
: 'These providers are near your location. Each has their own packages and pricing.';
|
: 'These providers are near your location. Each has their own packages and pricing.';
|
||||||
|
|
||||||
|
// ─── Price input local state (commits on blur / Enter) ───
|
||||||
|
const [priceMinInput, setPriceMinInput] = React.useState(String(filterValues.priceRange[0]));
|
||||||
|
const [priceMaxInput, setPriceMaxInput] = React.useState(String(filterValues.priceRange[1]));
|
||||||
|
|
||||||
|
// Sync local state when slider (or clear) changes the value
|
||||||
|
const rangeMin = filterValues.priceRange[0];
|
||||||
|
const rangeMax = filterValues.priceRange[1];
|
||||||
|
React.useEffect(() => {
|
||||||
|
setPriceMinInput(String(rangeMin));
|
||||||
|
setPriceMaxInput(String(rangeMax));
|
||||||
|
}, [rangeMin, rangeMax]);
|
||||||
|
|
||||||
|
const commitPriceRange = () => {
|
||||||
|
let lo = parseInt(priceMinInput, 10);
|
||||||
|
let hi = parseInt(priceMaxInput, 10);
|
||||||
|
if (isNaN(lo)) lo = minPrice;
|
||||||
|
if (isNaN(hi)) hi = maxPrice;
|
||||||
|
lo = Math.max(minPrice, Math.min(lo, maxPrice));
|
||||||
|
hi = Math.max(minPrice, Math.min(hi, maxPrice));
|
||||||
|
if (lo > hi) [lo, hi] = [hi, lo];
|
||||||
|
const newRange: [number, number] = [lo, hi];
|
||||||
|
if (newRange[0] !== filterValues.priceRange[0] || newRange[1] !== filterValues.priceRange[1]) {
|
||||||
|
onFilterChange({ ...filterValues, priceRange: newRange });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Active filter count ───
|
||||||
|
const activeCount =
|
||||||
|
(filterValues.tradition ? 1 : 0) +
|
||||||
|
filterValues.funeralTypes.length +
|
||||||
|
(filterValues.verifiedOnly ? 1 : 0) +
|
||||||
|
(filterValues.onlineArrangements ? 1 : 0) +
|
||||||
|
(filterValues.priceRange[0] !== minPrice || filterValues.priceRange[1] !== maxPrice ? 1 : 0);
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
onFilterChange({
|
||||||
|
...EMPTY_FILTER_VALUES,
|
||||||
|
priceRange: [minPrice, maxPrice],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFuneralTypeToggle = (value: string) => {
|
||||||
|
const current = filterValues.funeralTypes;
|
||||||
|
const next = current.includes(value) ? current.filter((v) => v !== value) : [...current, value];
|
||||||
|
onFilterChange({ ...filterValues, funeralTypes: next });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WizardLayout
|
<WizardLayout
|
||||||
variant="list-map"
|
variant="list-map"
|
||||||
navigation={navigation}
|
navigation={navigation}
|
||||||
|
progressStepper={progressStepper}
|
||||||
|
runningTotal={runningTotal}
|
||||||
showBackLink
|
showBackLink
|
||||||
backLabel="Back"
|
backLabel="Back"
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
@@ -187,27 +316,138 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Filters — right-aligned below search */}
|
{/* Filters — right-aligned below search */}
|
||||||
{filters && filters.length > 0 && (
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
|
||||||
<FilterPanel
|
<FilterPanel activeCount={activeCount} onClear={handleClear}>
|
||||||
activeCount={filters.filter((f) => f.active).length}
|
{/* ── Service tradition ── */}
|
||||||
onClear={onFilterClear}
|
<Box>
|
||||||
>
|
<Typography variant="label" sx={{ mb: 1, display: 'block' }}>
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
Service tradition
|
||||||
{filters.map((filter, index) => (
|
</Typography>
|
||||||
|
<Autocomplete
|
||||||
|
value={filterValues.tradition}
|
||||||
|
onChange={(_, newValue) => onFilterChange({ ...filterValues, tradition: newValue })}
|
||||||
|
options={traditionOptions}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField {...params} placeholder="Search traditions..." size="small" />
|
||||||
|
)}
|
||||||
|
clearOnEscape
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* ── Funeral type ── */}
|
||||||
|
<Box>
|
||||||
|
<Typography variant="label" sx={{ mb: 1.5, display: 'block' }}>
|
||||||
|
Funeral type
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||||
|
{funeralTypeOptions.map((option) => (
|
||||||
<Chip
|
<Chip
|
||||||
key={filter.label}
|
key={option.value}
|
||||||
label={filter.label}
|
label={option.label}
|
||||||
selected={filter.active}
|
selected={filterValues.funeralTypes.includes(option.value)}
|
||||||
onClick={onFilterToggle ? () => onFilterToggle(index) : undefined}
|
onClick={() => handleFuneralTypeToggle(option.value)}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* ── Provider features ── */}
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={filterValues.verifiedOnly}
|
||||||
|
onChange={(_, checked) =>
|
||||||
|
onFilterChange({ ...filterValues, verifiedOnly: checked })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Verified providers only"
|
||||||
|
sx={{ mx: 0 }}
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={filterValues.onlineArrangements}
|
||||||
|
onChange={(_, checked) =>
|
||||||
|
onFilterChange({ ...filterValues, onlineArrangements: checked })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Online arrangements available"
|
||||||
|
sx={{ mx: 0 }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* ── Price range ── */}
|
||||||
|
<Box>
|
||||||
|
<Typography variant="label" sx={{ mb: 1.5, display: 'block' }}>
|
||||||
|
Price range
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ px: 1, mb: 1.5 }}>
|
||||||
|
<Slider
|
||||||
|
value={filterValues.priceRange}
|
||||||
|
onChange={(_, newValue) =>
|
||||||
|
onFilterChange({
|
||||||
|
...filterValues,
|
||||||
|
priceRange: newValue as [number, number],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
min={minPrice}
|
||||||
|
max={maxPrice}
|
||||||
|
step={100}
|
||||||
|
valueLabelDisplay="auto"
|
||||||
|
valueLabelFormat={(v) => `$${v.toLocaleString('en-AU')}`}
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
value={priceMinInput}
|
||||||
|
onChange={(e) => setPriceMinInput(e.target.value.replace(/[^0-9]/g, ''))}
|
||||||
|
onBlur={commitPriceRange}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && commitPriceRange()}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: <InputAdornment position="start">$</InputAdornment>,
|
||||||
|
}}
|
||||||
|
inputProps={{
|
||||||
|
inputMode: 'numeric',
|
||||||
|
'aria-label': 'Minimum price',
|
||||||
|
}}
|
||||||
|
sx={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ alignSelf: 'center' }}>
|
||||||
|
–
|
||||||
|
</Typography>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
value={priceMaxInput}
|
||||||
|
onChange={(e) => setPriceMaxInput(e.target.value.replace(/[^0-9]/g, ''))}
|
||||||
|
onBlur={commitPriceRange}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && commitPriceRange()}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: <InputAdornment position="start">$</InputAdornment>,
|
||||||
|
}}
|
||||||
|
inputProps={{
|
||||||
|
inputMode: 'numeric',
|
||||||
|
'aria-label': 'Maximum price',
|
||||||
|
}}
|
||||||
|
sx={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
</FilterPanel>
|
</FilterPanel>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Results count */}
|
{/* Results count */}
|
||||||
<Typography
|
<Typography
|
||||||
|
|||||||
Reference in New Issue
Block a user