Files
Parsons/src/components/pages/CoffinsStep/CoffinsStep.tsx
Richie 1e91929411 Fix layout variants for VenueStep, CoffinsStep, CoffinDetailsStep
- VenueStep: centered-form → list-map (venue cards left, map slot right)
  Matches ProvidersStep pattern with vertical card stack + map placeholder
- CoffinsStep: centered-form → grid-sidebar (filter sidebar left, card grid right)
  Filters now in dedicated sidebar, cards fill the wider main area
- CoffinDetailsStep: centered-form → detail-toggles (profile left, options right)
  Coffin image + specs on left, option RadioGroups on right

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 15:16:01 +11:00

363 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<Theme>;
}
// ─── 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<CoffinsStepProps> = ({
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 });
};
// ─── Sidebar content (filters) ───
const sidebar = (
<Box sx={{ py: { xs: 0, md: 2 } }}>
<Typography variant="h5" sx={{ mb: 2, display: { xs: 'none', md: 'block' } }}>
Filters
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<TextField
select
label="Categories"
value={values.categoryFilter}
onChange={(e) => handleFilterChange('categoryFilter', e.target.value)}
fullWidth
>
{categories.map((cat) => (
<MenuItem key={cat.value} value={cat.value}>
{cat.label}
</MenuItem>
))}
</TextField>
<TextField
select
label="Price range"
value={values.priceFilter}
onChange={(e) => handleFilterChange('priceFilter', e.target.value)}
fullWidth
>
{priceRanges.map((range) => (
<MenuItem key={range.value} value={range.value}>
{range.label}
</MenuItem>
))}
</TextField>
</Box>
</Box>
);
// ─── Main content (card grid) ───
const mainContent = (
<Box
component="form"
noValidate
onSubmit={(e: React.FormEvent) => {
e.preventDefault();
onContinue();
}}
>
{/* Page heading */}
<Typography variant="h4" component="h1" sx={{ mb: 1 }} tabIndex={-1}>
Choose a coffin
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
{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.'}
</Typography>
<Typography variant="caption" color="text.secondary" sx={{ mb: 3, display: 'block' }}>
Selecting a coffin within your package allowance won&apos;t change your total. Coffins
outside the allowance will adjust the price.
</Typography>
{/* Results count */}
<Typography
variant="caption"
color="text.secondary"
sx={{ mb: 2, display: 'block' }}
aria-live="polite"
>
Showing {displayCount} coffin{displayCount !== 1 ? 's' : ''}
</Typography>
{/* Coffin card grid */}
<Box
role="radiogroup"
aria-label="Available coffins"
sx={{
display: 'grid',
gridTemplateColumns: {
xs: '1fr',
sm: 'repeat(2, 1fr)',
lg: 'repeat(3, 1fr)',
},
gap: 2,
mb: 3,
}}
>
{coffins.map((coffin, index) => (
<Card
key={coffin.id}
interactive
selected={coffin.id === values.selectedCoffinId}
padding="none"
onClick={() => 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 */}
<Box
sx={{
position: 'relative',
height: 180,
backgroundImage: `url(${coffin.imageUrl})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundColor: 'var(--fa-color-surface-subtle)',
}}
role="img"
aria-label={`Photo of ${coffin.name}`}
>
{coffin.isPopular && (
<Box sx={{ position: 'absolute', top: 8, left: 8 }}>
<Badge variant="soft" color="brand" aria-label="Most popular choice">
Most Popular
</Badge>
</Box>
)}
</Box>
{/* Content */}
<Box sx={{ p: 2 }}>
<Typography variant="h6" maxLines={2} sx={{ mb: 0.5 }}>
{coffin.name}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
{coffin.category}
</Typography>
<Typography variant="h6" color="primary">
${coffin.price.toLocaleString('en-AU')}
</Typography>
</Box>
</Card>
))}
</Box>
{/* Validation error */}
{errors?.selectedCoffinId && (
<Typography variant="body2" color="error" sx={{ mb: 2 }} role="alert">
{errors.selectedCoffinId}
</Typography>
)}
{/* Pagination */}
{totalPages > 1 && (
<Box sx={{ display: 'flex', justifyContent: 'center', mb: 3 }}>
<Pagination
count={totalPages}
page={values.page}
onChange={(_, page) => onChange({ ...values, page })}
color="primary"
/>
</Box>
)}
<Divider sx={{ my: 3 }} />
{/* CTAs */}
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexDirection: { xs: 'column-reverse', sm: 'row' },
gap: 2,
}}
>
{onSaveAndExit ? (
<Button variant="text" color="secondary" onClick={onSaveAndExit} type="button">
Save and continue later
</Button>
) : (
<Box />
)}
<Button type="submit" variant="contained" size="large" loading={loading}>
Continue
</Button>
</Box>
</Box>
);
return (
<WizardLayout
variant="grid-sidebar"
navigation={navigation}
progressStepper={progressStepper}
runningTotal={runningTotal}
showBackLink={!!onBack}
backLabel="Back"
onBack={onBack}
hideHelpBar={hideHelpBar}
sx={sx}
secondaryPanel={mainContent}
>
{sidebar}
</WizardLayout>
);
};
CoffinsStep.displayName = 'CoffinsStep';
export default CoffinsStep;