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:
defiQUG
2025-10-04 20:52:35 -07:00
parent 0393deeaa7
commit e1de9fa161
2 changed files with 222 additions and 23 deletions

View File

@@ -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>
)
}

View File

@@ -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' },
})
}
],
}