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]>
418 lines
16 KiB
TypeScript
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">
|
|
“{HOME_TESTIMONIAL.quote}”
|
|
</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'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>
|
|
)
|
|
}
|