Add PreviewStep page (wizard step 4)

Informational review step — no form fields. List-detail split:
ProviderCardCompact + "What happens next" numbered checklist + CTAs
(left), PackageDetail breakdown (right). Pre-planning variant shows
"Explore other options" tertiary CTA. Checklist reduces anxiety about
remaining steps. Pure presentation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 14:41:16 +11:00
parent a721f9cb1b
commit 42d87293fd
3 changed files with 393 additions and 0 deletions

View File

@@ -0,0 +1,163 @@
import type { Meta, StoryObj } from '@storybook/react';
import { PreviewStep } from './PreviewStep';
import type { PreviewStepPackage, PreviewStepProvider } from './PreviewStep';
import { Navigation } from '../../organisms/Navigation';
import Box from '@mui/material/Box';
// ─── Helpers ─────────────────────────────────────────────────────────────────
const FALogo = () => (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Box
component="img"
src="/brandlogo/logo-full.svg"
alt="Funeral Arranger"
sx={{ height: 28, display: { xs: 'none', md: 'block' } }}
/>
<Box
component="img"
src="/brandlogo/logo-short.svg"
alt="Funeral Arranger"
sx={{ height: 28, display: { xs: 'block', md: 'none' } }}
/>
</Box>
);
const nav = (
<Navigation
logo={<FALogo />}
items={[
{ label: 'FAQ', href: '/faq' },
{ label: 'Contact Us', href: '/contact' },
{ label: 'Log in', href: '/login' },
]}
/>
);
const mockProvider: PreviewStepProvider = {
name: 'H.Parsons Funeral Directors',
location: 'Wentworth, NSW',
imageUrl: 'https://placehold.co/120x80/E8E0D6/8B6F47?text=H.Parsons',
rating: 4.6,
reviewCount: 7,
};
const mockPackage: PreviewStepPackage = {
name: 'Everyday Funeral Package',
price: 2700,
sections: [
{
heading: 'Essentials',
items: [
{ name: 'Accommodation', price: 500 },
{ name: 'Death registration certificate', price: 150 },
{ name: 'Doctor fee for Cremation', price: 150 },
{ name: 'NSW Government Levy - Cremation', price: 83 },
{ name: 'Professional Mortuary Care', price: 1200 },
{ name: 'Professional Service Fee', price: 1120 },
],
},
{
heading: 'Complimentary Items',
items: [
{ name: 'Dressing Fee', price: 0 },
{ name: 'Viewing Fee', price: 0 },
],
},
],
total: 2700,
extras: {
heading: 'Extras',
items: [
{ name: 'Allowance for Flowers', price: 150, isAllowance: true },
{ name: 'Allowance for Master of Ceremonies', price: 500, isAllowance: true },
{ name: 'After Business Hours Service Surcharge', price: 150 },
{ name: 'Coffin Bearing by Funeral Directors', price: 1500 },
],
},
terms:
'This package includes a funeral service at a chapel or a church with a funeral procession. Pricing may vary based on additional selections.',
};
// ─── Meta ────────────────────────────────────────────────────────────────────
const meta: Meta<typeof PreviewStep> = {
title: 'Pages/PreviewStep',
component: PreviewStep,
tags: ['autodocs'],
parameters: {
layout: 'fullscreen',
},
};
export default meta;
type Story = StoryObj<typeof PreviewStep>;
// ─── Default (at-need) ─────────────────────────────────────────────────────
/** At-need flow — Continue CTA only, no explore option */
export const Default: Story = {
args: {
provider: mockProvider,
selectedPackage: mockPackage,
navigation: nav,
onContinue: () => alert('Continue with this package'),
onBack: () => alert('Back'),
},
};
// ─── Pre-planning ───────────────────────────────────────────────────────────
/** Pre-planning flow — shows "Explore other options" tertiary CTA */
export const PrePlanning: Story = {
args: {
provider: mockProvider,
selectedPackage: mockPackage,
navigation: nav,
isPrePlanning: true,
onContinue: () => alert('Continue'),
onBack: () => alert('Back'),
onExplore: () => alert('Explore other options'),
},
};
// ─── Loading ────────────────────────────────────────────────────────────────
/** Continue button in loading state */
export const Loading: Story = {
args: {
provider: mockProvider,
selectedPackage: mockPackage,
navigation: nav,
loading: true,
onContinue: () => {},
onBack: () => alert('Back'),
},
};
// ─── Minimal package ────────────────────────────────────────────────────────
/** Basic package with fewer inclusions */
export const MinimalPackage: Story = {
args: {
provider: mockProvider,
selectedPackage: {
name: 'Essential Cremation',
price: 1800,
sections: [
{
heading: 'Essentials',
items: [
{ name: 'Death registration certificate', price: 150 },
{ name: 'Professional Mortuary Care', price: 800 },
{ name: 'Professional Service Fee', price: 850 },
],
},
],
total: 1800,
},
navigation: nav,
onContinue: () => alert('Continue'),
onBack: () => alert('Back'),
},
};

View File

@@ -0,0 +1,228 @@
import React from 'react';
import Box from '@mui/material/Box';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import type { SxProps, Theme } from '@mui/material/styles';
import { WizardLayout } from '../../templates/WizardLayout';
import { ProviderCardCompact } from '../../molecules/ProviderCardCompact';
import { PackageDetail } from '../../organisms/PackageDetail';
import type { PackageSection } from '../../organisms/PackageDetail';
import { Typography } from '../../atoms/Typography';
import { Button } from '../../atoms/Button';
import { Divider } from '../../atoms/Divider';
// ─── Types ───────────────────────────────────────────────────────────────────
/** Provider summary for the compact card */
export interface PreviewStepProvider {
/** Provider name */
name: string;
/** Location */
location: string;
/** Image URL */
imageUrl?: string;
/** Rating */
rating?: number;
/** Review count */
reviewCount?: number;
}
/** Selected package data for the preview */
export interface PreviewStepPackage {
/** Package display name */
name: string;
/** Package price */
price: number;
/** Line item sections */
sections: PackageSection[];
/** Total */
total?: number;
/** Extras section */
extras?: PackageSection;
/** Terms */
terms?: string;
}
/** A step in the "What's next?" checklist */
export interface NextStepItem {
/** Step number (1-based) */
number: number;
/** Step description */
label: string;
}
/** Props for the PreviewStep page component */
export interface PreviewStepProps {
/** Provider summary */
provider: PreviewStepProvider;
/** Selected package details */
selectedPackage: PreviewStepPackage;
/** What's next checklist items */
nextSteps?: NextStepItem[];
/** Callback for the primary CTA */
onContinue: () => void;
/** Callback for the back button */
onBack: () => void;
/** Callback for "Explore other options" (pre-planning only) */
onExplore?: () => void;
/** Whether Continue is loading */
loading?: boolean;
/** Whether this is a pre-planning flow */
isPrePlanning?: boolean;
/** Navigation bar */
navigation?: React.ReactNode;
/** MUI sx prop */
sx?: SxProps<Theme>;
}
// ─── Default checklist ──────────────────────────────────────────────────────
const DEFAULT_NEXT_STEPS: NextStepItem[] = [
{ number: 1, label: 'Create your account to save your selections' },
{ number: 2, label: 'Choose a date and time for the service' },
{ number: 3, label: 'Select a venue' },
{ number: 4, label: 'Choose a coffin' },
{ number: 5, label: 'Review and confirm your arrangement' },
];
// ─── Component ───────────────────────────────────────────────────────────────
/**
* Step 4 — Package preview page for the FA arrangement wizard.
*
* Informational review step — no form fields. Shows the selected
* package breakdown and a "What's next?" orientation checklist to
* reduce anxiety about the remaining steps.
*
* List + Detail split: provider info + checklist + CTAs (left),
* PackageDetail breakdown (right).
*
* Pre-planning users see an additional "Explore other options" CTA.
*
* Pure presentation component — props in, callbacks out.
*
* Spec: documentation/steps/steps/04_preview.yaml
*/
export const PreviewStep: React.FC<PreviewStepProps> = ({
provider,
selectedPackage,
nextSteps = DEFAULT_NEXT_STEPS,
onContinue,
onBack,
onExplore,
loading = false,
isPrePlanning = false,
navigation,
sx,
}) => {
return (
<WizardLayout
variant="list-detail"
navigation={navigation}
showBackLink
backLabel="Back"
onBack={onBack}
sx={sx}
secondaryPanel={
<PackageDetail
name={selectedPackage.name}
price={selectedPackage.price}
sections={selectedPackage.sections}
total={selectedPackage.total}
extras={selectedPackage.extras}
terms={selectedPackage.terms}
/>
}
>
{/* Provider compact card */}
<Box sx={{ mb: 3 }}>
<ProviderCardCompact
name={provider.name}
location={provider.location}
imageUrl={provider.imageUrl}
rating={provider.rating}
reviewCount={provider.reviewCount}
/>
</Box>
{/* Page heading */}
<Typography variant="h4" component="h1" sx={{ mb: 0.5 }} tabIndex={-1}>
Your selected package
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 4 }}>
Here&apos;s what&apos;s included. You&apos;ll be able to customise everything in the next
steps.
</Typography>
{/* What's next? checklist */}
<Box sx={{ mb: 4 }}>
<Typography variant="h6" sx={{ mb: 1.5 }}>
What happens next
</Typography>
<List disablePadding>
{nextSteps.map((step) => (
<ListItem key={step.number} disablePadding sx={{ mb: 1, alignItems: 'flex-start' }}>
<ListItemIcon
sx={{
minWidth: 36,
mt: 0.25,
}}
>
<Box
sx={{
width: 24,
height: 24,
borderRadius: '50%',
bgcolor: 'var(--fa-color-brand-100)',
color: 'var(--fa-color-brand-700)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '0.75rem',
fontWeight: 600,
}}
>
{step.number}
</Box>
</ListItemIcon>
<ListItemText
primary={step.label}
primaryTypographyProps={{
variant: 'body2',
color: 'text.primary',
}}
/>
</ListItem>
))}
</List>
</Box>
<Divider sx={{ mb: 3 }} />
{/* Action buttons */}
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
gap: 2,
justifyContent: 'flex-end',
pb: 2,
}}
>
{isPrePlanning && onExplore && (
<Button variant="outlined" color="secondary" size="large" onClick={onExplore}>
Explore other options
</Button>
)}
<Button variant="contained" size="large" onClick={onContinue} loading={loading}>
Continue with this package
</Button>
</Box>
</WizardLayout>
);
};
PreviewStep.displayName = 'PreviewStep';
export default PreviewStep;

View File

@@ -0,0 +1,2 @@
export { default } from './PreviewStep';
export * from './PreviewStep';