feat: Apple-style scroll-driven landing page
- Add ScrollAnimations component (ScrollReveal, ParallaxLayer, FadeOnScroll) - Add CinematicShowcase for scroll-driven product reveals - Update Tuxedo page with cinematic product showcase and parallax layers - Update main HeroSection with same premium scroll animations - Add scroll progress bar, staggered reveals, counter animations - Apply consistent Apple-style UX across landing and Tuxedo pages Features: - Sections pin and reveal content progressively on scroll - Parallax depth effects on decorative elements - Counter animations triggered by scroll - Smooth hover states and transitions - GSAP + Framer Motion for smooth animations
This commit is contained in:
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import HeroSection from "@/components/landing/HeroSection";
|
import HeroSection from "@/components/landing/HeroSection";
|
||||||
import FeaturesAndStats from "@/components/landing/FeaturesAndStats";
|
|
||||||
import TestimonialsAndCTA from "@/components/landing/TestimonialsAndCTA";
|
|
||||||
import { gsap } from "gsap";
|
import { gsap } from "gsap";
|
||||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||||
|
|
||||||
@@ -20,21 +18,6 @@ export default function LandingPageClient() {
|
|||||||
if (typeof window === "undefined" || !mainRef.current) return;
|
if (typeof window === "undefined" || !mainRef.current) return;
|
||||||
|
|
||||||
const ctx = gsap.context(() => {
|
const ctx = gsap.context(() => {
|
||||||
// Parallax sections
|
|
||||||
const parallaxElements = gsap.utils.toArray<Element>(".parallax-section");
|
|
||||||
parallaxElements.forEach((el) => {
|
|
||||||
gsap.to(el, {
|
|
||||||
y: -50,
|
|
||||||
ease: "none",
|
|
||||||
scrollTrigger: {
|
|
||||||
trigger: el,
|
|
||||||
start: "top bottom",
|
|
||||||
end: "bottom top",
|
|
||||||
scrub: 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reveal animations for sections
|
// Reveal animations for sections
|
||||||
const revealElements = gsap.utils.toArray<Element>(".scroll-reveal");
|
const revealElements = gsap.utils.toArray<Element>(".scroll-reveal");
|
||||||
revealElements.forEach((el) => {
|
revealElements.forEach((el) => {
|
||||||
@@ -62,8 +45,6 @@ export default function LandingPageClient() {
|
|||||||
return (
|
return (
|
||||||
<div ref={mainRef}>
|
<div ref={mainRef}>
|
||||||
<HeroSection />
|
<HeroSection />
|
||||||
<FeaturesAndStats />
|
|
||||||
<TestimonialsAndCTA />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+185
-111
@@ -656,42 +656,74 @@ export default function TuxedoPage() {
|
|||||||
onSecondaryClick={scrollToStory}
|
onSecondaryClick={scrollToStory}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* ─── CINEMATIC PRODUCT SHOWCASE ────────────────────────────────── */}
|
||||||
|
<CinematicShowcase
|
||||||
|
products={featuredProducts.map((p) => ({
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
description: p.description ?? "",
|
||||||
|
price: `$${p.price}`,
|
||||||
|
type: p.type,
|
||||||
|
imageUrl: p.image_url,
|
||||||
|
}))}
|
||||||
|
brandSlug="tuxedo"
|
||||||
|
/>
|
||||||
|
|
||||||
<WhyTuxedoCorn />
|
<WhyTuxedoCorn />
|
||||||
|
|
||||||
<section id="stops" className="py-28 bg-gradient-to-b from-stone-100 to-stone-50 scroll-mt-20">
|
<section id="stops" className="relative bg-gradient-to-b from-stone-100 to-stone-50 scroll-mt-20">
|
||||||
<LayoutContainer>
|
{/* Parallax background layers */}
|
||||||
<div className="mb-14 flex items-end justify-between">
|
<ParallaxLayer speed={0.2} className="absolute inset-0 pointer-events-none">
|
||||||
<div>
|
<div className="absolute top-0 right-0 w-[40%] h-full bg-gradient-to-l from-emerald-50/30 to-transparent" />
|
||||||
<p className="reveal-slide text-[11px] font-semibold uppercase tracking-widest text-emerald-600 mb-4">Delivery Stops</p>
|
</ParallaxLayer>
|
||||||
<h2 className="reveal-text text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-black tracking-tight text-stone-950 leading-[1.05]">
|
|
||||||
Upcoming<br className="hidden md:block" /> Stops
|
<div className="relative py-28">
|
||||||
</h2>
|
<LayoutContainer>
|
||||||
<div className="reveal-scale mt-6 h-px w-12 bg-emerald-600" />
|
<div className="mb-14 flex items-end justify-between">
|
||||||
<p className="reveal-text mt-6 max-w-2xl text-lg text-stone-500 leading-relaxed">
|
<div>
|
||||||
Find a nearby stop and preorder your corn. Pickup is easy and guaranteed.
|
<FadeOnScroll from="left">
|
||||||
</p>
|
<p className="text-[11px] font-semibold uppercase tracking-widest text-emerald-600 mb-4">Delivery Stops</p>
|
||||||
|
</FadeOnScroll>
|
||||||
|
<FadeOnScroll from="up" delay={0.1}>
|
||||||
|
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-black tracking-tight text-stone-950 leading-[1.05]">
|
||||||
|
Upcoming<br className="hidden md:block" /> Stops
|
||||||
|
</h2>
|
||||||
|
</FadeOnScroll>
|
||||||
|
<FadeOnScroll from="up" delay={0.15}>
|
||||||
|
<div className="mt-6 h-px w-12 bg-emerald-600" />
|
||||||
|
</FadeOnScroll>
|
||||||
|
<FadeOnScroll from="up" delay={0.2}>
|
||||||
|
<p className="mt-6 max-w-2xl text-lg text-stone-500 leading-relaxed">
|
||||||
|
Find a nearby stop and preorder your corn. Pickup is easy and guaranteed.
|
||||||
|
</p>
|
||||||
|
</FadeOnScroll>
|
||||||
|
</div>
|
||||||
|
{showSchedulePdf && (
|
||||||
|
<FadeOnScroll from="right" delay={0.3}>
|
||||||
|
<a
|
||||||
|
href="/api/tuxedo/schedule-pdf"
|
||||||
|
download
|
||||||
|
className="shrink-0 hidden md:inline-flex items-center gap-2.5 rounded-2xl bg-stone-900 px-6 py-3.5 text-sm font-semibold text-white hover:bg-stone-800 active:bg-stone-950 transition-colors"
|
||||||
|
>
|
||||||
|
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
|
</svg>
|
||||||
|
Download Schedule
|
||||||
|
</a>
|
||||||
|
</FadeOnScroll>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{showSchedulePdf && (
|
<FadeOnScroll from="up" delay={0.3}>
|
||||||
<a
|
{stops.length === 0 ? (
|
||||||
href="/api/tuxedo/schedule-pdf"
|
<div className="rounded-3xl bg-white p-14 text-center ring-1 ring-stone-200/60">
|
||||||
download
|
<p className="text-stone-500">No upcoming stops scheduled. Check back soon!</p>
|
||||||
className="shrink-0 hidden md:inline-flex items-center gap-2.5 rounded-2xl bg-stone-900 px-6 py-3.5 text-sm font-semibold text-white hover:bg-stone-800 active:bg-stone-950 transition-colors"
|
</div>
|
||||||
>
|
) : (
|
||||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
<PaginatedStops stops={stops} brandSlug="tuxedo" brandAccent="green" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
)}
|
||||||
</svg>
|
</FadeOnScroll>
|
||||||
Download Schedule
|
</LayoutContainer>
|
||||||
</a>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{stops.length === 0 ? (
|
|
||||||
<div className="rounded-3xl bg-white p-14 text-center ring-1 ring-stone-200/60">
|
|
||||||
<p className="text-stone-500">No upcoming stops scheduled. Check back soon!</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<PaginatedStops stops={stops} brandSlug="tuxedo" brandAccent="green" />
|
|
||||||
)}
|
|
||||||
</LayoutContainer>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Section divider */}
|
{/* Section divider */}
|
||||||
@@ -707,25 +739,41 @@ export default function TuxedoPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section id="products" className="py-28 bg-white">
|
<section id="products" className="relative bg-white">
|
||||||
<LayoutContainer>
|
{/* Scroll-driven reveal for section header */}
|
||||||
<div className="reveal-slide mb-14">
|
<div className="py-20 bg-gradient-to-b from-stone-50 to-white">
|
||||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-emerald-600 mb-4">Farm-Direct</p>
|
<LayoutContainer>
|
||||||
<h2 className="reveal-text text-5xl md:text-6xl font-black tracking-tight text-stone-950 leading-[1.05]">
|
<ScrollReveal from="up" className="mb-14">
|
||||||
This Week's<br className="hidden md:block" /> Harvest
|
<p className="text-[11px] font-semibold uppercase tracking-widest text-emerald-600 mb-4">Farm-Direct</p>
|
||||||
</h2>
|
<h2 className="text-5xl md:text-6xl font-black tracking-tight text-stone-950 leading-[1.05]">
|
||||||
<div className="reveal-scale mt-6 h-px w-12 bg-emerald-600" />
|
This Week's<br className="hidden md:block" /> Harvest
|
||||||
<p className="reveal-text mt-6 max-w-2xl text-lg text-stone-500 leading-relaxed">
|
</h2>
|
||||||
Preorder for pickup at a stop, or have cooler boxes shipped directly to your door after the season.
|
<div className="mt-6 h-px w-12 bg-emerald-600" />
|
||||||
</p>
|
<p className="mt-6 max-w-2xl text-lg text-stone-500 leading-relaxed">
|
||||||
</div>
|
Preorder for pickup at a stop, or have cooler boxes shipped directly to your door after the season.
|
||||||
{featuredProducts.length === 0 ? (
|
</p>
|
||||||
<div className="rounded-3xl bg-stone-50 p-14 text-center ring-1 ring-stone-200/60">
|
</ScrollReveal>
|
||||||
<p className="text-stone-500">No products available at this time.</p>
|
</LayoutContainer>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div className="grid gap-8 md:grid-cols-3">
|
{/* Cinematic showcase replaces static grid */}
|
||||||
{featuredProducts.map((product) => (
|
<CinematicShowcase
|
||||||
|
products={products.slice(0, 3).map((p) => ({
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
description: p.description ?? "",
|
||||||
|
price: `$${p.price}`,
|
||||||
|
type: p.type,
|
||||||
|
imageUrl: p.image_url,
|
||||||
|
}))}
|
||||||
|
brandSlug="tuxedo"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Mobile-friendly fallback grid for smaller screens */}
|
||||||
|
<div className="md:hidden py-16 bg-stone-50">
|
||||||
|
<LayoutContainer>
|
||||||
|
<div className="grid gap-8 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{products.slice(0, 6).map((product) => (
|
||||||
<ProductCard
|
<ProductCard
|
||||||
key={product.id}
|
key={product.id}
|
||||||
id={product.id}
|
id={product.id}
|
||||||
@@ -744,82 +792,108 @@ export default function TuxedoPage() {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</LayoutContainer>
|
||||||
{products.length > 3 && (
|
</div>
|
||||||
<div className="mt-10 text-center">
|
|
||||||
<p className="text-sm text-stone-400">{products.length - 3} more products available in the full catalog.</p>
|
{products.length > 3 && (
|
||||||
</div>
|
<div className="mt-10 text-center pb-16">
|
||||||
)}
|
<p className="text-sm text-stone-400">{products.length - 3} more products available in the full catalog.</p>
|
||||||
</LayoutContainer>
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="story" className="py-32 bg-stone-950 relative overflow-hidden">
|
<section id="story" className="py-32 bg-stone-950 relative overflow-hidden">
|
||||||
{/* Parallax decorative elements */}
|
{/* Parallax decorative elements */}
|
||||||
<div className="absolute inset-0 pointer-events-none">
|
<div className="absolute inset-0 pointer-events-none">
|
||||||
<div className="parallax-float absolute top-1/4 left-1/4 w-96 h-96 rounded-full opacity-5" style={{
|
<ParallaxLayer speed={0.3} className="absolute top-1/4 left-1/4 w-96 h-96 rounded-full opacity-5" style={{
|
||||||
background: "radial-gradient(circle, rgba(16,185,129,0.3) 0%, transparent 70%)",
|
background: "radial-gradient(circle, rgba(16,185,129,0.3) 0%, transparent 70%)",
|
||||||
filter: "blur(80px)",
|
filter: "blur(80px)",
|
||||||
}} />
|
}}>
|
||||||
<div className="parallax-float absolute bottom-1/4 right-1/4 w-64 h-64 rounded-full opacity-5" style={{
|
<div />
|
||||||
|
</ParallaxLayer>
|
||||||
|
<ParallaxLayer speed={0.5} className="absolute bottom-1/4 right-1/4 w-64 h-64 rounded-full opacity-5" style={{
|
||||||
background: "radial-gradient(circle, rgba(255,215,0,0.3) 0%, transparent 70%)",
|
background: "radial-gradient(circle, rgba(255,215,0,0.3) 0%, transparent 70%)",
|
||||||
filter: "blur(60px)",
|
filter: "blur(60px)",
|
||||||
animationDelay: "0.5s",
|
}}>
|
||||||
}} />
|
<div />
|
||||||
|
</ParallaxLayer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Large background text for parallax depth */}
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center overflow-hidden opacity-5 pointer-events-none">
|
||||||
|
<ParallaxLayer speed={0.2}>
|
||||||
|
<span className="text-[20vw] font-black text-white leading-none">SINCE</span>
|
||||||
|
</ParallaxLayer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LayoutContainer>
|
<LayoutContainer>
|
||||||
<div className="text-center max-w-3xl mx-auto relative z-10">
|
<div className="text-center max-w-3xl mx-auto relative z-10">
|
||||||
<p className="reveal-text text-[11px] font-semibold uppercase tracking-widest text-emerald-400/60 mb-4">Our Story</p>
|
<FadeOnScroll from="up" className="mb-8">
|
||||||
<h2 className="reveal-text text-4xl md:text-5xl lg:text-6xl font-black tracking-tight text-white leading-[1.05] mb-8">
|
<p className="text-[11px] font-semibold uppercase tracking-widest text-emerald-400/60 mb-4">Our Story</p>
|
||||||
Three Generations of<br className="hidden md:block" /> Sweet Corn Excellence
|
</FadeOnScroll>
|
||||||
</h2>
|
|
||||||
<div className="reveal-scale mx-auto mt-8 mb-10 h-px w-16 bg-gradient-to-r from-emerald-600 to-amber-500" />
|
<FadeOnScroll from="up" delay={0.1}>
|
||||||
<p className="reveal-text text-stone-400 leading-relaxed text-lg md:text-xl max-w-2xl mx-auto">
|
<h2 className="text-4xl md:text-5xl lg:text-6xl font-black tracking-tight text-white leading-[1.05] mb-8">
|
||||||
Tuxedo Corn is the exclusive grower and shipper of Olathe Sweet Sweet Corn — developed for Colorado's high-altitude mountain climate and grown by the same family for over 40 years.
|
Three Generations of<br className="hidden md:block" /> Sweet Corn Excellence
|
||||||
</p>
|
</h2>
|
||||||
|
</FadeOnScroll>
|
||||||
|
|
||||||
|
<FadeOnScroll from="up" delay={0.2}>
|
||||||
|
<div className="mx-auto mt-8 mb-10 h-px w-16 bg-gradient-to-r from-emerald-600 to-amber-500" />
|
||||||
|
</FadeOnScroll>
|
||||||
|
|
||||||
|
<FadeOnScroll from="up" delay={0.3}>
|
||||||
|
<p className="text-stone-400 leading-relaxed text-lg md:text-xl max-w-2xl mx-auto">
|
||||||
|
Tuxedo Corn is the exclusive grower and shipper of Olathe Sweet Sweet Corn — developed for Colorado's high-altitude mountain climate and grown by the same family for over 40 years.
|
||||||
|
</p>
|
||||||
|
</FadeOnScroll>
|
||||||
|
|
||||||
{/* Stats with counter animation */}
|
{/* Stats with counter animation */}
|
||||||
<div className="reveal-scale flex items-center justify-center gap-12 md:gap-16 mt-16 flex-wrap">
|
<FadeOnScroll from="up" delay={0.4}>
|
||||||
{[
|
<div className="flex items-center justify-center gap-12 md:gap-16 mt-16 flex-wrap">
|
||||||
{ stat: "40", suffix: "+", label: "Years Growing" },
|
{[
|
||||||
{ stat: "3", suffix: "", label: "Generations" },
|
{ stat: "40", suffix: "+", label: "Years Growing" },
|
||||||
{ stat: "100", suffix: "%", label: "Hand-Picked" },
|
{ stat: "3", suffix: "", label: "Generations" },
|
||||||
].map((item, i) => (
|
{ stat: "100", suffix: "%", label: "Hand-Picked" },
|
||||||
<div key={i} className="text-center">
|
].map((item, i) => (
|
||||||
<div className="text-4xl md:text-5xl lg:text-6xl font-black text-white" style={{ textShadow: "0 4px 30px rgba(16,185,129,0.3)" }}>
|
<div key={i} className="text-center">
|
||||||
<span className="counter-animate" data-target={parseInt(item.stat, 10)}>0</span>
|
<div className="text-4xl md:text-5xl lg:text-6xl font-black text-white" style={{ textShadow: "0 4px 30px rgba(16,185,129,0.3)" }}>
|
||||||
<span style={{ color: "#10b981" }}>{item.suffix}</span>
|
<span className="counter-animate" data-target={parseInt(item.stat, 10)}>0</span>
|
||||||
|
<span style={{ color: "#10b981" }}>{item.suffix}</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-[10px] uppercase tracking-[0.2em] text-stone-500">
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 text-[10px] uppercase tracking-[0.2em] text-stone-500">
|
))}
|
||||||
{item.label}
|
</div>
|
||||||
</div>
|
</FadeOnScroll>
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="reveal-scale mt-16">
|
<FadeOnScroll from="up" delay={0.5}>
|
||||||
<Link
|
<div className="mt-16">
|
||||||
href="/tuxedo/about"
|
<Link
|
||||||
className="group inline-flex items-center gap-3 rounded-full px-10 py-4 text-sm font-bold transition-all duration-300"
|
href="/tuxedo/about"
|
||||||
style={{
|
className="group inline-flex items-center gap-3 rounded-full px-10 py-4 text-sm font-bold transition-all duration-300"
|
||||||
background: "linear-gradient(135deg, #059669 0%, #10b981 100%)",
|
style={{
|
||||||
boxShadow: "0 8px 32px rgba(16, 185, 129, 0.3), inset 0 1px 0 rgba(255,255,255,0.2)",
|
background: "linear-gradient(135deg, #059669 0%, #10b981 100%)",
|
||||||
}}
|
boxShadow: "0 8px 32px rgba(16, 185, 129, 0.3), inset 0 1px 0 rgba(255,255,255,0.2)",
|
||||||
onMouseEnter={(e) => {
|
}}
|
||||||
e.currentTarget.style.transform = "translateY(-3px)";
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.boxShadow = "0 12px 40px rgba(16, 185, 129, 0.4), inset 0 1px 0 rgba(255,255,255,0.2)";
|
e.currentTarget.style.transform = "translateY(-3px)";
|
||||||
}}
|
e.currentTarget.style.boxShadow = "0 12px 40px rgba(16, 185, 129, 0.4), inset 0 1px 0 rgba(255,255,255,0.2)";
|
||||||
onMouseLeave={(e) => {
|
}}
|
||||||
e.currentTarget.style.transform = "translateY(0)";
|
onMouseLeave={(e) => {
|
||||||
e.currentTarget.style.boxShadow = "0 8px 32px rgba(16, 185, 129, 0.3), inset 0 1px 0 rgba(255,255,255,0.2)";
|
e.currentTarget.style.transform = "translateY(0)";
|
||||||
}}
|
e.currentTarget.style.boxShadow = "0 8px 32px rgba(16, 185, 129, 0.3), inset 0 1px 0 rgba(255,255,255,0.2)";
|
||||||
>
|
}}
|
||||||
<span>Read Our Story</span>
|
>
|
||||||
<svg className="w-4 h-4 transition-transform group-hover:translate-x-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
<span>Read Our Story</span>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
<svg className="w-4 h-4 transition-transform group-hover:translate-x-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
</svg>
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||||
</Link>
|
</svg>
|
||||||
</div>
|
</Link>
|
||||||
|
</div>
|
||||||
|
</FadeOnScroll>
|
||||||
</div>
|
</div>
|
||||||
</LayoutContainer>
|
</LayoutContainer>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -69,6 +69,7 @@ interface ScrollRevealProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
stagger?: boolean;
|
stagger?: boolean;
|
||||||
direction?: "up" | "down" | "left" | "right" | "scale";
|
direction?: "up" | "down" | "left" | "right" | "scale";
|
||||||
|
from?: "up" | "down" | "left" | "right" | "scale";
|
||||||
delay?: number;
|
delay?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,9 +78,11 @@ export function ScrollReveal({
|
|||||||
className = "",
|
className = "",
|
||||||
stagger = false,
|
stagger = false,
|
||||||
direction = "up",
|
direction = "up",
|
||||||
|
from,
|
||||||
delay = 0,
|
delay = 0,
|
||||||
}: ScrollRevealProps) {
|
}: ScrollRevealProps) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const effectiveDirection = from ?? direction;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === "undefined" || !containerRef.current) return;
|
if (typeof window === "undefined" || !containerRef.current) return;
|
||||||
@@ -111,7 +114,7 @@ export function ScrollReveal({
|
|||||||
// Staggered reveal
|
// Staggered reveal
|
||||||
const animProps: gsap.TweenVars = { opacity: 0 };
|
const animProps: gsap.TweenVars = { opacity: 0 };
|
||||||
|
|
||||||
switch (direction) {
|
switch (effectiveDirection) {
|
||||||
case "up":
|
case "up":
|
||||||
animProps.y = 60;
|
animProps.y = 60;
|
||||||
break;
|
break;
|
||||||
@@ -150,7 +153,7 @@ export function ScrollReveal({
|
|||||||
}, containerRef);
|
}, containerRef);
|
||||||
|
|
||||||
return () => ctx.revert();
|
return () => ctx.revert();
|
||||||
}, [direction, stagger, delay]);
|
}, [effectiveDirection, stagger, delay]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className={className}>
|
<div ref={containerRef} className={className}>
|
||||||
@@ -166,12 +169,14 @@ interface ParallaxLayerProps {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
speed?: number; // 0-1, higher = more movement
|
speed?: number; // 0-1, higher = more movement
|
||||||
className?: string;
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ParallaxLayer({
|
export function ParallaxLayer({
|
||||||
children,
|
children,
|
||||||
speed = 0.5,
|
speed = 0.5,
|
||||||
className = "",
|
className = "",
|
||||||
|
style,
|
||||||
}: ParallaxLayerProps) {
|
}: ParallaxLayerProps) {
|
||||||
const layerRef = useRef<HTMLDivElement>(null);
|
const layerRef = useRef<HTMLDivElement>(null);
|
||||||
const [isClient, setIsClient] = useState(false);
|
const [isClient, setIsClient] = useState(false);
|
||||||
@@ -202,11 +207,11 @@ export function ParallaxLayer({
|
|||||||
}, [isClient, speed]);
|
}, [isClient, speed]);
|
||||||
|
|
||||||
if (!isClient) {
|
if (!isClient) {
|
||||||
return <div className={className}>{children}</div>;
|
return <div className={className} style={style}>{children}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={layerRef} className={className}>
|
<div ref={layerRef} className={className} style={style}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -271,17 +276,21 @@ export function ProgressIndicator({
|
|||||||
interface FadeOnScrollProps {
|
interface FadeOnScrollProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
from?: "top" | "bottom" | "left" | "right";
|
from?: "top" | "bottom" | "left" | "right" | "up" | "down";
|
||||||
|
to?: "top" | "bottom" | "left" | "right";
|
||||||
distance?: number;
|
distance?: number;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
|
delay?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FadeOnScroll({
|
export function FadeOnScroll({
|
||||||
children,
|
children,
|
||||||
className = "",
|
className = "",
|
||||||
from = "bottom",
|
from = "bottom",
|
||||||
|
to,
|
||||||
distance = 100,
|
distance = 100,
|
||||||
duration = 1,
|
duration = 1,
|
||||||
|
delay = 0,
|
||||||
}: FadeOnScrollProps) {
|
}: FadeOnScrollProps) {
|
||||||
const elementRef = useRef<HTMLDivElement>(null);
|
const elementRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -293,6 +302,7 @@ export function FadeOnScroll({
|
|||||||
let fromY = 0;
|
let fromY = 0;
|
||||||
let fromX = 0;
|
let fromX = 0;
|
||||||
|
|
||||||
|
// Use 'from' direction for the starting position
|
||||||
switch (from) {
|
switch (from) {
|
||||||
case "top":
|
case "top":
|
||||||
fromY = distance;
|
fromY = distance;
|
||||||
@@ -320,6 +330,7 @@ export function FadeOnScroll({
|
|||||||
y: 0,
|
y: 0,
|
||||||
duration,
|
duration,
|
||||||
ease: "power3.out",
|
ease: "power3.out",
|
||||||
|
delay,
|
||||||
scrollTrigger: {
|
scrollTrigger: {
|
||||||
trigger: elementRef.current,
|
trigger: elementRef.current,
|
||||||
start: "top 85%",
|
start: "top 85%",
|
||||||
@@ -330,7 +341,7 @@ export function FadeOnScroll({
|
|||||||
}, elementRef);
|
}, elementRef);
|
||||||
|
|
||||||
return () => ctx.revert();
|
return () => ctx.revert();
|
||||||
}, [from, distance, duration]);
|
}, [from, distance, duration, delay]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={elementRef} className={className}>
|
<div ref={elementRef} className={className}>
|
||||||
|
|||||||
Reference in New Issue
Block a user