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 { motion, useMotionValue, useSpring, useTransform } from 'framer-motion'
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import { motion, useMotionValue, useSpring, useTransform, useScroll } from 'framer-motion'
|
||||
import {
|
||||
ArrowRight,
|
||||
Backpack,
|
||||
@@ -64,6 +64,7 @@ interface CalloutProps {
|
||||
href: string
|
||||
accent: string
|
||||
icon: React.ComponentType<IconProps>
|
||||
index?: number
|
||||
}
|
||||
|
||||
interface PageShellProps {
|
||||
@@ -86,6 +87,71 @@ interface PolicySectionProps {
|
||||
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 ===================== */
|
||||
function useHashRoute() {
|
||||
const parse = () => (window.location.hash?.slice(1) || "/")
|
||||
@@ -252,16 +318,38 @@ function HomePage() {
|
||||
}
|
||||
|
||||
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 (
|
||||
<section className="relative isolate overflow-hidden">
|
||||
<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">
|
||||
<div>
|
||||
<section ref={containerRef} className="relative isolate overflow-hidden min-h-screen flex items-center">
|
||||
{/* 3D Background Layer */}
|
||||
<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
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
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—
|
||||
<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"/> Community-driven impact</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="relative">
|
||||
</ParallaxContainer>
|
||||
|
||||
<ParallaxContainer depth={0.5} className="relative">
|
||||
<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>
|
||||
</section>
|
||||
)
|
||||
@@ -421,12 +550,30 @@ function Impact() {
|
||||
]
|
||||
|
||||
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">
|
||||
<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">
|
||||
{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 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 (
|
||||
<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">
|
||||
<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">
|
||||
{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>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function Callout({ title, body, href, accent, icon: Icon }: CalloutProps) {
|
||||
function Callout({ title, body, href, accent, icon: Icon, index = 0 }: CalloutProps) {
|
||||
return (
|
||||
<div className="group card card-hover">
|
||||
<div className={`absolute -right-10 -top-10 h-36 w-36 rounded-full bg-gradient-to-br ${accent} opacity-30 blur-2xl`} />
|
||||
<div className={`mb-3 grid h-12 w-12 place-items-center rounded-xl bg-gradient-to-br ${accent} text-white shadow`}>
|
||||
<motion.div
|
||||
className="group card card-hover transform-gpu"
|
||||
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" />
|
||||
</div>
|
||||
</motion.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">
|
||||
|
||||
<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" />
|
||||
</a>
|
||||
</div>
|
||||
</motion.a>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -62,9 +62,27 @@ module.exports = {
|
||||
backdropBlur: {
|
||||
xs: '2px',
|
||||
},
|
||||
perspective: {
|
||||
'300': '300px',
|
||||
'500': '500px',
|
||||
'1000': '1000px',
|
||||
'2000': '2000px',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
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