From ac6828d925a0ead76cda572b3d17d1af9c9c06d5 Mon Sep 17 00:00:00 2001 From: Richie Date: Mon, 30 Mar 2026 20:12:12 +1100 Subject: [PATCH] VenueStep click-to-navigate, new VenueDetailStep, CoffinDetailsStep detail-toggles - VenueStep: simplified to click-to-navigate (like ProvidersStep) - Removed selection state, Continue button, inline detail, service toggles - Clicking a venue card triggers onSelectVenue navigation - VenueDetailStep: new page with detail-toggles layout - Left: venue image, description, features - Right: name, location, type, price, Add Venue CTA, address, religions, service toggles - CoffinDetailsStep: switched from centered-form to detail-toggles layout - Left: coffin image, description - Right: name, price, Add Coffin CTA, specs Co-Authored-By: Claude Opus 4.6 (1M context) --- .../CoffinDetailsStep/CoffinDetailsStep.tsx | 169 ++++--- .../VenueDetailStep.stories.tsx | 180 +++++++ .../pages/VenueDetailStep/VenueDetailStep.tsx | 303 ++++++++++++ src/components/pages/VenueDetailStep/index.ts | 7 + .../pages/VenueStep/VenueStep.stories.tsx | 176 +------ src/components/pages/VenueStep/VenueStep.tsx | 466 +++++------------- src/components/pages/VenueStep/index.ts | 8 +- 7 files changed, 726 insertions(+), 583 deletions(-) create mode 100644 src/components/pages/VenueDetailStep/VenueDetailStep.stories.tsx create mode 100644 src/components/pages/VenueDetailStep/VenueDetailStep.tsx create mode 100644 src/components/pages/VenueDetailStep/index.ts diff --git a/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.tsx b/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.tsx index 677a4ce..56e6e51 100644 --- a/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.tsx +++ b/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.tsx @@ -4,7 +4,6 @@ import type { SxProps, Theme } from '@mui/material/styles'; import { WizardLayout } from '../../templates/WizardLayout'; import { Typography } from '../../atoms/Typography'; import { Button } from '../../atoms/Button'; -import { Divider } from '../../atoms/Divider'; // ─── Types ─────────────────────────────────────────────────────────────────── @@ -55,9 +54,11 @@ export interface CoffinDetailsStepProps { /** * Step 11 — Coffin Details for the FA arrangement wizard. * - * Shows the selected coffin's full profile: image, specs, description, - * and price. Customisation options (handles, lining, nameplate) have - * been deferred as a future enhancement (D-G). + * Detail-toggles layout: coffin image + description on the left, + * name, specs, price, and CTA on the right. + * + * Customisation options (handles, lining, nameplate) have been + * deferred as a future enhancement (D-G). * * Pure presentation component — props in, callbacks out. * @@ -78,7 +79,7 @@ export const CoffinDetailsStep: React.FC = ({ }) => { return ( = ({ onBack={onBack} hideHelpBar={hideHelpBar} sx={sx} + secondaryPanel={ + + {/* Coffin name */} + + {coffin.name} + + + {/* Price */} + + ${coffin.price.toLocaleString('en-AU')} + + + {coffin.priceNote && ( + + {coffin.priceNote} + + )} + + {/* Add coffin CTA */} + { + e.preventDefault(); + if (!loading) onContinue(); + }} + > + + + + {onSaveAndExit && ( + + )} + + {/* Specs */} + {coffin.specs && coffin.specs.length > 0 && ( + + {coffin.specs.map((spec) => ( + + + {spec.label} + + {spec.value} + + ))} + + )} + + } > - - Your selected coffin - - - - {isPrePlanning - ? 'Here are the details of the coffin you selected. You can change your selection at any time.' - : 'Review the details of your selected coffin below.'} - - - {/* Coffin image */} + {/* Left panel: image + description */} = ({ }} /> - {/* Coffin name + description */} - - {coffin.name} - - {coffin.description && ( - + {coffin.description} )} - - {/* Specs grid */} - {coffin.specs && coffin.specs.length > 0 && ( - - {coffin.specs.map((spec) => ( - - - {spec.label} - - {spec.value} - - ))} - - )} - - {/* Price */} - - ${coffin.price.toLocaleString('en-AU')} - - {coffin.priceNote && ( - - {coffin.priceNote} - - )} - - - - {/* CTAs */} - { - e.preventDefault(); - if (!loading) onContinue(); - }} - sx={{ - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - flexDirection: { xs: 'column-reverse', sm: 'row' }, - gap: 2, - }} - > - {onSaveAndExit ? ( - - ) : ( - - )} - - ); }; diff --git a/src/components/pages/VenueDetailStep/VenueDetailStep.stories.tsx b/src/components/pages/VenueDetailStep/VenueDetailStep.stories.tsx new file mode 100644 index 0000000..30776d7 --- /dev/null +++ b/src/components/pages/VenueDetailStep/VenueDetailStep.stories.tsx @@ -0,0 +1,180 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { VenueDetailStep } from './VenueDetailStep'; +import type { VenueDetailStepValues, VenueDetail, VenueService } from './VenueDetailStep'; +import { Navigation } from '../../organisms/Navigation'; +import Box from '@mui/material/Box'; + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +const FALogo = () => ( + + + + +); + +const nav = ( + } + items={[ + { label: 'FAQ', href: '/faq' }, + { label: 'Contact Us', href: '/contact' }, + ]} + /> +); + +const sampleVenue: VenueDetail = { + id: 'west-chapel', + name: 'West Chapel', + imageUrl: 'https://images.unsplash.com/photo-1555396273-367ea4eb4db5?w=800&h=600&fit=crop', + location: 'Strathfield', + venueType: 'Indoor Chapel', + capacity: 120, + price: 900, + priceNote: 'Selecting this venue increases your plan total by $900.00', + description: + 'A modern chapel with natural light and flexible seating arrangements. Surrounded by established gardens with ample parking for guests. The venue features a covered portico entrance, audio-visual equipment, and climate control for year-round comfort.', + features: [ + 'Air conditioning', + 'Wheelchair accessible', + 'On-site parking', + 'Audio system', + 'Garden access', + ], + religions: ['No Religion', 'Civil Celebrant', 'Anglican', 'Catholic', 'Uniting Church'], + address: '42 The Boulevarde, Strathfield NSW 2135', +}; + +const sampleServices: VenueService[] = [ + { id: 'photo', name: 'Photo presentation', price: 150 }, + { id: 'streaming', name: 'Livestream', price: 200 }, + { id: 'recording', name: 'Recording', price: 100 }, +]; + +const defaultValues: VenueDetailStepValues = { + photoDisplay: false, + streaming: false, + recording: false, +}; + +// ─── Meta ──────────────────────────────────────────────────────────────────── + +const meta: Meta = { + title: 'Pages/VenueDetailStep', + component: VenueDetailStep, + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + }, +}; + +export default meta; +type Story = StoryObj; + +// ─── Default ─────────────────────────────────────────────────────────────── + +/** Full venue detail with service toggles */ +export const Default: Story = { + render: () => { + const [values, setValues] = useState({ ...defaultValues }); + + return ( + alert('Venue added!')} + onBack={() => alert('Back')} + onSaveAndExit={() => alert('Save and exit')} + services={sampleServices} + navigation={nav} + /> + ); + }, +}; + +// ─── With services selected ──────────────────────────────────────────────── + +/** Services already toggled on */ +export const WithServices: Story = { + render: () => { + const [values, setValues] = useState({ + photoDisplay: true, + streaming: true, + recording: false, + }); + + return ( + alert('Venue added!')} + onBack={() => alert('Back')} + services={sampleServices} + navigation={nav} + /> + ); + }, +}; + +// ─── Pre-planning ─────────────────────────────────────────────────────────── + +/** Pre-planning variant */ +export const PrePlanning: Story = { + render: () => { + const [values, setValues] = useState({ ...defaultValues }); + + return ( + alert('Venue selected!')} + onBack={() => alert('Back')} + services={sampleServices} + isPrePlanning + navigation={nav} + /> + ); + }, +}; + +// ─── Minimal venue ───────────────────────────────────────────────────────── + +/** Venue with minimal data (no features, religions, services) */ +export const Minimal: Story = { + render: () => { + const [values, setValues] = useState({ ...defaultValues }); + + return ( + alert('Venue added!')} + onBack={() => alert('Back')} + navigation={nav} + /> + ); + }, +}; diff --git a/src/components/pages/VenueDetailStep/VenueDetailStep.tsx b/src/components/pages/VenueDetailStep/VenueDetailStep.tsx new file mode 100644 index 0000000..f900dcd --- /dev/null +++ b/src/components/pages/VenueDetailStep/VenueDetailStep.tsx @@ -0,0 +1,303 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined'; +import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; +import type { SxProps, Theme } from '@mui/material/styles'; +import { WizardLayout } from '../../templates/WizardLayout'; +import { AddOnOption } from '../../molecules/AddOnOption'; +import { Chip } from '../../atoms/Chip'; +import { Typography } from '../../atoms/Typography'; +import { Button } from '../../atoms/Button'; +import { Divider } from '../../atoms/Divider'; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +/** Full venue detail data */ +export interface VenueDetail { + id: string; + name: string; + imageUrl: string; + location: string; + venueType?: string; + capacity?: number; + price?: number; + priceNote?: string; + description?: string; + features?: string[]; + religions?: string[]; + address?: string; +} + +/** Service add-on toggle */ +export interface VenueService { + id: string; + name: string; + description?: string; + price?: number; +} + +/** Form values for service toggles */ +export interface VenueDetailStepValues { + photoDisplay: boolean; + streaming: boolean; + recording: boolean; +} + +/** Props for the VenueDetailStep page component */ +export interface VenueDetailStepProps { + /** The venue to display */ + venue: VenueDetail; + /** Current service toggle values */ + values: VenueDetailStepValues; + /** Callback when service toggles change */ + onChange: (values: VenueDetailStepValues) => void; + /** Callback when "Add Venue" is clicked */ + onAddVenue: () => void; + /** Callback for back navigation */ + onBack?: () => void; + /** Callback for save-and-exit */ + onSaveAndExit?: () => void; + /** Whether the button is in a loading state */ + loading?: boolean; + /** Available venue services */ + services?: VenueService[]; + /** Whether this is a pre-planning flow */ + isPrePlanning?: boolean; + /** Navigation bar */ + navigation?: React.ReactNode; + /** Progress stepper */ + progressStepper?: React.ReactNode; + /** Running total */ + runningTotal?: React.ReactNode; + /** Hide the help bar */ + hideHelpBar?: boolean; + /** MUI sx prop */ + sx?: SxProps; +} + +// ─── Component ─────────────────────────────────────────────────────────────── + +/** + * Venue Detail page for the FA arrangement wizard. + * + * Detail-toggles layout: venue image + description on the left, + * venue info, price, CTA, and service toggles on the right. + * + * Reached by clicking a venue card on VenueStep. User reviews + * the venue and clicks "Add Venue" to select it. + * + * Pure presentation component — props in, callbacks out. + * + * Spec: documentation/steps/steps/07_venue_consolidated.yaml + */ +export const VenueDetailStep: React.FC = ({ + venue, + values, + onChange, + onAddVenue, + onBack, + onSaveAndExit, + loading = false, + services = [], + isPrePlanning = false, + navigation, + progressStepper, + runningTotal, + hideHelpBar, + sx, +}) => { + const handleToggle = (field: keyof VenueDetailStepValues, checked: boolean) => { + const next = { ...values, [field]: checked }; + if (field === 'streaming' && !checked) { + next.recording = false; + } + onChange(next); + }; + + return ( + + {/* Venue name */} + + {venue.name} + + + {/* Location */} + + + + {venue.location} + + + + {/* Venue type */} + {venue.venueType && ( + + {venue.venueType} + + )} + + {/* Price */} + {venue.price != null && ( + + ${venue.price.toLocaleString('en-AU')} + + )} + + {venue.priceNote && ( + + {venue.priceNote} + + )} + + {/* Add Venue CTA */} + { + e.preventDefault(); + if (!loading) onAddVenue(); + }} + > + + + + {onSaveAndExit && ( + + )} + + {/* Address */} + {venue.address && ( + <> + + Address + + + {venue.address} + + + )} + + {/* Supported religions */} + {venue.religions && venue.religions.length > 0 && ( + <> + + Supported service styles + + + {venue.religions.map((r) => ( + + ))} + + + )} + + {/* Service toggles */} + {services.length > 0 && ( + <> + + + Venue services + + + s.id === 'photo')?.price} + checked={values.photoDisplay} + onChange={(c) => handleToggle('photoDisplay', c)} + /> + s.id === 'streaming')?.price} + checked={values.streaming} + onChange={(c) => handleToggle('streaming', c)} + /> + {values.streaming && ( + s.id === 'recording')?.price} + checked={values.recording} + onChange={(c) => handleToggle('recording', c)} + /> + )} + + + )} + + } + > + {/* Left panel: image + description + features */} + + + {venue.description && ( + + {venue.description} + + )} + + {/* Features */} + {venue.features && venue.features.length > 0 && ( + + + Venue features + + + {venue.features.map((feature) => ( + + + {feature} + + ))} + + + )} + + ); +}; + +VenueDetailStep.displayName = 'VenueDetailStep'; +export default VenueDetailStep; diff --git a/src/components/pages/VenueDetailStep/index.ts b/src/components/pages/VenueDetailStep/index.ts new file mode 100644 index 0000000..eb181f0 --- /dev/null +++ b/src/components/pages/VenueDetailStep/index.ts @@ -0,0 +1,7 @@ +export { VenueDetailStep, default } from './VenueDetailStep'; +export type { + VenueDetailStepProps, + VenueDetailStepValues, + VenueDetail, + VenueService, +} from './VenueDetailStep'; diff --git a/src/components/pages/VenueStep/VenueStep.stories.tsx b/src/components/pages/VenueStep/VenueStep.stories.tsx index 217fcac..cbf9f67 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 { VenueStepValues, VenueStepErrors, Venue, VenueService } from './VenueStep'; +import type { Venue } from './VenueStep'; import { Navigation } from '../../organisms/Navigation'; import Box from '@mui/material/Box'; @@ -42,11 +42,6 @@ const sampleVenues: Venue[] = [ location: 'Strathfield', capacity: 120, price: 900, - description: - 'A modern chapel with natural light and flexible seating arrangements. Surrounded by established gardens.', - features: ['Air conditioning', 'Wheelchair accessible', 'On-site parking', 'Audio system'], - religions: ['No Religion', 'Civil Celebrant', 'Anglican', 'Catholic', 'Uniting Church'], - address: '42 The Boulevarde, Strathfield NSW 2135', }, { id: 'rose-garden', @@ -55,11 +50,6 @@ const sampleVenues: Venue[] = [ location: 'Homebush', capacity: 80, price: 750, - description: - 'An intimate setting surrounded by heritage rose gardens. Ideal for smaller gatherings.', - features: ['Garden access', 'Wheelchair accessible', 'Audio system'], - religions: ['No Religion', 'Civil Celebrant', 'Buddhism', 'Hinduism'], - address: '15 Olympic Park Road, Homebush NSW 2140', }, { id: 'memorial-hall', @@ -68,17 +58,6 @@ const sampleVenues: Venue[] = [ location: 'Strathfield', capacity: 250, price: 1200, - description: - 'A grand hall suited for large services. Full catering kitchen and stage with audio-visual equipment.', - features: [ - 'Full catering kitchen', - 'Stage', - 'Audio-visual system', - 'Wheelchair accessible', - 'Large car park', - ], - religions: ['All faiths welcome'], - address: '1 Redmyre Road, Strathfield NSW 2135', }, { id: 'lakeside', @@ -87,28 +66,9 @@ const sampleVenues: Venue[] = [ location: 'Concord', capacity: 60, price: 650, - description: - 'An open-air pavilion overlooking the lake. A peaceful setting for farewell services.', - features: ['Lake views', 'Natural setting', 'Covered pavilion'], - address: '8 Victoria Avenue, Concord NSW 2137', }, ]; -const sampleServices: VenueService[] = [ - { id: 'photo', name: 'Photo presentation', price: 150 }, - { id: 'streaming', name: 'Livestream', price: 200 }, - { id: 'recording', name: 'Recording', price: 100 }, -]; - -const defaultValues: VenueStepValues = { - search: '', - activeFilters: [], - selectedVenueId: null, - photoDisplay: false, - streaming: false, - recording: false, -}; - // ─── Meta ──────────────────────────────────────────────────────────────────── const meta: Meta = { @@ -123,88 +83,28 @@ const meta: Meta = { export default meta; type Story = StoryObj; -// ─── Interactive (default) ────────────────────────────────────────────────── +// ─── Default ─────────────────────────────────────────────────────────────── -/** Fully interactive — select a venue, see details + service toggles */ +/** Click-to-navigate — clicking a venue card triggers navigation */ export const Default: Story = { render: () => { - const [values, setValues] = useState({ ...defaultValues }); - const [errors, setErrors] = useState({}); - - const handleContinue = () => { - if (!values.selectedVenueId) { - setErrors({ selectedVenueId: 'Please choose a venue for the service.' }); - return; - } - setErrors({}); - alert(`Continue with venue: ${values.selectedVenueId}`); - }; + const [search, setSearch] = useState(''); + const [filters, setFilters] = useState([]); return ( { - setValues(v); - setErrors({}); - }} - onContinue={handleContinue} - onBack={() => alert('Back')} - onSaveAndExit={() => alert('Save and exit')} - errors={errors} venues={sampleVenues} - services={sampleServices} - locationName="Strathfield" - navigation={nav} - /> - ); - }, -}; - -// ─── Venue selected ───────────────────────────────────────────────────────── - -/** Venue already selected — detail panel and service toggles visible */ -export const VenueSelected: Story = { - render: () => { - const [values, setValues] = useState({ - ...defaultValues, - selectedVenueId: 'west-chapel', - photoDisplay: true, - }); - return ( - alert('Continue')} + onSelectVenue={(id) => alert(`Navigate to venue: ${id}`)} + searchQuery={search} + onSearchChange={setSearch} + activeFilters={filters} + onFilterToggle={(key) => + setFilters((prev) => + prev.includes(key) ? prev.filter((f) => f !== key) : [...prev, key], + ) + } + onFilterClear={() => setFilters([])} onBack={() => alert('Back')} - onSaveAndExit={() => alert('Save')} - venues={sampleVenues} - services={sampleServices} - locationName="Strathfield" - navigation={nav} - /> - ); - }, -}; - -// ─── With streaming + recording ───────────────────────────────────────────── - -/** Streaming enabled, revealing recording toggle */ -export const WithStreaming: Story = { - render: () => { - const [values, setValues] = useState({ - ...defaultValues, - selectedVenueId: 'memorial-hall', - streaming: true, - recording: true, - }); - return ( - alert('Continue')} - onBack={() => alert('Back')} - venues={sampleVenues} - services={sampleServices} locationName="Strathfield" navigation={nav} /> @@ -217,15 +117,15 @@ export const WithStreaming: Story = { /** Pre-planning variant copy */ export const PrePlanning: Story = { render: () => { - const [values, setValues] = useState({ ...defaultValues }); + const [search, setSearch] = useState(''); + return ( alert('Continue')} + venues={sampleVenues} + onSelectVenue={(id) => alert(`Navigate to venue: ${id}`)} + searchQuery={search} + onSearchChange={setSearch} onBack={() => alert('Back')} - venues={sampleVenues} - services={sampleServices} isPrePlanning navigation={nav} /> @@ -233,42 +133,20 @@ export const PrePlanning: Story = { }, }; -// ─── Validation error ─────────────────────────────────────────────────────── - -/** No venue selected with error */ -export const WithError: Story = { - render: () => { - const [values, setValues] = useState({ ...defaultValues }); - return ( - {}} - errors={{ selectedVenueId: 'Please choose a venue for the service.' }} - venues={sampleVenues} - services={sampleServices} - locationName="Strathfield" - navigation={nav} - /> - ); - }, -}; - // ─── Empty state ──────────────────────────────────────────────────────────── /** No venues found */ export const NoVenues: Story = { render: () => { - const [values, setValues] = useState({ - ...defaultValues, - search: 'Wollongong', - }); + const [search, setSearch] = useState('Wollongong'); + return ( {}} venues={[]} + onSelectVenue={() => {}} + searchQuery={search} + onSearchChange={setSearch} + onBack={() => alert('Back')} locationName="Wollongong" navigation={nav} /> diff --git a/src/components/pages/VenueStep/VenueStep.tsx b/src/components/pages/VenueStep/VenueStep.tsx index 9c0d1ac..83bb390 100644 --- a/src/components/pages/VenueStep/VenueStep.tsx +++ b/src/components/pages/VenueStep/VenueStep.tsx @@ -3,16 +3,12 @@ import Box from '@mui/material/Box'; import TextField from '@mui/material/TextField'; import InputAdornment from '@mui/material/InputAdornment'; import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined'; -import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; import type { SxProps, Theme } from '@mui/material/styles'; import { WizardLayout } from '../../templates/WizardLayout'; import { VenueCard } from '../../molecules/VenueCard'; -import { AddOnOption } from '../../molecules/AddOnOption'; import { FilterPanel } from '../../molecules/FilterPanel'; -import { Collapse } from '../../atoms/Collapse'; import { Chip } from '../../atoms/Chip'; import { Typography } from '../../atoms/Typography'; -import { Button } from '../../atoms/Button'; // ─── Types ─────────────────────────────────────────────────────────────────── @@ -24,65 +20,30 @@ export interface Venue { location: string; capacity?: number; price?: number; - description?: string; - features?: string[]; - religions?: string[]; - address?: string; -} - -/** Service add-on toggle */ -export interface VenueService { - id: string; - name: string; - description?: string; - price?: number; -} - -/** Form values for the venue step */ -export interface VenueStepValues { - /** Search query */ - search: string; - /** Active filter chip keys */ - activeFilters: string[]; - /** Selected venue ID */ - selectedVenueId: string | null; - /** Photo presentation enabled */ - photoDisplay: boolean; - /** Livestream enabled */ - streaming: boolean; - /** Recording enabled (depends on streaming) */ - recording: boolean; -} - -/** Field-level error messages */ -export interface VenueStepErrors { - selectedVenueId?: string; } /** Props for the VenueStep page component */ export interface VenueStepProps { - /** Current form values */ - values: VenueStepValues; - /** Callback when any field value changes */ - onChange: (values: VenueStepValues) => void; - /** Callback when the Continue button is clicked */ - onContinue: () => void; - /** Callback for back navigation */ - onBack?: () => void; - /** Callback for save-and-exit */ - onSaveAndExit?: () => void; - /** Field-level validation errors */ - errors?: VenueStepErrors; - /** Whether the Continue button is in a loading state */ - loading?: boolean; - /** Available venues */ + /** List of venues to display */ venues: Venue[]; - /** Available service add-ons shown after venue selection */ - services?: VenueService[]; + /** Callback when a venue card is clicked — triggers navigation to VenueDetailStep */ + onSelectVenue: (id: string) => void; + /** Search query value */ + searchQuery: string; + /** Callback when search query changes */ + onSearchChange: (query: string) => void; + /** Callback when search is submitted */ + onSearch?: (query: string) => void; /** Filter chip options */ filterOptions?: Array<{ key: string; label: string }>; + /** Active filter keys */ + activeFilters?: string[]; + /** Callback when a filter chip is toggled */ + onFilterToggle?: (key: string) => void; /** Callback to clear all filters */ onFilterClear?: () => void; + /** Callback for back navigation */ + onBack: () => void; /** Location name for the results count */ locationName?: string; /** Whether this is a pre-planning flow */ @@ -91,12 +52,6 @@ export interface VenueStepProps { mapPanel?: React.ReactNode; /** Navigation bar — passed through to WizardLayout */ navigation?: React.ReactNode; - /** Progress stepper — passed through to WizardLayout */ - progressStepper?: React.ReactNode; - /** Running total — passed through to WizardLayout */ - runningTotal?: React.ReactNode; - /** Hide the help bar */ - hideHelpBar?: boolean; /** MUI sx prop for the root */ sx?: SxProps; } @@ -104,77 +59,50 @@ export interface VenueStepProps { // ─── Component ─────────────────────────────────────────────────────────────── /** - * Step 7 — Service Venue for the FA arrangement wizard. + * Step 7 — Service Venue selection for the FA arrangement wizard. * - * Consolidated from 3 baseline steps (venues + venue details + venue services) - * into a single step with progressive disclosure (Rec #5). + * List + Map split layout. Left panel shows a scrollable list of + * venue cards with search and filter button. Right panel is a + * slot for map integration. * - * Three phases: - * 1. Venue card grid with search/filters (grid-sidebar layout) - * 2. Selected venue detail (inline Collapse below grid) - * 3. Service toggles (photo presentation, streaming, recording) + * Click-to-navigate: clicking a venue card triggers navigation + * to VenueDetailStep — no selection state or Continue button. * * Pure presentation component — props in, callbacks out. * * Spec: documentation/steps/steps/07_venue_consolidated.yaml */ export const VenueStep: React.FC = ({ - values, - onChange, - onContinue, - onBack, - onSaveAndExit, - errors, - loading = false, venues, - services = [], + onSelectVenue, + searchQuery, + onSearchChange, + onSearch, filterOptions = [ { key: 'features', label: 'Venue Features' }, { key: 'religion', label: 'Religion' }, ], + activeFilters = [], + onFilterToggle, onFilterClear, + onBack, locationName, isPrePlanning = false, mapPanel, navigation, - progressStepper, - runningTotal, - hideHelpBar, sx, }) => { - const selectedVenue = venues.find((v) => v.id === values.selectedVenueId) ?? null; - const hasSelection = selectedVenue !== null; - - const handleVenueSelect = (venueId: string) => { - onChange({ ...values, selectedVenueId: venueId }); - }; - - const handleFilterToggle = (key: string) => { - const next = values.activeFilters.includes(key) - ? values.activeFilters.filter((f) => f !== key) - : [...values.activeFilters, key]; - onChange({ ...values, activeFilters: next }); - }; - - const handleToggle = (field: 'photoDisplay' | 'streaming' | 'recording', checked: boolean) => { - const next = { ...values, [field]: checked }; - // Disable recording when streaming is turned off - if (field === 'streaming' && !checked) { - next.recording = false; - } - onChange(next); - }; + const subheading = isPrePlanning + ? 'Browse available venues. Your choice can be changed later.' + : 'Choose a venue for the funeral service. You can filter by location, features, and religion.'; return ( = ({ ) } > + {/* Sticky header — stays pinned while card list scrolls */} { - e.preventDefault(); - if (!loading) onContinue(); + sx={{ + position: 'sticky', + top: 0, + zIndex: 1, + bgcolor: 'background.default', + pt: 2, + pb: 1, + mx: { xs: -2, md: -3 }, + px: { xs: 2, md: 3 }, }} > - {/* Sticky header — stays pinned while card list scrolls */} - + Where would you like the service? + + + {subheading} + + + {/* Location search */} + onSearchChange(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && onSearch) { + e.preventDefault(); + onSearch(searchQuery); + } }} + fullWidth + size="small" + InputProps={{ + startAdornment: ( + + + + ), + }} + sx={{ mb: 1.5 }} + /> + + {/* Filters — right-aligned below search */} + + + + {filterOptions.map((filter) => ( + onFilterToggle(filter.key) : undefined} + variant="outlined" + size="small" + /> + ))} + + + + + {/* Results count */} + - - Where would you like the service? - + {venues.length} venue{venues.length !== 1 ? 's' : ''} + {locationName ? ` near ${locationName}` : ''} found + + - - {isPrePlanning - ? 'Browse available venues. Your choice can be changed later.' - : 'Choose a venue for the funeral service. You can filter by location, features, and religion.'} - - - {/* ─── Location search ─── */} - onChange({ ...values, search: e.target.value })} - fullWidth - size="small" - InputProps={{ - startAdornment: ( - - - - ), - }} - sx={{ mb: 1.5 }} + {/* Venue list — click-to-navigate */} + + {venues.map((venue) => ( + onSelectVenue(venue.id)} + aria-label={`${venue.name}, ${venue.location}${venue.price ? `, $${venue.price}` : ''}`} /> + ))} - {/* ─── Filters — right-aligned below search ─── */} - - - - {filterOptions.map((filter) => ( - handleFilterToggle(filter.key)} - selected={values.activeFilters.includes(filter.key)} - /> - ))} - - - - - {/* ─── Results count ─── */} - - {venues.length} venue{venues.length !== 1 ? 's' : ''} - {locationName ? ` near ${locationName}` : ''} found - - - - {/* ─── Venue card grid ─── */} - - {venues.map((venue, index) => ( - handleVenueSelect(venue.id)} - role="radio" - aria-checked={venue.id === values.selectedVenueId} - tabIndex={ - values.selectedVenueId === null - ? index === 0 - ? 0 - : -1 - : venue.id === values.selectedVenueId - ? 0 - : -1 - } - /> - ))} - - {venues.length === 0 && ( - - - No venues found in this area. - - - Try adjusting your search or clearing filters. - - - )} - - - {/* Validation error */} - {errors?.selectedVenueId && ( - - {errors.selectedVenueId} - - )} - - {/* ─── Selected venue detail (progressive disclosure) ─── */} - - {selectedVenue && ( - - - {selectedVenue.name} - - - {selectedVenue.address && ( - - - - {selectedVenue.address} - - - )} - - {selectedVenue.description && ( - - {selectedVenue.description} - - )} - - {/* Features */} - {selectedVenue.features && selectedVenue.features.length > 0 && ( - - - Features - - - {selectedVenue.features.map((feature) => ( - - - {feature} - - ))} - - - )} - - {/* Supported religions */} - {selectedVenue.religions && selectedVenue.religions.length > 0 && ( - - - Supported service styles - - - {selectedVenue.religions.map((r) => ( - - ))} - - - )} - - )} - - - {/* ─── Service toggles (after venue selection) ─── */} - - - - Venue services + {venues.length === 0 && ( + + + No venues found in this area. + + + Try adjusting your search or clearing filters. - - s.id === 'photo')?.price} - checked={values.photoDisplay} - onChange={(c) => handleToggle('photoDisplay', c)} - /> - - s.id === 'streaming')?.price} - checked={values.streaming} - onChange={(c) => handleToggle('streaming', c)} - /> - - - s.id === 'recording')?.price} - checked={values.recording} - onChange={(c) => handleToggle('recording', c)} - /> - - - - {/* CTAs */} - - {onSaveAndExit && ( - - )} - - + )} ); diff --git a/src/components/pages/VenueStep/index.ts b/src/components/pages/VenueStep/index.ts index a65a48a..4d92864 100644 --- a/src/components/pages/VenueStep/index.ts +++ b/src/components/pages/VenueStep/index.ts @@ -1,8 +1,2 @@ export { VenueStep, default } from './VenueStep'; -export type { - VenueStepProps, - VenueStepValues, - VenueStepErrors, - Venue, - VenueService, -} from './VenueStep'; +export type { VenueStepProps, Venue } from './VenueStep';