Add sort dropdown + list/map view toggle to ProvidersStep & VenueStep

New control bar below search on both listing pages:
- Left: results count (passive)
- Right: sort select (Recommended/Nearest/Price), list/map toggle, filters

Sort: compact TextField select with 4 options, 0.813rem font.
View toggle: MUI ToggleButtonGroup with list/map icons, brand highlight.
Control bar wraps gracefully on narrow panels (flex-wrap).

New types: ProviderSortBy, VenueSortBy, ListViewMode.
Stories updated with interactive sort + view state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-31 16:47:50 +11:00
parent a3069b2ee6
commit e3090e6aed
4 changed files with 233 additions and 28 deletions

View File

@@ -5,6 +5,11 @@ 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 MenuItem from '@mui/material/MenuItem';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import ToggleButton from '@mui/material/ToggleButton';
import ViewListOutlinedIcon from '@mui/icons-material/ViewListOutlined';
import MapOutlinedIcon from '@mui/icons-material/MapOutlined';
import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined';
import type { SxProps, Theme } from '@mui/material/styles';
import { WizardLayout } from '../../templates/WizardLayout';
@@ -65,6 +70,12 @@ export interface ProviderFilterValues {
priceRange: [number, number];
}
/** Sort options for the provider list */
export type ProviderSortBy = 'recommended' | 'nearest' | 'price_low' | 'price_high';
/** View mode for the listing */
export type ListViewMode = 'list' | 'map';
/** Props for the ProvidersStep page component */
export interface ProvidersStepProps {
/** List of providers to display */
@@ -89,6 +100,14 @@ export interface ProvidersStepProps {
minPrice?: number;
/** Maximum price for the slider (default 15000) */
maxPrice?: number;
/** Current sort order */
sortBy?: ProviderSortBy;
/** Callback when sort order changes */
onSortChange?: (sort: ProviderSortBy) => void;
/** Current view mode */
viewMode?: ListViewMode;
/** Callback when view mode changes */
onViewModeChange?: (mode: ListViewMode) => void;
/** Callback for the Back button */
onBack: () => void;
/** Map panel content — slot for future map integration */
@@ -140,6 +159,13 @@ const DEFAULT_FUNERAL_TYPES: FuneralTypeOption[] = [
{ value: 'burial_only', label: 'Burial Only' },
];
const SORT_OPTIONS: { value: ProviderSortBy; label: string }[] = [
{ value: 'recommended', label: 'Recommended' },
{ value: 'nearest', label: 'Nearest' },
{ value: 'price_low', label: 'Price: Low to High' },
{ value: 'price_high', label: 'Price: High to Low' },
];
export const EMPTY_FILTER_VALUES: ProviderFilterValues = {
tradition: null,
funeralTypes: [],
@@ -197,6 +223,10 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
funeralTypeOptions = DEFAULT_FUNERAL_TYPES,
minPrice = 0,
maxPrice = 15000,
sortBy = 'recommended',
onSortChange,
viewMode = 'list',
onViewModeChange,
onBack,
mapPanel,
navigation,
@@ -335,8 +365,76 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
sx={{ mb: 1.5 }}
/>
{/* Filters — right-aligned below search */}
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
{/* Control bar — results count, sort, view toggle, filters */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1.5,
flexWrap: 'wrap',
}}
>
{/* Results count — left */}
<Typography
variant="caption"
color="text.secondary"
sx={{ mr: 'auto' }}
aria-live="polite"
>
{providers.length} provider{providers.length !== 1 ? 's' : ''} found
</Typography>
{/* Sort */}
<TextField
select
value={sortBy}
onChange={(e) => onSortChange?.(e.target.value as ProviderSortBy)}
size="small"
aria-label="Sort providers"
sx={{
minWidth: 160,
'& .MuiOutlinedInput-root': { fontSize: '0.813rem' },
'& .MuiSelect-select': { py: '5px' },
}}
>
{SORT_OPTIONS.map((opt) => (
<MenuItem key={opt.value} value={opt.value} sx={{ fontSize: '0.813rem' }}>
{opt.label}
</MenuItem>
))}
</TextField>
{/* View toggle */}
<ToggleButtonGroup
value={viewMode}
exclusive
onChange={(_, val) => val && onViewModeChange?.(val as ListViewMode)}
size="small"
aria-label="View mode"
sx={{
'& .MuiToggleButton-root': {
px: 1,
py: 0.5,
border: '1px solid',
borderColor: 'divider',
'&.Mui-selected': {
bgcolor: 'var(--fa-color-brand-100)',
color: 'primary.main',
borderColor: 'primary.main',
'&:hover': { bgcolor: 'var(--fa-color-brand-200)' },
},
},
}}
>
<ToggleButton value="list" aria-label="List view">
<ViewListOutlinedIcon sx={{ fontSize: 18 }} />
</ToggleButton>
<ToggleButton value="map" aria-label="Map view">
<MapOutlinedIcon sx={{ fontSize: 18 }} />
</ToggleButton>
</ToggleButtonGroup>
{/* Filters */}
<FilterPanel activeCount={activeCount} onClear={handleClear}>
{/* ── Location ── */}
<Box>
@@ -511,16 +609,6 @@ export const ProvidersStep: React.FC<ProvidersStepProps> = ({
</Box>
</FilterPanel>
</Box>
{/* Results count */}
<Typography
variant="caption"
color="text.secondary"
sx={{ mb: 0, display: 'block' }}
aria-live="polite"
>
{providers.length} provider{providers.length !== 1 ? 's' : ''} found
</Typography>
</Box>
{/* Provider list — click-to-navigate (D-D) */}