diff --git a/src/components/pages/ProvidersStep/ProvidersStep.stories.tsx b/src/components/pages/ProvidersStep/ProvidersStep.stories.tsx index 0dd074d..ec8cba8 100644 --- a/src/components/pages/ProvidersStep/ProvidersStep.stories.tsx +++ b/src/components/pages/ProvidersStep/ProvidersStep.stories.tsx @@ -1,7 +1,12 @@ import { useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import { ProvidersStep } from './ProvidersStep'; -import type { ProviderData, ProviderFilterValues } from './ProvidersStep'; +import type { + ProviderData, + ProviderFilterValues, + ProviderSortBy, + ListViewMode, +} from './ProvidersStep'; import { EMPTY_FILTER_VALUES } from './ProvidersStep'; import { Navigation } from '../../organisms/Navigation'; import Box from '@mui/material/Box'; @@ -118,6 +123,8 @@ export const Default: Story = { render: () => { const [query, setQuery] = useState(''); const [filters, setFilters] = useState(EMPTY_FILTER_VALUES); + const [sort, setSort] = useState('recommended'); + const [view, setView] = useState('list'); const filtered = mockProviders.filter((p) => p.location.toLowerCase().includes(query.toLowerCase()), @@ -131,6 +138,10 @@ export const Default: Story = { onSearchChange={setQuery} filterValues={filters} onFilterChange={setFilters} + sortBy={sort} + onSortChange={setSort} + viewMode={view} + onViewModeChange={setView} onBack={() => alert('Back')} navigation={nav} /> @@ -151,6 +162,8 @@ export const WithActiveFilters: Story = { onlineArrangements: false, priceRange: [0, 2000], }); + const [sort, setSort] = useState('nearest'); + const [view, setView] = useState('list'); const filtered = mockProviders.filter((p) => p.location.toLowerCase().includes(query.toLowerCase()), @@ -164,6 +177,10 @@ export const WithActiveFilters: Story = { onSearchChange={setQuery} filterValues={filters} onFilterChange={setFilters} + sortBy={sort} + onSortChange={setSort} + viewMode={view} + onViewModeChange={setView} onBack={() => alert('Back')} navigation={nav} /> diff --git a/src/components/pages/ProvidersStep/ProvidersStep.tsx b/src/components/pages/ProvidersStep/ProvidersStep.tsx index 5179887..6f9149a 100644 --- a/src/components/pages/ProvidersStep/ProvidersStep.tsx +++ b/src/components/pages/ProvidersStep/ProvidersStep.tsx @@ -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 = ({ 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 = ({ sx={{ mb: 1.5 }} /> - {/* Filters — right-aligned below search */} - + {/* Control bar — results count, sort, view toggle, filters */} + + {/* Results count — left */} + + {providers.length} provider{providers.length !== 1 ? 's' : ''} found + + + {/* Sort */} + 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) => ( + + {opt.label} + + ))} + + + {/* View toggle */} + 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)' }, + }, + }, + }} + > + + + + + + + + + {/* Filters */} {/* ── Location ── */} @@ -511,16 +609,6 @@ export const ProvidersStep: React.FC = ({ - - {/* Results count */} - - {providers.length} provider{providers.length !== 1 ? 's' : ''} found - {/* Provider list — click-to-navigate (D-D) */} diff --git a/src/components/pages/VenueStep/VenueStep.stories.tsx b/src/components/pages/VenueStep/VenueStep.stories.tsx index 77fc435..23a36c4 100644 --- a/src/components/pages/VenueStep/VenueStep.stories.tsx +++ b/src/components/pages/VenueStep/VenueStep.stories.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import { VenueStep } from './VenueStep'; -import type { Venue, VenueFilterValues } from './VenueStep'; +import type { Venue, VenueFilterValues, VenueSortBy, ListViewMode } from './VenueStep'; import { EMPTY_VENUE_FILTERS } from './VenueStep'; import { Navigation } from '../../organisms/Navigation'; import Box from '@mui/material/Box'; @@ -91,6 +91,8 @@ export const Default: Story = { render: () => { const [search, setSearch] = useState(''); const [filters, setFilters] = useState(EMPTY_VENUE_FILTERS); + const [sort, setSort] = useState('recommended'); + const [view, setView] = useState('list'); return ( alert('Back')} locationName="Strathfield" navigation={nav} @@ -110,7 +116,7 @@ export const Default: Story = { // ─── With active filters ──────────────────────────────────────────────────── -/** Filters pre-applied — chapel + video streaming + Catholic */ +/** Filters pre-applied — chapel + video streaming + Catholic, sorted nearest */ export const WithActiveFilters: Story = { render: () => { const [search, setSearch] = useState('Strathfield'); @@ -120,6 +126,8 @@ export const WithActiveFilters: Story = { photoDisplay: false, tradition: 'Catholic', }); + const [sort, setSort] = useState('nearest'); + const [view, setView] = useState('list'); const filtered = sampleVenues.filter((v) => v.location.toLowerCase().includes(search.toLowerCase()), @@ -133,6 +141,10 @@ export const WithActiveFilters: Story = { onSearchChange={setSearch} filterValues={filters} onFilterChange={setFilters} + sortBy={sort} + onSortChange={setSort} + viewMode={view} + onViewModeChange={setView} onBack={() => alert('Back')} locationName="Strathfield" navigation={nav} diff --git a/src/components/pages/VenueStep/VenueStep.tsx b/src/components/pages/VenueStep/VenueStep.tsx index e55bd1d..b28b8ce 100644 --- a/src/components/pages/VenueStep/VenueStep.tsx +++ b/src/components/pages/VenueStep/VenueStep.tsx @@ -4,6 +4,11 @@ import TextField from '@mui/material/TextField'; import InputAdornment from '@mui/material/InputAdornment'; import Autocomplete from '@mui/material/Autocomplete'; import FormControlLabel from '@mui/material/FormControlLabel'; +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'; @@ -46,6 +51,12 @@ export interface VenueFilterValues { tradition: string | null; } +/** Sort options for the venue list */ +export type VenueSortBy = 'recommended' | 'nearest' | 'price_low' | 'price_high'; + +/** View mode for the listing */ +export type ListViewMode = 'list' | 'map'; + /** Props for the VenueStep page component */ export interface VenueStepProps { /** List of venues to display */ @@ -66,6 +77,14 @@ export interface VenueStepProps { venueTypeOptions?: VenueTypeOption[]; /** Available service tradition options for the autocomplete */ traditionOptions?: string[]; + /** Current sort order */ + sortBy?: VenueSortBy; + /** Callback when sort order changes */ + onSortChange?: (sort: VenueSortBy) => void; + /** Current view mode */ + viewMode?: ListViewMode; + /** Callback when view mode changes */ + onViewModeChange?: (mode: ListViewMode) => void; /** Callback for back navigation */ onBack: () => void; /** Location name for the results count */ @@ -120,6 +139,13 @@ const DEFAULT_TRADITIONS = [ 'Uniting Church', ]; +const SORT_OPTIONS: { value: VenueSortBy; 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_VENUE_FILTERS: VenueFilterValues = { venueTypes: [], videoStreaming: false, @@ -171,6 +197,10 @@ export const VenueStep: React.FC = ({ onFilterChange, venueTypeOptions = DEFAULT_VENUE_TYPES, traditionOptions = DEFAULT_TRADITIONS, + sortBy = 'recommended', + onSortChange, + viewMode = 'list', + onViewModeChange, onBack, locationName, isPrePlanning = false, @@ -280,8 +310,77 @@ export const VenueStep: React.FC = ({ sx={{ mb: 1.5 }} /> - {/* Filters — right-aligned below search */} - + {/* Control bar — results count, sort, view toggle, filters */} + + {/* Results count — left */} + + {venues.length} venue{venues.length !== 1 ? 's' : ''} + {locationName ? ` near ${locationName}` : ''} found + + + {/* Sort */} + onSortChange?.(e.target.value as VenueSortBy)} + size="small" + aria-label="Sort venues" + sx={{ + minWidth: 160, + '& .MuiOutlinedInput-root': { fontSize: '0.813rem' }, + '& .MuiSelect-select': { py: '5px' }, + }} + > + {SORT_OPTIONS.map((opt) => ( + + {opt.label} + + ))} + + + {/* View toggle */} + 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)' }, + }, + }, + }} + > + + + + + + + + + {/* Filters */} {/* ── Location ── */} @@ -395,17 +494,6 @@ export const VenueStep: React.FC = ({ - - {/* Results count */} - - {venues.length} venue{venues.length !== 1 ? 's' : ''} - {locationName ? ` near ${locationName}` : ''} found - {/* Venue list — click-to-navigate */}