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