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:
163
src/components/pages/PreviewStep/PreviewStep.stories.tsx
Normal file
163
src/components/pages/PreviewStep/PreviewStep.stories.tsx
Normal 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'),
|
||||||
|
},
|
||||||
|
};
|
||||||
228
src/components/pages/PreviewStep/PreviewStep.tsx
Normal file
228
src/components/pages/PreviewStep/PreviewStep.tsx
Normal 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's what's included. You'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;
|
||||||
2
src/components/pages/PreviewStep/index.ts
Normal file
2
src/components/pages/PreviewStep/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default } from './PreviewStep';
|
||||||
|
export * from './PreviewStep';
|
||||||
Reference in New Issue
Block a user