feat: implement 3D parallax effects and floating particles in the main app layout; enhance Tailwind CSS configuration with perspective utilities
This commit is contained in:
227
src/App.tsx
227
src/App.tsx
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect, useRef } from 'react'
|
||||||
import { motion, useMotionValue, useSpring, useTransform } from 'framer-motion'
|
import { motion, useMotionValue, useSpring, useTransform, useScroll } from 'framer-motion'
|
||||||
import {
|
import {
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
Backpack,
|
Backpack,
|
||||||
@@ -64,6 +64,7 @@ interface CalloutProps {
|
|||||||
href: string
|
href: string
|
||||||
accent: string
|
accent: string
|
||||||
icon: React.ComponentType<IconProps>
|
icon: React.ComponentType<IconProps>
|
||||||
|
index?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PageShellProps {
|
interface PageShellProps {
|
||||||
@@ -86,6 +87,71 @@ interface PolicySectionProps {
|
|||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===================== 3D Parallax Components ===================== */
|
||||||
|
|
||||||
|
// 3D Parallax Container Component
|
||||||
|
interface ParallaxContainerProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
depth?: number
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function ParallaxContainer({ children, depth = 1, className = '' }: ParallaxContainerProps) {
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
const { scrollYProgress } = useScroll({
|
||||||
|
target: ref,
|
||||||
|
offset: ['start end', 'end start']
|
||||||
|
})
|
||||||
|
|
||||||
|
const y = useTransform(scrollYProgress, [0, 1], [0, -50 * depth])
|
||||||
|
const rotateX = useTransform(scrollYProgress, [0, 1], [0, 5 * depth])
|
||||||
|
const scale = useTransform(scrollYProgress, [0, 0.5, 1], [0.8, 1, 1.1])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
ref={ref}
|
||||||
|
style={{ y, rotateX, scale }}
|
||||||
|
className={`transform-gpu ${className}`}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
whileInView={{ opacity: 1 }}
|
||||||
|
viewport={{ once: true, margin: '-100px' }}
|
||||||
|
transition={{ duration: 0.6, ease: 'easeOut' }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Floating Particles Background
|
||||||
|
function FloatingParticles() {
|
||||||
|
const particles = Array.from({ length: 50 }, (_, i) => i)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||||
|
{particles.map((i) => (
|
||||||
|
<motion.div
|
||||||
|
key={i}
|
||||||
|
className="absolute w-1 h-1 bg-primary-400/20 rounded-full"
|
||||||
|
style={{
|
||||||
|
left: `${Math.random() * 100}%`,
|
||||||
|
top: `${Math.random() * 100}%`,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
y: [-20, -100, -20],
|
||||||
|
x: [-10, 10, -10],
|
||||||
|
opacity: [0, 1, 0],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: Math.random() * 10 + 5,
|
||||||
|
repeat: Infinity,
|
||||||
|
delay: Math.random() * 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/* ===================== Router ===================== */
|
/* ===================== Router ===================== */
|
||||||
function useHashRoute() {
|
function useHashRoute() {
|
||||||
const parse = () => (window.location.hash?.slice(1) || "/")
|
const parse = () => (window.location.hash?.slice(1) || "/")
|
||||||
@@ -252,16 +318,38 @@ function HomePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Hero() {
|
function Hero() {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const { scrollYProgress } = useScroll({
|
||||||
|
target: containerRef,
|
||||||
|
offset: ['start start', 'end start']
|
||||||
|
})
|
||||||
|
|
||||||
|
const backgroundY = useTransform(scrollYProgress, [0, 1], ['0%', '50%'])
|
||||||
|
const contentY = useTransform(scrollYProgress, [0, 1], ['0%', '100%'])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative isolate overflow-hidden">
|
<section ref={containerRef} className="relative isolate overflow-hidden min-h-screen flex items-center">
|
||||||
<div className="mx-auto grid max-w-7xl items-center gap-10 px-4 py-20 sm:px-6 lg:grid-cols-2 lg:gap-12 lg:py-28 lg:px-8">
|
{/* 3D Background Layer */}
|
||||||
<div>
|
<motion.div
|
||||||
|
className="absolute inset-0 bg-gradient-to-br from-primary-50/50 via-white to-secondary-50/50 dark:from-gray-900/50 dark:via-gray-800/50 dark:to-gray-900/50"
|
||||||
|
style={{ y: backgroundY }}
|
||||||
|
>
|
||||||
|
<FloatingParticles />
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Content Layer with Parallax */}
|
||||||
|
<motion.div
|
||||||
|
className="mx-auto grid max-w-7xl items-center gap-10 px-4 py-20 sm:px-6 lg:grid-cols-2 lg:gap-12 lg:py-28 lg:px-8 relative z-10"
|
||||||
|
style={{ y: contentY }}
|
||||||
|
>
|
||||||
|
<ParallaxContainer depth={0.3}>
|
||||||
<motion.h1
|
<motion.h1
|
||||||
initial={{ opacity: 0, y: 12 }}
|
initial={{ opacity: 0, y: 12 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ duration: 0.6 }}
|
transition={{ duration: 0.6 }}
|
||||||
className="text-balance text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl"
|
className="text-balance text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl transform-gpu"
|
||||||
|
style={{ transformStyle: 'preserve-3d' }}
|
||||||
>
|
>
|
||||||
Equipping kids for success—
|
Equipping kids for success—
|
||||||
<span className="relative whitespace-pre gradient-text"> school supplies, clothing, & more</span>
|
<span className="relative whitespace-pre gradient-text"> school supplies, clothing, & more</span>
|
||||||
@@ -287,10 +375,51 @@ function Hero() {
|
|||||||
<li className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-emerald-500"/> Donations tax-deductible</li>
|
<li className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-emerald-500"/> Donations tax-deductible</li>
|
||||||
<li className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-emerald-500"/> Community-driven impact</li>
|
<li className="flex items-center gap-2"><CheckCircle2 className="h-4 w-4 text-emerald-500"/> Community-driven impact</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</ParallaxContainer>
|
||||||
<div className="relative">
|
|
||||||
|
<ParallaxContainer depth={0.5} className="relative">
|
||||||
<HeroShowcase />
|
<HeroShowcase />
|
||||||
</div>
|
</ParallaxContainer>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* 3D Floating Elements */}
|
||||||
|
<div className="absolute inset-0 pointer-events-none">
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-20 left-10 text-primary-300 dark:text-primary-600"
|
||||||
|
animate={{
|
||||||
|
y: [-10, 10, -10],
|
||||||
|
rotateY: [0, 360],
|
||||||
|
z: [0, 50, 0]
|
||||||
|
}}
|
||||||
|
transition={{ duration: 6, repeat: Infinity, ease: "easeInOut" }}
|
||||||
|
style={{ transformStyle: 'preserve-3d' }}
|
||||||
|
>
|
||||||
|
<Heart className="h-8 w-8" />
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-32 right-20 text-secondary-300 dark:text-secondary-600"
|
||||||
|
animate={{
|
||||||
|
y: [10, -10, 10],
|
||||||
|
rotateX: [0, 180, 360],
|
||||||
|
z: [0, 30, 0]
|
||||||
|
}}
|
||||||
|
transition={{ duration: 4, repeat: Infinity, ease: "easeInOut", delay: 1 }}
|
||||||
|
style={{ transformStyle: 'preserve-3d' }}
|
||||||
|
>
|
||||||
|
<Sparkles className="h-6 w-6" />
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
className="absolute bottom-32 left-20 text-primary-300 dark:text-primary-600"
|
||||||
|
animate={{
|
||||||
|
y: [-8, 8, -8],
|
||||||
|
rotateZ: [0, 180, 360],
|
||||||
|
z: [0, 40, 0]
|
||||||
|
}}
|
||||||
|
transition={{ duration: 8, repeat: Infinity, ease: "easeInOut", delay: 2 }}
|
||||||
|
style={{ transformStyle: 'preserve-3d' }}
|
||||||
|
>
|
||||||
|
<Users className="h-7 w-7" />
|
||||||
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
@@ -421,12 +550,30 @@ function Impact() {
|
|||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="impact" className="relative py-24">
|
<section id="impact" className="relative py-24 overflow-hidden">
|
||||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
<SectionHeader eyebrow="Impact" title="Every gift moves a student forward" subtitle="Transparent, measurable outcomes powered by local partnerships." />
|
<ParallaxContainer depth={0.3}>
|
||||||
|
<SectionHeader eyebrow="Impact" title="Every gift moves a student forward" subtitle="Transparent, measurable outcomes powered by local partnerships." />
|
||||||
|
</ParallaxContainer>
|
||||||
|
|
||||||
<div className="mt-12 grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
<div className="mt-12 grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
{stats.map((s, i) => (
|
{stats.map((s, i) => (
|
||||||
<Stat key={i} label={s.label} value={s.value} />
|
<ParallaxContainer key={i} depth={0.4 + i * 0.1}>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.6, delay: i * 0.1 }}
|
||||||
|
whileHover={{
|
||||||
|
scale: 1.05,
|
||||||
|
rotateY: 5,
|
||||||
|
z: 50
|
||||||
|
}}
|
||||||
|
style={{ transformStyle: 'preserve-3d' }}
|
||||||
|
>
|
||||||
|
<Stat label={s.label} value={s.value} />
|
||||||
|
</motion.div>
|
||||||
|
</ParallaxContainer>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-10 grid items-center gap-6 rounded-2xl border border-emerald-500/20 bg-emerald-500/10 p-6 sm:grid-cols-2">
|
<div className="mt-10 grid items-center gap-6 rounded-2xl border border-emerald-500/20 bg-emerald-500/10 p-6 sm:grid-cols-2">
|
||||||
@@ -549,30 +696,64 @@ function GetInvolved() {
|
|||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="get-involved" className="relative py-24">
|
<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">
|
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
<SectionHeader eyebrow="Get involved" title="There's a role for everyone" subtitle="Give monthly, share skills, or introduce us to your school." />
|
<ParallaxContainer depth={0.3}>
|
||||||
|
<SectionHeader eyebrow="Get involved" title="There's a role for everyone" subtitle="Give monthly, share skills, or introduce us to your school." />
|
||||||
|
</ParallaxContainer>
|
||||||
|
|
||||||
<div className="mt-10 grid gap-6 md:grid-cols-3">
|
<div className="mt-10 grid gap-6 md:grid-cols-3">
|
||||||
{options.map((o, i) => (<Callout key={i} {...o} />))}
|
{options.map((o, i) => (
|
||||||
|
<ParallaxContainer key={i} depth={0.5 + i * 0.1}>
|
||||||
|
<Callout {...o} index={i} />
|
||||||
|
</ParallaxContainer>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Callout({ title, body, href, accent, icon: Icon }: CalloutProps) {
|
function Callout({ title, body, href, accent, icon: Icon, index = 0 }: CalloutProps) {
|
||||||
return (
|
return (
|
||||||
<div className="group card card-hover">
|
<motion.div
|
||||||
<div className={`absolute -right-10 -top-10 h-36 w-36 rounded-full bg-gradient-to-br ${accent} opacity-30 blur-2xl`} />
|
className="group card card-hover transform-gpu"
|
||||||
<div className={`mb-3 grid h-12 w-12 place-items-center rounded-xl bg-gradient-to-br ${accent} text-white shadow`}>
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.6, delay: index * 0.1 }}
|
||||||
|
whileHover={{
|
||||||
|
scale: 1.05,
|
||||||
|
rotateY: index % 2 === 0 ? 5 : -5,
|
||||||
|
z: 50
|
||||||
|
}}
|
||||||
|
style={{ transformStyle: 'preserve-3d' }}
|
||||||
|
>
|
||||||
|
<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`} />
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className={`mb-3 grid h-12 w-12 place-items-center rounded-xl bg-gradient-to-br ${accent} text-white shadow`}
|
||||||
|
whileHover={{
|
||||||
|
rotateY: 180,
|
||||||
|
scale: 1.1
|
||||||
|
}}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
<Icon className="h-6 w-6" />
|
<Icon className="h-6 w-6" />
|
||||||
</div>
|
</motion.div>
|
||||||
|
|
||||||
<div className="font-semibold tracking-tight">{title}</div>
|
<div className="font-semibold tracking-tight">{title}</div>
|
||||||
<p className="mt-2 text-sm text-neutral-700 dark:text-neutral-300">{body}</p>
|
<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">
|
|
||||||
|
<motion.a
|
||||||
|
href={href}
|
||||||
|
className="mt-4 inline-flex items-center text-sm text-neutral-800 underline-offset-4 hover:underline dark:text-neutral-200"
|
||||||
|
whileHover={{ x: 5 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
Learn more <ArrowRight className="ml-1 h-4 w-4" />
|
Learn more <ArrowRight className="ml-1 h-4 w-4" />
|
||||||
</a>
|
</motion.a>
|
||||||
</div>
|
</motion.div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,9 +62,27 @@ module.exports = {
|
|||||||
backdropBlur: {
|
backdropBlur: {
|
||||||
xs: '2px',
|
xs: '2px',
|
||||||
},
|
},
|
||||||
|
perspective: {
|
||||||
|
'300': '300px',
|
||||||
|
'500': '500px',
|
||||||
|
'1000': '1000px',
|
||||||
|
'2000': '2000px',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
require('@tailwindcss/typography'),
|
require('@tailwindcss/typography'),
|
||||||
|
function({ addUtilities }) {
|
||||||
|
addUtilities({
|
||||||
|
'.perspective-300': { perspective: '300px' },
|
||||||
|
'.perspective-500': { perspective: '500px' },
|
||||||
|
'.perspective-1000': { perspective: '1000px' },
|
||||||
|
'.perspective-2000': { perspective: '2000px' },
|
||||||
|
'.transform-style-preserve-3d': { 'transform-style': 'preserve-3d' },
|
||||||
|
'.transform-style-flat': { 'transform-style': 'flat' },
|
||||||
|
'.backface-hidden': { 'backface-visibility': 'hidden' },
|
||||||
|
'.backface-visible': { 'backface-visibility': 'visible' },
|
||||||
|
})
|
||||||
|
}
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user