diff --git a/src/components/pages/CoffinsStep/CoffinsStep.stories.tsx b/src/components/pages/CoffinsStep/CoffinsStep.stories.tsx new file mode 100644 index 0000000..763908a --- /dev/null +++ b/src/components/pages/CoffinsStep/CoffinsStep.stories.tsx @@ -0,0 +1,200 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { CoffinsStep } from './CoffinsStep'; +import type { CoffinsStepValues, CoffinsStepErrors, Coffin } from './CoffinsStep'; +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 sampleCoffins: Coffin[] = [ + { + id: 'cedar-classic', + name: 'Cedar Classic', + imageUrl: 'https://images.unsplash.com/photo-1618220179428-22790b461013?w=400&h=300&fit=crop', + price: 2800, + category: 'Solid Timber', + isPopular: true, + }, + { + id: 'oak-heritage', + name: 'Oak Heritage', + imageUrl: 'https://images.unsplash.com/photo-1555041469-a586c1029ad9?w=400&h=300&fit=crop', + price: 3500, + category: 'Solid Timber', + }, + { + id: 'eco-willow', + name: 'Eco Willow Basket', + imageUrl: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400&h=300&fit=crop', + price: 1200, + category: 'Environmental', + isPopular: true, + }, + { + id: 'maple-serenity', + name: 'Maple Serenity', + imageUrl: 'https://images.unsplash.com/photo-1449247709967-d4461a6a6103?w=400&h=300&fit=crop', + price: 4200, + category: 'Solid Timber', + }, + { + id: 'custom-rose', + name: 'Custom Rose Garden', + imageUrl: 'https://images.unsplash.com/photo-1490750967868-88aa4f44baee?w=400&h=300&fit=crop', + price: 1800, + category: 'Custom Board', + }, + { + id: 'bronze-eternal', + name: 'Bronze Eternal', + imageUrl: 'https://images.unsplash.com/photo-1432462770865-65b70566d673?w=400&h=300&fit=crop', + price: 6500, + category: 'Protective Metal', + }, +]; + +const defaultValues: CoffinsStepValues = { + selectedCoffinId: null, + categoryFilter: 'all', + priceFilter: 'all', + page: 1, +}; + +// ─── Meta ──────────────────────────────────────────────────────────────────── + +const meta: Meta = { + title: 'Pages/CoffinsStep', + component: CoffinsStep, + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + }, +}; + +export default meta; +type Story = StoryObj; + +// ─── Interactive (default) ────────────────────────────────────────────────── + +/** Full catalogue with filters */ +export const Default: Story = { + render: () => { + const [values, setValues] = useState({ ...defaultValues }); + const [errors, setErrors] = useState({}); + + const handleContinue = () => { + if (!values.selectedCoffinId) { + setErrors({ selectedCoffinId: 'Please choose a coffin to continue.' }); + return; + } + alert(`Selected: ${values.selectedCoffinId}`); + }; + + return ( + { + setValues(v); + setErrors({}); + }} + onContinue={handleContinue} + onBack={() => alert('Back')} + onSaveAndExit={() => alert('Save')} + errors={errors} + coffins={sampleCoffins} + totalCount={19} + totalPages={2} + navigation={nav} + /> + ); + }, +}; + +// ─── Coffin selected ──────────────────────────────────────────────────────── + +/** A coffin is already selected */ +export const CoffinSelected: Story = { + render: () => { + const [values, setValues] = useState({ + ...defaultValues, + selectedCoffinId: 'cedar-classic', + }); + return ( + alert('Continue')} + onBack={() => alert('Back')} + coffins={sampleCoffins} + navigation={nav} + /> + ); + }, +}; + +// ─── Pre-planning ─────────────────────────────────────────────────────────── + +/** Pre-planning variant */ +export const PrePlanning: Story = { + render: () => { + const [values, setValues] = useState({ ...defaultValues }); + return ( + alert('Continue')} + onBack={() => alert('Back')} + coffins={sampleCoffins} + isPrePlanning + navigation={nav} + /> + ); + }, +}; + +// ─── Validation error ─────────────────────────────────────────────────────── + +/** No coffin selected with error */ +export const WithError: Story = { + render: () => { + const [values, setValues] = useState({ ...defaultValues }); + return ( + {}} + errors={{ selectedCoffinId: 'Please choose a coffin to continue.' }} + coffins={sampleCoffins} + navigation={nav} + /> + ); + }, +}; diff --git a/src/components/pages/CoffinsStep/CoffinsStep.tsx b/src/components/pages/CoffinsStep/CoffinsStep.tsx new file mode 100644 index 0000000..7d8a074 --- /dev/null +++ b/src/components/pages/CoffinsStep/CoffinsStep.tsx @@ -0,0 +1,350 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import TextField from '@mui/material/TextField'; +import MenuItem from '@mui/material/MenuItem'; +import Pagination from '@mui/material/Pagination'; +import type { SxProps, Theme } from '@mui/material/styles'; +import { WizardLayout } from '../../templates/WizardLayout'; +import { Card } from '../../atoms/Card'; +import { Badge } from '../../atoms/Badge'; +import { Typography } from '../../atoms/Typography'; +import { Button } from '../../atoms/Button'; +import { Divider } from '../../atoms/Divider'; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +/** A coffin available for selection */ +export interface Coffin { + id: string; + name: string; + imageUrl: string; + price: number; + category: string; + isPopular?: boolean; +} + +/** Filter category option */ +export interface CoffinCategory { + value: string; + label: string; +} + +/** Price range filter option */ +export interface CoffinPriceRange { + value: string; + label: string; +} + +/** Form values for the coffins step */ +export interface CoffinsStepValues { + /** Selected coffin ID */ + selectedCoffinId: string | null; + /** Active category filter */ + categoryFilter: string; + /** Active price filter */ + priceFilter: string; + /** Current page (1-indexed) */ + page: number; +} + +/** Field-level error messages */ +export interface CoffinsStepErrors { + selectedCoffinId?: string; +} + +/** Props for the CoffinsStep page component */ +export interface CoffinsStepProps { + /** Current form values */ + values: CoffinsStepValues; + /** Callback when any field value changes */ + onChange: (values: CoffinsStepValues) => 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?: CoffinsStepErrors; + /** Whether the Continue button is in a loading state */ + loading?: boolean; + /** Available coffins (already filtered by parent) */ + coffins: Coffin[]; + /** Total count before pagination (for display) */ + totalCount?: number; + /** Total pages */ + totalPages?: number; + /** Category filter options */ + categories?: CoffinCategory[]; + /** Price range filter options */ + priceRanges?: CoffinPriceRange[]; + /** 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 ─────────────────────────────────────────────────────────────── + +/** + * Step 10 — Coffin selection for the FA arrangement wizard. + * + * Grid-sidebar layout: filter sidebar (left/top on mobile) + coffin + * card grid (main area). Cards show image, name, price, and optional + * "Most Popular" badge (Rec #10). Pagination at the bottom. + * + * Uses Australian terminology: "coffin" not "casket" (both types may + * appear in the catalogue — the product type is shown per card). + * + * Pure presentation component — props in, callbacks out. + * Filtering and pagination logic handled by parent. + * + * Spec: documentation/steps/steps/10_coffins.yaml + */ +export const CoffinsStep: React.FC = ({ + values, + onChange, + onContinue, + onBack, + onSaveAndExit, + errors, + loading = false, + coffins, + totalCount, + totalPages = 1, + categories = [ + { value: 'all', label: 'All categories' }, + { value: 'solid_timber', label: 'Solid Timber' }, + { value: 'custom_board', label: 'Custom Board' }, + { value: 'environmental', label: 'Environmental' }, + { value: 'designer', label: 'Designer' }, + { value: 'protective_metal', label: 'Protective Metal' }, + ], + priceRanges = [ + { value: 'all', label: 'All prices' }, + { value: 'under_2000', label: 'Under $2,000' }, + { value: '2000_4000', label: '$2,000 – $4,000' }, + { value: 'over_4000', label: 'Over $4,000' }, + ], + isPrePlanning = false, + navigation, + progressStepper, + runningTotal, + hideHelpBar, + sx, +}) => { + const displayCount = totalCount ?? coffins.length; + + const handleFilterChange = (field: 'categoryFilter' | 'priceFilter', value: string) => { + onChange({ ...values, [field]: value, page: 1 }); + }; + + return ( + + {/* Page heading */} + + Choose a coffin + + + + {isPrePlanning + ? 'Browse the range to get an idea of styles and pricing. You can change your selection later.' + : 'Browse the range available with your selected provider. Use the filters to narrow your options.'} + + + + Selecting a coffin within your package allowance won't change your total. Coffins + outside the allowance will adjust the price. + + + { + e.preventDefault(); + onContinue(); + }} + > + {/* ─── Filters ─── */} + + handleFilterChange('categoryFilter', e.target.value)} + sx={{ minWidth: 200 }} + > + {categories.map((cat) => ( + + {cat.label} + + ))} + + + handleFilterChange('priceFilter', e.target.value)} + sx={{ minWidth: 200 }} + > + {priceRanges.map((range) => ( + + {range.label} + + ))} + + + + {/* ─── Results count ─── */} + + Showing {displayCount} coffin{displayCount !== 1 ? 's' : ''} + + + {/* ─── Coffin card grid ─── */} + + {coffins.map((coffin, index) => ( + onChange({ ...values, selectedCoffinId: coffin.id })} + role="radio" + aria-checked={coffin.id === values.selectedCoffinId} + tabIndex={ + values.selectedCoffinId === null + ? index === 0 + ? 0 + : -1 + : coffin.id === values.selectedCoffinId + ? 0 + : -1 + } + sx={{ overflow: 'hidden' }} + > + {/* Image */} + + {coffin.isPopular && ( + + + Most Popular + + + )} + + + {/* Content */} + + + {coffin.name} + + + {coffin.category} + + + ${coffin.price.toLocaleString('en-AU')} + + + + ))} + + + {/* Validation error */} + {errors?.selectedCoffinId && ( + + {errors.selectedCoffinId} + + )} + + {/* Pagination */} + {totalPages > 1 && ( + + onChange({ ...values, page })} + color="primary" + /> + + )} + + + + {/* CTAs */} + + {onSaveAndExit ? ( + + ) : ( + + )} + + + + + ); +}; + +CoffinsStep.displayName = 'CoffinsStep'; +export default CoffinsStep; diff --git a/src/components/pages/CoffinsStep/index.ts b/src/components/pages/CoffinsStep/index.ts new file mode 100644 index 0000000..6f16c39 --- /dev/null +++ b/src/components/pages/CoffinsStep/index.ts @@ -0,0 +1,9 @@ +export { CoffinsStep, default } from './CoffinsStep'; +export type { + CoffinsStepProps, + CoffinsStepValues, + CoffinsStepErrors, + Coffin, + CoffinCategory, + CoffinPriceRange, +} from './CoffinsStep';