diff --git a/src/components/organisms/FuneralFinder/FuneralFinder.stories.tsx b/src/components/organisms/FuneralFinder/FuneralFinder.stories.tsx
index bb40a39..7d9d4fa 100644
--- a/src/components/organisms/FuneralFinder/FuneralFinder.stories.tsx
+++ b/src/components/organisms/FuneralFinder/FuneralFinder.stories.tsx
@@ -4,26 +4,31 @@ import { FuneralFinder } from './FuneralFinder';
import { Navigation } from '../Navigation';
import { Typography } from '../../atoms/Typography';
+// ─── Shared data ────────────────────────────────────────────────────────────
+
const funeralTypes = [
- { id: 'cremation', label: 'Cremation' },
- { id: 'burial', label: 'Burial' },
- { id: 'memorial', label: 'Memorial' },
- { id: 'catholic', label: 'Catholic' },
- { id: 'direct-cremation', label: 'Direct Cremation' },
- { id: 'natural-burial', label: 'Natural Burial' },
+ { 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 },
+];
+
+const themeOptions = [
+ { id: 'eco-friendly', label: 'Eco-friendly' },
+ { id: 'budget-friendly', label: 'Budget-friendly' },
+ { id: 'religious', label: 'Religious specialisation' },
];
const FALogoNav = () => (
);
+// ─── Meta ───────────────────────────────────────────────────────────────────
+
const meta: Meta = {
title: 'Organisms/FuneralFinder',
component: FuneralFinder,
tags: ['autodocs'],
- parameters: {
- layout: 'centered',
- },
+ parameters: { layout: 'centered' },
decorators: [
(Story) => (
@@ -36,41 +41,67 @@ const meta: Meta = {
export default meta;
type Story = StoryObj;
-// --- Default -----------------------------------------------------------------
+// ─── Default ────────────────────────────────────────────────────────────────
-/** Initial state — step 1 active, all others locked */
+/** Initial state — full feature set with types, themes, and explore-all. */
export const Default: Story = {
args: {
funeralTypes,
+ themeOptions,
onSearch: (params) => alert(JSON.stringify(params, null, 2)),
},
};
-// --- Fewer Funeral Types -----------------------------------------------------
+// ─── Without Themes ─────────────────────────────────────────────────────────
-/** With only 3 funeral types — shows compact chip row */
-export const FewerTypes: Story = {
+/** No theme options — skips the preferences section on the final step. */
+export const WithoutThemes: Story = {
args: {
- funeralTypes: funeralTypes.slice(0, 3),
+ funeralTypes,
onSearch: (params) => alert(JSON.stringify(params, null, 2)),
},
};
-// --- Custom Heading ----------------------------------------------------------
+// ─── Without Explore All ────────────────────────────────────────────────────
-/** With custom heading and subheading */
+/** Explore-all option hidden — users must pick a specific type. */
+export const WithoutExploreAll: Story = {
+ args: {
+ funeralTypes,
+ themeOptions,
+ showExploreAll: false,
+ onSearch: (params) => alert(JSON.stringify(params, null, 2)),
+ },
+};
+
+// ─── Loading State ──────────────────────────────────────────────────────────
+
+/** CTA in loading state — shows spinner, button disabled. */
+export const Loading: Story = {
+ args: {
+ funeralTypes,
+ themeOptions,
+ loading: true,
+ onSearch: (params) => alert(JSON.stringify(params, null, 2)),
+ },
+};
+
+// ─── Custom Heading ─────────────────────────────────────────────────────────
+
+/** Custom heading and subheading for alternate page contexts. */
export const CustomHeading: Story = {
args: {
funeralTypes,
+ themeOptions,
heading: 'Compare funeral directors in your area',
subheading: 'Transparent pricing · No hidden fees · 24/7',
onSearch: (params) => alert(JSON.stringify(params, null, 2)),
},
};
-// --- In Hero Context (Desktop) -----------------------------------------------
+// ─── In Hero Context (Desktop) ──────────────────────────────────────────────
-/** As it appears in the homepage hero — desktop layout */
+/** As it appears in the homepage hero — desktop layout. */
export const InHeroDesktop: Story = {
decorators: [
(Story) => (
@@ -90,8 +121,6 @@ export const InHeroDesktop: Story = {
{ label: 'Log in', href: '/login' },
]}
/>
-
- {/* Hero section */}
- {/* Left: heading + search widget */}
Discover, Explore, and Plan Funerals in Minutes
@@ -131,20 +155,19 @@ export const InHeroDesktop: Story = {
Whether you're thinking ahead or arranging for a loved one, find
trusted local providers with transparent pricing.
-
alert(JSON.stringify(params, null, 2))}
/>
-
- {/* Right: hero image placeholder */}
(
@@ -175,29 +198,14 @@ export const InHeroMobile: Story = {
{ label: 'Log in', href: '/login' },
]}
/>
-
- {/* Hero heading */}
-
-
+
+
Discover, Explore, and Plan Funerals in Minutes
Find trusted local providers with transparent pricing, at your own pace.
-
- {/* Hero image */}
-
- {/* Search widget — overlaps image slightly */}
alert(JSON.stringify(params, null, 2))}
/>
diff --git a/src/components/organisms/FuneralFinder/FuneralFinder.tsx b/src/components/organisms/FuneralFinder/FuneralFinder.tsx
index 05f3e00..80e2fb6 100644
--- a/src/components/organisms/FuneralFinder/FuneralFinder.tsx
+++ b/src/components/organisms/FuneralFinder/FuneralFinder.tsx
@@ -1,10 +1,13 @@
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 ───────────────────────────────────────────────────────────────────
@@ -15,6 +18,20 @@ export interface FuneralTypeOption {
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 */
@@ -23,8 +40,12 @@ export interface FuneralSearchParams {
intent: 'arrange' | 'preplan';
/** Only present when intent is "preplan" */
planningFor?: 'myself' | 'someone-else';
- /** Selected funeral type ID */
- funeralTypeId: string;
+ /** 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;
}
@@ -33,12 +54,18 @@ export interface FuneralSearchParams {
export interface FuneralFinderProps {
/** Available funeral types — dynamic list from API */
funeralTypes: FuneralTypeOption[];
- /** Called when the user clicks "Find funeral directors" */
+ /** 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;
}
@@ -47,9 +74,21 @@ export interface FuneralFinderProps {
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 (
+
+
+ {children}
+
+
+ );
+}
+
/** Large tappable option for binary choices (intent, planning-for) */
function ChoiceCard({
label,
@@ -69,8 +108,7 @@ function ChoiceCard({
aria-checked={selected}
onClick={onClick}
sx={{
- flex: 1,
- minWidth: 0,
+ width: '100%',
px: 2.5,
py: 2,
border: '2px solid',
@@ -85,6 +123,7 @@ function ChoiceCard({
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,
@@ -107,16 +146,7 @@ function ChoiceCard({
{description && (
-
+
{description}
)}
@@ -124,13 +154,17 @@ function ChoiceCard({
);
}
-/** Funeral type option — generous pill button */
-function TypePill({
+/** Funeral type card — compact selectable card */
+function TypeCard({
label,
+ description,
+ note,
selected,
onClick,
}: {
label: string;
+ description?: string;
+ note?: string;
selected: boolean;
onClick: () => void;
}) {
@@ -141,30 +175,54 @@ function TypePill({
aria-checked={selected}
onClick={onClick}
sx={{
- px: 2.5,
- py: 1.25,
+ 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-full)',
+ borderRadius: 'var(--fa-border-radius-md)',
bgcolor: selected ? 'var(--fa-color-surface-warm)' : 'var(--fa-color-surface-default)',
cursor: 'pointer',
fontFamily: 'inherit',
- fontSize: '0.875rem',
- fontWeight: selected ? 600 : 500,
- color: selected ? 'var(--fa-color-brand-700)' : 'text.primary',
- whiteSpace: 'nowrap',
+ 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,
},
}}
>
- {label}
+
+ {selected && (
+
+ )}
+
+ {label}
+
+
+ {description && (
+
+ {description}
+
+ )}
+ {note && (
+
+ {note}
+
+ )}
);
}
@@ -186,7 +244,7 @@ function CompletedRow({
alignItems: 'baseline',
flexWrap: 'wrap',
gap: 0.75,
- py: 1,
+ py: 1.5,
borderBottom: '1px solid',
borderColor: 'var(--fa-color-neutral-100)',
}}
@@ -201,7 +259,9 @@ function CompletedRow({
component="button"
variant="caption"
onClick={onChangeClick}
- sx={{ color: 'text.secondary', ml: 'auto' }}
+ underline="hover"
+ aria-label={`Change ${question.toLowerCase()}`}
+ sx={{ color: 'text.secondary', ml: 'auto', minHeight: 44, display: 'inline-flex', alignItems: 'center' }}
>
Change
@@ -209,63 +269,94 @@ function CompletedRow({
);
}
+// ─── 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 directors.
- * Each question appears naturally after the previous is answered. Completed
- * answers collapse to a compact row with a "Change" link to revert.
+ * 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?" → dynamic pill buttons
- * 4. "Where are you located?" → suburb/postcode input
- * 5. CTA: "Find funeral directors"
- *
- * Composes Typography + Button + Input + Link + custom ChoiceCard/TypePill.
+ * 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(
(
{
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(null);
const [planningFor, setPlanningFor] = React.useState(null);
- const [funeralTypeId, setFuneralTypeId] = React.useState(null);
+ const [typeSelection, setTypeSelection] = React.useState(null);
+ const [servicePref, setServicePref] = React.useState('either');
+ const [serviceAnswered, setServiceAnswered] = React.useState(false);
+ const [selectedThemes, setSelectedThemes] = React.useState([]);
const [location, setLocation] = React.useState('');
+ const [locationError, setLocationError] = React.useState('');
+ const [showIntentPrompt, setShowIntentPrompt] = React.useState(false);
const [editingStep, setEditingStep] = React.useState(null);
const needsPlanningFor = intent === 'preplan';
- const funeralTypeLabel = funeralTypes.find((ft) => ft.id === funeralTypeId)?.label;
+ 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?
+ // Which step is currently active? (0 = all complete)
const activeStep = (() => {
- if (editingStep) return editingStep;
+ if (editingStep !== null) return editingStep;
if (!intent) return 1;
if (needsPlanningFor && !planningFor) return 2;
- if (!funeralTypeId) return 3;
- return 4;
+ if (!typeSelection) return 3;
+ if (showServiceStep && !serviceAnswered) return 4;
+ return 0;
})();
- const canSubmit =
- intent !== null &&
- (!needsPlanningFor || planningFor !== null) &&
- funeralTypeId !== null &&
- location.trim().length > 0;
+ // ─── Labels ─────────────────────────────────────────────────────
+ const intentLabel = intent === 'arrange' ? 'Arrange a funeral now' : 'Pre-plan a funeral';
+ const planningForLabel = planningFor === 'myself' ? 'Myself' : 'Someone else';
- // ─── Handlers ────────────────────────────────────────────────────
+ 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);
};
@@ -274,26 +365,57 @@ export const FuneralFinder = React.forwardRef {
- setFuneralTypeId(id);
+ 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 = () => {
- if (!canSubmit || !intent || !funeralTypeId) return;
+ // 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,
+ funeralTypeId: isExploreAll ? null : (typeSelection ?? null),
+ servicePreference: (showServiceStep && serviceAnswered) ? servicePref : 'either',
+ themes: selectedThemes,
location: location.trim(),
});
};
- // ─── Render ──────────────────────────────────────────────────────
+ // ─── Render ─────────────────────────────────────────────────────
return (
{/* Header */}
{heading}
{subheading}
+
- {/* Completed answers */}
- {intent && activeStep > 1 && editingStep !== 1 && (
- revertTo(1)}
- />
- )}
- {needsPlanningFor && planningFor && activeStep > 2 && editingStep !== 2 && (
- revertTo(2)}
- />
- )}
- {funeralTypeId && funeralTypeLabel && activeStep > 3 && editingStep !== 3 && (
- revertTo(3)}
- />
- )}
+ {/* ── Completed rows ─────────────────────────────────────── */}
+
+ revertTo(1)} />
+
+
+ revertTo(2)} />
+
+
+ revertTo(3)} />
+
+
+ revertTo(4)} />
+
- {/* Active question */}
- 1 && editingStep !== 1 ? 3 : 0 }}>
- {/* Step 1: Intent */}
- {activeStep === 1 && (
-
-
- How can we help you today?
+ {/* ── Step 1: Intent ─────────────────────────────────────── */}
+
+
+ {showIntentPrompt && (
+
+ Please let us know how we can help
-
- selectIntent('arrange')}
- />
- selectIntent('preplan')}
- />
-
-
- )}
-
- {/* Step 2: Planning for (conditional) */}
- {activeStep === 2 && needsPlanningFor && (
-
-
- Who are you planning for?
-
-
- selectPlanningFor('myself')}
- />
- selectPlanningFor('someone-else')}
- />
-
-
- )}
-
- {/* Step 3: Funeral type */}
- {activeStep === 3 && (
-
-
- What type of funeral are you considering?
-
-
- {funeralTypes.map((ft) => (
- selectFuneralType(ft.id)}
- />
- ))}
-
-
- )}
-
- {/* Step 4: Location */}
- {activeStep === 4 && (
-
-
- Where are you located?
-
- setLocation(e.target.value)}
- size="small"
- fullWidth
- inputProps={{ 'aria-label': 'Location — suburb or postcode' }}
- onKeyDown={(e) => {
- if (e.key === 'Enter' && canSubmit) handleSubmit();
- }}
+ )}
+ How can we help you today?
+
+ selectIntent('arrange')}
+ />
+ selectIntent('preplan')}
/>
- )}
-
+
+
+
+ {/* ── Step 2: Planning for (conditional) ─────────────────── */}
+
+
+ Who are you planning for?
+
+ selectPlanningFor('myself')}
+ />
+ selectPlanningFor('someone-else')}
+ />
+
+
+
+
+ {/* ── Step 3: Type + Preferences ─────────────────────────── */}
+
+
+ What type of funeral are you considering?
+
+ {funeralTypes.map((ft) => (
+ selectType(ft.id)}
+ />
+ ))}
+ {showExploreAll && (
+ selectType('all')}
+ />
+ )}
+
+
+ {/* Theme preferences — optional, inside type step */}
+ {themeOptions.length > 0 && (
+
+
+
+ Any preferences?
+
+
+ (optional)
+
+
+
+ {themeOptions.map((theme) => {
+ const isSelected = selectedThemes.includes(theme.id);
+ return (
+ toggleTheme(theme.id)}
+ clickable
+ aria-pressed={isSelected}
+ sx={{ height: 44 }}
+ />
+ );
+ })}
+
+
+ )}
+
+
+
+ {/* ── Step 4: Service (conditional, auto-advance) ────────── */}
+
+
+ Would you like a service?
+
+ {SERVICE_OPTIONS.map((opt) => (
+ selectService(opt.value)}
+ clickable
+ aria-pressed={serviceAnswered && servicePref === opt.value}
+ sx={{ justifyContent: 'flex-start', height: 44, borderRadius: 'var(--fa-border-radius-md)' }}
+ />
+ ))}
+
+
+
+
+ {/* ── Always visible: Location + CTA ─────────────────────── */}
+
+
+ Where are you looking?
+
+ {
+ 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();
+ }}
+ />
- {/* CTA — only visible once we're on the location step or beyond */}
- {activeStep >= 4 && (
- )}
+
);
},