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:
@@ -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<VenueFilterValues>(EMPTY_VENUE_FILTERS);
|
||||
const [sort, setSort] = useState<VenueSortBy>('recommended');
|
||||
const [view, setView] = useState<ListViewMode>('list');
|
||||
|
||||
return (
|
||||
<VenueStep
|
||||
@@ -100,6 +102,10 @@ export const Default: Story = {
|
||||
onSearchChange={setSearch}
|
||||
filterValues={filters}
|
||||
onFilterChange={setFilters}
|
||||
sortBy={sort}
|
||||
onSortChange={setSort}
|
||||
viewMode={view}
|
||||
onViewModeChange={setView}
|
||||
onBack={() => 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<VenueSortBy>('nearest');
|
||||
const [view, setView] = useState<ListViewMode>('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}
|
||||
|
||||
@@ -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<VenueStepProps> = ({
|
||||
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<VenueStepProps> = ({
|
||||
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"
|
||||
>
|
||||
{venues.length} venue{venues.length !== 1 ? 's' : ''}
|
||||
{locationName ? ` near ${locationName}` : ''} found
|
||||
</Typography>
|
||||
|
||||
{/* Sort */}
|
||||
<TextField
|
||||
select
|
||||
value={sortBy}
|
||||
onChange={(e) => 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) => (
|
||||
<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>
|
||||
@@ -395,17 +494,6 @@ export const VenueStep: React.FC<VenueStepProps> = ({
|
||||
</Box>
|
||||
</FilterPanel>
|
||||
</Box>
|
||||
|
||||
{/* Results count */}
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ mb: 0, display: 'block' }}
|
||||
aria-live="polite"
|
||||
>
|
||||
{venues.length} venue{venues.length !== 1 ? 's' : ''}
|
||||
{locationName ? ` near ${locationName}` : ''} found
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Venue list — click-to-navigate */}
|
||||
|
||||
Reference in New Issue
Block a user