diff --git a/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.stories.tsx b/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.stories.tsx new file mode 100644 index 0000000..7d5beab --- /dev/null +++ b/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.stories.tsx @@ -0,0 +1,205 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { CoffinDetailsStep } from './CoffinDetailsStep'; +import type { CoffinDetailsStepValues, CoffinProfile, ProductOption } from './CoffinDetailsStep'; +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 sampleCoffin: CoffinProfile = { + name: 'Cedar Classic', + imageUrl: 'https://images.unsplash.com/photo-1618220179428-22790b461013?w=600&h=400&fit=crop', + description: + 'A beautifully crafted solid cedar coffin with a natural satin finish. Handmade with care and attention to detail.', + specs: [ + { label: 'Range', value: 'Solid Timber' }, + { label: 'Shape', value: 'Coffin' }, + { label: 'Wood', value: 'Cedar' }, + { label: 'Finish', value: 'Satin' }, + { label: 'Hardware', value: 'Brass' }, + ], + price: 2800, + 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 = { + title: 'Pages/CoffinDetailsStep', + component: CoffinDetailsStep, + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + }, +}; + +export default meta; +type Story = StoryObj; + +// ─── Interactive (default) ────────────────────────────────────────────────── + +/** Full customisation flow */ +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} + /> + ); + }, +}; + +// ─── Pre-planning ─────────────────────────────────────────────────────────── + +/** Pre-planning variant */ +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} + /> + ); + }, +}; diff --git a/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.tsx b/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.tsx new file mode 100644 index 0000000..f161354 --- /dev/null +++ b/src/components/pages/CoffinDetailsStep/CoffinDetailsStep.tsx @@ -0,0 +1,350 @@ +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'; +import { Button } from '../../atoms/Button'; +import { Divider } from '../../atoms/Divider'; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +/** Coffin specification key-value pair */ +export interface CoffinSpec { + label: string; + 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; + imageUrl: string; + description?: string; + specs?: CoffinSpec[]; + price: number; + 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 */ + onBack?: () => void; + /** Callback for save-and-exit */ + onSaveAndExit?: () => void; + /** Whether the Continue button is in a loading state */ + 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 */ + navigation?: React.ReactNode; + /** Progress stepper */ + progressStepper?: React.ReactNode; + /** Running total */ + runningTotal?: React.ReactNode; + /** Hide the help bar */ + hideHelpBar?: boolean; + /** MUI sx prop */ + 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". + * + * 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, + runningTotal, + hideHelpBar, + sx, +}) => { + return ( + + {/* Page heading */} + + 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. + + + {/* ─── Coffin profile ─── */} + + + {/* Image */} + + + {/* Details */} + + + {coffin.name} + + + {coffin.description && ( + + {coffin.description} + + )} + + {/* Specs */} + {coffin.specs && coffin.specs.length > 0 && ( + + {coffin.specs.map((spec) => ( + + + {spec.label} + + {spec.value} + + ))} + + )} + + {/* Price */} + + ${coffin.price.toLocaleString('en-AU')} + + {coffin.priceNote && ( + + {coffin.priceNote} + + )} + + + + + { + e.preventDefault(); + onContinue(); + }} + > + {/* ─── Option sections ─── */} + onChange({ ...values, handlesId: id })} + /> + + onChange({ ...values, liningId: id })} + /> + + onChange({ ...values, namePlateId: id })} + /> + + + + {/* CTAs */} + + {onSaveAndExit ? ( + + ) : ( + + )} + + + + + ); +}; + +CoffinDetailsStep.displayName = 'CoffinDetailsStep'; +export default CoffinDetailsStep; diff --git a/src/components/pages/CoffinDetailsStep/index.ts b/src/components/pages/CoffinDetailsStep/index.ts new file mode 100644 index 0000000..53c3505 --- /dev/null +++ b/src/components/pages/CoffinDetailsStep/index.ts @@ -0,0 +1,8 @@ +export { CoffinDetailsStep, default } from './CoffinDetailsStep'; +export type { + CoffinDetailsStepProps, + CoffinDetailsStepValues, + CoffinProfile, + CoffinSpec, + ProductOption, +} from './CoffinDetailsStep';