From be99acd51ee6237ec4758e10af43ab8dcf99dd3f Mon Sep 17 00:00:00 2001 From: Richie Date: Sun, 29 Mar 2026 22:29:24 +1100 Subject: [PATCH] Batch 4: Click-to-navigate providers (D-D), simplify coffin details (D-G) ProvidersStep (D-D): - Remove selection state (selectedProviderId) and Continue button - Clicking a provider card triggers navigation directly - Remove radiogroup pattern, error, loading props - Cards are now simple interactive links, not radio buttons - Stories updated: removed WithSelection, WithError, Loading CoffinDetailsStep (D-G): - Remove all customisation (handles, lining, nameplate) - Remove OptionSection helper, ProductOption/CoffinDetailsStepValues types - Simplified to coffin profile (image, specs, price) + Continue CTA - Changed from detail-toggles split to centered-form layout - Customisation noted as future enhancement - Updated index.ts re-exports to match simplified API - Stories simplified: Default, PrePlanning, MinimalInfo, Loading Co-Authored-By: Claude Opus 4.6 (1M context) --- .../CoffinDetailsStep.stories.tsx | 176 +++--------- .../CoffinDetailsStep/CoffinDetailsStep.tsx | 256 ++++-------------- .../pages/CoffinDetailsStep/index.ts | 8 +- .../ProvidersStep/ProvidersStep.stories.tsx | 104 +------ .../pages/ProvidersStep/ProvidersStep.tsx | 60 +--- 5 files changed, 106 insertions(+), 498 deletions(-) diff --git a/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.stories.tsx b/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.stories.tsx index 7d5beab..f4d3dd4 100644 --- a/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.stories.tsx +++ b/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.stories.tsx @@ -1,7 +1,6 @@ -import { useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import { CoffinDetailsStep } from './CoffinDetailsStep'; -import type { CoffinDetailsStepValues, CoffinProfile, ProductOption } from './CoffinDetailsStep'; +import type { CoffinProfile } from './CoffinDetailsStep'; import { Navigation } from '../../organisms/Navigation'; import Box from '@mui/material/Box'; @@ -50,54 +49,6 @@ const sampleCoffin: CoffinProfile = { priceNote: 'Selecting this coffin does not change your plan total.', }; -const handleOptions: ProductOption[] = [ - { - id: 'brass-bar', - name: 'Brass Bar Handle', - description: 'Traditional brass bar handles', - price: 0, - }, - { - id: 'gold-swing', - name: 'Gold Swing Handle', - description: 'Elegant gold swing-arm handles', - price: 150, - }, - { - id: 'timber-bar', - name: 'Timber Bar Handle', - description: 'Matching timber bar handles', - price: 80, - }, -]; - -const liningOptions: ProductOption[] = [ - { id: 'white-satin', name: 'White Satin', description: 'Classic white satin interior', price: 0 }, - { id: 'cream-silk', name: 'Cream Silk', description: 'Premium cream silk lining', price: 120 }, - { id: 'blue-velvet', name: 'Blue Velvet', description: 'Royal blue velvet interior', price: 180 }, -]; - -const namePlateOptions: ProductOption[] = [ - { - id: 'standard-brass', - name: 'Standard Brass', - description: 'Engraved brass name plate', - price: 0, - }, - { - id: 'premium-silver', - name: 'Premium Silver', - description: 'Silver-plated name plate with decorative border', - price: 95, - }, -]; - -const defaultValues: CoffinDetailsStepValues = { - handlesId: null, - liningId: null, - namePlateId: null, -}; - // ─── Meta ──────────────────────────────────────────────────────────────────── const meta: Meta = { @@ -112,94 +63,57 @@ const meta: Meta = { export default meta; type Story = StoryObj; -// ─── Interactive (default) ────────────────────────────────────────────────── +// ─── Default ──────────────────────────────────────────────────────────────── -/** Full customisation flow */ +/** Coffin details — simplified view with profile + CTA (D-G) */ export const Default: Story = { - render: () => { - const [values, setValues] = useState({ ...defaultValues }); - return ( - alert(`Options: ${JSON.stringify(values)}`)} - onBack={() => alert('Back to coffin selection')} - onSaveAndExit={() => alert('Save')} - coffin={sampleCoffin} - handleOptions={handleOptions} - liningOptions={liningOptions} - namePlateOptions={namePlateOptions} - navigation={nav} - /> - ); - }, -}; - -// ─── With selections ──────────────────────────────────────────────────────── - -/** All options pre-selected */ -export const WithSelections: Story = { - render: () => { - const [values, setValues] = useState({ - handlesId: 'gold-swing', - liningId: 'cream-silk', - namePlateId: 'standard-brass', - }); - return ( - alert('Continue')} - onBack={() => alert('Back')} - coffin={sampleCoffin} - handleOptions={handleOptions} - liningOptions={liningOptions} - namePlateOptions={namePlateOptions} - navigation={nav} - /> - ); - }, -}; - -// ─── Minimal options ──────────────────────────────────────────────────────── - -/** Only handles available (lining + nameplate not offered by provider) */ -export const MinimalOptions: Story = { - render: () => { - const [values, setValues] = useState({ ...defaultValues }); - return ( - alert('Continue')} - onBack={() => alert('Back')} - coffin={sampleCoffin} - handleOptions={handleOptions} - navigation={nav} - /> - ); + args: { + coffin: sampleCoffin, + onContinue: () => alert('Continue'), + onBack: () => alert('Back to coffin selection'), + onSaveAndExit: () => alert('Save'), + navigation: nav, }, }; // ─── Pre-planning ─────────────────────────────────────────────────────────── -/** Pre-planning variant */ +/** Pre-planning variant — softer copy */ export const PrePlanning: Story = { - render: () => { - const [values, setValues] = useState({ ...defaultValues }); - return ( - alert('Continue')} - onBack={() => alert('Back')} - coffin={sampleCoffin} - handleOptions={handleOptions} - liningOptions={liningOptions} - namePlateOptions={namePlateOptions} - isPrePlanning - navigation={nav} - /> - ); + args: { + coffin: sampleCoffin, + onContinue: () => alert('Continue'), + onBack: () => alert('Back'), + isPrePlanning: true, + navigation: nav, + }, +}; + +// ─── Minimal info ─────────────────────────────────────────────────────────── + +/** Coffin with no specs or description */ +export const MinimalInfo: Story = { + args: { + coffin: { + name: 'Standard White', + imageUrl: 'https://placehold.co/600x400/F5F5F0/8B8B7E?text=Standard+White', + price: 1200, + }, + onContinue: () => alert('Continue'), + onBack: () => alert('Back'), + navigation: nav, + }, +}; + +// ─── Loading ──────────────────────────────────────────────────────────────── + +/** Continue button loading */ +export const Loading: Story = { + args: { + coffin: sampleCoffin, + onContinue: () => {}, + onBack: () => alert('Back'), + loading: true, + navigation: nav, }, }; diff --git a/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.tsx b/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.tsx index bb9b820..e292445 100644 --- a/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.tsx +++ b/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.tsx @@ -1,11 +1,5 @@ import React from 'react'; import Box from '@mui/material/Box'; -import Paper from '@mui/material/Paper'; -import FormControl from '@mui/material/FormControl'; -import FormLabel from '@mui/material/FormLabel'; -import FormControlLabel from '@mui/material/FormControlLabel'; -import RadioGroup from '@mui/material/RadioGroup'; -import Radio from '@mui/material/Radio'; import type { SxProps, Theme } from '@mui/material/styles'; import { WizardLayout } from '../../templates/WizardLayout'; import { Typography } from '../../atoms/Typography'; @@ -20,15 +14,6 @@ export interface CoffinSpec { value: string; } -/** A product customisation option (handle, lining, nameplate) */ -export interface ProductOption { - id: string; - name: string; - description?: string; - price?: number; - imageUrl?: string; -} - /** Selected coffin profile data */ export interface CoffinProfile { name: string; @@ -39,22 +24,8 @@ export interface CoffinProfile { priceNote?: string; } -/** Form values for the coffin details step */ -export interface CoffinDetailsStepValues { - /** Selected handle option ID */ - handlesId: string | null; - /** Selected lining option ID */ - liningId: string | null; - /** Selected nameplate option ID */ - namePlateId: string | null; -} - /** Props for the CoffinDetailsStep page component */ export interface CoffinDetailsStepProps { - /** Current form values */ - values: CoffinDetailsStepValues; - /** Callback when any field value changes */ - onChange: (values: CoffinDetailsStepValues) => void; /** Callback when the Continue button is clicked */ onContinue: () => void; /** Callback for back navigation */ @@ -65,12 +36,6 @@ export interface CoffinDetailsStepProps { loading?: boolean; /** The selected coffin profile */ coffin: CoffinProfile; - /** Available handle options */ - handleOptions?: ProductOption[]; - /** Available lining options */ - liningOptions?: ProductOption[]; - /** Available nameplate options */ - namePlateOptions?: ProductOption[]; /** Whether this is a pre-planning flow */ isPrePlanning?: boolean; /** Navigation bar */ @@ -85,102 +50,25 @@ export interface CoffinDetailsStepProps { sx?: SxProps; } -// ─── Option section helper ─────────────────────────────────────────────────── - -const OptionSection: React.FC<{ - legend: string; - options: ProductOption[]; - selectedId: string | null; - onChange: (id: string) => void; -}> = ({ legend, options, selectedId, onChange }) => { - if (options.length === 0) return null; - - return ( - - - - - {legend} - - - onChange(e.target.value)} - sx={{ gap: 1.5 }} - > - {options.map((opt) => ( - } - label={ - - - {opt.name} - {opt.description && ( - - {opt.description} - - )} - - {opt.price != null && ( - - {opt.price === 0 ? 'Included' : `+$${opt.price.toLocaleString('en-AU')}`} - - )} - - } - sx={{ - alignItems: 'flex-start', - mx: 0, - py: 1.5, - px: 2, - borderRadius: 1, - border: 1, - borderColor: selectedId === opt.id ? 'var(--fa-color-border-brand)' : 'divider', - bgcolor: selectedId === opt.id ? 'var(--fa-color-surface-warm)' : 'transparent', - '&:hover': { bgcolor: 'action.hover' }, - '& .MuiFormControlLabel-label': { flex: 1 }, - }} - /> - ))} - - - - ); -}; - // ─── Component ─────────────────────────────────────────────────────────────── /** * Step 11 — Coffin Details for the FA arrangement wizard. * - * Customise the selected coffin — choose handles, lining, and nameplate. - * Shows coffin profile at top (image, specs, description, price note). - * Three option sections below, each as a RadioGroup in a Paper card. - * - * Price impact shown inline per option ("Included" or "+$X"). - * Options within package allowance show "Included". + * 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). * * Pure presentation component — props in, callbacks out. * * Spec: documentation/steps/steps/11_coffin_details.yaml */ export const CoffinDetailsStep: React.FC = ({ - values, - onChange, onContinue, onBack, onSaveAndExit, loading = false, coffin, - handleOptions = [], - liningOptions = [], - namePlateOptions = [], isPrePlanning = false, navigation, progressStepper, @@ -188,10 +76,29 @@ export const CoffinDetailsStep: React.FC = ({ hideHelpBar, sx, }) => { - // ─── Left panel: Coffin profile (image + specs) ─── - const profilePanel = ( - - {/* Image */} + return ( + + + 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 */} = ({ }} /> + {/* Coffin name + description */} {coffin.name} @@ -217,7 +125,7 @@ export const CoffinDetailsStep: React.FC = ({ )} - {/* Specs */} + {/* Specs grid */} {coffin.specs && coffin.specs.length > 0 && ( = ({ gridTemplateColumns: 'auto 1fr', gap: 0.5, columnGap: 2, - mb: 2, + mb: 3, }} > {coffin.specs.map((spec) => ( @@ -240,106 +148,38 @@ export const CoffinDetailsStep: React.FC = ({ )} {/* Price */} - + ${coffin.price.toLocaleString('en-AU')} {coffin.priceNote && ( - + {coffin.priceNote} )} - - ); - // ─── Right panel: Option selectors + CTAs ─── - const optionsPanel = ( - - - Coffin details - - - - {isPrePlanning - ? 'These options let you personalise the coffin. You can change these later.' - : 'Personalise your chosen coffin with handles, lining, and a name plate.'} - - - - Each option shows the price impact on your plan total. Options within your package allowance - are included at no extra cost. - + + {/* CTAs */} { - e.preventDefault(); - if (!loading) onContinue(); + sx={{ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + flexDirection: { xs: 'column-reverse', sm: 'row' }, + gap: 2, }} > - onChange({ ...values, handlesId: id })} - /> - - onChange({ ...values, liningId: id })} - /> - - onChange({ ...values, namePlateId: id })} - /> - - - - {/* CTAs */} - - {onSaveAndExit ? ( - - ) : ( - - )} - - + ) : ( + + )} + - - ); - - return ( - - {profilePanel} ); }; diff --git a/src/components/pages/CoffinDetailsStep/index.ts b/src/components/pages/CoffinDetailsStep/index.ts index 53c3505..8573423 100644 --- a/src/components/pages/CoffinDetailsStep/index.ts +++ b/src/components/pages/CoffinDetailsStep/index.ts @@ -1,8 +1,2 @@ export { CoffinDetailsStep, default } from './CoffinDetailsStep'; -export type { - CoffinDetailsStepProps, - CoffinDetailsStepValues, - CoffinProfile, - CoffinSpec, - ProductOption, -} from './CoffinDetailsStep'; +export type { CoffinDetailsStepProps, CoffinProfile, CoffinSpec } from './CoffinDetailsStep'; diff --git a/src/components/pages/ProvidersStep/ProvidersStep.stories.tsx b/src/components/pages/ProvidersStep/ProvidersStep.stories.tsx index 5cc0829..d927378 100644 --- a/src/components/pages/ProvidersStep/ProvidersStep.stories.tsx +++ b/src/components/pages/ProvidersStep/ProvidersStep.stories.tsx @@ -118,66 +118,27 @@ type Story = StoryObj; // ─── Interactive (default) ────────────────────────────────────────────────── -/** Fully interactive — search, filter, select a provider, continue */ +/** Click-to-navigate — clicking a provider triggers navigation (D-D) */ export const Default: Story = { render: () => { - const [selectedId, setSelectedId] = useState(null); const [query, setQuery] = useState(''); const [filters, setFilters] = useState(defaultFilters); - const [error, setError] = useState(); const filtered = mockProviders.filter((p) => p.name.toLowerCase().includes(query.toLowerCase()), ); - const handleContinue = () => { - if (!selectedId) { - setError('Please choose a funeral provider to continue.'); - return; - } - setError(undefined); - alert(`Continue with provider: ${selectedId}`); - }; - return ( { - setSelectedId(id); - setError(undefined); - }} + onSelectProvider={(id) => alert(`Navigate to provider: ${id}`)} searchQuery={query} onSearchChange={setQuery} filters={filters} onFilterToggle={(i) => setFilters((prev) => prev.map((f, idx) => (idx === i ? { ...f, active: !f.active } : f))) } - onContinue={handleContinue} - onBack={() => alert('Back')} - error={error} - navigation={nav} - /> - ); - }, -}; - -// ─── With selected provider ───────────────────────────────────────────────── - -/** Provider already selected — ready to continue */ -export const WithSelection: Story = { - render: () => { - const [selectedId, setSelectedId] = useState('parsons'); - const [query, setQuery] = useState(''); - - return ( - alert('Continue')} + onFilterClear={() => setFilters((prev) => prev.map((f) => ({ ...f, active: false })))} onBack={() => alert('Back')} navigation={nav} /> @@ -190,17 +151,14 @@ export const WithSelection: Story = { /** Pre-planning flow — softer copy */ export const PrePlanning: Story = { render: () => { - const [selectedId, setSelectedId] = useState(null); const [query, setQuery] = useState(''); return ( alert(`Navigate to provider: ${id}`)} searchQuery={query} onSearchChange={setQuery} - onContinue={() => alert('Continue')} onBack={() => alert('Back')} navigation={nav} isPrePlanning @@ -209,30 +167,6 @@ export const PrePlanning: Story = { }, }; -// ─── Validation error ─────────────────────────────────────────────────────── - -/** No provider selected, error shown */ -export const WithError: Story = { - render: () => { - const [selectedId, setSelectedId] = useState(null); - const [query, setQuery] = useState(''); - - return ( - {}} - onBack={() => alert('Back')} - error="Please choose a funeral provider to continue." - navigation={nav} - /> - ); - }, -}; - // ─── Empty results ────────────────────────────────────────────────────────── /** Search yielded no results */ @@ -243,11 +177,9 @@ export const EmptyResults: Story = { return ( {}} searchQuery={query} onSearchChange={setQuery} - onContinue={() => {}} onBack={() => alert('Back')} navigation={nav} /> @@ -255,45 +187,19 @@ export const EmptyResults: Story = { }, }; -// ─── Loading state ────────────────────────────────────────────────────────── - -/** Continue button loading */ -export const Loading: Story = { - render: () => { - const [query, setQuery] = useState(''); - - return ( - {}} - searchQuery={query} - onSearchChange={setQuery} - onContinue={() => {}} - onBack={() => alert('Back')} - loading - navigation={nav} - /> - ); - }, -}; - // ─── Single provider ──────────────────────────────────────────────────────── /** Only one provider available */ export const SingleProvider: Story = { render: () => { - const [selectedId, setSelectedId] = useState(null); const [query, setQuery] = useState(''); return ( alert(`Navigate to provider: ${id}`)} searchQuery={query} onSearchChange={setQuery} - onContinue={() => alert('Continue')} onBack={() => alert('Back')} navigation={nav} /> diff --git a/src/components/pages/ProvidersStep/ProvidersStep.tsx b/src/components/pages/ProvidersStep/ProvidersStep.tsx index 960d560..8811446 100644 --- a/src/components/pages/ProvidersStep/ProvidersStep.tsx +++ b/src/components/pages/ProvidersStep/ProvidersStep.tsx @@ -7,7 +7,6 @@ import { SearchBar } from '../../molecules/SearchBar'; import { FilterPanel } from '../../molecules/FilterPanel'; import { Chip } from '../../atoms/Chip'; import { Typography } from '../../atoms/Typography'; -import { Button } from '../../atoms/Button'; // ─── Types ─────────────────────────────────────────────────────────────────── @@ -49,9 +48,7 @@ export interface ProviderFilter { export interface ProvidersStepProps { /** List of providers to display */ providers: ProviderData[]; - /** Currently selected provider ID */ - selectedProviderId: string | null; - /** Callback when a provider is selected */ + /** Callback when a provider card is clicked — triggers navigation (D-D) */ onSelectProvider: (id: string) => void; /** Search query value */ searchQuery: string; @@ -65,14 +62,8 @@ export interface ProvidersStepProps { onFilterToggle?: (index: number) => void; /** Callback to clear all filters */ onFilterClear?: () => void; - /** Callback for the Continue button */ - onContinue: () => void; /** Callback for the Back button */ onBack: () => void; - /** Validation error message */ - error?: string; - /** Whether the Continue action is loading */ - loading?: boolean; /** Map panel content — slot for future map integration */ mapPanel?: React.ReactNode; /** Navigation bar — passed through to WizardLayout */ @@ -89,11 +80,11 @@ export interface ProvidersStepProps { * Step 2 — Provider selection page for the FA arrangement wizard. * * List + Map split layout. Left panel shows a scrollable list of - * provider cards with search and filter chips. Right panel is a + * provider cards with search and filter button. Right panel is a * slot for future map integration. * - * Uses radiogroup pattern for card selection — arrow keys navigate - * between cards, Space/Enter selects. + * Click-to-navigate (D-D): clicking a provider card triggers + * navigation directly — no selection state or Continue button. * * Pure presentation component — props in, callbacks out. * @@ -101,7 +92,6 @@ export interface ProvidersStepProps { */ export const ProvidersStep: React.FC = ({ providers, - selectedProviderId, onSelectProvider, searchQuery, onSearchChange, @@ -109,10 +99,7 @@ export const ProvidersStep: React.FC = ({ filters, onFilterToggle, onFilterClear, - onContinue, onBack, - error, - loading = false, mapPanel, navigation, isPrePlanning = false, @@ -212,22 +199,10 @@ export const ProvidersStep: React.FC = ({ - {/* Error message */} - {error && ( - - {error} - - )} - - {/* Provider list — radiogroup pattern */} + {/* Provider list — click-to-navigate (D-D) */} {providers.map((provider) => ( = ({ rating={provider.rating} reviewCount={provider.reviewCount} startingPrice={provider.startingPrice} - selected={selectedProviderId === provider.id} onClick={() => onSelectProvider(provider.id)} - role="radio" - aria-checked={selectedProviderId === provider.id} aria-label={`${provider.name}, ${provider.location}${provider.rating ? `, rated ${provider.rating}` : ''}${provider.startingPrice ? `, from $${provider.startingPrice}` : ''}`} /> ))} {providers.length === 0 && ( - + No providers found matching your search. @@ -264,19 +231,6 @@ export const ProvidersStep: React.FC = ({ )} - - {/* Continue button */} - - - ); };