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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user