Remove ArrangementForm (premature) + fix Footer P2s from audit
- ArrangementForm reverted to planned — need more building blocks first - Footer: extract shared overline + contact link sx, use overlineSm variant - Footer: remove hardcoded fontSize/fontWeight on contact links Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -51,7 +51,7 @@ duplicates) and MUST update it after completing one.
|
|||||||
|-----------|--------|-------------|-------|
|
|-----------|--------|-------------|-------|
|
||||||
| ServiceSelector | done | ServiceOption × n + Typography + Button | Single-select service panel for arrangement flow. Heading + subheading + ServiceOption list (radiogroup) + optional continue Button. Manages selection state via selectedId/onSelect. maxDescriptionLines pass-through. |
|
| ServiceSelector | done | ServiceOption × n + Typography + Button | Single-select service panel for arrangement flow. Heading + subheading + ServiceOption list (radiogroup) + optional continue Button. Manages selection state via selectedId/onSelect. maxDescriptionLines pass-through. |
|
||||||
| PricingTable | planned | PriceCard × n + Typography | Comparative pricing display |
|
| PricingTable | planned | PriceCard × n + Typography | Comparative pricing display |
|
||||||
| ArrangementForm | review | StepIndicator + ServiceSelector + AddOnOption + Button + Typography | Multi-step arrangement wizard. Controlled by parent (currentStep/onNext/onBack). Each step renders arbitrary content with consistent nav buttons. canContinue per step. Back/Continue/Complete labels configurable. |
|
| ArrangementForm | planned | StepIndicator + ServiceSelector + AddOnOption + Button + Typography | Multi-step arrangement wizard. Deferred — build remaining atoms/molecules first. |
|
||||||
| Navigation | done | AppBar + Link + IconButton + Button + Divider + Drawer | Responsive site header. Desktop: logo left, links right, optional CTA. Mobile: hamburger + drawer with nav items, CTA, help footer. Sticky, grey surface bg (surface.subtle). Real FA logo from brandassets/. Maps to Figma Main Nav (14:108) + Mobile Header (2391:41508). |
|
| Navigation | done | AppBar + Link + IconButton + Button + Divider + Drawer | Responsive site header. Desktop: logo left, links right, optional CTA. Mobile: hamburger + drawer with nav items, CTA, help footer. Sticky, grey surface bg (surface.subtle). Real FA logo from brandassets/. Maps to Figma Main Nav (14:108) + Mobile Header (2391:41508). |
|
||||||
| Footer | done | Link × n + Typography + Divider + Container + Grid | Dark espresso (brand.950) site footer. Logo + tagline + contact (phone/email) + link group columns + legal bar. Semantic HTML (footer, nav, ul). Critique: 38/40 (Excellent). |
|
| Footer | done | Link × n + Typography + Divider + Container + Grid | Dark espresso (brand.950) site footer. Logo + tagline + contact (phone/email) + link group columns + legal bar. Semantic HTML (footer, nav, ul). Critique: 38/40 (Excellent). |
|
||||||
|
|
||||||
|
|||||||
@@ -1,380 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import type { Meta, StoryObj } from '@storybook/react';
|
|
||||||
import Box from '@mui/material/Box';
|
|
||||||
import { ArrangementForm } from './ArrangementForm';
|
|
||||||
import { ServiceSelector } from '../ServiceSelector';
|
|
||||||
import { AddOnOption } from '../../molecules/AddOnOption';
|
|
||||||
import { Typography } from '../../atoms/Typography';
|
|
||||||
import { Navigation } from '../Navigation';
|
|
||||||
|
|
||||||
// ─── Shared data ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const serviceTypes = [
|
|
||||||
{ id: 'burial', name: 'Traditional Burial', price: 4200, description: 'Full service with chapel ceremony, viewing, hearse, and graveside committal.' },
|
|
||||||
{ id: 'cremation', name: 'Cremation with Service', price: 2800, description: 'Chapel ceremony followed by cremation. Ashes returned in a standard urn.' },
|
|
||||||
{ id: 'direct-cremation', name: 'Direct Cremation', price: 1600, description: 'Simple cremation without a formal service. Ashes returned within 5 business days.' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const coffinOptions = [
|
|
||||||
{ id: 'eco', name: 'Eco Willow', price: 850, description: 'Handwoven natural willow. Biodegradable and sustainable.' },
|
|
||||||
{ id: 'classic', name: 'Classic Maple', price: 1400, description: 'Solid maple with satin finish and brass handles.' },
|
|
||||||
{ id: 'premium', name: 'Premium Oak', price: 2200, description: 'Quarter-sawn oak with high-gloss lacquer and gold-plated handles.' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const addOns = [
|
|
||||||
{ id: 'flowers', name: 'Floral Arrangements', price: 450, description: 'Seasonal flowers for the chapel and casket spray.' },
|
|
||||||
{ id: 'video', name: 'Memorial Video', price: 350, description: 'Professional video tribute with photos and music, played during the service.' },
|
|
||||||
{ id: 'catering', name: 'Wake Catering', price: 800, description: 'Light refreshments for up to 50 guests after the service.' },
|
|
||||||
{ id: 'transport', name: 'Family Limousine', price: 300, description: 'Luxury vehicle for immediate family to and from the service.' },
|
|
||||||
{ id: 'death-notice', name: 'Newspaper Death Notice', price: 180, description: 'Published in one major metropolitan newspaper of your choice.' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const FALogoNav = () => (
|
|
||||||
<Box
|
|
||||||
component="img"
|
|
||||||
src="/brandlogo/logo-full.svg"
|
|
||||||
alt="Funeral Arranger"
|
|
||||||
sx={{ height: 28 }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
// ─── Meta ────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const meta: Meta<typeof ArrangementForm> = {
|
|
||||||
title: 'Organisms/ArrangementForm',
|
|
||||||
component: ArrangementForm,
|
|
||||||
tags: ['autodocs'],
|
|
||||||
parameters: {
|
|
||||||
layout: 'centered',
|
|
||||||
},
|
|
||||||
decorators: [
|
|
||||||
(Story) => (
|
|
||||||
<Box sx={{ maxWidth: 600, width: '100%', mx: 'auto', py: 4 }}>
|
|
||||||
<Story />
|
|
||||||
</Box>
|
|
||||||
),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
type Story = StoryObj<typeof ArrangementForm>;
|
|
||||||
|
|
||||||
// --- Default (Static) --------------------------------------------------------
|
|
||||||
|
|
||||||
/** Static view at step 1 — shows structure without interactivity */
|
|
||||||
export const Default: Story = {
|
|
||||||
args: {
|
|
||||||
heading: 'Plan your arrangement',
|
|
||||||
subheading: 'We\'ll guide you through each step. You can go back and change your selections at any time.',
|
|
||||||
steps: [
|
|
||||||
{ label: 'Service', content: <Typography color="text.secondary">Service selection content</Typography> },
|
|
||||||
{ label: 'Coffin', content: <Typography color="text.secondary">Coffin selection content</Typography> },
|
|
||||||
{ label: 'Extras', content: <Typography color="text.secondary">Optional extras content</Typography> },
|
|
||||||
{ label: 'Review', content: <Typography color="text.secondary">Review summary content</Typography> },
|
|
||||||
],
|
|
||||||
currentStep: 0,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Interactive Full Flow ---------------------------------------------------
|
|
||||||
|
|
||||||
/** Complete 4-step arrangement flow with real components */
|
|
||||||
export const InteractiveFlow: Story = {
|
|
||||||
render: () => {
|
|
||||||
const [step, setStep] = useState(0);
|
|
||||||
const [serviceId, setServiceId] = useState<string | undefined>();
|
|
||||||
const [coffinId, setCoffinId] = useState<string | undefined>();
|
|
||||||
const [selectedAddOns, setSelectedAddOns] = useState<Record<string, boolean>>({});
|
|
||||||
|
|
||||||
const toggleAddOn = (id: string, checked: boolean) => {
|
|
||||||
setSelectedAddOns((prev) => ({ ...prev, [id]: checked }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const getServiceName = () => serviceTypes.find((s) => s.id === serviceId)?.name ?? '—';
|
|
||||||
const getServicePrice = () => serviceTypes.find((s) => s.id === serviceId)?.price ?? 0;
|
|
||||||
const getCoffinName = () => coffinOptions.find((c) => c.id === coffinId)?.name ?? '—';
|
|
||||||
const getCoffinPrice = () => coffinOptions.find((c) => c.id === coffinId)?.price ?? 0;
|
|
||||||
const getAddOnTotal = () =>
|
|
||||||
addOns.filter((a) => selectedAddOns[a.id]).reduce((sum, a) => sum + a.price, 0);
|
|
||||||
const getTotal = () => getServicePrice() + getCoffinPrice() + getAddOnTotal();
|
|
||||||
|
|
||||||
const steps = [
|
|
||||||
{
|
|
||||||
label: 'Service',
|
|
||||||
canContinue: !!serviceId,
|
|
||||||
content: (
|
|
||||||
<ServiceSelector
|
|
||||||
heading="Choose a service type"
|
|
||||||
subheading="Prices are starting estimates and may vary."
|
|
||||||
items={serviceTypes}
|
|
||||||
selectedId={serviceId}
|
|
||||||
onSelect={setServiceId}
|
|
||||||
maxDescriptionLines={2}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Coffin',
|
|
||||||
canContinue: !!coffinId,
|
|
||||||
content: (
|
|
||||||
<ServiceSelector
|
|
||||||
heading="Choose a coffin"
|
|
||||||
subheading="All coffins include a fitted interior lining and nameplate."
|
|
||||||
items={coffinOptions}
|
|
||||||
selectedId={coffinId}
|
|
||||||
onSelect={setCoffinId}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Extras',
|
|
||||||
canContinue: true,
|
|
||||||
content: (
|
|
||||||
<Box>
|
|
||||||
<Typography variant="h4" component="h2" sx={{ mb: 1 }}>
|
|
||||||
Optional extras
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
|
||||||
Add any optional services. You can skip this step if none are needed.
|
|
||||||
</Typography>
|
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
|
||||||
{addOns.map((addOn) => (
|
|
||||||
<AddOnOption
|
|
||||||
key={addOn.id}
|
|
||||||
name={addOn.name}
|
|
||||||
price={addOn.price}
|
|
||||||
description={addOn.description}
|
|
||||||
checked={!!selectedAddOns[addOn.id]}
|
|
||||||
onChange={(checked) => toggleAddOn(addOn.id, checked)}
|
|
||||||
maxDescriptionLines={1}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Review',
|
|
||||||
canContinue: true,
|
|
||||||
content: (
|
|
||||||
<Box>
|
|
||||||
<Typography variant="h4" component="h2" sx={{ mb: 3 }}>
|
|
||||||
Review your arrangement
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{/* Service */}
|
|
||||||
<Box sx={{ mb: 2, pb: 2, borderBottom: '1px solid', borderColor: 'divider' }}>
|
|
||||||
<Typography variant="label" color="text.secondary" sx={{ display: 'block', mb: 0.5 }}>
|
|
||||||
Service
|
|
||||||
</Typography>
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
||||||
<Typography variant="body1">{getServiceName()}</Typography>
|
|
||||||
<Typography variant="body1" fontWeight={600}>
|
|
||||||
${getServicePrice().toLocaleString('en-AU')}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Coffin */}
|
|
||||||
<Box sx={{ mb: 2, pb: 2, borderBottom: '1px solid', borderColor: 'divider' }}>
|
|
||||||
<Typography variant="label" color="text.secondary" sx={{ display: 'block', mb: 0.5 }}>
|
|
||||||
Coffin
|
|
||||||
</Typography>
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
||||||
<Typography variant="body1">{getCoffinName()}</Typography>
|
|
||||||
<Typography variant="body1" fontWeight={600}>
|
|
||||||
${getCoffinPrice().toLocaleString('en-AU')}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Extras */}
|
|
||||||
{addOns.filter((a) => selectedAddOns[a.id]).length > 0 && (
|
|
||||||
<Box sx={{ mb: 2, pb: 2, borderBottom: '1px solid', borderColor: 'divider' }}>
|
|
||||||
<Typography variant="label" color="text.secondary" sx={{ display: 'block', mb: 0.5 }}>
|
|
||||||
Extras
|
|
||||||
</Typography>
|
|
||||||
{addOns
|
|
||||||
.filter((a) => selectedAddOns[a.id])
|
|
||||||
.map((addOn) => (
|
|
||||||
<Box key={addOn.id} sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
|
|
||||||
<Typography variant="body2">{addOn.name}</Typography>
|
|
||||||
<Typography variant="body2" fontWeight={600}>
|
|
||||||
${addOn.price.toLocaleString('en-AU')}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Total */}
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', pt: 1 }}>
|
|
||||||
<Typography variant="h5">Estimated total</Typography>
|
|
||||||
<Typography variant="h5" color="primary">
|
|
||||||
${getTotal().toLocaleString('en-AU')}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Typography variant="captionSm" color="text.secondary" sx={{ mt: 2, display: 'block' }}>
|
|
||||||
This is an estimate only. Final pricing will be confirmed by your chosen funeral director.
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ArrangementForm
|
|
||||||
heading="Plan your arrangement"
|
|
||||||
subheading="We'll guide you through each step. You can go back and change your selections at any time."
|
|
||||||
steps={steps}
|
|
||||||
currentStep={step}
|
|
||||||
onNext={() => setStep((s) => Math.min(s + 1, steps.length - 1))}
|
|
||||||
onBack={() => setStep((s) => Math.max(s - 1, 0))}
|
|
||||||
onComplete={() => alert(`Arrangement complete! Total: $${getTotal().toLocaleString('en-AU')}`)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Mid-Flow ----------------------------------------------------------------
|
|
||||||
|
|
||||||
/** Starts at step 2 with prior selections visible */
|
|
||||||
export const MidFlow: Story = {
|
|
||||||
args: {
|
|
||||||
steps: [
|
|
||||||
{ label: 'Service', content: <Typography color="text.secondary">Completed</Typography> },
|
|
||||||
{ label: 'Coffin', content: <Typography color="text.secondary">Completed</Typography> },
|
|
||||||
{
|
|
||||||
label: 'Extras',
|
|
||||||
content: (
|
|
||||||
<Box>
|
|
||||||
<Typography variant="h4" component="h2" sx={{ mb: 1 }}>
|
|
||||||
Optional extras
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
|
||||||
Add any optional services you'd like.
|
|
||||||
</Typography>
|
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
|
||||||
{addOns.slice(0, 3).map((addOn) => (
|
|
||||||
<AddOnOption
|
|
||||||
key={addOn.id}
|
|
||||||
name={addOn.name}
|
|
||||||
price={addOn.price}
|
|
||||||
description={addOn.description}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{ label: 'Review', content: <Typography color="text.secondary">Review step</Typography> },
|
|
||||||
],
|
|
||||||
currentStep: 2,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Two Steps ---------------------------------------------------------------
|
|
||||||
|
|
||||||
/** Simplified 2-step flow for simpler arrangements */
|
|
||||||
export const TwoSteps: Story = {
|
|
||||||
render: () => {
|
|
||||||
const [step, setStep] = useState(0);
|
|
||||||
const [serviceId, setServiceId] = useState<string | undefined>();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ArrangementForm
|
|
||||||
heading="Quick arrangement"
|
|
||||||
steps={[
|
|
||||||
{
|
|
||||||
label: 'Service',
|
|
||||||
canContinue: !!serviceId,
|
|
||||||
content: (
|
|
||||||
<ServiceSelector
|
|
||||||
heading="Choose a service"
|
|
||||||
items={serviceTypes}
|
|
||||||
selectedId={serviceId}
|
|
||||||
onSelect={setServiceId}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Review',
|
|
||||||
content: (
|
|
||||||
<Box>
|
|
||||||
<Typography variant="h4" component="h2" sx={{ mb: 2 }}>
|
|
||||||
Confirm your selection
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body1">
|
|
||||||
{serviceTypes.find((s) => s.id === serviceId)?.name ?? 'Nothing selected'}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
currentStep={step}
|
|
||||||
onNext={() => setStep(1)}
|
|
||||||
onBack={() => setStep(0)}
|
|
||||||
onComplete={() => alert('Complete!')}
|
|
||||||
completeLabel="Confirm arrangement"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- In Page Context ---------------------------------------------------------
|
|
||||||
|
|
||||||
/** Full page with Navigation wrapping the arrangement flow */
|
|
||||||
export const InPageContext: Story = {
|
|
||||||
decorators: [
|
|
||||||
(Story) => (
|
|
||||||
<Box sx={{ maxWidth: 'none', width: '100%' }}>
|
|
||||||
<Story />
|
|
||||||
</Box>
|
|
||||||
),
|
|
||||||
],
|
|
||||||
render: () => {
|
|
||||||
const [step, setStep] = useState(0);
|
|
||||||
const [serviceId, setServiceId] = useState<string | undefined>();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Navigation
|
|
||||||
logo={<FALogoNav />}
|
|
||||||
items={[
|
|
||||||
{ label: 'FAQ', href: '/faq' },
|
|
||||||
{ label: 'Contact Us', href: '/contact' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<Box sx={{ maxWidth: 600, mx: 'auto', px: { xs: 2, md: 4 }, py: { xs: 3, md: 6 } }}>
|
|
||||||
<ArrangementForm
|
|
||||||
heading="Plan your arrangement"
|
|
||||||
subheading="Step by step, at your own pace."
|
|
||||||
steps={[
|
|
||||||
{
|
|
||||||
label: 'Service',
|
|
||||||
canContinue: !!serviceId,
|
|
||||||
content: (
|
|
||||||
<ServiceSelector
|
|
||||||
heading="Choose a service type"
|
|
||||||
items={serviceTypes}
|
|
||||||
selectedId={serviceId}
|
|
||||||
onSelect={setServiceId}
|
|
||||||
maxDescriptionLines={2}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{ label: 'Coffin', content: <Typography color="text.secondary">Coffin step...</Typography> },
|
|
||||||
{ label: 'Extras', content: <Typography color="text.secondary">Extras step...</Typography> },
|
|
||||||
{ label: 'Review', content: <Typography color="text.secondary">Review step...</Typography> },
|
|
||||||
]}
|
|
||||||
currentStep={step}
|
|
||||||
onNext={() => setStep((s) => s + 1)}
|
|
||||||
onBack={() => setStep((s) => s - 1)}
|
|
||||||
onComplete={() => alert('Done!')}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import Box from '@mui/material/Box';
|
|
||||||
import type { SxProps, Theme } from '@mui/material/styles';
|
|
||||||
import { Typography } from '../../atoms/Typography';
|
|
||||||
import { Button } from '../../atoms/Button';
|
|
||||||
import { StepIndicator } from '../../molecules/StepIndicator';
|
|
||||||
|
|
||||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/** A single step in the arrangement flow */
|
|
||||||
export interface ArrangementStep {
|
|
||||||
/** Step label shown in the StepIndicator */
|
|
||||||
label: string;
|
|
||||||
/** Step content — rendered when this step is active */
|
|
||||||
content: React.ReactNode;
|
|
||||||
/** Whether the user can proceed past this step. Defaults to true. */
|
|
||||||
canContinue?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Props for the FA ArrangementForm organism */
|
|
||||||
export interface ArrangementFormProps {
|
|
||||||
/** The steps in the arrangement flow */
|
|
||||||
steps: ArrangementStep[];
|
|
||||||
/** Current step index (0-based). Controlled by parent. */
|
|
||||||
currentStep: number;
|
|
||||||
/** Called when the user advances to the next step */
|
|
||||||
onNext?: () => void;
|
|
||||||
/** Called when the user goes back to the previous step */
|
|
||||||
onBack?: () => void;
|
|
||||||
/** Called when the user completes the final step */
|
|
||||||
onComplete?: () => void;
|
|
||||||
/** Label for the next button — defaults to "Continue" */
|
|
||||||
nextLabel?: string;
|
|
||||||
/** Label for the back button — defaults to "Back" */
|
|
||||||
backLabel?: string;
|
|
||||||
/** Label for the final step's button — defaults to "Review arrangement" */
|
|
||||||
completeLabel?: string;
|
|
||||||
/** Optional heading above the step content */
|
|
||||||
heading?: string;
|
|
||||||
/** Optional subheading below the heading */
|
|
||||||
subheading?: string;
|
|
||||||
/** MUI sx prop for the root element */
|
|
||||||
sx?: SxProps<Theme>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Component ───────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Multi-step arrangement form for the FA design system.
|
|
||||||
*
|
|
||||||
* The core planning flow — guides users through service selection,
|
|
||||||
* coffin choice, venue, extras, and review. Each step renders
|
|
||||||
* arbitrary content (ServiceSelector, AddOnOption lists, forms, etc.)
|
|
||||||
* with consistent navigation and progress indication.
|
|
||||||
*
|
|
||||||
* Composes StepIndicator + Typography + Button + step content.
|
|
||||||
*
|
|
||||||
* State is controlled by the parent — the form doesn't own step
|
|
||||||
* state or selection state. This keeps it composable and testable.
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* ```tsx
|
|
||||||
* <ArrangementForm
|
|
||||||
* steps={[
|
|
||||||
* { label: 'Service', content: <ServiceSelector ... />, canContinue: !!selected },
|
|
||||||
* { label: 'Coffin', content: <ServiceSelector ... /> },
|
|
||||||
* { label: 'Extras', content: <AddOnList ... /> },
|
|
||||||
* { label: 'Review', content: <ReviewSummary ... /> },
|
|
||||||
* ]}
|
|
||||||
* currentStep={step}
|
|
||||||
* onNext={() => setStep(s => s + 1)}
|
|
||||||
* onBack={() => setStep(s => s - 1)}
|
|
||||||
* onComplete={() => submit()}
|
|
||||||
* />
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export const ArrangementForm = React.forwardRef<HTMLDivElement, ArrangementFormProps>(
|
|
||||||
(
|
|
||||||
{
|
|
||||||
steps,
|
|
||||||
currentStep,
|
|
||||||
onNext,
|
|
||||||
onBack,
|
|
||||||
onComplete,
|
|
||||||
nextLabel = 'Continue',
|
|
||||||
backLabel = 'Back',
|
|
||||||
completeLabel = 'Review arrangement',
|
|
||||||
heading,
|
|
||||||
subheading,
|
|
||||||
sx,
|
|
||||||
},
|
|
||||||
ref,
|
|
||||||
) => {
|
|
||||||
const isFirstStep = currentStep === 0;
|
|
||||||
const isLastStep = currentStep === steps.length - 1;
|
|
||||||
const activeStep = steps[currentStep];
|
|
||||||
const canContinue = activeStep?.canContinue ?? true;
|
|
||||||
|
|
||||||
const stepLabels = steps.map((s) => ({ label: s.label }));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
ref={ref}
|
|
||||||
sx={[
|
|
||||||
{ width: '100%' },
|
|
||||||
...(Array.isArray(sx) ? sx : [sx]),
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{/* Progress indicator */}
|
|
||||||
<StepIndicator
|
|
||||||
steps={stepLabels}
|
|
||||||
currentStep={currentStep}
|
|
||||||
sx={{ mb: 4 }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Optional heading */}
|
|
||||||
{heading && (
|
|
||||||
<Box sx={{ mb: 3 }}>
|
|
||||||
<Typography variant="h3" component="h1" sx={{ mb: subheading ? 1 : 0 }}>
|
|
||||||
{heading}
|
|
||||||
</Typography>
|
|
||||||
{subheading && (
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
{subheading}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Step content */}
|
|
||||||
<Box sx={{ mb: 4 }}>
|
|
||||||
{activeStep?.content}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Navigation buttons */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
gap: 2,
|
|
||||||
flexDirection: { xs: 'column-reverse', sm: 'row' },
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{!isFirstStep ? (
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
color="secondary"
|
|
||||||
size="large"
|
|
||||||
onClick={onBack}
|
|
||||||
sx={{ minWidth: 120 }}
|
|
||||||
>
|
|
||||||
{backLabel}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Box />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
size="large"
|
|
||||||
disabled={!canContinue}
|
|
||||||
onClick={isLastStep ? onComplete : onNext}
|
|
||||||
sx={{ minWidth: { xs: '100%', sm: 200 } }}
|
|
||||||
>
|
|
||||||
{isLastStep ? completeLabel : nextLabel}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
ArrangementForm.displayName = 'ArrangementForm';
|
|
||||||
export default ArrangementForm;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { ArrangementForm, type ArrangementFormProps, type ArrangementStep } from './ArrangementForm';
|
|
||||||
@@ -80,6 +80,19 @@ export const Footer = React.forwardRef<HTMLDivElement, FooterProps>(
|
|||||||
const year = new Date().getFullYear();
|
const year = new Date().getFullYear();
|
||||||
const copyrightText = copyright || `\u00A9 ${year} Funeral Arranger. All rights reserved.`;
|
const copyrightText = copyright || `\u00A9 ${year} Funeral Arranger. All rights reserved.`;
|
||||||
|
|
||||||
|
const overlineSx = {
|
||||||
|
color: 'var(--fa-color-brand-400)',
|
||||||
|
textTransform: 'uppercase' as const,
|
||||||
|
letterSpacing: '0.08em',
|
||||||
|
display: 'block',
|
||||||
|
mb: 0.5,
|
||||||
|
};
|
||||||
|
|
||||||
|
const contactLinkSx = {
|
||||||
|
color: 'var(--fa-color-white)',
|
||||||
|
'&:hover': { color: 'var(--fa-color-brand-300)' },
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -114,26 +127,12 @@ export const Footer = React.forwardRef<HTMLDivElement, FooterProps>(
|
|||||||
<Box sx={{ mt: 3 }}>
|
<Box sx={{ mt: 3 }}>
|
||||||
{phone && (
|
{phone && (
|
||||||
<Box sx={{ mb: 1 }}>
|
<Box sx={{ mb: 1 }}>
|
||||||
<Typography
|
<Typography variant="overlineSm" sx={overlineSx}>
|
||||||
variant="captionSm"
|
|
||||||
sx={{
|
|
||||||
color: 'var(--fa-color-brand-400)',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '0.08em',
|
|
||||||
display: 'block',
|
|
||||||
mb: 0.5,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Call us
|
Call us
|
||||||
</Typography>
|
</Typography>
|
||||||
<Link
|
<Link
|
||||||
href={`tel:${phone.replace(/\s/g, '')}`}
|
href={`tel:${phone.replace(/\s/g, '')}`}
|
||||||
sx={{
|
sx={{ ...contactLinkSx, fontWeight: 600 }}
|
||||||
color: 'var(--fa-color-white)',
|
|
||||||
fontWeight: 600,
|
|
||||||
fontSize: '1rem',
|
|
||||||
'&:hover': { color: 'var(--fa-color-brand-300)' },
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{phone}
|
{phone}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -141,25 +140,12 @@ export const Footer = React.forwardRef<HTMLDivElement, FooterProps>(
|
|||||||
)}
|
)}
|
||||||
{email && (
|
{email && (
|
||||||
<Box>
|
<Box>
|
||||||
<Typography
|
<Typography variant="overlineSm" sx={overlineSx}>
|
||||||
variant="captionSm"
|
|
||||||
sx={{
|
|
||||||
color: 'var(--fa-color-brand-400)',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '0.08em',
|
|
||||||
display: 'block',
|
|
||||||
mb: 0.5,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Email
|
Email
|
||||||
</Typography>
|
</Typography>
|
||||||
<Link
|
<Link
|
||||||
href={`mailto:${email}`}
|
href={`mailto:${email}`}
|
||||||
sx={{
|
sx={contactLinkSx}
|
||||||
color: 'var(--fa-color-white)',
|
|
||||||
fontWeight: 500,
|
|
||||||
'&:hover': { color: 'var(--fa-color-brand-300)' },
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{email}
|
{email}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
Reference in New Issue
Block a user