format: Apply Prettier to existing codebase
Formatting-only changes across all component and story files. No logic or behaviour changes — only whitespace, line breaks, and trailing commas. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,12 +15,7 @@ const FALogoInverse = () => (
|
||||
);
|
||||
|
||||
const FALogoNav = () => (
|
||||
<Box
|
||||
component="img"
|
||||
src="/brandlogo/logo-full.svg"
|
||||
alt="Funeral Arranger"
|
||||
sx={{ height: 28 }}
|
||||
/>
|
||||
<Box component="img" src="/brandlogo/logo-full.svg" alt="Funeral Arranger" sx={{ height: 28 }} />
|
||||
);
|
||||
|
||||
const defaultLinkGroups = [
|
||||
@@ -169,8 +164,8 @@ export const FullPage: Story = {
|
||||
Find a funeral director
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 4, maxWidth: 600 }}>
|
||||
Compare trusted funeral directors in your area. View services,
|
||||
pricing, and reviews to find the right support for your family.
|
||||
Compare trusted funeral directors in your area. View services, pricing, and reviews to
|
||||
find the right support for your family.
|
||||
</Typography>
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<Box
|
||||
|
||||
@@ -64,19 +64,7 @@ export interface FooterProps {
|
||||
* ```
|
||||
*/
|
||||
export const Footer = React.forwardRef<HTMLDivElement, FooterProps>(
|
||||
(
|
||||
{
|
||||
logo,
|
||||
tagline,
|
||||
linkGroups = [],
|
||||
phone,
|
||||
email,
|
||||
copyright,
|
||||
legalLinks = [],
|
||||
sx,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
({ logo, tagline, linkGroups = [], phone, email, copyright, legalLinks = [], sx }, ref) => {
|
||||
const year = new Date().getFullYear();
|
||||
const copyrightText = copyright || `\u00A9 ${year} Funeral Arranger. All rights reserved.`;
|
||||
|
||||
@@ -143,10 +131,7 @@ export const Footer = React.forwardRef<HTMLDivElement, FooterProps>(
|
||||
<Typography variant="overlineSm" sx={overlineSx}>
|
||||
Email
|
||||
</Typography>
|
||||
<Link
|
||||
href={`mailto:${email}`}
|
||||
sx={contactLinkSx}
|
||||
>
|
||||
<Link href={`mailto:${email}`} sx={contactLinkSx}>
|
||||
{email}
|
||||
</Link>
|
||||
</Box>
|
||||
@@ -157,7 +142,15 @@ export const Footer = React.forwardRef<HTMLDivElement, FooterProps>(
|
||||
|
||||
{/* Link group columns */}
|
||||
{linkGroups.map((group) => (
|
||||
<Grid item xs={6} sm={4} md key={group.heading} component="nav" aria-label={group.heading}>
|
||||
<Grid
|
||||
item
|
||||
xs={6}
|
||||
sm={4}
|
||||
md
|
||||
key={group.heading}
|
||||
component="nav"
|
||||
aria-label={group.heading}
|
||||
>
|
||||
<Typography
|
||||
variant="label"
|
||||
sx={{
|
||||
@@ -170,7 +163,14 @@ export const Footer = React.forwardRef<HTMLDivElement, FooterProps>(
|
||||
</Typography>
|
||||
<Box
|
||||
component="ul"
|
||||
sx={{ listStyle: 'none', p: 0, m: 0, display: 'flex', flexDirection: 'column', gap: 1.5 }}
|
||||
sx={{
|
||||
listStyle: 'none',
|
||||
p: 0,
|
||||
m: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 1.5,
|
||||
}}
|
||||
>
|
||||
{group.links.map((link) => (
|
||||
<li key={link.label}>
|
||||
@@ -206,10 +206,7 @@ export const Footer = React.forwardRef<HTMLDivElement, FooterProps>(
|
||||
py: 3,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="captionSm"
|
||||
sx={{ color: 'var(--fa-color-brand-400)' }}
|
||||
>
|
||||
<Typography variant="captionSm" sx={{ color: 'var(--fa-color-brand-400)' }}>
|
||||
{copyrightText}
|
||||
</Typography>
|
||||
|
||||
|
||||
@@ -9,7 +9,12 @@ import { Typography } from '../../atoms/Typography';
|
||||
const funeralTypes = [
|
||||
{ id: 'cremation', label: 'Cremation', hasServiceOption: true },
|
||||
{ id: 'burial', label: 'Burial', hasServiceOption: true },
|
||||
{ id: 'water-burial', label: 'Water Burial', note: 'Available in QLD only', hasServiceOption: false },
|
||||
{
|
||||
id: 'water-burial',
|
||||
label: 'Water Burial',
|
||||
note: 'Available in QLD only',
|
||||
hasServiceOption: false,
|
||||
},
|
||||
];
|
||||
|
||||
const themeOptions = [
|
||||
@@ -152,8 +157,8 @@ export const InHeroDesktop: Story = {
|
||||
color="text.secondary"
|
||||
sx={{ textAlign: 'center', mb: 4, maxWidth: 440, mx: 'auto' }}
|
||||
>
|
||||
Whether you're thinking ahead or arranging for a loved one, find
|
||||
trusted local providers with transparent pricing.
|
||||
Whether you're thinking ahead or arranging for a loved one, find trusted local
|
||||
providers with transparent pricing.
|
||||
</Typography>
|
||||
<FuneralFinder
|
||||
funeralTypes={funeralTypes}
|
||||
@@ -199,7 +204,11 @@ export const InHeroMobile: Story = {
|
||||
]}
|
||||
/>
|
||||
<Box sx={{ bgcolor: 'var(--fa-color-brand-100)', px: 3, py: 4, textAlign: 'center' }}>
|
||||
<Typography variant="h3" component="h1" sx={{ mb: 1.5, color: 'var(--fa-color-brand-950)' }}>
|
||||
<Typography
|
||||
variant="h3"
|
||||
component="h1"
|
||||
sx={{ mb: 1.5, color: 'var(--fa-color-brand-950)' }}
|
||||
>
|
||||
Discover, Explore, and Plan Funerals in Minutes
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
@@ -210,7 +219,8 @@ export const InHeroMobile: Story = {
|
||||
sx={{
|
||||
height: 180,
|
||||
bgcolor: 'var(--fa-color-brand-200)',
|
||||
backgroundImage: 'url(https://images.unsplash.com/photo-1516733968668-dbdce39c0571?w=800&h=400&fit=crop)',
|
||||
backgroundImage:
|
||||
'url(https://images.unsplash.com/photo-1516733968668-dbdce39c0571?w=800&h=400&fit=crop)',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
}}
|
||||
|
||||
@@ -146,7 +146,11 @@ function ChoiceCard({
|
||||
</Typography>
|
||||
</Box>
|
||||
{description && (
|
||||
<Typography variant="caption" component="span" sx={{ display: 'block', mt: 0.5, color: 'text.secondary' }}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="span"
|
||||
sx={{ display: 'block', mt: 0.5, color: 'text.secondary' }}
|
||||
>
|
||||
{description}
|
||||
</Typography>
|
||||
)}
|
||||
@@ -214,12 +218,20 @@ function TypeCard({
|
||||
</Typography>
|
||||
</Box>
|
||||
{description && (
|
||||
<Typography variant="caption" component="span" sx={{ display: 'block', mt: 0.25, color: 'text.secondary' }}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="span"
|
||||
sx={{ display: 'block', mt: 0.25, color: 'text.secondary' }}
|
||||
>
|
||||
{description}
|
||||
</Typography>
|
||||
)}
|
||||
{note && (
|
||||
<Typography variant="captionSm" component="span" sx={{ display: 'block', mt: 0.5, color: 'text.secondary', fontWeight: 500 }}>
|
||||
<Typography
|
||||
variant="captionSm"
|
||||
component="span"
|
||||
sx={{ display: 'block', mt: 0.5, color: 'text.secondary', fontWeight: 500 }}
|
||||
>
|
||||
{note}
|
||||
</Typography>
|
||||
)}
|
||||
@@ -261,7 +273,13 @@ function CompletedRow({
|
||||
onClick={onChangeClick}
|
||||
underline="hover"
|
||||
aria-label={`Change ${question.toLowerCase()}`}
|
||||
sx={{ color: 'text.secondary', ml: 'auto', minHeight: 44, display: 'inline-flex', alignItems: 'center' }}
|
||||
sx={{
|
||||
color: 'text.secondary',
|
||||
ml: 'auto',
|
||||
minHeight: 44,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
Change
|
||||
</Link>
|
||||
@@ -348,9 +366,11 @@ export const FuneralFinder = React.forwardRef<HTMLDivElement, FuneralFinderProps
|
||||
const typeSummary = [typeLabel, themeSuffix].filter(Boolean).join(', ');
|
||||
|
||||
const serviceLabel =
|
||||
servicePref === 'with-service' ? 'With a service'
|
||||
: servicePref === 'without-service' ? 'No service'
|
||||
: 'Flexible';
|
||||
servicePref === 'with-service'
|
||||
? 'With a service'
|
||||
: servicePref === 'without-service'
|
||||
? 'No service'
|
||||
: 'Flexible';
|
||||
|
||||
// ─── Handlers ───────────────────────────────────────────────────
|
||||
const selectIntent = (value: Intent) => {
|
||||
@@ -409,7 +429,7 @@ export const FuneralFinder = React.forwardRef<HTMLDivElement, FuneralFinderProps
|
||||
intent,
|
||||
planningFor: needsPlanningFor ? (planningFor ?? undefined) : undefined,
|
||||
funeralTypeId: isExploreAll ? null : (typeSelection ?? null),
|
||||
servicePreference: (showServiceStep && serviceAnswered) ? servicePref : 'either',
|
||||
servicePreference: showServiceStep && serviceAnswered ? servicePref : 'either',
|
||||
themes: selectedThemes,
|
||||
location: location.trim(),
|
||||
});
|
||||
@@ -448,16 +468,32 @@ export const FuneralFinder = React.forwardRef<HTMLDivElement, FuneralFinderProps
|
||||
|
||||
{/* ── Completed rows ─────────────────────────────────────── */}
|
||||
<Collapse in={intent !== null && activeStep !== 1} timeout={250}>
|
||||
<CompletedRow question="I'm here to" answer={intentLabel} onChangeClick={() => revertTo(1)} />
|
||||
<CompletedRow
|
||||
question="I'm here to"
|
||||
answer={intentLabel}
|
||||
onChangeClick={() => revertTo(1)}
|
||||
/>
|
||||
</Collapse>
|
||||
<Collapse in={needsPlanningFor && planningFor !== null && activeStep !== 2} timeout={250}>
|
||||
<CompletedRow question="Planning for" answer={planningForLabel} onChangeClick={() => revertTo(2)} />
|
||||
<CompletedRow
|
||||
question="Planning for"
|
||||
answer={planningForLabel}
|
||||
onChangeClick={() => revertTo(2)}
|
||||
/>
|
||||
</Collapse>
|
||||
<Collapse in={typeSelected && activeStep !== 3} timeout={250}>
|
||||
<CompletedRow question="Looking for" answer={typeSummary} onChangeClick={() => revertTo(3)} />
|
||||
<CompletedRow
|
||||
question="Looking for"
|
||||
answer={typeSummary}
|
||||
onChangeClick={() => revertTo(3)}
|
||||
/>
|
||||
</Collapse>
|
||||
<Collapse in={showServiceStep && serviceAnswered && activeStep !== 4} timeout={250}>
|
||||
<CompletedRow question="Service" answer={serviceLabel} onChangeClick={() => revertTo(4)} />
|
||||
<CompletedRow
|
||||
question="Service"
|
||||
answer={serviceLabel}
|
||||
onChangeClick={() => revertTo(4)}
|
||||
/>
|
||||
</Collapse>
|
||||
|
||||
{/* ── Step 1: Intent ─────────────────────────────────────── */}
|
||||
@@ -467,13 +503,22 @@ export const FuneralFinder = React.forwardRef<HTMLDivElement, FuneralFinderProps
|
||||
<Typography
|
||||
variant="caption"
|
||||
role="alert"
|
||||
sx={{ color: 'var(--fa-color-brand-600)', textAlign: 'center', display: 'block', mb: 1.5 }}
|
||||
sx={{
|
||||
color: 'var(--fa-color-brand-600)',
|
||||
textAlign: 'center',
|
||||
display: 'block',
|
||||
mb: 1.5,
|
||||
}}
|
||||
>
|
||||
Please let us know how we can help
|
||||
</Typography>
|
||||
)}
|
||||
<StepHeading>How can we help you today?</StepHeading>
|
||||
<Box role="radiogroup" aria-label="How can we help" sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
<Box
|
||||
role="radiogroup"
|
||||
aria-label="How can we help"
|
||||
sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}
|
||||
>
|
||||
<ChoiceCard
|
||||
label="Arrange a funeral now"
|
||||
description="Someone has passed and I need to make arrangements"
|
||||
@@ -494,7 +539,11 @@ export const FuneralFinder = React.forwardRef<HTMLDivElement, FuneralFinderProps
|
||||
<Collapse in={activeStep === 2 && needsPlanningFor} timeout={250}>
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<StepHeading>Who are you planning for?</StepHeading>
|
||||
<Box role="radiogroup" aria-label="Who are you planning for" sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
<Box
|
||||
role="radiogroup"
|
||||
aria-label="Who are you planning for"
|
||||
sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}
|
||||
>
|
||||
<ChoiceCard
|
||||
label="Myself"
|
||||
description="I want to plan my own funeral in advance"
|
||||
@@ -515,7 +564,11 @@ export const FuneralFinder = React.forwardRef<HTMLDivElement, FuneralFinderProps
|
||||
<Collapse in={activeStep === 3} timeout={250}>
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<StepHeading>What type of funeral are you considering?</StepHeading>
|
||||
<Box role="radiogroup" aria-label="Type of funeral" sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
<Box
|
||||
role="radiogroup"
|
||||
aria-label="Type of funeral"
|
||||
sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}
|
||||
>
|
||||
{funeralTypes.map((ft) => (
|
||||
<TypeCard
|
||||
key={ft.id}
|
||||
@@ -543,11 +596,20 @@ export const FuneralFinder = React.forwardRef<HTMLDivElement, FuneralFinderProps
|
||||
<Typography variant="body2" component="span" sx={{ fontWeight: 600 }}>
|
||||
Any preferences?
|
||||
</Typography>
|
||||
<Typography variant="caption" component="span" color="text.secondary" sx={{ ml: 0.75 }}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="span"
|
||||
color="text.secondary"
|
||||
sx={{ ml: 0.75 }}
|
||||
>
|
||||
(optional)
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box role="group" aria-label="Preferences" sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||
<Box
|
||||
role="group"
|
||||
aria-label="Preferences"
|
||||
sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}
|
||||
>
|
||||
{themeOptions.map((theme) => {
|
||||
const isSelected = selectedThemes.includes(theme.id);
|
||||
return (
|
||||
@@ -573,7 +635,11 @@ export const FuneralFinder = React.forwardRef<HTMLDivElement, FuneralFinderProps
|
||||
<Collapse in={activeStep === 4 && showServiceStep} timeout={250}>
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<StepHeading>Would you like a service?</StepHeading>
|
||||
<Box role="group" aria-label="Service preference" sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Box
|
||||
role="group"
|
||||
aria-label="Service preference"
|
||||
sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}
|
||||
>
|
||||
{SERVICE_OPTIONS.map((opt) => (
|
||||
<Chip
|
||||
key={opt.value}
|
||||
@@ -583,7 +649,11 @@ export const FuneralFinder = React.forwardRef<HTMLDivElement, FuneralFinderProps
|
||||
onClick={() => selectService(opt.value)}
|
||||
clickable
|
||||
aria-pressed={serviceAnswered && servicePref === opt.value}
|
||||
sx={{ justifyContent: 'flex-start', height: 44, borderRadius: 'var(--fa-border-radius-md)' }}
|
||||
sx={{
|
||||
justifyContent: 'flex-start',
|
||||
height: 44,
|
||||
borderRadius: 'var(--fa-border-radius-md)',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
@@ -41,12 +41,8 @@ export const BelowMasthead: Story = {
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ fontSize: '2rem', fontWeight: 700, mb: 1 }}>
|
||||
Funeral Arranger
|
||||
</Box>
|
||||
<Box sx={{ opacity: 0.8 }}>
|
||||
Find trusted funeral directors near you
|
||||
</Box>
|
||||
<Box sx={{ fontSize: '2rem', fontWeight: 700, mb: 1 }}>Funeral Arranger</Box>
|
||||
<Box sx={{ opacity: 0.8 }}>Find trusted funeral directors near you</Box>
|
||||
</Box>
|
||||
{/* Widget below masthead */}
|
||||
<Box sx={{ maxWidth: 560, mx: 'auto', mt: -4, px: 2, position: 'relative', zIndex: 1 }}>
|
||||
|
||||
@@ -103,7 +103,10 @@ function StepCircle({
|
||||
transition: 'background-color 200ms ease, color 200ms ease',
|
||||
...(usePrimary
|
||||
? { bgcolor: 'var(--fa-color-brand-500)', color: 'common.white' }
|
||||
: { bgcolor: 'var(--fa-color-brand-200, #EBDAC8)', color: 'var(--fa-color-brand-700, #8B4E0D)' }),
|
||||
: {
|
||||
bgcolor: 'var(--fa-color-brand-200, #EBDAC8)',
|
||||
color: 'var(--fa-color-brand-700, #8B4E0D)',
|
||||
}),
|
||||
// Connector line from bottom of this circle toward the next
|
||||
...(showConnector && {
|
||||
'&::after': {
|
||||
@@ -245,7 +248,8 @@ export const FuneralFinderV2 = React.forwardRef<HTMLDivElement, FuneralFinderV2P
|
||||
}
|
||||
: { lookingTo: false, planningFor: false, funeralType: false, location: false };
|
||||
|
||||
const hasErrors = submitted && (errs.lookingTo || errs.planningFor || errs.funeralType || errs.location);
|
||||
const hasErrors =
|
||||
submitted && (errs.lookingTo || errs.planningFor || errs.funeralType || errs.location);
|
||||
|
||||
// ─── Handlers ────────────────────────────────────────────────
|
||||
const handleLookingTo = (e: SelectChangeEvent<string>) => {
|
||||
@@ -311,11 +315,7 @@ export const FuneralFinderV2 = React.forwardRef<HTMLDivElement, FuneralFinderV2P
|
||||
>
|
||||
{heading}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ textAlign: 'center', mb: 0 }}
|
||||
>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', mb: 0 }}>
|
||||
{subheading}
|
||||
</Typography>
|
||||
<Divider sx={{ my: 3.5 }} />
|
||||
@@ -326,7 +326,10 @@ export const FuneralFinderV2 = React.forwardRef<HTMLDivElement, FuneralFinderV2P
|
||||
<Box sx={{ display: 'flex', gap: 2.5, alignItems: 'flex-end' }}>
|
||||
<StepCircle step={1} completed={!!lookingTo} active showConnector />
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1, color: 'var(--fa-color-brand-700)' }}>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{ fontWeight: 600, mb: 1, color: 'var(--fa-color-brand-700)' }}
|
||||
>
|
||||
I’m looking to…
|
||||
</Typography>
|
||||
<Select
|
||||
@@ -352,7 +355,14 @@ export const FuneralFinderV2 = React.forwardRef<HTMLDivElement, FuneralFinderV2P
|
||||
<Box sx={{ display: 'flex', gap: 2.5, alignItems: 'flex-end' }}>
|
||||
<StepCircle step={2} completed={!!planningFor} showConnector />
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1, color: lookingTo ? 'var(--fa-color-brand-700)' : 'text.disabled' }}>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
mb: 1,
|
||||
color: lookingTo ? 'var(--fa-color-brand-700)' : 'text.disabled',
|
||||
}}
|
||||
>
|
||||
I’m planning for
|
||||
</Typography>
|
||||
<Select
|
||||
@@ -383,7 +393,14 @@ export const FuneralFinderV2 = React.forwardRef<HTMLDivElement, FuneralFinderV2P
|
||||
<Box sx={{ display: 'flex', gap: 2.5, alignItems: 'flex-end' }}>
|
||||
<StepCircle step={3} completed={!!funeralType} showConnector />
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1, color: step3Disabled ? 'text.disabled' : 'var(--fa-color-brand-700)' }}>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
mb: 1,
|
||||
color: step3Disabled ? 'text.disabled' : 'var(--fa-color-brand-700)',
|
||||
}}
|
||||
>
|
||||
Type of funeral
|
||||
</Typography>
|
||||
<Select
|
||||
@@ -410,7 +427,14 @@ export const FuneralFinderV2 = React.forwardRef<HTMLDivElement, FuneralFinderV2P
|
||||
<Box sx={{ display: 'flex', gap: 2.5, alignItems: 'flex-end' }}>
|
||||
<StepCircle step={4} completed={location.trim().length >= 3} />
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1, color: step4Disabled ? 'text.disabled' : 'var(--fa-color-brand-700)' }}>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
mb: 1,
|
||||
color: step4Disabled ? 'text.disabled' : 'var(--fa-color-brand-700)',
|
||||
}}
|
||||
>
|
||||
Looking for providers in
|
||||
</Typography>
|
||||
<Input
|
||||
|
||||
@@ -33,8 +33,7 @@ export const BelowMasthead: Story = {
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
background:
|
||||
'linear-gradient(160deg, #2C2E35 0%, #4C5B6B 60%, #6B3C13 100%)',
|
||||
background: 'linear-gradient(160deg, #2C2E35 0%, #4C5B6B 60%, #6B3C13 100%)',
|
||||
color: '#fff',
|
||||
py: 8,
|
||||
px: 4,
|
||||
|
||||
@@ -75,28 +75,15 @@ const FUNERAL_TYPE_OPTIONS: { value: FuneralType; label: string }[] = [
|
||||
|
||||
/** Hoisted outside component to avoid re-creation on render */
|
||||
const selectPlaceholder = (
|
||||
<span style={{ color: 'var(--fa-color-text-disabled)' }}>
|
||||
Select funeral type
|
||||
</span>
|
||||
<span style={{ color: 'var(--fa-color-text-disabled)' }}>Select funeral type</span>
|
||||
);
|
||||
|
||||
// ─── Sub-components ──────────────────────────────────────────────────────────
|
||||
|
||||
/** Uppercase section label — overline style */
|
||||
function SectionLabel({
|
||||
children,
|
||||
id,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
id?: string;
|
||||
}) {
|
||||
function SectionLabel({ children, id }: { children: React.ReactNode; id?: string }) {
|
||||
return (
|
||||
<Typography
|
||||
variant="overline"
|
||||
component="div"
|
||||
id={id}
|
||||
sx={{ color: 'text.secondary' }}
|
||||
>
|
||||
<Typography variant="overline" component="div" id={id} sx={{ color: 'text.secondary' }}>
|
||||
{children}
|
||||
</Typography>
|
||||
);
|
||||
@@ -138,8 +125,7 @@ const StatusCard = React.forwardRef<
|
||||
cursor: 'pointer',
|
||||
fontFamily: 'inherit',
|
||||
textAlign: 'center',
|
||||
transition:
|
||||
'border-color 200ms ease, background-color 200ms ease, transform 100ms ease',
|
||||
transition: 'border-color 200ms ease, background-color 200ms ease, transform 100ms ease',
|
||||
'&:hover': {
|
||||
borderColor: selected
|
||||
? 'var(--fa-color-border-brand, #BA834E)'
|
||||
@@ -164,9 +150,7 @@ const StatusCard = React.forwardRef<
|
||||
fontWeight: 600,
|
||||
display: 'block',
|
||||
mb: 0.75,
|
||||
color: selected
|
||||
? 'var(--fa-color-text-brand, #B0610F)'
|
||||
: 'text.primary',
|
||||
color: selected ? 'var(--fa-color-text-brand, #B0610F)' : 'text.primary',
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
@@ -253,317 +237,309 @@ const selectMenuProps = {
|
||||
* Required fields: status + location (min 3 chars).
|
||||
* Funeral type defaults to "show all" if not selected.
|
||||
*/
|
||||
export const FuneralFinderV3 = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
FuneralFinderV3Props
|
||||
>((props, ref) => {
|
||||
const {
|
||||
onSearch,
|
||||
loading = false,
|
||||
heading = 'Find funeral directors near you',
|
||||
subheading =
|
||||
"Tell us what you need and we\u2019ll show options in your area.",
|
||||
sx,
|
||||
} = props;
|
||||
export const FuneralFinderV3 = React.forwardRef<HTMLDivElement, FuneralFinderV3Props>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
onSearch,
|
||||
loading = false,
|
||||
heading = 'Find funeral directors near you',
|
||||
subheading = 'Tell us what you need and we\u2019ll show options in your area.',
|
||||
sx,
|
||||
} = props;
|
||||
|
||||
// ─── IDs for aria-labelledby ──────────────────────────────
|
||||
const id = React.useId();
|
||||
const statusLabelId = `${id}-status`;
|
||||
const funeralTypeLabelId = `${id}-funeral-type`;
|
||||
const locationLabelId = `${id}-location`;
|
||||
// ─── IDs for aria-labelledby ──────────────────────────────
|
||||
const id = React.useId();
|
||||
const statusLabelId = `${id}-status`;
|
||||
const funeralTypeLabelId = `${id}-funeral-type`;
|
||||
const locationLabelId = `${id}-location`;
|
||||
|
||||
// ─── State ───────────────────────────────────────────────
|
||||
const [status, setStatus] = React.useState<Status | ''>('immediate');
|
||||
const [funeralType, setFuneralType] = React.useState<FuneralType | ''>('');
|
||||
const [location, setLocation] = React.useState('');
|
||||
const [errors, setErrors] = React.useState<{
|
||||
status?: boolean;
|
||||
location?: boolean;
|
||||
}>({});
|
||||
// ─── State ───────────────────────────────────────────────
|
||||
const [status, setStatus] = React.useState<Status | ''>('immediate');
|
||||
const [funeralType, setFuneralType] = React.useState<FuneralType | ''>('');
|
||||
const [location, setLocation] = React.useState('');
|
||||
const [errors, setErrors] = React.useState<{
|
||||
status?: boolean;
|
||||
location?: boolean;
|
||||
}>({});
|
||||
|
||||
// ─── Refs ────────────────────────────────────────────────
|
||||
const statusSectionRef = React.useRef<HTMLDivElement>(null);
|
||||
const locationSectionRef = React.useRef<HTMLDivElement>(null);
|
||||
const locationInputRef = React.useRef<HTMLInputElement>(null);
|
||||
const cardRefs = React.useRef<(HTMLButtonElement | null)[]>([null, null]);
|
||||
// ─── Refs ────────────────────────────────────────────────
|
||||
const statusSectionRef = React.useRef<HTMLDivElement>(null);
|
||||
const locationSectionRef = React.useRef<HTMLDivElement>(null);
|
||||
const locationInputRef = React.useRef<HTMLInputElement>(null);
|
||||
const cardRefs = React.useRef<(HTMLButtonElement | null)[]>([null, null]);
|
||||
|
||||
// ─── Clear errors as fields are filled ───────────────────
|
||||
const prevStatus = React.useRef(status);
|
||||
React.useEffect(() => {
|
||||
if (status !== prevStatus.current) {
|
||||
prevStatus.current = status;
|
||||
if (status && errors.status) {
|
||||
setErrors((prev) => ({ ...prev, status: false }));
|
||||
// ─── Clear errors as fields are filled ───────────────────
|
||||
const prevStatus = React.useRef(status);
|
||||
React.useEffect(() => {
|
||||
if (status !== prevStatus.current) {
|
||||
prevStatus.current = status;
|
||||
if (status && errors.status) {
|
||||
setErrors((prev) => ({ ...prev, status: false }));
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [status, errors.status]);
|
||||
}, [status, errors.status]);
|
||||
|
||||
const prevLocation = React.useRef(location);
|
||||
React.useEffect(() => {
|
||||
if (location !== prevLocation.current) {
|
||||
prevLocation.current = location;
|
||||
if (location.trim().length >= 3 && errors.location) {
|
||||
setErrors((prev) => ({ ...prev, location: false }));
|
||||
const prevLocation = React.useRef(location);
|
||||
React.useEffect(() => {
|
||||
if (location !== prevLocation.current) {
|
||||
prevLocation.current = location;
|
||||
if (location.trim().length >= 3 && errors.location) {
|
||||
setErrors((prev) => ({ ...prev, location: false }));
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [location, errors.location]);
|
||||
}, [location, errors.location]);
|
||||
|
||||
// ─── Radiogroup keyboard nav (WAI-ARIA pattern) ──────────
|
||||
const activeStatusIndex = status
|
||||
? STATUS_OPTIONS.findIndex((o) => o.key === status)
|
||||
: 0;
|
||||
// ─── Radiogroup keyboard nav (WAI-ARIA pattern) ──────────
|
||||
const activeStatusIndex = status ? STATUS_OPTIONS.findIndex((o) => o.key === status) : 0;
|
||||
|
||||
const handleStatusKeyDown = (e: React.KeyboardEvent) => {
|
||||
const isNext = e.key === 'ArrowRight' || e.key === 'ArrowDown';
|
||||
const isPrev = e.key === 'ArrowLeft' || e.key === 'ArrowUp';
|
||||
if (!isNext && !isPrev) return;
|
||||
e.preventDefault();
|
||||
const current = cardRefs.current.indexOf(
|
||||
e.target as HTMLButtonElement,
|
||||
);
|
||||
if (current === -1) return;
|
||||
const next = isNext
|
||||
? Math.min(current + 1, STATUS_OPTIONS.length - 1)
|
||||
: Math.max(current - 1, 0);
|
||||
if (next !== current) {
|
||||
cardRefs.current[next]?.focus();
|
||||
setStatus(STATUS_OPTIONS[next].key);
|
||||
}
|
||||
};
|
||||
const handleStatusKeyDown = (e: React.KeyboardEvent) => {
|
||||
const isNext = e.key === 'ArrowRight' || e.key === 'ArrowDown';
|
||||
const isPrev = e.key === 'ArrowLeft' || e.key === 'ArrowUp';
|
||||
if (!isNext && !isPrev) return;
|
||||
e.preventDefault();
|
||||
const current = cardRefs.current.indexOf(e.target as HTMLButtonElement);
|
||||
if (current === -1) return;
|
||||
const next = isNext
|
||||
? Math.min(current + 1, STATUS_OPTIONS.length - 1)
|
||||
: Math.max(current - 1, 0);
|
||||
if (next !== current) {
|
||||
cardRefs.current[next]?.focus();
|
||||
setStatus(STATUS_OPTIONS[next].key);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── Handlers ────────────────────────────────────────────
|
||||
const handleFuneralType = (e: SelectChangeEvent<string>) => {
|
||||
setFuneralType(e.target.value as FuneralType);
|
||||
};
|
||||
// ─── Handlers ────────────────────────────────────────────
|
||||
const handleFuneralType = (e: SelectChangeEvent<string>) => {
|
||||
setFuneralType(e.target.value as FuneralType);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!status) {
|
||||
setErrors({ status: true });
|
||||
statusSectionRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
const handleSubmit = () => {
|
||||
if (!status) {
|
||||
setErrors({ status: true });
|
||||
statusSectionRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (location.trim().length < 3) {
|
||||
setErrors({ location: true });
|
||||
locationSectionRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
locationInputRef.current?.focus();
|
||||
return;
|
||||
}
|
||||
setErrors({});
|
||||
onSearch?.({
|
||||
status,
|
||||
funeralType: funeralType || 'show-all',
|
||||
location: location.trim(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (location.trim().length < 3) {
|
||||
setErrors({ location: true });
|
||||
locationSectionRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
locationInputRef.current?.focus();
|
||||
return;
|
||||
}
|
||||
setErrors({});
|
||||
onSearch?.({
|
||||
status,
|
||||
funeralType: funeralType || 'show-all',
|
||||
location: location.trim(),
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
// ─── Render ──────────────────────────────────────────────
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
role="search"
|
||||
aria-label="Find funeral directors"
|
||||
sx={[
|
||||
{
|
||||
bgcolor: 'var(--fa-color-surface-raised, #fff)',
|
||||
borderRadius: 'var(--fa-card-border-radius-default, 8px)',
|
||||
boxShadow: 'var(--fa-card-shadow-default)',
|
||||
px: { xs: 3.5, sm: 5 },
|
||||
py: { xs: 4, sm: 5 },
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
},
|
||||
...(Array.isArray(sx) ? sx : [sx]),
|
||||
]}
|
||||
>
|
||||
{/* ── Header ──────────────────────────────────────────── */}
|
||||
<Box sx={{ textAlign: 'center' }}>
|
||||
<Typography
|
||||
variant="h3"
|
||||
component="h2"
|
||||
sx={{
|
||||
fontFamily: 'var(--fa-font-family-display)',
|
||||
fontWeight: 400,
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
{heading}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{subheading}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* ── How can we help ─────────────────────────────────── */}
|
||||
<Box ref={statusSectionRef}>
|
||||
<SectionLabel id={statusLabelId}>How Can We Help</SectionLabel>
|
||||
<Box
|
||||
role="radiogroup"
|
||||
aria-labelledby={statusLabelId}
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: { xs: '1fr', sm: '1fr 1fr' },
|
||||
gap: 2,
|
||||
mt: 2,
|
||||
}}
|
||||
>
|
||||
{STATUS_OPTIONS.map((opt, i) => (
|
||||
<StatusCard
|
||||
key={opt.key}
|
||||
ref={(el) => {
|
||||
cardRefs.current[i] = el;
|
||||
}}
|
||||
title={opt.title}
|
||||
description={opt.description}
|
||||
selected={status === opt.key}
|
||||
onClick={() => setStatus(opt.key)}
|
||||
tabIndex={i === activeStatusIndex ? 0 : -1}
|
||||
onKeyDown={handleStatusKeyDown}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
<Box aria-live="polite" sx={{ textAlign: 'center' }}>
|
||||
{errors.status && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
role="alert"
|
||||
sx={{
|
||||
color: 'var(--fa-color-text-brand, #B0610F)',
|
||||
display: 'block',
|
||||
mt: 1,
|
||||
}}
|
||||
>
|
||||
Please select how we can help
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* ── Funeral Type ────────────────────────────────────── */}
|
||||
<Box>
|
||||
<SectionLabel id={funeralTypeLabelId}>Funeral Type</SectionLabel>
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Select
|
||||
value={funeralType}
|
||||
onChange={handleFuneralType}
|
||||
displayEmpty
|
||||
renderValue={(v) =>
|
||||
v
|
||||
? FUNERAL_TYPE_OPTIONS.find((o) => o.value === v)?.label
|
||||
: selectPlaceholder
|
||||
}
|
||||
MenuProps={selectMenuProps}
|
||||
// ─── Render ──────────────────────────────────────────────
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
role="search"
|
||||
aria-label="Find funeral directors"
|
||||
sx={[
|
||||
{
|
||||
bgcolor: 'var(--fa-color-surface-raised, #fff)',
|
||||
borderRadius: 'var(--fa-card-border-radius-default, 8px)',
|
||||
boxShadow: 'var(--fa-card-shadow-default)',
|
||||
px: { xs: 3.5, sm: 5 },
|
||||
py: { xs: 4, sm: 5 },
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
},
|
||||
...(Array.isArray(sx) ? sx : [sx]),
|
||||
]}
|
||||
>
|
||||
{/* ── Header ──────────────────────────────────────────── */}
|
||||
<Box sx={{ textAlign: 'center' }}>
|
||||
<Typography
|
||||
variant="h3"
|
||||
component="h2"
|
||||
sx={{
|
||||
...fieldBaseSx,
|
||||
'& .MuiSelect-select': {
|
||||
...fieldInputStyles,
|
||||
minHeight: 'unset !important',
|
||||
},
|
||||
'& .MuiSelect-icon': {
|
||||
color: 'var(--fa-color-text-disabled)',
|
||||
right: 12,
|
||||
},
|
||||
fontFamily: 'var(--fa-font-family-display)',
|
||||
fontWeight: 400,
|
||||
mb: 1,
|
||||
}}
|
||||
inputProps={{ 'aria-labelledby': funeralTypeLabelId }}
|
||||
>
|
||||
{FUNERAL_TYPE_OPTIONS.map((o) => (
|
||||
<MenuItem key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{heading}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{subheading}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* ── Location ────────────────────────────────────────── */}
|
||||
<Box ref={locationSectionRef}>
|
||||
<SectionLabel id={locationLabelId}>Location</SectionLabel>
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<OutlinedInput
|
||||
value={location}
|
||||
onChange={(e) => setLocation(e.target.value)}
|
||||
placeholder="Enter suburb or postcode"
|
||||
inputRef={locationInputRef}
|
||||
startAdornment={
|
||||
<InputAdornment position="start" sx={{ ml: 0.5 }}>
|
||||
<LocationOnOutlinedIcon
|
||||
sx={{
|
||||
fontSize: 20,
|
||||
color: 'var(--fa-color-text-disabled)',
|
||||
}}
|
||||
/>
|
||||
</InputAdornment>
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') handleSubmit();
|
||||
}}
|
||||
<Divider />
|
||||
|
||||
{/* ── How can we help ─────────────────────────────────── */}
|
||||
<Box ref={statusSectionRef}>
|
||||
<SectionLabel id={statusLabelId}>How Can We Help</SectionLabel>
|
||||
<Box
|
||||
role="radiogroup"
|
||||
aria-labelledby={statusLabelId}
|
||||
sx={{
|
||||
...fieldBaseSx,
|
||||
'& .MuiOutlinedInput-input': {
|
||||
...fieldInputStyles,
|
||||
'&::placeholder': {
|
||||
color: 'var(--fa-color-text-disabled)',
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
display: 'grid',
|
||||
gridTemplateColumns: { xs: '1fr', sm: '1fr 1fr' },
|
||||
gap: 2,
|
||||
mt: 2,
|
||||
}}
|
||||
inputProps={{
|
||||
'aria-labelledby': locationLabelId,
|
||||
'aria-required': true,
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{STATUS_OPTIONS.map((opt, i) => (
|
||||
<StatusCard
|
||||
key={opt.key}
|
||||
ref={(el) => {
|
||||
cardRefs.current[i] = el;
|
||||
}}
|
||||
title={opt.title}
|
||||
description={opt.description}
|
||||
selected={status === opt.key}
|
||||
onClick={() => setStatus(opt.key)}
|
||||
tabIndex={i === activeStatusIndex ? 0 : -1}
|
||||
onKeyDown={handleStatusKeyDown}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
<Box aria-live="polite" sx={{ textAlign: 'center' }}>
|
||||
{errors.status && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
role="alert"
|
||||
sx={{
|
||||
color: 'var(--fa-color-text-brand, #B0610F)',
|
||||
display: 'block',
|
||||
mt: 1,
|
||||
}}
|
||||
>
|
||||
Please select how we can help
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box aria-live="polite">
|
||||
{errors.location && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
role="alert"
|
||||
|
||||
{/* ── Funeral Type ────────────────────────────────────── */}
|
||||
<Box>
|
||||
<SectionLabel id={funeralTypeLabelId}>Funeral Type</SectionLabel>
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Select
|
||||
value={funeralType}
|
||||
onChange={handleFuneralType}
|
||||
displayEmpty
|
||||
renderValue={(v) =>
|
||||
v ? FUNERAL_TYPE_OPTIONS.find((o) => o.value === v)?.label : selectPlaceholder
|
||||
}
|
||||
MenuProps={selectMenuProps}
|
||||
sx={{
|
||||
color: 'var(--fa-color-text-brand, #B0610F)',
|
||||
display: 'block',
|
||||
mt: 1,
|
||||
...fieldBaseSx,
|
||||
'& .MuiSelect-select': {
|
||||
...fieldInputStyles,
|
||||
minHeight: 'unset !important',
|
||||
},
|
||||
'& .MuiSelect-icon': {
|
||||
color: 'var(--fa-color-text-disabled)',
|
||||
right: 12,
|
||||
},
|
||||
}}
|
||||
inputProps={{ 'aria-labelledby': funeralTypeLabelId }}
|
||||
>
|
||||
Please enter a suburb or postcode
|
||||
</Typography>
|
||||
)}
|
||||
{FUNERAL_TYPE_OPTIONS.map((o) => (
|
||||
<MenuItem key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* ── Location ────────────────────────────────────────── */}
|
||||
<Box ref={locationSectionRef}>
|
||||
<SectionLabel id={locationLabelId}>Location</SectionLabel>
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<OutlinedInput
|
||||
value={location}
|
||||
onChange={(e) => setLocation(e.target.value)}
|
||||
placeholder="Enter suburb or postcode"
|
||||
inputRef={locationInputRef}
|
||||
startAdornment={
|
||||
<InputAdornment position="start" sx={{ ml: 0.5 }}>
|
||||
<LocationOnOutlinedIcon
|
||||
sx={{
|
||||
fontSize: 20,
|
||||
color: 'var(--fa-color-text-disabled)',
|
||||
}}
|
||||
/>
|
||||
</InputAdornment>
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') handleSubmit();
|
||||
}}
|
||||
sx={{
|
||||
...fieldBaseSx,
|
||||
'& .MuiOutlinedInput-input': {
|
||||
...fieldInputStyles,
|
||||
'&::placeholder': {
|
||||
color: 'var(--fa-color-text-disabled)',
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
}}
|
||||
inputProps={{
|
||||
'aria-labelledby': locationLabelId,
|
||||
'aria-required': true,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box aria-live="polite">
|
||||
{errors.location && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
role="alert"
|
||||
sx={{
|
||||
color: 'var(--fa-color-text-brand, #B0610F)',
|
||||
display: 'block',
|
||||
mt: 1,
|
||||
}}
|
||||
>
|
||||
Please enter a suburb or postcode
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* ── CTA ─────────────────────────────────────────────── */}
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
fullWidth
|
||||
loading={loading}
|
||||
endIcon={!loading ? <ArrowForwardIcon /> : undefined}
|
||||
onClick={handleSubmit}
|
||||
sx={{ minHeight: 52 }}
|
||||
>
|
||||
Find Funeral Directors
|
||||
</Button>
|
||||
<Typography
|
||||
variant="captionSm"
|
||||
color="text.secondary"
|
||||
sx={{ textAlign: 'center', display: 'block', mt: 1.5 }}
|
||||
>
|
||||
Free to use · No obligation
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* ── CTA ─────────────────────────────────────────────── */}
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
fullWidth
|
||||
loading={loading}
|
||||
endIcon={!loading ? <ArrowForwardIcon /> : undefined}
|
||||
onClick={handleSubmit}
|
||||
sx={{ minHeight: 52 }}
|
||||
>
|
||||
Find Funeral Directors
|
||||
</Button>
|
||||
<Typography
|
||||
variant="captionSm"
|
||||
color="text.secondary"
|
||||
sx={{ textAlign: 'center', display: 'block', mt: 1.5 }}
|
||||
>
|
||||
Free to use · No obligation
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
FuneralFinderV3.displayName = 'FuneralFinderV3';
|
||||
export default FuneralFinderV3;
|
||||
|
||||
@@ -77,11 +77,7 @@ export const WithCTA: Story = {
|
||||
export const WithPageContent: Story = {
|
||||
render: () => (
|
||||
<Box>
|
||||
<Navigation
|
||||
logo={<FALogo />}
|
||||
items={defaultItems}
|
||||
ctaLabel="Start planning"
|
||||
/>
|
||||
<Navigation logo={<FALogo />} items={defaultItems} ctaLabel="Start planning" />
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: 'lg',
|
||||
@@ -94,8 +90,8 @@ export const WithPageContent: Story = {
|
||||
Find a funeral director
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 4, maxWidth: 600 }}>
|
||||
Compare trusted funeral directors in your area. View services,
|
||||
pricing, and reviews to find the right support for your family.
|
||||
Compare trusted funeral directors in your area. View services, pricing, and reviews to
|
||||
find the right support for your family.
|
||||
</Typography>
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<Box
|
||||
|
||||
@@ -156,11 +156,7 @@ export const Navigation = React.forwardRef<HTMLDivElement, NavigationProps>(
|
||||
))}
|
||||
|
||||
{ctaLabel && (
|
||||
<Button
|
||||
variant="contained"
|
||||
size="medium"
|
||||
onClick={onCtaClick}
|
||||
>
|
||||
<Button variant="contained" size="medium" onClick={onCtaClick}>
|
||||
{ctaLabel}
|
||||
</Button>
|
||||
)}
|
||||
@@ -193,14 +189,8 @@ export const Navigation = React.forwardRef<HTMLDivElement, NavigationProps>(
|
||||
bgcolor: 'var(--fa-color-surface-subtle)',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
{logo}
|
||||
</Box>
|
||||
<IconButton
|
||||
aria-label="Close menu"
|
||||
onClick={handleDrawerToggle}
|
||||
size="small"
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>{logo}</Box>
|
||||
<IconButton aria-label="Close menu" onClick={handleDrawerToggle} size="small">
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
@@ -10,44 +10,136 @@ import { Button } from '../../atoms/Button';
|
||||
import { Navigation } from '../Navigation';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
|
||||
const DEMO_IMAGE = 'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=400&h=300&fit=crop';
|
||||
const DEMO_IMAGE =
|
||||
'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=400&h=300&fit=crop';
|
||||
|
||||
const essentials = [
|
||||
{ name: 'Accommodation', price: 1500, info: 'Refrigerated holding of the deceased prior to the funeral service.' },
|
||||
{ name: 'Death Registration Certificate', price: 1500, info: 'Lodgement of death registration with NSW Registry of Births, Deaths & Marriages.' },
|
||||
{ name: 'Doctor Fee for Cremation', price: 1500, info: 'Statutory medical referee fee required for all cremations in NSW.' },
|
||||
{ name: 'NSW Government Levy — Cremation', price: 1500, info: 'NSW Government cremation levy as set by the Department of Health.' },
|
||||
{ name: 'Professional Mortuary Care', price: 1500, info: 'Preparation and care of the deceased.' },
|
||||
{ name: 'Professional Service Fee', price: 1500, info: 'Coordination of all funeral arrangements and services.' },
|
||||
{ name: 'Allowance for Coffin', price: 1500, isAllowance: true, info: 'Allowance amount — upgrade options available during arrangement.' },
|
||||
{ name: 'Allowance for Crematorium', price: 1500, isAllowance: true, info: 'Allowance for crematorium fees — varies by location.' },
|
||||
{ name: 'Allowance for Hearse', price: 1500, isAllowance: true, info: 'Allowance for hearse transfer — distance surcharges may apply.' },
|
||||
{
|
||||
name: 'Accommodation',
|
||||
price: 1500,
|
||||
info: 'Refrigerated holding of the deceased prior to the funeral service.',
|
||||
},
|
||||
{
|
||||
name: 'Death Registration Certificate',
|
||||
price: 1500,
|
||||
info: 'Lodgement of death registration with NSW Registry of Births, Deaths & Marriages.',
|
||||
},
|
||||
{
|
||||
name: 'Doctor Fee for Cremation',
|
||||
price: 1500,
|
||||
info: 'Statutory medical referee fee required for all cremations in NSW.',
|
||||
},
|
||||
{
|
||||
name: 'NSW Government Levy — Cremation',
|
||||
price: 1500,
|
||||
info: 'NSW Government cremation levy as set by the Department of Health.',
|
||||
},
|
||||
{
|
||||
name: 'Professional Mortuary Care',
|
||||
price: 1500,
|
||||
info: 'Preparation and care of the deceased.',
|
||||
},
|
||||
{
|
||||
name: 'Professional Service Fee',
|
||||
price: 1500,
|
||||
info: 'Coordination of all funeral arrangements and services.',
|
||||
},
|
||||
{
|
||||
name: 'Allowance for Coffin',
|
||||
price: 1500,
|
||||
isAllowance: true,
|
||||
info: 'Allowance amount — upgrade options available during arrangement.',
|
||||
},
|
||||
{
|
||||
name: 'Allowance for Crematorium',
|
||||
price: 1500,
|
||||
isAllowance: true,
|
||||
info: 'Allowance for crematorium fees — varies by location.',
|
||||
},
|
||||
{
|
||||
name: 'Allowance for Hearse',
|
||||
price: 1500,
|
||||
isAllowance: true,
|
||||
info: 'Allowance for hearse transfer — distance surcharges may apply.',
|
||||
},
|
||||
];
|
||||
|
||||
const complimentary = [
|
||||
{ name: 'Dressing Fee', info: 'Dressing and preparation of the deceased — included at no charge.' },
|
||||
{
|
||||
name: 'Dressing Fee',
|
||||
info: 'Dressing and preparation of the deceased — included at no charge.',
|
||||
},
|
||||
{ name: 'Viewing Fee', info: 'One private family viewing — included at no charge.' },
|
||||
];
|
||||
|
||||
const extras = {
|
||||
heading: 'Extras',
|
||||
items: [
|
||||
{ name: 'Allowance for Flowers', price: 1500, isAllowance: true, info: 'Seasonal floral arrangements for the service.' },
|
||||
{ name: 'Allowance for Master of Ceremonies', price: 1500, isAllowance: true, info: 'Professional celebrant or MC for the funeral service.' },
|
||||
{ name: 'After Business Hours Service Surcharge', price: 1500, info: 'Additional fee for services held outside standard business hours.' },
|
||||
{ name: 'After Hours Prayers', price: 1500, info: 'Evening prayer service at the funeral home.' },
|
||||
{ name: 'Coffin Bearing by Funeral Directors', price: 1500, info: 'Professional pallbearing by funeral directors.' },
|
||||
{ name: 'Digital Recording', price: 1500, info: 'Professional video recording of the funeral service.' },
|
||||
{
|
||||
name: 'Allowance for Flowers',
|
||||
price: 1500,
|
||||
isAllowance: true,
|
||||
info: 'Seasonal floral arrangements for the service.',
|
||||
},
|
||||
{
|
||||
name: 'Allowance for Master of Ceremonies',
|
||||
price: 1500,
|
||||
isAllowance: true,
|
||||
info: 'Professional celebrant or MC for the funeral service.',
|
||||
},
|
||||
{
|
||||
name: 'After Business Hours Service Surcharge',
|
||||
price: 1500,
|
||||
info: 'Additional fee for services held outside standard business hours.',
|
||||
},
|
||||
{
|
||||
name: 'After Hours Prayers',
|
||||
price: 1500,
|
||||
info: 'Evening prayer service at the funeral home.',
|
||||
},
|
||||
{
|
||||
name: 'Coffin Bearing by Funeral Directors',
|
||||
price: 1500,
|
||||
info: 'Professional pallbearing by funeral directors.',
|
||||
},
|
||||
{
|
||||
name: 'Digital Recording',
|
||||
price: 1500,
|
||||
info: 'Professional video recording of the funeral service.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const termsText = '* This package includes a funeral service at a chapel or a church with a funeral procession following to the crematorium. It includes many of the most commonly selected funeral options preselected for you. Many people choose this package for the extended funeral rituals — of course, you can tailor the funeral service to meet your needs and budget as you go through the selections.';
|
||||
const termsText =
|
||||
'* This package includes a funeral service at a chapel or a church with a funeral procession following to the crematorium. It includes many of the most commonly selected funeral options preselected for you. Many people choose this package for the extended funeral rituals — of course, you can tailor the funeral service to meet your needs and budget as you go through the selections.';
|
||||
|
||||
const packages = [
|
||||
{ id: 'everyday', name: 'Everyday Funeral Package', price: 900, description: 'Our most popular package with all essential services included. Suitable for a traditional chapel or church service.' },
|
||||
{ id: 'deluxe', name: 'Deluxe Funeral Package', price: 1200, description: 'An enhanced package with premium coffin and additional floral arrangements.' },
|
||||
{ id: 'essential', name: 'Essential Funeral Package', price: 600, description: 'A simple, dignified service covering all necessary arrangements.' },
|
||||
{ id: 'catholic', name: 'Catholic Service', price: 950, description: 'A service tailored for Catholic traditions including prayers and church ceremony.' },
|
||||
{
|
||||
id: 'everyday',
|
||||
name: 'Everyday Funeral Package',
|
||||
price: 900,
|
||||
description:
|
||||
'Our most popular package with all essential services included. Suitable for a traditional chapel or church service.',
|
||||
},
|
||||
{
|
||||
id: 'deluxe',
|
||||
name: 'Deluxe Funeral Package',
|
||||
price: 1200,
|
||||
description: 'An enhanced package with premium coffin and additional floral arrangements.',
|
||||
},
|
||||
{
|
||||
id: 'essential',
|
||||
name: 'Essential Funeral Package',
|
||||
price: 600,
|
||||
description: 'A simple, dignified service covering all necessary arrangements.',
|
||||
},
|
||||
{
|
||||
id: 'catholic',
|
||||
name: 'Catholic Service',
|
||||
price: 950,
|
||||
description:
|
||||
'A service tailored for Catholic traditions including prayers and church ceremony.',
|
||||
},
|
||||
];
|
||||
|
||||
const funeralTypes = ['All', 'Cremation', 'Burial', 'Memorial', 'Catholic', 'Direct Cremation'];
|
||||
@@ -101,9 +193,7 @@ export const CompareLoading: Story = {
|
||||
args: {
|
||||
name: 'Everyday Funeral Package',
|
||||
price: 900,
|
||||
sections: [
|
||||
{ heading: 'Essentials', items: essentials.slice(0, 4) },
|
||||
],
|
||||
sections: [{ heading: 'Essentials', items: essentials.slice(0, 4) }],
|
||||
total: 6000,
|
||||
onArrange: () => alert('Make Arrangement'),
|
||||
onCompare: () => {},
|
||||
|
||||
@@ -155,15 +155,14 @@ export const PackageDetail = React.forwardRef<HTMLDivElement, PackageDetailProps
|
||||
<Typography variant="h3" component="h2">
|
||||
{name}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ mt: 0.5, color: 'primary.main', fontWeight: 600 }}
|
||||
>
|
||||
<Typography variant="h5" sx={{ mt: 0.5, color: 'primary.main', fontWeight: 600 }}>
|
||||
${price.toLocaleString('en-AU')}
|
||||
</Typography>
|
||||
|
||||
{/* CTA buttons */}
|
||||
<Box sx={{ display: 'flex', flexDirection: { xs: 'column', sm: 'row' }, gap: 1.5, mt: 2.5 }}>
|
||||
<Box
|
||||
sx={{ display: 'flex', flexDirection: { xs: 'column', sm: 'row' }, gap: 1.5, mt: 2.5 }}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
@@ -198,9 +197,7 @@ export const PackageDetail = React.forwardRef<HTMLDivElement, PackageDetailProps
|
||||
))}
|
||||
|
||||
{/* Total — separates included content from extras */}
|
||||
{total != null && (
|
||||
<LineItem name="Total" price={total} variant="total" />
|
||||
)}
|
||||
{total != null && <LineItem name="Total" price={total} variant="total" />}
|
||||
|
||||
{/* Extras — additional cost items after the total */}
|
||||
{extras && extras.items.length > 0 && (
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
export { PackageDetail, type PackageDetailProps, type PackageSection, type PackageLineItem } from './PackageDetail';
|
||||
export {
|
||||
PackageDetail,
|
||||
type PackageDetailProps,
|
||||
type PackageSection,
|
||||
type PackageLineItem,
|
||||
} from './PackageDetail';
|
||||
|
||||
@@ -37,10 +37,30 @@ const serviceTypes = [
|
||||
];
|
||||
|
||||
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.' },
|
||||
{ id: 'simple', name: 'Simple Pine', price: 600, description: 'Unfinished pine. Can be personalised with paint, photos, or messages.' },
|
||||
{
|
||||
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.',
|
||||
},
|
||||
{
|
||||
id: 'simple',
|
||||
name: 'Simple Pine',
|
||||
price: 600,
|
||||
description: 'Unfinished pine. Can be personalised with paint, photos, or messages.',
|
||||
},
|
||||
];
|
||||
|
||||
const meta: Meta<typeof ServiceSelector> = {
|
||||
@@ -72,7 +92,7 @@ type Story = StoryObj<typeof ServiceSelector>;
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
heading: 'Choose a service type',
|
||||
subheading: 'Select the type of service you\'d like to arrange. Prices are starting estimates.',
|
||||
subheading: "Select the type of service you'd like to arrange. Prices are starting estimates.",
|
||||
items: serviceTypes,
|
||||
continueLabel: 'Continue',
|
||||
},
|
||||
@@ -180,7 +200,13 @@ export const WithDisabledOptions: Story = {
|
||||
const [selected, setSelected] = useState<string | undefined>();
|
||||
|
||||
const items = serviceTypes.map((item) =>
|
||||
item.id === 'memorial' ? { ...item, disabled: true, description: item.description + ' (Currently unavailable at this location.)' } : item,
|
||||
item.id === 'memorial'
|
||||
? {
|
||||
...item,
|
||||
disabled: true,
|
||||
description: item.description + ' (Currently unavailable at this location.)',
|
||||
}
|
||||
: item,
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -227,7 +253,11 @@ export const InArrangementFlow: Story = {
|
||||
maxDescriptionLines={2}
|
||||
/>
|
||||
|
||||
<Typography variant="captionSm" color="text.secondary" sx={{ mt: 3, textAlign: 'center', display: 'block' }}>
|
||||
<Typography
|
||||
variant="captionSm"
|
||||
color="text.secondary"
|
||||
sx={{ mt: 3, textAlign: 'center', display: 'block' }}
|
||||
>
|
||||
All prices are estimates and may vary based on your specific requirements.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -92,13 +92,7 @@ export const ServiceSelector = React.forwardRef<HTMLDivElement, ServiceSelectorP
|
||||
const isContinueDisabled = continueDisabled ?? nothingSelected;
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
sx={[
|
||||
{ width: '100%' },
|
||||
...(Array.isArray(sx) ? sx : [sx]),
|
||||
]}
|
||||
>
|
||||
<Box ref={ref} sx={[{ width: '100%' }, ...(Array.isArray(sx) ? sx : [sx])]}>
|
||||
{/* Header */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="h4" component="h2" sx={{ mb: subheading ? 1 : 0 }}>
|
||||
|
||||
Reference in New Issue
Block a user