diff --git a/src/components/organisms/FuneralFinder/FuneralFinderV2.stories.tsx b/src/components/organisms/FuneralFinder/FuneralFinderV2.stories.tsx new file mode 100644 index 0000000..c4dc175 --- /dev/null +++ b/src/components/organisms/FuneralFinder/FuneralFinderV2.stories.tsx @@ -0,0 +1,69 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import Box from '@mui/material/Box'; +import { FuneralFinderV2 } from './FuneralFinderV2'; + +const meta: Meta = { + title: 'Organisms/FuneralFinderV2', + component: FuneralFinderV2, + parameters: { + layout: 'padded', + }, + args: { + onSearch: (params) => { + console.log('Search params:', params); + }, + }, +}; + +export default meta; +type Story = StoryObj; + +/** 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) => ( + + {/* Simulated masthead */} + + + Funeral Arranger + + + Find trusted funeral directors near you + + + {/* Widget below masthead */} + + + + + ), + ], +}; + +/** Constrained width — typical sidebar or narrow column */ +export const Narrow: Story = { + decorators: [ + (Story) => ( + + + + ), + ], +}; diff --git a/src/components/organisms/FuneralFinder/FuneralFinderV2.tsx b/src/components/organisms/FuneralFinder/FuneralFinderV2.tsx new file mode 100644 index 0000000..5dc0500 --- /dev/null +++ b/src/components/organisms/FuneralFinder/FuneralFinderV2.tsx @@ -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; +} + +// ─── 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 ( + + {completed ? ( + + ) : ( + + {step} + + )} + + ); +} + +// ─── Shared styles ─────────────────────────────────────────────────────────── + +const selectSx: SxProps = { + 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( + ( + { + 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(''); + const [planningFor, setPlanningFor] = React.useState(''); + const [funeralType, setFuneralType] = React.useState(''); + 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) => { + const val = e.target.value as LookingTo; + setLookingTo(val); + if (val === 'arrange-now') { + setPlanningFor('someone-else'); + } else { + setPlanningFor(''); + } + }; + + const handlePlanningFor = (e: SelectChangeEvent) => { + setPlanningFor(e.target.value as PlanningFor); + }; + + const handleFuneralType = (e: SelectChangeEvent) => { + 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 = ( + Select… + ); + + const findLabel = (opts: { value: string; label: string }[], val: string) => + opts.find((o) => o.value === val)?.label ?? ''; + + // ─── Render ────────────────────────────────────────────────── + return ( + + {/* ── Header ──────────────────────────────────────────── */} + + {heading} + + + {subheading} + + + + {/* ── Steps ───────────────────────────────────────────── */} + + {/* Step 1: I'm looking to */} + + + + + I’m looking to… + + + + + + {/* Step 2: I'm planning for */} + + + + + I’m planning for + + + + + + {/* Step 3: Type of funeral */} + + + + + Type of funeral + + + + + + {/* Step 4: Location */} + + = 3} /> + + + Looking for providers in + + 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', + }, + }} + /> + + + + + {/* ── CTA ─────────────────────────────────────────────── */} + + + + + Free to use · No obligation + + + + {/* Error hint — always rendered to maintain fixed height */} + + {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'} + + + ); + }, +); + +FuneralFinderV2.displayName = 'FuneralFinderV2'; +export default FuneralFinderV2; diff --git a/src/components/organisms/FuneralFinder/index.ts b/src/components/organisms/FuneralFinder/index.ts index d4f9fa4..0dc7c6e 100644 --- a/src/components/organisms/FuneralFinder/index.ts +++ b/src/components/organisms/FuneralFinder/index.ts @@ -4,3 +4,9 @@ export { type FuneralTypeOption, type FuneralSearchParams, } from './FuneralFinder'; + +export { + FuneralFinderV2, + type FuneralFinderV2Props, + type FuneralFinderV2SearchParams, +} from './FuneralFinderV2';