HomePage V2: full-bleed hero, stats bar, discover section, editorial testimonials

New layout inspired by reference designs:
- Full-bleed hero with parsonshero.png + gradient overlay (heroImageUrl prop)
- Trust stats bar: 1,500+ families, 4.9 rating, 300+ directors
- "See what you'll discover" section: map placeholder + featured ProviderCards
- Editorial testimonials: alternating left/right with quote marks (no cards)
- Minimal FAQ: borderless accordion with divider lines
- V1 split hero preserved when heroImage prop used instead

New types: TrustStat, FeaturedProvider.
New props: heroImageUrl, stats, featuredProviders, discoverMapSlot,
  onSelectFeaturedProvider.
V1 stories unchanged, new V2 story added.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-31 20:08:53 +11:00
parent 51918ea503
commit 44f159a453
2 changed files with 416 additions and 160 deletions

View File

@@ -5,6 +5,7 @@ import AccessTimeIcon from '@mui/icons-material/AccessTime';
import SearchIcon from '@mui/icons-material/Search'; import SearchIcon from '@mui/icons-material/Search';
import SupportAgentOutlinedIcon from '@mui/icons-material/SupportAgentOutlined'; import SupportAgentOutlinedIcon from '@mui/icons-material/SupportAgentOutlined';
import { HomePage } from './HomePage'; import { HomePage } from './HomePage';
import type { FeaturedProvider, TrustStat } from './HomePage';
import { Navigation } from '../../organisms/Navigation'; import { Navigation } from '../../organisms/Navigation';
import { Footer } from '../../organisms/Footer'; import { Footer } from '../../organisms/Footer';
@@ -264,3 +265,68 @@ export const Mobile: Story = {
viewport: { defaultViewport: 'mobile1' }, viewport: { defaultViewport: 'mobile1' },
}, },
}; };
// ─── V2 data ────────────────────────────────────────────────────────────────
const trustStats: TrustStat[] = [
{ value: '1,500+', label: 'Families helped' },
{ value: '4.9', label: 'Google Rating' },
{ value: '300+', label: 'Funeral directors' },
];
const featuredProviders: FeaturedProvider[] = [
{
id: 'parsons',
name: 'H.Parsons Funeral Directors',
location: 'Wentworth, NSW',
verified: true,
imageUrl: 'https://placehold.co/600x200/E8E0D6/8B6F47?text=H.Parsons',
logoUrl: 'https://placehold.co/64x64/FEF9F5/BA834E?text=HP',
rating: 4.6,
reviewCount: 7,
startingPrice: 900,
},
{
id: 'rankins',
name: 'Rankins Funeral Services',
location: 'Wollongong, NSW',
verified: true,
imageUrl: 'https://placehold.co/600x200/D7E1E2/4C5B6B?text=Rankins',
logoUrl: 'https://placehold.co/64x64/F2F5F6/4C5B6B?text=R',
rating: 4.8,
reviewCount: 23,
startingPrice: 1200,
},
{
id: 'easy-funerals',
name: 'Easy Funerals',
location: 'Sydney, NSW',
verified: true,
imageUrl: 'https://placehold.co/600x200/F0F7F0/3B7A3B?text=Easy+Funerals',
logoUrl: 'https://placehold.co/64x64/F0F7F0/3B7A3B?text=EF',
rating: 4.5,
reviewCount: 42,
startingPrice: 850,
},
];
// ─── V2 Story ───────────────────────────────────────────────────────────────
/** V2 layout — full-bleed hero, stats bar, map + provider cards, editorial testimonials */
export const V2: Story = {
args: {
navigation: nav,
footer,
heroImageUrl: '/brandassets/images/heroes/parsonshero.png',
stats: trustStats,
featuredProviders,
onSelectFeaturedProvider: (id) => console.log('Featured provider:', id),
features,
googleRating: 4.9,
googleReviewCount: 2340,
testimonials,
faqItems,
onSearch: (params) => console.log('Search:', params),
onCtaClick: () => console.log('CTA clicked'),
},
};

View File

@@ -5,6 +5,7 @@ import Accordion from '@mui/material/Accordion';
import AccordionSummary from '@mui/material/AccordionSummary'; import AccordionSummary from '@mui/material/AccordionSummary';
import AccordionDetails from '@mui/material/AccordionDetails'; import AccordionDetails from '@mui/material/AccordionDetails';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import FormatQuoteIcon from '@mui/icons-material/FormatQuote';
import StarIcon from '@mui/icons-material/Star'; import StarIcon from '@mui/icons-material/Star';
import StarHalfIcon from '@mui/icons-material/StarHalf'; import StarHalfIcon from '@mui/icons-material/StarHalf';
import StarBorderIcon from '@mui/icons-material/StarBorder'; import StarBorderIcon from '@mui/icons-material/StarBorder';
@@ -12,6 +13,7 @@ import type { SxProps, Theme } from '@mui/material/styles';
import { Typography } from '../../atoms/Typography'; import { Typography } from '../../atoms/Typography';
import { Button } from '../../atoms/Button'; import { Button } from '../../atoms/Button';
import { Card } from '../../atoms/Card'; import { Card } from '../../atoms/Card';
import { ProviderCard } from '../../molecules/ProviderCard';
import { FuneralFinderV3, type FuneralFinderV3SearchParams } from '../../organisms/FuneralFinder'; import { FuneralFinderV3, type FuneralFinderV3SearchParams } from '../../organisms/FuneralFinder';
// ─── Types ─────────────────────────────────────────────────────────────────── // ─── Types ───────────────────────────────────────────────────────────────────
@@ -43,6 +45,25 @@ export interface FAQItem {
answer: React.ReactNode; answer: React.ReactNode;
} }
/** A trust stat for the stats bar */
export interface TrustStat {
value: string;
label: string;
}
/** A featured provider for the discover section */
export interface FeaturedProvider {
id: string;
name: string;
location: string;
verified?: boolean;
imageUrl?: string;
logoUrl?: string;
rating?: number;
reviewCount?: number;
startingPrice?: number;
}
/** Props for the HomePage component */ /** Props for the HomePage component */
export interface HomePageProps { export interface HomePageProps {
/** Navigation bar */ /** Navigation bar */
@@ -54,19 +75,31 @@ export interface HomePageProps {
heroHeading?: string; heroHeading?: string;
/** Hero subheading paragraph */ /** Hero subheading paragraph */
heroSubheading?: string; heroSubheading?: string;
/** Hero image slot */ /** Hero image slot (split layout — V1) */
heroImage?: React.ReactNode; heroImage?: React.ReactNode;
/** Hero background image URL (full-bleed layout — V2). Takes priority over heroImage. */
heroImageUrl?: string;
/** FuneralFinder search callback */ /** FuneralFinder search callback */
onSearch?: (params: FuneralFinderV3SearchParams) => void; onSearch?: (params: FuneralFinderV3SearchParams) => void;
/** FuneralFinder loading state */ /** FuneralFinder loading state */
searchLoading?: boolean; searchLoading?: boolean;
/** Trust stats bar (e.g. "1,500+ families helped") */
stats?: TrustStat[];
/** Partner logos for the trust carousel */ /** Partner logos for the trust carousel */
partnerLogos?: PartnerLogo[]; partnerLogos?: PartnerLogo[];
/** Trust line text above the carousel */ /** Trust line text above the carousel */
partnerTrustLine?: string; partnerTrustLine?: string;
/** Featured providers for the "Discover" section */
featuredProviders?: FeaturedProvider[];
/** Map slot for the discover section */
discoverMapSlot?: React.ReactNode;
/** Callback when a featured provider card is clicked */
onSelectFeaturedProvider?: (id: string) => void;
/** Feature cards for "Why Use FA" */ /** Feature cards for "Why Use FA" */
features?: FeatureCard[]; features?: FeatureCard[];
@@ -136,10 +169,15 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
heroHeading = 'Compare Funeral Directors Near You and Arrange with Confidence', heroHeading = 'Compare Funeral Directors Near You and Arrange with Confidence',
heroSubheading = "Funeral planning doesn't have to be overwhelming. Whether you're thinking ahead or arranging for a loved one, find trusted local providers with transparent pricing, all at your own pace.", heroSubheading = "Funeral planning doesn't have to be overwhelming. Whether you're thinking ahead or arranging for a loved one, find trusted local providers with transparent pricing, all at your own pace.",
heroImage, heroImage,
heroImageUrl,
onSearch, onSearch,
searchLoading, searchLoading,
stats = [],
partnerLogos = [], partnerLogos = [],
partnerTrustLine = 'Providing services from hundreds of trusted funeral homes across Australia', partnerTrustLine = 'Providing services from hundreds of trusted funeral homes across Australia',
featuredProviders = [],
discoverMapSlot,
onSelectFeaturedProvider,
features = [], features = [],
googleRating, googleRating,
googleReviewCount, googleReviewCount,
@@ -152,6 +190,8 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
}, },
ref, ref,
) => { ) => {
const isFullBleedHero = !!heroImageUrl;
return ( return (
<Box <Box
ref={ref} ref={ref}
@@ -167,13 +207,56 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
{/* ═══════════════════════════════════════════════════════════════════ {/* ═══════════════════════════════════════════════════════════════════
Section 1: Hero Section 1: Hero
═══════════════════════════════════════════════════════════════════ */} ═══════════════════════════════════════════════════════════════════ */}
{isFullBleedHero ? (
/* ── V2: Full-bleed hero with text overlay ── */
<Box <Box
component="section" component="section"
aria-labelledby="hero-heading" aria-labelledby="hero-heading"
sx={{ sx={{
bgcolor: 'var(--fa-color-surface-warm)', position: 'relative',
overflow: 'hidden', minHeight: { xs: 420, md: 520 },
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundImage: `url(${heroImageUrl})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
'&::after': {
content: '""',
position: 'absolute',
inset: 0,
background:
'linear-gradient(180deg, rgba(0,0,0,0.35) 0%, rgba(0,0,0,0.15) 50%, rgba(0,0,0,0.4) 100%)',
},
}} }}
>
<Container
maxWidth="md"
sx={{ position: 'relative', zIndex: 1, textAlign: 'center', py: 6 }}
>
<Typography
variant="display3"
component="h1"
id="hero-heading"
tabIndex={-1}
sx={{ mb: 2, color: 'var(--fa-color-white)' }}
>
{heroHeading}
</Typography>
<Typography
variant="body1"
sx={{ color: 'rgba(255,255,255,0.85)', maxWidth: 560, mx: 'auto' }}
>
{heroSubheading}
</Typography>
</Container>
</Box>
) : (
/* ── V1: Split hero ── */
<Box
component="section"
aria-labelledby="hero-heading"
sx={{ bgcolor: 'var(--fa-color-surface-warm)', overflow: 'hidden' }}
> >
<Container <Container
maxWidth="lg" maxWidth="lg"
@@ -185,7 +268,6 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
py: { xs: 4, md: 0 }, py: { xs: 4, md: 0 },
}} }}
> >
{/* Hero text */}
<Box <Box
sx={{ sx={{
flex: { md: '0 0 50%' }, flex: { md: '0 0 50%' },
@@ -207,8 +289,6 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
{heroSubheading} {heroSubheading}
</Typography> </Typography>
</Box> </Box>
{/* Hero image */}
<Box <Box
role="img" role="img"
aria-label="Family planning together" aria-label="Family planning together"
@@ -234,6 +314,7 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
</Box> </Box>
</Container> </Container>
</Box> </Box>
)}
{/* ═══════════════════════════════════════════════════════════════════ {/* ═══════════════════════════════════════════════════════════════════
Section 2: FuneralFinder Widget (overlapping card) Section 2: FuneralFinder Widget (overlapping card)
@@ -256,6 +337,131 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
</Box> </Box>
</Box> </Box>
{/* ═══════════════════════════════════════════════════════════════════
Section 2b: Stats Bar (V2 only)
═══════════════════════════════════════════════════════════════════ */}
{stats.length > 0 && (
<Box
component="section"
aria-label="Trust statistics"
sx={{ py: { xs: 4, md: 5 }, mt: { xs: 4, md: 5 } }}
>
<Container maxWidth="md">
<Box
sx={{
display: 'flex',
justifyContent: 'center',
flexWrap: 'wrap',
gap: { xs: 3, md: 6 },
textAlign: 'center',
}}
>
{stats.map((stat) => (
<Box key={stat.label}>
<Typography
variant="h3"
component="p"
sx={{ color: 'primary.main', mb: 0.5 }}
>
{stat.value}
</Typography>
<Typography variant="body2" color="text.secondary">
{stat.label}
</Typography>
</Box>
))}
</Box>
</Container>
</Box>
)}
{/* ═══════════════════════════════════════════════════════════════════
Section 2c: Discover — Map + Featured Providers (V2)
═══════════════════════════════════════════════════════════════════ */}
{featuredProviders.length > 0 && (
<Box
component="section"
aria-labelledby="discover-heading"
sx={{
bgcolor: 'var(--fa-color-surface-subtle)',
py: { xs: 6, md: 10 },
}}
>
<Container maxWidth="lg">
<Box sx={{ textAlign: 'center', mb: { xs: 4, md: 6 } }}>
<Typography
variant="display3"
component="h2"
id="discover-heading"
sx={{ mb: 1.5, color: 'text.primary' }}
>
See what you&rsquo;ll discover
</Typography>
<Typography
variant="body1"
color="text.secondary"
sx={{ maxWidth: 520, mx: 'auto' }}
>
From trusted local providers to personalised options, find the right care near
you.
</Typography>
</Box>
<Box
sx={{
display: 'grid',
gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' },
gap: 3,
alignItems: 'start',
}}
>
{/* Map placeholder */}
<Box
sx={{
borderRadius: 2,
overflow: 'hidden',
minHeight: { xs: 240, md: 400 },
bgcolor: 'var(--fa-color-surface-cool)',
border: '1px solid',
borderColor: 'divider',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{discoverMapSlot || (
<Typography variant="body2" color="text.secondary">
Map coming soon
</Typography>
)}
</Box>
{/* Featured provider cards */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{featuredProviders.map((provider) => (
<ProviderCard
key={provider.id}
name={provider.name}
location={provider.location}
verified={provider.verified}
imageUrl={provider.imageUrl}
logoUrl={provider.logoUrl}
rating={provider.rating}
reviewCount={provider.reviewCount}
startingPrice={provider.startingPrice}
onClick={
onSelectFeaturedProvider
? () => onSelectFeaturedProvider(provider.id)
: undefined
}
/>
))}
</Box>
</Box>
</Container>
</Box>
)}
{/* ═══════════════════════════════════════════════════════════════════ {/* ═══════════════════════════════════════════════════════════════════
Section 3: Partner Logos Carousel Section 3: Partner Logos Carousel
═══════════════════════════════════════════════════════════════════ */} ═══════════════════════════════════════════════════════════════════ */}
@@ -319,12 +525,9 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
'100%': { transform: 'translateX(-50%)' }, '100%': { transform: 'translateX(-50%)' },
}, },
'&:hover': { animationPlayState: 'paused' }, '&:hover': { animationPlayState: 'paused' },
'@media (prefers-reduced-motion: reduce)': { '@media (prefers-reduced-motion: reduce)': { animation: 'none' },
animation: 'none',
},
}} }}
> >
{/* Duplicate logos for seamless infinite scroll */}
{[...partnerLogos, ...partnerLogos].map((logo, i) => ( {[...partnerLogos, ...partnerLogos].map((logo, i) => (
<Box <Box
key={`${logo.alt}-${i}`} key={`${logo.alt}-${i}`}
@@ -339,10 +542,7 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
filter: 'grayscale(100%)', filter: 'grayscale(100%)',
opacity: 0.5, opacity: 0.5,
transition: 'opacity 0.2s, filter 0.2s', transition: 'opacity 0.2s, filter 0.2s',
'&:hover': { '&:hover': { filter: 'grayscale(0%)', opacity: 1 },
filter: 'grayscale(0%)',
opacity: 1,
},
flexShrink: 0, flexShrink: 0,
}} }}
/> />
@@ -442,112 +642,100 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
)} )}
{/* ═══════════════════════════════════════════════════════════════════ {/* ═══════════════════════════════════════════════════════════════════
Section 5: Reviews / Testimonials Section 5: Testimonials
═══════════════════════════════════════════════════════════════════ */} ═══════════════════════════════════════════════════════════════════ */}
{(testimonials.length > 0 || googleRating != null) && ( {testimonials.length > 0 && (
<Box <Box
component="section" component="section"
aria-labelledby="reviews-heading" aria-labelledby="reviews-heading"
sx={{ sx={{
bgcolor: 'var(--fa-color-brand-950)',
py: { xs: 6, md: 10 }, py: { xs: 6, md: 10 },
bgcolor: 'var(--fa-color-surface-default)',
}} }}
> >
<Container maxWidth="lg"> <Container maxWidth="md">
<Box sx={{ textAlign: 'center', mb: { xs: 3, md: 5 } }}>
<Typography
variant="overline"
sx={{ color: 'var(--fa-color-brand-400)', mb: 1.5, display: 'block' }}
>
What our customers say
</Typography>
<Typography <Typography
variant="h2" variant="h2"
component="h2" component="h2"
id="reviews-heading" id="reviews-heading"
sx={{ color: 'var(--fa-color-white)', mb: 3 }} sx={{ textAlign: 'center', mb: 1, color: 'text.primary' }}
> >
How we have helped families like yours Testimonials
</Typography> </Typography>
{/* Google aggregate */}
{googleRating != null && ( {googleRating != null && (
<Box sx={{ mb: { xs: 3, md: 5 } }}> <Box sx={{ textAlign: 'center', mb: { xs: 4, md: 6 } }}>
<Typography
variant="overline"
sx={{
color: 'var(--fa-color-neutral-400)',
mb: 0.5,
display: 'block',
}}
>
Google Reviews
</Typography>
<Typography
variant="h1"
component="p"
sx={{ color: 'var(--fa-color-white)', mb: 0.5 }}
>
{googleRating}
</Typography>
<StarRating
rating={googleRating}
size={20}
color="var(--fa-color-brand-400)"
/>
{googleReviewCount != null && (
<Typography
variant="caption"
sx={{ color: 'var(--fa-color-neutral-400)', mt: 0.5, display: 'block' }}
>
{googleReviewCount.toLocaleString('en-AU')} reviews
</Typography>
)}
</Box>
)}
</Box>
{/* Testimonial cards */}
{testimonials.length > 0 && (
<Box <Box
sx={{ sx={{
display: 'grid', display: 'inline-flex',
gridTemplateColumns: {
xs: '1fr',
md: 'repeat(3, 1fr)',
},
gap: 3,
}}
>
{testimonials.map((t, i) => (
<Card key={`${t.name}-${i}`} variant="elevated" padding="default">
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
mb: 2, gap: 1,
mt: 1,
}} }}
> >
<Typography variant="h6" component="p"> <StarRating rating={googleRating} size={16} />
<Typography variant="body2" color="text.secondary">
{googleRating} Google Rating
{googleReviewCount
? ` · ${googleReviewCount.toLocaleString('en-AU')} reviews`
: ''}
</Typography>
</Box>
</Box>
)}
{/* Editorial testimonials — alternating alignment */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{testimonials.map((t, i) => {
const isRight = i % 2 === 1;
return (
<Box
key={`${t.name}-${i}`}
sx={{
textAlign: isRight ? 'right' : 'left',
maxWidth: '85%',
ml: isRight ? 'auto' : 0,
mr: isRight ? 0 : 'auto',
}}
>
<FormatQuoteIcon
sx={{
fontSize: 32,
color: 'var(--fa-color-brand-300)',
transform: isRight ? 'scaleX(-1)' : 'none',
mb: 1,
}}
/>
<Typography
variant="h5"
component="blockquote"
sx={{ mb: 2, fontWeight: 400 }}
>
{t.quote}
</Typography>
<Typography
variant="label"
component="cite"
sx={{ fontStyle: 'normal', display: 'block', mb: 0.5 }}
>
{t.name} {t.name}
</Typography> </Typography>
<StarRating rating={t.rating} size={14} /> <Box
</Box> sx={{
<Typography display: 'inline-flex',
variant="body2" alignItems: 'center',
color="text.secondary" gap: 1,
sx={{ mb: 2, fontStyle: 'italic' }} }}
> >
&ldquo;{t.quote}&rdquo; <StarRating rating={t.rating} size={12} />
<Typography variant="caption" color="text.secondary">
· {t.timeAgo}
</Typography> </Typography>
<Typography variant="caption" color="text.tertiary">
{t.timeAgo}
</Typography>
</Card>
))}
</Box> </Box>
)} </Box>
);
})}
</Box>
</Container> </Container>
</Box> </Box>
)} )}
@@ -597,32 +785,34 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
id="faq-heading" id="faq-heading"
sx={{ textAlign: 'center', mb: { xs: 4, md: 6 }, color: 'text.primary' }} sx={{ textAlign: 'center', mb: { xs: 4, md: 6 }, color: 'text.primary' }}
> >
Frequently Asked Questions FAQ
</Typography> </Typography>
<Box sx={{ maxWidth: 800, mx: 'auto' }}> <Box sx={{ maxWidth: 700, mx: 'auto' }}>
{faqItems.map((item, i) => ( {faqItems.map((item, i) => (
<Accordion <Accordion
key={i} key={i}
disableGutters disableGutters
elevation={0} elevation={0}
sx={{ sx={{
border: 1, bgcolor: 'transparent',
borderBottom: '1px solid',
borderColor: 'divider', borderColor: 'divider',
borderRadius: '8px !important',
mb: 2,
'&:before': { display: 'none' }, '&:before': { display: 'none' },
overflow: 'hidden', '&:first-of-type': {
borderTop: '1px solid',
borderColor: 'divider',
},
}} }}
> >
<AccordionSummary expandIcon={<ExpandMoreIcon />} sx={{ px: 3, py: 1 }}> <AccordionSummary expandIcon={<ExpandMoreIcon />} sx={{ px: 0, py: 1.5 }}>
<Typography variant="h6" component="h3"> <Typography variant="body1" sx={{ fontWeight: 500 }}>
{item.question} {item.question}
</Typography> </Typography>
</AccordionSummary> </AccordionSummary>
<AccordionDetails sx={{ px: 3, pb: 3 }}> <AccordionDetails sx={{ px: 0, pb: 3 }}>
{typeof item.answer === 'string' ? ( {typeof item.answer === 'string' ? (
<Typography variant="body1" color="text.secondary"> <Typography variant="body2" color="text.secondary">
{item.answer} {item.answer}
</Typography> </Typography>
) : ( ) : (