Files
miracles_in_motion/src/routes/HomePageRoute.tsx
T
defiQUGandCursor 4e4df13ab1 P1: path routing, prerender shells, E2E, and HelmetProvider donate fix.
Replace hash URLs with path-based routing, post-build prerender for money pages, Playwright smoke tests, and wrap the app in HelmetProvider so /donate mounts correctly.

Co-authored-by: Cursor <[email protected]>
2026-06-16 11:11:20 -07:00

418 lines
16 KiB
TypeScript

import { type ComponentType, lazy, Suspense } from 'react'
import { motion } from 'framer-motion'
import {
ArrowRight,
Globe,
Heart,
Mail,
MapPin,
Shield,
Star,
Users,
Home as HomeIcon,
} from 'lucide-react'
import { HomeHero } from '../components/home/HomeHero'
import { AmbientOrb } from '../components/motion/AmbientOrb'
import { RevealSection } from '../components/motion/RevealSection'
import { hoverMotionProps, useMotionSafe } from '../hooks/useMotionSafe'
import { HOME_TESTIMONIAL, HOW_IT_WORKS, SITE, WHAT_WE_DO } from '../content/siteContent'
const AwarenessCampaigns = lazy(() =>
import('../components/home/AwarenessCampaigns').then((m) => ({ default: m.AwarenessCampaigns })),
)
const CommunityGallery = lazy(() =>
import('../components/home/CommunityGallery').then((m) => ({ default: m.CommunityGallery })),
)
const PartnerStrip = lazy(() =>
import('../components/home/PartnerStrip').then((m) => ({ default: m.PartnerStrip })),
)
const WhoWeServe = lazy(() =>
import('../components/home/WhoWeServe').then((m) => ({ default: m.WhoWeServe })),
)
interface CalloutProps {
title: string
body: string
href: string
accent: string
icon: ComponentType<{ className?: string }>
index?: number
}
interface SectionHeaderProps {
eyebrow?: string
title: string
subtitle?: string
}
interface FeatureCardProps {
icon: ComponentType<{ className?: string }>
title: string
body: string
}
/** Above-the-fold only — no framer-motion below this chunk boundary. */
export default function HomePageRoute() {
return (
<>
<HomeHero />
<Suspense fallback={null}>
<WhoWeServe />
<AwarenessCampaigns />
<PartnerStrip />
<Programs />
<CommunityGallery />
<Impact />
<HowItWorks />
<Testimonial />
<GetInvolved />
<CTA />
</Suspense>
</>
)
}
function Programs() {
const icons = [Users, Heart, HomeIcon, Globe, Shield, Star] as const
const items = WHAT_WE_DO.slice(0, 3).map((p, idx) => ({
icon: icons[idx] ?? Heart,
title: p.title,
body: p.body,
}))
return (
<section id="programs" className="relative py-20 overflow-hidden">
<div className="absolute inset-0 pointer-events-none" aria-hidden="true">
<AmbientOrb
className="absolute top-10 right-10 w-32 h-32 bg-gradient-to-br from-primary-100/20 to-secondary-100/20 rounded-full blur-xl"
animate={{ y: [-20, 20, -20], scale: [0.9, 1.05, 0.9] }}
transition={{ duration: 18, repeat: Infinity, ease: 'easeInOut' }}
/>
</div>
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 relative z-10">
<RevealSection>
<SectionHeader
eyebrow="What we do"
title="Compassionate support when it matters most"
subtitle="Outreach, advocacy, emergency assistance, and resource navigation for vulnerable populations."
/>
</RevealSection>
<div className="mt-10 grid gap-6 md:grid-cols-3">
{items.map((i, idx) => (
<RevealSection key={idx} delay={idx * 0.06}>
<FeatureCard icon={i.icon} title={i.title} body={i.body} />
</RevealSection>
))}
</div>
<div className="mt-10 text-center">
<a href="/what-we-do" className="btn-secondary">
See all programs <ArrowRight className="ml-1 inline h-4 w-4" />
</a>
</div>
</div>
</section>
)
}
function FeatureCard({ icon: Icon, title, body }: FeatureCardProps) {
const { reduceMotion } = useMotionSafe()
return (
<motion.div
className="group card card-hover cursor-pointer"
{...hoverMotionProps(reduceMotion, { scale: 1.02 })}
transition={{ duration: 0.2, ease: 'easeOut' }}
>
<div className="flex items-center gap-3">
<div className="grid h-10 w-10 place-items-center rounded-xl bg-gradient-to-br from-primary-500 to-secondary-600 text-white shadow">
<Icon className="h-5 w-5" aria-hidden="true" />
</div>
<div className="font-semibold tracking-tight">{title}</div>
</div>
<p className="mt-3 text-sm leading-6 text-neutral-700 dark:text-neutral-300">{body}</p>
<a
href="/what-we-do"
className="mt-5 inline-flex items-center text-sm text-primary-700 transition hover:underline dark:text-primary-300"
>
Learn more <ArrowRight className="ml-1 h-4 w-4" />
</a>
</motion.div>
)
}
function Impact() {
const pillars = [
{ label: 'Community outreach', desc: 'Direct support for underserved families' },
{ label: 'Emergency assistance', desc: 'Crisis, fire, and disaster relief' },
{ label: 'Advocacy & navigation', desc: 'Connecting people to hope and resources' },
{ label: 'Faith & restoration', desc: 'Dignity, empathy, and compassionate care' },
]
return (
<section id="impact" className="relative py-24 overflow-hidden">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 relative z-10">
<RevealSection>
<SectionHeader eyebrow="Our heart" title="Healing begins when people feel seen" subtitle={SITE.closingLine} />
</RevealSection>
<div className="mt-12 grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
{pillars.map((s, i) => (
<RevealSection key={i} delay={i * 0.06}>
<div className="card h-full text-center">
<div className="font-semibold tracking-tight text-primary-800 dark:text-primary-100">{s.label}</div>
<p className="mt-2 text-sm text-neutral-600 dark:text-neutral-400">{s.desc}</p>
</div>
</RevealSection>
))}
</div>
<div className="mt-10 grid items-center gap-6 rounded-2xl border border-secondary-300/30 bg-secondary-50/50 p-6 sm:grid-cols-2 dark:border-secondary-700/30 dark:bg-secondary-900/20">
<div className="text-sm text-primary-900 dark:text-primary-100">
<div className="font-medium">Your gift restores hope</div>
<p className="mt-3 leading-relaxed">
Donations support outreach, emergency assistance, wellness support, and resource navigation for women,
families, survivors, and communities in crisis throughout Los Angeles County.
</p>
</div>
<div className="flex flex-wrap items-center justify-end gap-3">
<a href="/donate" className="btn-primary">
Donate <ArrowRight className="ml-2 h-4 w-4" />
</a>
<a href="/stories" className="btn-secondary">
Stories of hope
</a>
</div>
</div>
</div>
</section>
)
}
function HowItWorks() {
const steps = HOW_IT_WORKS.steps.map((s) => ({ title: s.title, body: s.body }))
return (
<section id="how" className="relative py-24 overflow-hidden">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 relative z-10">
<RevealSection>
<SectionHeader eyebrow={HOW_IT_WORKS.eyebrow} title={HOW_IT_WORKS.title} subtitle={HOW_IT_WORKS.subtitle} />
</RevealSection>
<div className="mt-10 grid gap-6 md:grid-cols-2">
<RevealSection>
<div className="card">
<ol className="relative space-y-6">
{steps.map((s, i) => (
<li key={i} className="relative pl-8">
<div
className="absolute left-0 top-1 grid h-6 w-6 place-items-center rounded-full bg-gradient-to-br from-primary-500 to-secondary-600 text-white shadow text-xs"
aria-hidden="true"
>
{i + 1}
</div>
<div className="font-medium">{s.title}</div>
<p className="text-sm text-neutral-700 dark:text-neutral-300">{s.body}</p>
</li>
))}
</ol>
</div>
</RevealSection>
<div className="grid gap-6">
<RevealSection delay={0.08}>
<div className="rounded-2xl border border-primary-500/20 bg-primary-50/80 p-6 dark:border-primary-700/30 dark:bg-primary-900/20">
<div className="flex items-center gap-3">
<div className="grid h-10 w-10 place-items-center rounded-xl bg-primary-600 text-white">
<Heart className="h-5 w-5" aria-hidden="true" />
</div>
<div className="font-semibold tracking-tight">Donor promise</div>
</div>
<p className="mt-2 text-sm text-primary-900 dark:text-primary-100">{HOW_IT_WORKS.donorPromise}</p>
</div>
</RevealSection>
<RevealSection delay={0.12}>
<div className="rounded-2xl border border-secondary-500/20 bg-secondary-500/10 p-6">
<div className="flex items-center gap-3">
<div className="grid h-10 w-10 place-items-center rounded-xl bg-secondary-500 text-white">
<Users className="h-5 w-5" aria-hidden="true" />
</div>
<div className="font-semibold tracking-tight">For partners & volunteers</div>
</div>
<p className="mt-2 text-sm text-secondary-950 dark:text-secondary-100">{HOW_IT_WORKS.partnerNote}</p>
</div>
</RevealSection>
</div>
</div>
</div>
</section>
)
}
function Testimonial() {
return (
<section className="relative py-20">
<div className="mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
<div className="card p-8">
<div className="absolute -left-10 -top-10 h-60 w-60 rounded-full bg-gradient-to-tr from-primary-500/30 to-secondary-600/30 blur-3xl" />
<blockquote className="text-balance text-xl leading-8 text-neutral-800 dark:text-neutral-100">
&ldquo;{HOME_TESTIMONIAL.quote}&rdquo;
</blockquote>
<div className="mt-4 flex items-center gap-3 text-sm text-neutral-600 dark:text-neutral-300">
<Star className="h-4 w-4 text-secondary-500" />
{HOME_TESTIMONIAL.attribution}
</div>
</div>
</div>
</section>
)
}
function GetInvolved() {
const options = [
{
title: 'Donate',
body: 'Restore hope through outreach, emergency aid, and compassionate care.',
href: '/donate',
accent: 'from-secondary-500 to-primary-500',
icon: Heart,
},
{
title: 'Volunteer',
body: 'Serve at outreach events and support families in your community.',
href: '/volunteers',
accent: 'from-sky-500 to-secondary-500',
icon: Users,
},
{
title: 'Partner with us',
body: "Churches, businesses, and organizations — let's serve together.",
href: '/contact',
accent: 'from-primary-500 to-secondary-500',
icon: Globe,
},
]
return (
<section id="get-involved" className="relative py-24 overflow-hidden">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<RevealSection>
<SectionHeader
eyebrow="Get involved"
title="There is a role for everyone"
subtitle="Give, volunteer, or partner — every act of compassion builds hope."
/>
</RevealSection>
<div className="mt-10 grid gap-6 md:grid-cols-3">
{options.map((o, i) => (
<RevealSection key={i} delay={i * 0.06}>
<Callout {...o} index={i} />
</RevealSection>
))}
</div>
</div>
</section>
)
}
function Callout({ title, body, href, accent, icon: Icon, index = 0 }: CalloutProps) {
const { reduceMotion } = useMotionSafe()
const enterMotion = reduceMotion
? { initial: false as const }
: {
initial: { opacity: 0, y: 16 } as const,
whileInView: { opacity: 1, y: 0 } as const,
viewport: { once: true } as const,
transition: { duration: 0.5, delay: index * 0.08 } as const,
}
return (
<motion.div className="group card card-hover" {...enterMotion} {...hoverMotionProps(reduceMotion, { scale: 1.02 })}>
<div
className={`absolute -right-10 -top-10 h-36 w-36 rounded-full bg-gradient-to-br ${accent} opacity-30 blur-2xl group-hover:opacity-50 transition-opacity duration-300`}
aria-hidden="true"
/>
<div className={`mb-3 grid h-12 w-12 place-items-center rounded-xl bg-gradient-to-br ${accent} text-white shadow`}>
<Icon className="h-6 w-6" aria-hidden="true" />
</div>
<div className="font-semibold tracking-tight">{title}</div>
<p className="mt-2 text-sm text-neutral-700 dark:text-neutral-300">{body}</p>
<a
href={href}
className="mt-4 inline-flex items-center text-sm text-neutral-800 underline-offset-4 hover:underline dark:text-neutral-200"
>
Learn more <ArrowRight className="ml-1 h-4 w-4" aria-hidden="true" />
</a>
</motion.div>
)
}
function CTA() {
return (
<section className="relative py-24">
<div className="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
<div className="relative overflow-hidden rounded-3xl border border-white/30 bg-gradient-to-br from-primary-600 via-secondary-600 to-secondary-700 p-8 text-white shadow-2xl dark:border-white/10">
<div
className="absolute inset-0 opacity-30 [mask-image:radial-gradient(60%_80%_at_70%_50%,white,transparent)]"
style={{
backgroundImage:
"url('data:image/svg+xml;utf8,<svg xmlns=\\'http://www.w3.org/2000/svg\\' width=\\'400\\' height=\\'400\\'><defs><pattern id=\\'g\\' width=\\'40\\' height=\\'40\\' patternUnits=\\'userSpaceOnUse\\'><path d=\\'M0 20 H40 M20 0 V40\\' stroke=\\'white\\' stroke-opacity=\\'0.25\\' stroke-width=\\'1\\'/></pattern></defs><rect width=\\'100%\\' height=\\'100%\\' fill=\\'url(%23g)\\'/></svg>')",
}}
/>
<div className="grid items-center gap-8 md:grid-cols-2">
<div>
<h2 className="font-serif text-balance text-3xl font-semibold tracking-tight sm:text-4xl">
Help restore hope in someone&apos;s hardest season
</h2>
<p className="mt-3 max-w-md text-white/90">
Your generosity supports outreach, emergency assistance, and resource navigation for families facing
crisis throughout Los Angeles County.
</p>
<div className="mt-6 flex flex-wrap items-center gap-3">
<a href="/donate" className="btn-white">
Donate now
</a>
<a
href="/request-assistance"
className="inline-flex items-center gap-2 rounded-full border-2 border-white/40 px-6 py-3 text-sm font-medium text-white backdrop-blur transition hover:bg-white/10 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2"
>
Request assistance <ArrowRight className="ml-1 h-4 w-4" />
</a>
</div>
</div>
<div className="relative">
<div className="rounded-2xl border border-white/40 bg-white/10 p-6 backdrop-blur">
<h3 className="font-medium tracking-tight">Contact us</h3>
<ul className="mt-3 space-y-2 text-sm text-white/90">
<li className="flex items-center gap-2">
<Mail className="h-4 w-4" />{' '}
<a href={SITE.emailHref} className="underline">
{SITE.email}
</a>
</li>
<li className="flex items-start gap-2">
<MapPin className="mt-0.5 h-4 w-4 shrink-0" /> {SITE.serviceArea}
</li>
</ul>
<p className="mt-3 text-xs text-white/80">
{SITE.legalStatus}. EIN {SITE.ein}.
</p>
</div>
</div>
</div>
</div>
</div>
</section>
)
}
function SectionHeader({ eyebrow, title, subtitle }: SectionHeaderProps) {
return (
<div className="section-header">
{eyebrow && <div className="section-eyebrow">{eyebrow}</div>}
<h2 className="section-title">{title}</h2>
{subtitle && <p className="section-subtitle">{subtitle}</p>}
</div>
)
}