Files
FuneralArranger/src/components/organisms/Footer/Footer.tsx
Richie fcc69446f3 Upgrade to React 19, MUI v7, Storybook 9
- React 18 → 19, MUI v5 → v7, Storybook 8 → 9
- Fix Grid v2 API in Footer (remove item prop, use size prop)
- Inline provider fixtures (was importing from excluded demo dir)
- Remove consolidated SB addons (essentials, storysource, blocks)
- Update addon-designs to SB9-compatible version
- Add autodocs via tags in preview config
- Add Tailwind v3, PostCSS, autoprefixer dev deps (config next)
- Zero TypeScript errors, Storybook starts clean

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-22 13:21:52 +10:00

265 lines
8.7 KiB
TypeScript

import React from 'react';
import Box from '@mui/material/Box';
import Container from '@mui/material/Container';
import Grid from '@mui/material/Grid';
import type { SxProps, Theme } from '@mui/material/styles';
import { Typography } from '../../atoms/Typography';
import { Link } from '../../atoms/Link';
import { Divider } from '../../atoms/Divider';
// ─── Types ───────────────────────────────────────────────────────────────────
/** A group of links in the footer */
export interface FooterLinkGroup {
/** Group heading */
heading: string;
/** Links within this group */
links: { label: string; href: string; onClick?: () => void }[];
}
/** Props for the FA Footer organism */
export interface FooterProps {
/** Site logo — rendered in the footer top row */
logo: React.ReactNode;
/** Optional tagline below the logo */
tagline?: string;
/** Link groups displayed as columns */
linkGroups?: FooterLinkGroup[];
/** Phone number displayed in the contact section */
phone?: string;
/** Email address displayed in the contact section */
email?: string;
/** Copyright text — defaults to current year + "Funeral Arranger" */
copyright?: string;
/** Optional legal links shown in the bottom bar (Privacy, Terms, etc.) */
legalLinks?: { label: string; href: string; onClick?: () => void }[];
/** MUI sx prop for the root element */
sx?: SxProps<Theme>;
}
// ─── Component ───────────────────────────────────────────────────────────────
/**
* Site footer for the FA design system.
*
* Multi-column footer with logo, link groups, contact info, and legal bar.
* Designed for warmth and trust — uses the brand's dark surface with
* inverse (white) text.
*
* Composes Typography + Link + Divider.
*
* Usage:
* ```tsx
* <Footer
* logo={<img src="/brandlogo/logo-full.svg" alt="Funeral Arranger" height={28} />}
* tagline="Helping Australian families plan with confidence"
* linkGroups={[
* { heading: 'Services', links: [{ label: 'Find a Director', href: '/directors' }] },
* { heading: 'Support', links: [{ label: 'FAQ', href: '/faq' }] },
* ]}
* phone="1800 987 888"
* email="support@funeralarranger.com.au"
* legalLinks={[{ label: 'Privacy Policy', href: '/privacy' }]}
* />
* ```
*/
export const Footer = React.forwardRef<HTMLDivElement, FooterProps>(
({ logo, tagline, linkGroups = [], phone, email, copyright, legalLinks = [], sx }, ref) => {
const year = new Date().getFullYear();
const copyrightText = copyright || `\u00A9 ${year} Funeral Arranger. All rights reserved.`;
const overlineSx = {
color: 'text.secondary',
textTransform: 'uppercase' as const,
letterSpacing: '0.08em',
display: 'block',
mb: 0.5,
};
const contactLinkSx = {
color: 'text.primary',
'&:hover': { color: 'var(--fa-color-brand-600)' },
};
return (
<Box
ref={ref}
component="footer"
sx={[
{
bgcolor: 'var(--fa-color-surface-subtle)',
color: 'text.primary',
pt: { xs: 5, md: 8 },
pb: 0,
},
...(Array.isArray(sx) ? sx : [sx]),
]}
>
<Container maxWidth="lg">
{/* Main footer content */}
<Grid container spacing={{ xs: 4, md: 6 }}>
{/* Logo + tagline column */}
<Grid size={{ xs: 12, md: 4 }}>
<Box sx={{ mb: 2 }}>{logo}</Box>
{tagline && (
<Typography
variant="body2"
sx={{ color: 'text.secondary', maxWidth: { xs: '100%', md: 280 } }}
>
{tagline}
</Typography>
)}
{/* Contact info below tagline */}
{(phone || email) && (
<Box sx={{ mt: 3 }}>
{phone && (
<Box sx={{ mb: 1 }}>
<Typography variant="overlineSm" sx={overlineSx}>
Call us
</Typography>
<Link
href={`tel:${phone.replace(/\s/g, '')}`}
sx={{
...contactLinkSx,
fontWeight: 600,
display: 'inline-flex',
alignItems: 'center',
minHeight: 44,
}}
>
{phone}
</Link>
</Box>
)}
{email && (
<Box>
<Typography variant="overlineSm" sx={overlineSx}>
Email
</Typography>
<Link
href={`mailto:${email}`}
sx={{
...contactLinkSx,
display: 'inline-flex',
alignItems: 'center',
minHeight: 44,
}}
>
{email}
</Link>
</Box>
)}
</Box>
)}
</Grid>
{/* Link group columns */}
{linkGroups.map((group) => (
<Grid
size={{ xs: 6, sm: 4, md: 'grow' }}
key={group.heading}
component="nav"
aria-label={group.heading}
>
<Typography
variant="label"
sx={{
color: 'text.secondary',
mb: 2,
display: 'block',
}}
>
{group.heading}
</Typography>
<Box
component="ul"
sx={{
listStyle: 'none',
p: 0,
m: 0,
display: 'flex',
flexDirection: 'column',
gap: 1.5,
}}
>
{group.links.map((link) => (
<li key={link.label}>
<Link
href={link.href}
onClick={link.onClick}
sx={{
color: 'text.primary',
fontSize: '0.875rem',
fontWeight: 500,
display: 'inline-flex',
alignItems: 'center',
minHeight: 44,
'&:hover': { color: 'var(--fa-color-brand-600)' },
}}
>
{link.label}
</Link>
</li>
))}
</Box>
</Grid>
))}
</Grid>
{/* Bottom bar */}
<Divider sx={{ mt: { xs: 5, md: 8 } }} />
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', md: 'row' },
justifyContent: 'space-between',
alignItems: { xs: 'center', md: 'center' },
gap: 1.5,
py: 3,
}}
>
<Typography variant="captionSm" sx={{ color: 'text.secondary' }}>
{copyrightText}
</Typography>
{legalLinks.length > 0 && (
<Box
sx={{
display: 'flex',
gap: 3,
flexWrap: 'wrap',
justifyContent: 'center',
}}
>
{legalLinks.map((link) => (
<Link
key={link.label}
href={link.href}
onClick={link.onClick}
sx={{
color: 'text.secondary',
fontSize: '0.75rem',
fontWeight: 500,
display: 'inline-flex',
alignItems: 'center',
minHeight: 44,
'&:hover': { color: 'var(--fa-color-brand-600)' },
}}
>
{link.label}
</Link>
))}
</Box>
)}
</Box>
</Container>
</Box>
);
},
);
Footer.displayName = 'Footer';
export default Footer;