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>
712 lines
24 KiB
TypeScript
712 lines
24 KiB
TypeScript
import React from 'react';
|
|
import Box from '@mui/material/Box';
|
|
import Collapse from '@mui/material/Collapse';
|
|
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
|
import type { SxProps, Theme } from '@mui/material/styles';
|
|
import { Typography } from '../../atoms/Typography';
|
|
import { Button } from '../../atoms/Button';
|
|
import { Input } from '../../atoms/Input';
|
|
import { Chip } from '../../atoms/Chip';
|
|
import { Divider } from '../../atoms/Divider';
|
|
import { Link } from '../../atoms/Link';
|
|
|
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
|
|
/** A funeral type option (dynamic, from API) */
|
|
export interface FuneralTypeOption {
|
|
/** Unique identifier */
|
|
id: string;
|
|
/** Display label */
|
|
label: string;
|
|
/** Brief description shown below the label */
|
|
description?: string;
|
|
/** Availability note, e.g., "Available in QLD only" */
|
|
note?: string;
|
|
/** Whether this type supports with/without service toggle */
|
|
hasServiceOption?: boolean;
|
|
}
|
|
|
|
/** A thematic filter option */
|
|
export interface ThemeOption {
|
|
/** Unique identifier */
|
|
id: string;
|
|
/** Display label */
|
|
label: string;
|
|
}
|
|
|
|
/** Search parameters returned when the user submits */
|
|
export interface FuneralSearchParams {
|
|
/** "arrange" (immediate need) or "preplan" */
|
|
intent: 'arrange' | 'preplan';
|
|
/** Only present when intent is "preplan" */
|
|
planningFor?: 'myself' | 'someone-else';
|
|
/** Selected funeral type ID, or null if "Explore all" / not specified */
|
|
funeralTypeId: string | null;
|
|
/** "with-service", "without-service", or "either" */
|
|
servicePreference: 'with-service' | 'without-service' | 'either';
|
|
/** Selected theme filter IDs (may be empty) */
|
|
themes: string[];
|
|
/** Suburb or postcode entered */
|
|
location: string;
|
|
}
|
|
|
|
/** Props for the FA FuneralFinder organism */
|
|
export interface FuneralFinderProps {
|
|
/** Available funeral types — dynamic list from API */
|
|
funeralTypes: FuneralTypeOption[];
|
|
/** Optional thematic filter options (e.g., eco-friendly, budget-friendly) */
|
|
themeOptions?: ThemeOption[];
|
|
/** Called when the user clicks "Find funeral providers" */
|
|
onSearch?: (params: FuneralSearchParams) => void;
|
|
/** Whether a search is in progress — shows loading state on the CTA */
|
|
loading?: boolean;
|
|
/** Optional heading override */
|
|
heading?: string;
|
|
/** Optional subheading override */
|
|
subheading?: string;
|
|
/** Show "Explore all options" choice. Default true. */
|
|
showExploreAll?: boolean;
|
|
/** MUI sx prop for the root card */
|
|
sx?: SxProps<Theme>;
|
|
}
|
|
|
|
// ─── Internal types ──────────────────────────────────────────────────────────
|
|
|
|
type Intent = 'arrange' | 'preplan' | null;
|
|
type PlanningFor = 'myself' | 'someone-else' | null;
|
|
type ServicePref = 'with-service' | 'without-service' | 'either';
|
|
|
|
// ─── Sub-components ──────────────────────────────────────────────────────────
|
|
|
|
/** Step question heading — centered, larger than card labels */
|
|
function StepHeading({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<Box sx={{ mb: 2.5 }}>
|
|
<Typography variant="bodyLg" sx={{ fontWeight: 600, textAlign: 'center' }}>
|
|
{children}
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
/** Large tappable option for binary choices (intent, planning-for) */
|
|
function ChoiceCard({
|
|
label,
|
|
description,
|
|
selected,
|
|
onClick,
|
|
}: {
|
|
label: string;
|
|
description?: string;
|
|
selected: boolean;
|
|
onClick: () => void;
|
|
}) {
|
|
return (
|
|
<Box
|
|
component="button"
|
|
role="radio"
|
|
aria-checked={selected}
|
|
onClick={onClick}
|
|
sx={{
|
|
width: '100%',
|
|
px: 2.5,
|
|
py: 2,
|
|
border: '2px solid',
|
|
borderColor: selected ? 'var(--fa-color-brand-500)' : 'var(--fa-color-neutral-200)',
|
|
borderRadius: 'var(--fa-border-radius-md)',
|
|
bgcolor: selected ? 'var(--fa-color-surface-warm)' : 'var(--fa-color-surface-default)',
|
|
cursor: 'pointer',
|
|
fontFamily: 'inherit',
|
|
textAlign: 'left',
|
|
transition: 'border-color 150ms ease-in-out, background-color 150ms ease-in-out',
|
|
'&:hover': {
|
|
borderColor: 'var(--fa-color-brand-400)',
|
|
bgcolor: selected ? 'var(--fa-color-surface-warm)' : 'var(--fa-color-brand-50)',
|
|
},
|
|
'&:active': { filter: 'brightness(0.95)' },
|
|
'&:focus-visible': {
|
|
outline: '2px solid var(--fa-color-brand-500)',
|
|
outlineOffset: 2,
|
|
},
|
|
}}
|
|
>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
{selected && (
|
|
<CheckCircleOutlineIcon
|
|
aria-hidden="true"
|
|
sx={{ fontSize: 20, color: 'var(--fa-color-brand-500)', flexShrink: 0 }}
|
|
/>
|
|
)}
|
|
<Typography
|
|
variant="body1"
|
|
component="span"
|
|
sx={{ fontWeight: 600, color: selected ? 'var(--fa-color-brand-700)' : 'text.primary' }}
|
|
>
|
|
{label}
|
|
</Typography>
|
|
</Box>
|
|
{description && (
|
|
<Typography
|
|
variant="caption"
|
|
component="span"
|
|
sx={{ display: 'block', mt: 0.5, color: 'text.secondary' }}
|
|
>
|
|
{description}
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
/** Funeral type card — compact selectable card */
|
|
function TypeCard({
|
|
label,
|
|
description,
|
|
note,
|
|
selected,
|
|
onClick,
|
|
}: {
|
|
label: string;
|
|
description?: string;
|
|
note?: string;
|
|
selected: boolean;
|
|
onClick: () => void;
|
|
}) {
|
|
return (
|
|
<Box
|
|
component="button"
|
|
role="radio"
|
|
aria-checked={selected}
|
|
onClick={onClick}
|
|
sx={{
|
|
width: '100%',
|
|
minHeight: 44,
|
|
px: 2,
|
|
py: 2,
|
|
border: '2px solid',
|
|
borderColor: selected ? 'var(--fa-color-brand-500)' : 'var(--fa-color-neutral-200)',
|
|
borderRadius: 'var(--fa-border-radius-md)',
|
|
bgcolor: selected ? 'var(--fa-color-surface-warm)' : 'var(--fa-color-surface-default)',
|
|
cursor: 'pointer',
|
|
fontFamily: 'inherit',
|
|
textAlign: 'left',
|
|
transition: 'border-color 150ms ease-in-out, background-color 150ms ease-in-out',
|
|
'&:hover': {
|
|
borderColor: 'var(--fa-color-brand-400)',
|
|
bgcolor: selected ? 'var(--fa-color-surface-warm)' : 'var(--fa-color-brand-50)',
|
|
},
|
|
'&:active': { filter: 'brightness(0.95)' },
|
|
'&:focus-visible': {
|
|
outline: '2px solid var(--fa-color-brand-500)',
|
|
outlineOffset: 2,
|
|
},
|
|
}}
|
|
>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
{selected && (
|
|
<CheckCircleOutlineIcon
|
|
aria-hidden="true"
|
|
sx={{ fontSize: 20, color: 'var(--fa-color-brand-500)', flexShrink: 0 }}
|
|
/>
|
|
)}
|
|
<Typography
|
|
variant="body2"
|
|
component="span"
|
|
sx={{ fontWeight: 600, color: selected ? 'var(--fa-color-brand-700)' : 'text.primary' }}
|
|
>
|
|
{label}
|
|
</Typography>
|
|
</Box>
|
|
{description && (
|
|
<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 }}
|
|
>
|
|
{note}
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
/** Completed answer row — question + answer + change link */
|
|
function CompletedRow({
|
|
question,
|
|
answer,
|
|
onChangeClick,
|
|
}: {
|
|
question: string;
|
|
answer: string;
|
|
onChangeClick: () => void;
|
|
}) {
|
|
return (
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'baseline',
|
|
flexWrap: 'wrap',
|
|
gap: 0.75,
|
|
py: 1.5,
|
|
borderBottom: '1px solid',
|
|
borderColor: 'var(--fa-color-neutral-100)',
|
|
}}
|
|
>
|
|
<Typography variant="body2" color="text.secondary" component="span">
|
|
{question}
|
|
</Typography>
|
|
<Typography variant="body2" component="span" sx={{ fontWeight: 600 }}>
|
|
{answer}
|
|
</Typography>
|
|
<Link
|
|
component="button"
|
|
variant="caption"
|
|
onClick={onChangeClick}
|
|
underline="hover"
|
|
aria-label={`Change ${question.toLowerCase()}`}
|
|
sx={{
|
|
color: 'text.secondary',
|
|
ml: 'auto',
|
|
minHeight: 44,
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
}}
|
|
>
|
|
Change
|
|
</Link>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
// ─── Service preference options ──────────────────────────────────────────────
|
|
|
|
const SERVICE_OPTIONS: { value: ServicePref; label: string }[] = [
|
|
{ value: 'with-service', label: 'With a service' },
|
|
{ value: 'without-service', label: 'No service' },
|
|
{ value: 'either', label: "I'm flexible" },
|
|
];
|
|
|
|
// ─── Component ───────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Hero search widget for the FA homepage.
|
|
*
|
|
* Guides users through a conversational stepped flow to find funeral providers.
|
|
* Every question is its own step that collapses to a compact summary row once
|
|
* answered. The location input and CTA are always visible at the bottom —
|
|
* minimum search requirements are intent + location; all other fields default
|
|
* to "show all" if not explicitly answered.
|
|
*
|
|
* Flow:
|
|
* 1. "How can we help?" → Arrange now / Pre-plan
|
|
* 2. "Who is this for?" (pre-plan only) → Myself / Someone else
|
|
* 3. "What type of funeral?" → type cards + "Explore all" + optional theme chips
|
|
* 4. "Would you like a service?" (conditional) → chips (auto-advance)
|
|
* 5. Location + CTA (always visible)
|
|
*/
|
|
export const FuneralFinder = React.forwardRef<HTMLDivElement, FuneralFinderProps>(
|
|
(
|
|
{
|
|
funeralTypes,
|
|
themeOptions = [],
|
|
onSearch,
|
|
loading = false,
|
|
heading = 'Find funeral directors near you',
|
|
subheading = "Tell us a little about what you're looking for and we'll show you options in your area.",
|
|
showExploreAll = true,
|
|
sx,
|
|
},
|
|
ref,
|
|
) => {
|
|
const [intent, setIntent] = React.useState<Intent>(null);
|
|
const [planningFor, setPlanningFor] = React.useState<PlanningFor>(null);
|
|
const [typeSelection, setTypeSelection] = React.useState<string | null>(null);
|
|
const [servicePref, setServicePref] = React.useState<ServicePref>('either');
|
|
const [serviceAnswered, setServiceAnswered] = React.useState(false);
|
|
const [selectedThemes, setSelectedThemes] = React.useState<string[]>([]);
|
|
const [location, setLocation] = React.useState('');
|
|
const [locationError, setLocationError] = React.useState('');
|
|
const [showIntentPrompt, setShowIntentPrompt] = React.useState(false);
|
|
const [editingStep, setEditingStep] = React.useState<number | null>(null);
|
|
|
|
const needsPlanningFor = intent === 'preplan';
|
|
const isExploreAll = typeSelection === 'all';
|
|
const selectedType = funeralTypes.find((ft) => ft.id === typeSelection);
|
|
const showServiceStep = !isExploreAll && selectedType?.hasServiceOption === true;
|
|
const typeSelected = typeSelection !== null;
|
|
|
|
// Which step is currently active? (0 = all complete)
|
|
const activeStep = (() => {
|
|
if (editingStep !== null) return editingStep;
|
|
if (!intent) return 1;
|
|
if (needsPlanningFor && !planningFor) return 2;
|
|
if (!typeSelection) return 3;
|
|
if (showServiceStep && !serviceAnswered) return 4;
|
|
return 0;
|
|
})();
|
|
|
|
// ─── Labels ─────────────────────────────────────────────────────
|
|
const intentLabel = intent === 'arrange' ? 'Arrange a funeral now' : 'Pre-plan a funeral';
|
|
const planningForLabel = planningFor === 'myself' ? 'Myself' : 'Someone else';
|
|
|
|
const typeLabel = isExploreAll ? 'All options' : (selectedType?.label ?? '');
|
|
const themeSuffix = selectedThemes
|
|
.map((id) => themeOptions.find((t) => t.id === id)?.label?.toLowerCase())
|
|
.filter(Boolean)
|
|
.join(', ');
|
|
const typeSummary = [typeLabel, themeSuffix].filter(Boolean).join(', ');
|
|
|
|
const serviceLabel =
|
|
servicePref === 'with-service'
|
|
? 'With a service'
|
|
: servicePref === 'without-service'
|
|
? 'No service'
|
|
: 'Flexible';
|
|
|
|
// ─── Handlers ───────────────────────────────────────────────────
|
|
const selectIntent = (value: Intent) => {
|
|
setIntent(value);
|
|
if (value === 'arrange') setPlanningFor(null);
|
|
setShowIntentPrompt(false);
|
|
setEditingStep(null);
|
|
};
|
|
|
|
const selectPlanningFor = (value: PlanningFor) => {
|
|
setPlanningFor(value);
|
|
setEditingStep(null);
|
|
};
|
|
|
|
const selectType = (id: string) => {
|
|
setTypeSelection(id);
|
|
if (id === 'all' || !funeralTypes.find((ft) => ft.id === id)?.hasServiceOption) {
|
|
setServicePref('either');
|
|
setServiceAnswered(false);
|
|
}
|
|
setEditingStep(null);
|
|
};
|
|
|
|
const selectService = (value: ServicePref) => {
|
|
setServicePref(value);
|
|
setServiceAnswered(true);
|
|
setEditingStep(null);
|
|
};
|
|
|
|
const toggleTheme = (id: string) => {
|
|
setSelectedThemes((prev) =>
|
|
prev.includes(id) ? prev.filter((t) => t !== id) : [...prev, id],
|
|
);
|
|
};
|
|
|
|
const revertTo = (step: number) => {
|
|
setEditingStep(step);
|
|
};
|
|
|
|
const handleSubmit = () => {
|
|
// Minimum: intent + location
|
|
if (!intent) {
|
|
setEditingStep(null);
|
|
setShowIntentPrompt(true);
|
|
return;
|
|
}
|
|
if (location.trim().length < 3) {
|
|
setLocationError('Please enter a suburb or postcode');
|
|
return;
|
|
}
|
|
setLocationError('');
|
|
setShowIntentPrompt(false);
|
|
|
|
// Smart defaults — missing optional fields default to "all"
|
|
onSearch?.({
|
|
intent,
|
|
planningFor: needsPlanningFor ? (planningFor ?? undefined) : undefined,
|
|
funeralTypeId: isExploreAll ? null : (typeSelection ?? null),
|
|
servicePreference: showServiceStep && serviceAnswered ? servicePref : 'either',
|
|
themes: selectedThemes,
|
|
location: location.trim(),
|
|
});
|
|
};
|
|
|
|
// ─── Render ─────────────────────────────────────────────────────
|
|
return (
|
|
<Box
|
|
ref={ref}
|
|
sx={[
|
|
{
|
|
bgcolor: 'background.paper',
|
|
borderRadius: 'var(--fa-card-border-radius-default)',
|
|
boxShadow: 'var(--fa-shadow-md)',
|
|
p: { xs: 3, sm: 4 },
|
|
},
|
|
...(Array.isArray(sx) ? sx : [sx]),
|
|
]}
|
|
>
|
|
{/* Header */}
|
|
<Typography
|
|
variant="h2"
|
|
component="h2"
|
|
sx={{ fontFamily: 'var(--fa-font-family-display)', textAlign: 'center', mb: 1.5 }}
|
|
>
|
|
{heading}
|
|
</Typography>
|
|
<Typography
|
|
variant="body2"
|
|
color="text.secondary"
|
|
sx={{ textAlign: 'center', maxWidth: 400, mx: 'auto' }}
|
|
>
|
|
{subheading}
|
|
</Typography>
|
|
<Divider sx={{ mt: 3, mb: 0 }} />
|
|
|
|
{/* ── Completed rows ─────────────────────────────────────── */}
|
|
<Collapse in={intent !== null && activeStep !== 1} timeout={250}>
|
|
<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)}
|
|
/>
|
|
</Collapse>
|
|
<Collapse in={typeSelected && activeStep !== 3} timeout={250}>
|
|
<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)}
|
|
/>
|
|
</Collapse>
|
|
|
|
{/* ── Step 1: Intent ─────────────────────────────────────── */}
|
|
<Collapse in={activeStep === 1} timeout={250}>
|
|
<Box sx={{ mt: 3 }}>
|
|
{showIntentPrompt && (
|
|
<Typography
|
|
variant="caption"
|
|
role="alert"
|
|
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 }}
|
|
>
|
|
<ChoiceCard
|
|
label="Arrange a funeral now"
|
|
description="Someone has passed and I need to make arrangements"
|
|
selected={intent === 'arrange'}
|
|
onClick={() => selectIntent('arrange')}
|
|
/>
|
|
<ChoiceCard
|
|
label="Pre-plan a funeral"
|
|
description="I'd like to plan ahead for the future"
|
|
selected={intent === 'preplan'}
|
|
onClick={() => selectIntent('preplan')}
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
</Collapse>
|
|
|
|
{/* ── Step 2: Planning for (conditional) ─────────────────── */}
|
|
<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 }}
|
|
>
|
|
<ChoiceCard
|
|
label="Myself"
|
|
description="I want to plan my own funeral in advance"
|
|
selected={planningFor === 'myself'}
|
|
onClick={() => selectPlanningFor('myself')}
|
|
/>
|
|
<ChoiceCard
|
|
label="Someone else"
|
|
description="I'm helping a family member or friend plan ahead"
|
|
selected={planningFor === 'someone-else'}
|
|
onClick={() => selectPlanningFor('someone-else')}
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
</Collapse>
|
|
|
|
{/* ── Step 3: Type + Preferences ─────────────────────────── */}
|
|
<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 }}
|
|
>
|
|
{funeralTypes.map((ft) => (
|
|
<TypeCard
|
|
key={ft.id}
|
|
label={ft.label}
|
|
description={ft.description}
|
|
note={ft.note}
|
|
selected={typeSelection === ft.id}
|
|
onClick={() => selectType(ft.id)}
|
|
/>
|
|
))}
|
|
{showExploreAll && (
|
|
<TypeCard
|
|
label="Explore all options"
|
|
description="Browse everything available in your area"
|
|
selected={isExploreAll}
|
|
onClick={() => selectType('all')}
|
|
/>
|
|
)}
|
|
</Box>
|
|
|
|
{/* Theme preferences — optional, inside type step */}
|
|
{themeOptions.length > 0 && (
|
|
<Box sx={{ mt: 3 }}>
|
|
<Box sx={{ mb: 1.5 }}>
|
|
<Typography variant="body2" component="span" sx={{ fontWeight: 600 }}>
|
|
Any preferences?
|
|
</Typography>
|
|
<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' }}
|
|
>
|
|
{themeOptions.map((theme) => {
|
|
const isSelected = selectedThemes.includes(theme.id);
|
|
return (
|
|
<Chip
|
|
key={theme.id}
|
|
label={theme.label}
|
|
variant="outlined"
|
|
selected={isSelected}
|
|
onClick={() => toggleTheme(theme.id)}
|
|
clickable
|
|
aria-pressed={isSelected}
|
|
sx={{ height: 44 }}
|
|
/>
|
|
);
|
|
})}
|
|
</Box>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
</Collapse>
|
|
|
|
{/* ── Step 4: Service (conditional, auto-advance) ────────── */}
|
|
<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 }}
|
|
>
|
|
{SERVICE_OPTIONS.map((opt) => (
|
|
<Chip
|
|
key={opt.value}
|
|
label={opt.label}
|
|
variant="outlined"
|
|
selected={serviceAnswered && servicePref === opt.value}
|
|
onClick={() => selectService(opt.value)}
|
|
clickable
|
|
aria-pressed={serviceAnswered && servicePref === opt.value}
|
|
sx={{
|
|
justifyContent: 'flex-start',
|
|
height: 44,
|
|
borderRadius: 'var(--fa-border-radius-md)',
|
|
}}
|
|
/>
|
|
))}
|
|
</Box>
|
|
</Box>
|
|
</Collapse>
|
|
|
|
{/* ── Always visible: Location + CTA ─────────────────────── */}
|
|
<Box sx={{ mt: 3 }}>
|
|
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1.5 }}>
|
|
Where are you looking?
|
|
</Typography>
|
|
<Input
|
|
placeholder="Suburb or postcode"
|
|
value={location}
|
|
onChange={(e) => {
|
|
setLocation(e.target.value);
|
|
if (locationError) setLocationError('');
|
|
}}
|
|
size="small"
|
|
fullWidth
|
|
error={!!locationError}
|
|
helperText={locationError || 'Enter a suburb name or 4-digit postcode'}
|
|
inputProps={{ 'aria-label': 'Where are you looking — suburb or postcode' }}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') handleSubmit();
|
|
}}
|
|
/>
|
|
|
|
<Box sx={{ mt: 3 }}>
|
|
<Button
|
|
variant="contained"
|
|
size="large"
|
|
fullWidth
|
|
loading={loading}
|
|
disabled={loading}
|
|
onClick={handleSubmit}
|
|
>
|
|
Find funeral providers
|
|
</Button>
|
|
<Typography
|
|
variant="captionSm"
|
|
color="text.secondary"
|
|
sx={{ textAlign: 'center', display: 'block', mt: 1.5 }}
|
|
>
|
|
Free to use · No obligation
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
},
|
|
);
|
|
|
|
FuneralFinder.displayName = 'FuneralFinder';
|
|
export default FuneralFinder;
|