Add FuneralFinder v2 — quick-form stepped search widget
- 4-step vertical form: intent, planning-for, type, location - Sequential unlock: each step enables when the previous is filled - Step circles (48px, brand-200) transition to brand-500 check on completion - Connector lines between circles for visual progression - Conditional logic: arrange-now auto-sets step 2 to "Someone else" - CTA disabled until location filled, trust signal below - Display serif heading + subheading with divider - Critique 33/40 (Good), Audit 18/20 (Excellent) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,69 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import { FuneralFinderV2 } from './FuneralFinderV2';
|
||||||
|
|
||||||
|
const meta: Meta<typeof FuneralFinderV2> = {
|
||||||
|
title: 'Organisms/FuneralFinderV2',
|
||||||
|
component: FuneralFinderV2,
|
||||||
|
parameters: {
|
||||||
|
layout: 'padded',
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
onSearch: (params) => {
|
||||||
|
console.log('Search params:', params);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof FuneralFinderV2>;
|
||||||
|
|
||||||
|
/** Default empty state — all 3 steps ready for input */
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
/** Loading state — CTA shows spinner */
|
||||||
|
export const Loading: Story = {
|
||||||
|
args: { loading: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Placed below a masthead-style header to preview in context */
|
||||||
|
export const BelowMasthead: Story = {
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<Box>
|
||||||
|
{/* Simulated masthead */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'var(--fa-color-sage-800, #4c5459)',
|
||||||
|
color: '#fff',
|
||||||
|
py: 6,
|
||||||
|
px: 4,
|
||||||
|
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>
|
||||||
|
{/* Widget below masthead */}
|
||||||
|
<Box sx={{ maxWidth: 560, mx: 'auto', mt: -4, px: 2, position: 'relative', zIndex: 1 }}>
|
||||||
|
<Story />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Constrained width — typical sidebar or narrow column */
|
||||||
|
export const Narrow: Story = {
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<Box sx={{ maxWidth: 400, mx: 'auto' }}>
|
||||||
|
<Story />
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
490
src/components/organisms/FuneralFinder/FuneralFinderV2.tsx
Normal file
490
src/components/organisms/FuneralFinder/FuneralFinderV2.tsx
Normal file
@@ -0,0 +1,490 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Select, { type SelectChangeEvent } from '@mui/material/Select';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import CheckIcon from '@mui/icons-material/Check';
|
||||||
|
import type { SxProps, Theme } from '@mui/material/styles';
|
||||||
|
import { Typography } from '../../atoms/Typography';
|
||||||
|
import { Button } from '../../atoms/Button';
|
||||||
|
import { Input } from '../../atoms/Input';
|
||||||
|
import { Divider } from '../../atoms/Divider';
|
||||||
|
|
||||||
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type LookingTo = 'arrange-now' | 'expected' | 'future';
|
||||||
|
type PlanningFor = 'myself' | 'someone-else';
|
||||||
|
type FuneralType =
|
||||||
|
| 'cremation-funeral'
|
||||||
|
| 'cremation-only'
|
||||||
|
| 'burial-funeral'
|
||||||
|
| 'graveside-only'
|
||||||
|
| 'water-cremation';
|
||||||
|
|
||||||
|
/** Search parameters returned on form submission */
|
||||||
|
export interface FuneralFinderV2SearchParams {
|
||||||
|
/** User's situation — immediate, expected, or future need */
|
||||||
|
lookingTo: LookingTo;
|
||||||
|
/** Who the funeral is for */
|
||||||
|
planningFor: PlanningFor;
|
||||||
|
/** Type of funeral selected */
|
||||||
|
funeralType: FuneralType;
|
||||||
|
/** Suburb or postcode */
|
||||||
|
location: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Props for the FuneralFinder v2 quick-form organism */
|
||||||
|
export interface FuneralFinderV2Props {
|
||||||
|
/** Called when the user submits with valid data */
|
||||||
|
onSearch?: (params: FuneralFinderV2SearchParams) => void;
|
||||||
|
/** Shows loading state on the CTA */
|
||||||
|
loading?: boolean;
|
||||||
|
/** Optional heading override */
|
||||||
|
heading?: string;
|
||||||
|
/** Optional subheading override */
|
||||||
|
subheading?: string;
|
||||||
|
/** MUI sx override for the root container */
|
||||||
|
sx?: SxProps<Theme>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Options ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const LOOKING_TO_OPTIONS: { value: LookingTo; label: string }[] = [
|
||||||
|
{ value: 'arrange-now', label: 'Arrange a funeral for someone who has passed' },
|
||||||
|
{ value: 'expected', label: 'Plan ahead for someone who is unwell' },
|
||||||
|
{ value: 'future', label: "Plan for a future need that isn't expected soon" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const PLANNING_FOR_OPTIONS: { value: PlanningFor; label: string }[] = [
|
||||||
|
{ value: 'someone-else', label: 'Someone else' },
|
||||||
|
{ value: 'myself', label: 'Myself' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const FUNERAL_TYPE_OPTIONS: { value: FuneralType; label: string }[] = [
|
||||||
|
{ value: 'cremation-funeral', label: 'Cremation with funeral' },
|
||||||
|
{ value: 'cremation-only', label: 'Cremation only (no funeral, no attendance)' },
|
||||||
|
{ value: 'burial-funeral', label: 'Burial with funeral' },
|
||||||
|
{ value: 'graveside-only', label: 'Graveside burial only' },
|
||||||
|
{ value: 'water-cremation', label: 'Water cremation (QLD only)' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ─── Layout constants ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const STEP_CIRCLE_SIZE = 48;
|
||||||
|
|
||||||
|
// ─── Sub-components ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Step number circle — transitions to a check icon when the step is completed */
|
||||||
|
function StepCircle({
|
||||||
|
step,
|
||||||
|
completed,
|
||||||
|
showConnector = false,
|
||||||
|
}: {
|
||||||
|
step: number;
|
||||||
|
completed: boolean;
|
||||||
|
/** Show a vertical connector line below this circle to the next step */
|
||||||
|
showConnector?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: STEP_CIRCLE_SIZE,
|
||||||
|
height: STEP_CIRCLE_SIZE,
|
||||||
|
borderRadius: '50%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexShrink: 0,
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 1,
|
||||||
|
transition: 'background-color 200ms ease, color 200ms ease',
|
||||||
|
...(completed
|
||||||
|
? { bgcolor: 'var(--fa-color-brand-500)', color: 'common.white' }
|
||||||
|
: { 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': {
|
||||||
|
content: '""',
|
||||||
|
position: 'absolute',
|
||||||
|
top: '100%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
width: 3,
|
||||||
|
height: 60,
|
||||||
|
backgroundColor: 'var(--fa-color-neutral-200)',
|
||||||
|
zIndex: -1,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{completed ? (
|
||||||
|
<CheckIcon sx={{ fontSize: 24 }} />
|
||||||
|
) : (
|
||||||
|
<Typography
|
||||||
|
variant="captionSm"
|
||||||
|
component="span"
|
||||||
|
sx={{ fontWeight: 700, fontSize: '1.375rem', lineHeight: 1, color: 'inherit' }}
|
||||||
|
>
|
||||||
|
{step}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Shared styles ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const selectSx: SxProps<Theme> = {
|
||||||
|
width: '100%',
|
||||||
|
bgcolor: 'var(--fa-color-surface-default, #fff)',
|
||||||
|
'.MuiOutlinedInput-notchedOutline': {
|
||||||
|
borderColor: 'var(--fa-color-neutral-200)',
|
||||||
|
borderRadius: 'var(--fa-border-radius-md, 8px)',
|
||||||
|
},
|
||||||
|
'&:hover:not(.Mui-disabled) .MuiOutlinedInput-notchedOutline': {
|
||||||
|
borderColor: 'var(--fa-color-brand-400)',
|
||||||
|
},
|
||||||
|
'&.Mui-focused': {
|
||||||
|
boxShadow: 'none',
|
||||||
|
},
|
||||||
|
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||||
|
borderColor: 'var(--fa-color-brand-400)',
|
||||||
|
borderWidth: 1,
|
||||||
|
},
|
||||||
|
'&.Mui-disabled': {
|
||||||
|
opacity: 0.6,
|
||||||
|
'.MuiOutlinedInput-notchedOutline': {
|
||||||
|
borderStyle: 'dashed',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'&.Mui-error .MuiOutlinedInput-notchedOutline': {
|
||||||
|
borderColor: 'var(--fa-color-feedback-error-500, #d32f2f)',
|
||||||
|
},
|
||||||
|
'.MuiSelect-select': {
|
||||||
|
py: '14px',
|
||||||
|
px: 2,
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
minHeight: 'unset !important',
|
||||||
|
},
|
||||||
|
'.MuiSelect-icon': {
|
||||||
|
color: 'var(--fa-color-neutral-400)',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectMenuProps = {
|
||||||
|
PaperProps: {
|
||||||
|
sx: {
|
||||||
|
mt: 0.5,
|
||||||
|
borderRadius: 'var(--fa-border-radius-md, 8px)',
|
||||||
|
boxShadow: 'var(--fa-shadow-md)',
|
||||||
|
minWidth: 280,
|
||||||
|
'& .MuiMenuItem-root': {
|
||||||
|
py: 1.5,
|
||||||
|
px: 2,
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
whiteSpace: 'normal',
|
||||||
|
'&:hover': { bgcolor: 'var(--fa-color-brand-50)' },
|
||||||
|
'&.Mui-selected': {
|
||||||
|
bgcolor: 'var(--fa-color-surface-warm)',
|
||||||
|
fontWeight: 600,
|
||||||
|
'&:hover': { bgcolor: 'var(--fa-color-surface-warm)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Component ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quick-form funeral search widget for the homepage hero.
|
||||||
|
*
|
||||||
|
* Three dropdown selects (intent, planning-for, type), a location input,
|
||||||
|
* and a CTA — all always visible at a fixed height. Step numbers transition
|
||||||
|
* to check icons as the user completes each field.
|
||||||
|
*
|
||||||
|
* Conditional logic:
|
||||||
|
* - "Arrange a funeral for someone who has passed" auto-sets step 2
|
||||||
|
* to "Someone else" and disables it.
|
||||||
|
* - "Myself" is only available for pre-planning paths (expected / future).
|
||||||
|
*/
|
||||||
|
export const FuneralFinderV2 = React.forwardRef<HTMLDivElement, FuneralFinderV2Props>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
onSearch,
|
||||||
|
loading = false,
|
||||||
|
heading = 'Find funeral directors near you',
|
||||||
|
subheading = "Tell us what you need and we'll show options in your area.",
|
||||||
|
sx,
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
// ─── State ───────────────────────────────────────────────────
|
||||||
|
const [lookingTo, setLookingTo] = React.useState<LookingTo | ''>('');
|
||||||
|
const [planningFor, setPlanningFor] = React.useState<PlanningFor | ''>('');
|
||||||
|
const [funeralType, setFuneralType] = React.useState<FuneralType | ''>('');
|
||||||
|
const [location, setLocation] = React.useState('');
|
||||||
|
const [submitted, setSubmitted] = React.useState(false);
|
||||||
|
|
||||||
|
// ─── Derived ─────────────────────────────────────────────────
|
||||||
|
const isArrangeNow = lookingTo === 'arrange-now';
|
||||||
|
const step2Disabled = !lookingTo || isArrangeNow;
|
||||||
|
const step3Disabled = !planningFor;
|
||||||
|
const step4Disabled = !funeralType;
|
||||||
|
|
||||||
|
// Errors only show after first submit attempt, then clear as fields fill
|
||||||
|
const errs = submitted
|
||||||
|
? {
|
||||||
|
lookingTo: !lookingTo,
|
||||||
|
planningFor: !planningFor,
|
||||||
|
funeralType: !funeralType,
|
||||||
|
location: location.trim().length < 3,
|
||||||
|
}
|
||||||
|
: { lookingTo: false, planningFor: false, funeralType: false, location: false };
|
||||||
|
|
||||||
|
const hasErrors = submitted && (errs.lookingTo || errs.planningFor || errs.funeralType || errs.location);
|
||||||
|
|
||||||
|
// ─── Handlers ────────────────────────────────────────────────
|
||||||
|
const handleLookingTo = (e: SelectChangeEvent<string>) => {
|
||||||
|
const val = e.target.value as LookingTo;
|
||||||
|
setLookingTo(val);
|
||||||
|
if (val === 'arrange-now') {
|
||||||
|
setPlanningFor('someone-else');
|
||||||
|
} else {
|
||||||
|
setPlanningFor('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePlanningFor = (e: SelectChangeEvent<string>) => {
|
||||||
|
setPlanningFor(e.target.value as PlanningFor);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFuneralType = (e: SelectChangeEvent<string>) => {
|
||||||
|
setFuneralType(e.target.value as FuneralType);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
setSubmitted(true);
|
||||||
|
if (!lookingTo || !planningFor || !funeralType || location.trim().length < 3) return;
|
||||||
|
onSearch?.({
|
||||||
|
lookingTo,
|
||||||
|
planningFor,
|
||||||
|
funeralType,
|
||||||
|
location: location.trim(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Helpers ─────────────────────────────────────────────────
|
||||||
|
const placeholder = (
|
||||||
|
<span style={{ color: 'var(--fa-color-neutral-400)' }}>Select…</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
const findLabel = (opts: { value: string; label: string }[], val: string) =>
|
||||||
|
opts.find((o) => o.value === val)?.label ?? '';
|
||||||
|
|
||||||
|
// ─── Render ──────────────────────────────────────────────────
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
ref={ref}
|
||||||
|
role="search"
|
||||||
|
aria-label="Find funeral providers"
|
||||||
|
sx={[
|
||||||
|
{
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
borderRadius: 'var(--fa-card-border-radius-default, 12px)',
|
||||||
|
boxShadow: 'var(--fa-shadow-md)',
|
||||||
|
px: { xs: 3, sm: 4 },
|
||||||
|
pb: { xs: 3, sm: 4 },
|
||||||
|
pt: { xs: 4, sm: 5 },
|
||||||
|
},
|
||||||
|
...(Array.isArray(sx) ? sx : [sx]),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{/* ── Header ──────────────────────────────────────────── */}
|
||||||
|
<Typography
|
||||||
|
variant="h3"
|
||||||
|
component="h2"
|
||||||
|
sx={{ fontFamily: 'var(--fa-font-family-display)', textAlign: 'center', mb: 1 }}
|
||||||
|
>
|
||||||
|
{heading}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ textAlign: 'center', mb: 0 }}
|
||||||
|
>
|
||||||
|
{subheading}
|
||||||
|
</Typography>
|
||||||
|
<Divider sx={{ my: 3.5 }} />
|
||||||
|
|
||||||
|
{/* ── Steps ───────────────────────────────────────────── */}
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||||
|
{/* Step 1: I'm looking to */}
|
||||||
|
<Box sx={{ display: 'flex', gap: 2.5, alignItems: 'flex-end' }}>
|
||||||
|
<StepCircle step={1} completed={!!lookingTo} showConnector />
|
||||||
|
<Box sx={{ flex: 1 }}>
|
||||||
|
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1, color: 'var(--fa-color-brand-700)' }}>
|
||||||
|
I’m looking to…
|
||||||
|
</Typography>
|
||||||
|
<Select
|
||||||
|
value={lookingTo}
|
||||||
|
onChange={handleLookingTo}
|
||||||
|
displayEmpty
|
||||||
|
error={errs.lookingTo}
|
||||||
|
renderValue={(v) => (v ? findLabel(LOOKING_TO_OPTIONS, v) : placeholder)}
|
||||||
|
MenuProps={selectMenuProps}
|
||||||
|
sx={selectSx}
|
||||||
|
inputProps={{ 'aria-label': "I'm looking to", 'aria-required': true }}
|
||||||
|
>
|
||||||
|
{LOOKING_TO_OPTIONS.map((o) => (
|
||||||
|
<MenuItem key={o.value} value={o.value}>
|
||||||
|
{o.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Step 2: I'm planning for */}
|
||||||
|
<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' }}>
|
||||||
|
I’m planning for
|
||||||
|
</Typography>
|
||||||
|
<Select
|
||||||
|
value={planningFor}
|
||||||
|
onChange={handlePlanningFor}
|
||||||
|
displayEmpty
|
||||||
|
disabled={step2Disabled}
|
||||||
|
error={errs.planningFor}
|
||||||
|
renderValue={(v) => (v ? findLabel(PLANNING_FOR_OPTIONS, v) : placeholder)}
|
||||||
|
MenuProps={selectMenuProps}
|
||||||
|
sx={selectSx}
|
||||||
|
inputProps={{ 'aria-label': "I'm planning for", 'aria-required': true }}
|
||||||
|
>
|
||||||
|
{PLANNING_FOR_OPTIONS.map((o) => (
|
||||||
|
<MenuItem
|
||||||
|
key={o.value}
|
||||||
|
value={o.value}
|
||||||
|
disabled={isArrangeNow && o.value === 'myself'}
|
||||||
|
>
|
||||||
|
{o.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Step 3: Type of funeral */}
|
||||||
|
<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)' }}>
|
||||||
|
Type of funeral
|
||||||
|
</Typography>
|
||||||
|
<Select
|
||||||
|
value={funeralType}
|
||||||
|
onChange={handleFuneralType}
|
||||||
|
displayEmpty
|
||||||
|
disabled={step3Disabled}
|
||||||
|
error={errs.funeralType}
|
||||||
|
renderValue={(v) => (v ? findLabel(FUNERAL_TYPE_OPTIONS, v) : placeholder)}
|
||||||
|
MenuProps={selectMenuProps}
|
||||||
|
sx={selectSx}
|
||||||
|
inputProps={{ 'aria-label': 'Type of funeral', 'aria-required': true }}
|
||||||
|
>
|
||||||
|
{FUNERAL_TYPE_OPTIONS.map((o) => (
|
||||||
|
<MenuItem key={o.value} value={o.value}>
|
||||||
|
{o.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Step 4: Location */}
|
||||||
|
<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)' }}>
|
||||||
|
Looking for providers in
|
||||||
|
</Typography>
|
||||||
|
<Input
|
||||||
|
placeholder="Suburb or postcode"
|
||||||
|
value={location}
|
||||||
|
onChange={(e) => setLocation(e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
disabled={step4Disabled}
|
||||||
|
error={errs.location}
|
||||||
|
inputProps={{ 'aria-label': 'Suburb or postcode', 'aria-required': true }}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') handleSubmit();
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
'&.Mui-focused': { boxShadow: 'none' },
|
||||||
|
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||||
|
borderColor: 'var(--fa-color-brand-400)',
|
||||||
|
borderWidth: '1px',
|
||||||
|
},
|
||||||
|
'& .MuiOutlinedInput-input': {
|
||||||
|
py: '14px',
|
||||||
|
px: 2,
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* ── CTA ─────────────────────────────────────────────── */}
|
||||||
|
<Divider sx={{ my: 4.5 }} />
|
||||||
|
<Box>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="large"
|
||||||
|
fullWidth
|
||||||
|
loading={loading}
|
||||||
|
disabled={loading || location.trim().length < 3}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
>
|
||||||
|
Find Providers
|
||||||
|
</Button>
|
||||||
|
<Typography
|
||||||
|
variant="captionSm"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ textAlign: 'center', display: 'block', mt: 1.5 }}
|
||||||
|
>
|
||||||
|
Free to use · No obligation
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Error hint — always rendered to maintain fixed height */}
|
||||||
|
<Typography
|
||||||
|
variant="captionSm"
|
||||||
|
role={hasErrors ? 'alert' : undefined}
|
||||||
|
sx={{
|
||||||
|
mt: 1,
|
||||||
|
minHeight: '1.25rem',
|
||||||
|
textAlign: 'center',
|
||||||
|
color: hasErrors ? 'error.main' : 'transparent',
|
||||||
|
transition: 'color 200ms ease',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{errs.location
|
||||||
|
? 'Please enter a suburb or postcode'
|
||||||
|
: errs.lookingTo
|
||||||
|
? 'Please tell us what you need help with'
|
||||||
|
: errs.funeralType
|
||||||
|
? 'Please select a funeral type'
|
||||||
|
: errs.planningFor
|
||||||
|
? 'Please select who you are planning for'
|
||||||
|
: '\u00A0'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
FuneralFinderV2.displayName = 'FuneralFinderV2';
|
||||||
|
export default FuneralFinderV2;
|
||||||
@@ -4,3 +4,9 @@ export {
|
|||||||
type FuneralTypeOption,
|
type FuneralTypeOption,
|
||||||
type FuneralSearchParams,
|
type FuneralSearchParams,
|
||||||
} from './FuneralFinder';
|
} from './FuneralFinder';
|
||||||
|
|
||||||
|
export {
|
||||||
|
FuneralFinderV2,
|
||||||
|
type FuneralFinderV2Props,
|
||||||
|
type FuneralFinderV2SearchParams,
|
||||||
|
} from './FuneralFinderV2';
|
||||||
|
|||||||
Reference in New Issue
Block a user