From bf4dc09e65cc4771ff6e0ddd89083f3f0644c9a0 Mon Sep 17 00:00:00 2001 From: default Date: Tue, 2 Jun 2026 15:29:39 +0000 Subject: [PATCH] fix: restore full landing page with FeaturesAndStats and TestimonialsAndCTA --- src/app/LandingPageClient.tsx | 64 +++- src/app/tuxedo/page.tsx | 2 + .../storefront/CinematicShowcase.tsx | 291 +++++++++++++++ src/components/ui/ScrollAnimations.tsx | 340 ++++++++++++++++++ 4 files changed, 696 insertions(+), 1 deletion(-) create mode 100644 src/components/storefront/CinematicShowcase.tsx create mode 100644 src/components/ui/ScrollAnimations.tsx diff --git a/src/app/LandingPageClient.tsx b/src/app/LandingPageClient.tsx index 60c926a..908bb6a 100644 --- a/src/app/LandingPageClient.tsx +++ b/src/app/LandingPageClient.tsx @@ -1,7 +1,69 @@ "use client"; +import { useEffect, useRef } from "react"; import HeroSection from "@/components/landing/HeroSection"; +import FeaturesAndStats from "@/components/landing/FeaturesAndStats"; +import TestimonialsAndCTA from "@/components/landing/TestimonialsAndCTA"; +import { gsap } from "gsap"; +import { ScrollTrigger } from "gsap/ScrollTrigger"; + +// Register GSAP plugins +if (typeof window !== "undefined") { + gsap.registerPlugin(ScrollTrigger); +} export default function LandingPageClient() { - return ; + const mainRef = useRef(null); + + // Global scroll animations + useEffect(() => { + if (typeof window === "undefined" || !mainRef.current) return; + + const ctx = gsap.context(() => { + // Parallax sections + const parallaxElements = gsap.utils.toArray(".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 + const revealElements = gsap.utils.toArray(".scroll-reveal"); + revealElements.forEach((el) => { + gsap.fromTo( + el, + { opacity: 0, y: 60 }, + { + opacity: 1, + y: 0, + duration: 1, + ease: "power3.out", + scrollTrigger: { + trigger: el, + start: "top 85%", + toggleActions: "play none none reverse", + }, + } + ); + }); + }, mainRef); + + return () => ctx.revert(); + }, []); + + return ( +
+ + + +
+ ); } \ No newline at end of file diff --git a/src/app/tuxedo/page.tsx b/src/app/tuxedo/page.tsx index e37241a..b50f56e 100644 --- a/src/app/tuxedo/page.tsx +++ b/src/app/tuxedo/page.tsx @@ -5,12 +5,14 @@ import Link from "next/link"; import { motion, useInView, AnimatePresence } from "framer-motion"; import { useCart } from "@/context/CartContext"; import TuxedoVideoHero from "@/components/storefront/TuxedoVideoHero"; +import CinematicShowcase from "@/components/storefront/CinematicShowcase"; import LayoutContainer from "@/components/layout/LayoutContainer"; import ProductCard from "@/components/storefront/ProductCard"; import PaginatedStops from "@/components/storefront/PaginatedStops"; import StorefrontHeader from "@/components/storefront/StorefrontHeader"; import StorefrontFooter from "@/components/storefront/StorefrontFooter"; import BrandStylesProvider from "@/components/storefront/BrandStylesProvider"; +import { ScrollReveal, ParallaxLayer, FadeOnScroll } from "@/components/ui/ScrollAnimations"; import { supabase } from "@/lib/supabase"; import { getBrandSettingsPublic } from "@/actions/brand-settings"; import { gsap } from "gsap"; diff --git a/src/components/storefront/CinematicShowcase.tsx b/src/components/storefront/CinematicShowcase.tsx new file mode 100644 index 0000000..f34182d --- /dev/null +++ b/src/components/storefront/CinematicShowcase.tsx @@ -0,0 +1,291 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { gsap } from "gsap"; +import { ScrollTrigger } from "gsap/ScrollTrigger"; +import Image from "next/image"; + +if (typeof window !== "undefined") { + gsap.registerPlugin(ScrollTrigger); +} + +interface Product { + id: string; + name: string; + description: string | null; + price: string; + type: string; + imageUrl: string | null; +} + +interface CinematicShowcaseProps { + products: Product[]; + brandSlug?: string; +} + +// Single product card with scroll-driven morphing +function MorphingProductCard({ + product, + index, + isActive, +}: { + product: Product; + index: number; + isActive: boolean; +}) { + const cardRef = useRef(null); + const imageRef = useRef(null); + + useEffect(() => { + if (typeof window === "undefined" || !cardRef.current) return; + + const ctx = gsap.context(() => { + // Morphing effect based on scroll position + gsap.to(cardRef.current, { + scale: isActive ? 1.02 : 0.95, + opacity: isActive ? 1 : 0.5, + y: isActive ? 0 : index * 20, + duration: 0.6, + ease: "power3.out", + }); + + // Subtle rotation for depth + gsap.to(cardRef.current, { + rotateY: isActive ? 0 : (index % 2 === 0 ? -5 : 5), + duration: 0.8, + ease: "power2.out", + }); + }, cardRef); + + return () => ctx.revert(); + }, [isActive, index]); + + return ( +
+
+ {/* Image with parallax */} +
+ {product.imageUrl && ( + {product.name} + )} + {/* Gradient overlay */} +
+
+ + {/* Content */} +
+

+ {product.type} +

+

+ {product.name} +

+ {product.description && ( +

+ {product.description} +

+ )} +

{product.price}

+
+ + {/* Shine effect */} +
+
+
+ ); +} + +export default function CinematicShowcase({ + products, + brandSlug = "tuxedo", +}: CinematicShowcaseProps) { + const containerRef = useRef(null); + const [activeIndex, setActiveIndex] = useState(0); + const [scrollProgress, setScrollProgress] = useState(0); + + useEffect(() => { + if (typeof window === "undefined" || !containerRef.current) return; + + const ctx = gsap.context(() => { + // Scroll-driven product switching + const productSwitchTrigger = ScrollTrigger.create({ + trigger: containerRef.current, + start: "top top", + end: "bottom bottom", + scrub: 1, + onUpdate: (self) => { + setScrollProgress(self.progress); + + // Calculate which product should be active + const progress = self.progress; + const productCount = products.length; + const segmentSize = 1 / productCount; + + let newIndex = Math.min( + Math.floor(progress / segmentSize), + productCount - 1 + ); + newIndex = Math.max(0, newIndex); + + setActiveIndex(newIndex); + }, + }); + + // Parallax for background text + gsap.to(".showcase-headline", { + y: -100, + ease: "none", + scrollTrigger: { + trigger: containerRef.current, + start: "top top", + end: "bottom bottom", + scrub: 1, + }, + }); + + // Scale reveal for cards + gsap.fromTo( + ".product-card-wrapper", + { opacity: 0, y: 100, scale: 0.9 }, + { + opacity: 1, + y: 0, + scale: 1, + duration: 0.8, + stagger: 0.15, + ease: "back.out(1.4)", + scrollTrigger: { + trigger: containerRef.current, + start: "top 80%", + toggleActions: "play none none reverse", + }, + } + ); + }, containerRef); + + return () => ctx.revert(); + }, [products.length]); + + if (products.length === 0) return null; + + return ( +
+ {/* Sticky container */} +
+ {/* Background headline with parallax */} +
+

+ HARVEST +

+
+ + {/* Progress dots */} +
+ {products.map((_, i) => ( +
+ ))} +
+ + {/* Progress bar */} +
+
+
+ + {/* Main content */} +
+ {/* Section header */} +
+

+ This Season's Harvest +

+

+ Picked This +
+ Morning +

+
+ + {/* Product showcase */} +
+
+ {products.map((product, i) => ( +
+ +
+ ))} +
+
+ + {/* Active product info */} +
+
+

+ {products[activeIndex]?.name} +

+

+ {products[activeIndex]?.description} +

+
+
+
+
+
+ ); +} diff --git a/src/components/ui/ScrollAnimations.tsx b/src/components/ui/ScrollAnimations.tsx new file mode 100644 index 0000000..675d59f --- /dev/null +++ b/src/components/ui/ScrollAnimations.tsx @@ -0,0 +1,340 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { gsap } from "gsap"; +import { ScrollTrigger } from "gsap/ScrollTrigger"; + +// Register GSAP plugins +if (typeof window !== "undefined") { + gsap.registerPlugin(ScrollTrigger); +} + +interface StickyScrollSectionProps { + children: React.ReactNode; + height?: string; + className?: string; +} + +export default function StickyScrollSection({ + children, + height = "300vh", + className = "", +}: StickyScrollSectionProps) { + const containerRef = useRef(null); + const [isStuck, setIsStuck] = useState(false); + + useEffect(() => { + if (typeof window === "undefined" || !containerRef.current) return; + + const ctx = gsap.context(() => { + const trigger = ScrollTrigger.create({ + trigger: containerRef.current, + start: "top top", + end: "bottom bottom", + pin: true, + pinSpacing: false, + onToggle: (self) => { + setIsStuck(self.isActive); + }, + }); + + return () => trigger.kill(); + }, containerRef); + + return () => ctx.revert(); + }, []); + + return ( +
+
+ {children} +
+
+ ); +} + +// ───────────────────────────────────────────────────────────────────────────── +// SCROLL-REVEAL WRAPPER - Layers content as user scrolls +// ───────────────────────────────────────────────────────────────────────────── +interface ScrollRevealProps { + children: React.ReactNode; + className?: string; + stagger?: boolean; + direction?: "up" | "down" | "left" | "right" | "scale"; + delay?: number; +} + +export function ScrollReveal({ + children, + className = "", + stagger = false, + direction = "up", + delay = 0, +}: ScrollRevealProps) { + const containerRef = useRef(null); + + useEffect(() => { + if (typeof window === "undefined" || !containerRef.current) return; + + const ctx = gsap.context(() => { + const elements = containerRef.current?.querySelectorAll("[data-reveal]"); + + if (!elements?.length) { + // Single child reveal + gsap.fromTo( + containerRef.current, + { opacity: 0, y: 60 }, + { + opacity: 1, + y: 0, + duration: 1, + ease: "power3.out", + delay, + scrollTrigger: { + trigger: containerRef.current, + start: "top 80%", + toggleActions: "play none none reverse", + }, + } + ); + return; + } + + // Staggered reveal + const animProps: gsap.TweenVars = { opacity: 0 }; + + switch (direction) { + case "up": + animProps.y = 60; + break; + case "down": + animProps.y = -60; + break; + case "left": + animProps.x = 60; + break; + case "right": + animProps.x = -60; + break; + case "scale": + animProps.scale = 0.9; + break; + } + + gsap.fromTo( + elements, + animProps, + { + opacity: 1, + x: 0, + y: 0, + scale: 1, + duration: 0.8, + stagger: stagger ? 0.1 : 0, + ease: "power3.out", + scrollTrigger: { + trigger: containerRef.current, + start: "top 80%", + toggleActions: "play none none reverse", + }, + } + ); + }, containerRef); + + return () => ctx.revert(); + }, [direction, stagger, delay]); + + return ( +
+ {children} +
+ ); +} + +// ───────────────────────────────────────────────────────────────────────────── +// PARALLAX LAYER - Depth effect on scroll +// ───────────────────────────────────────────────────────────────────────────── +interface ParallaxLayerProps { + children: React.ReactNode; + speed?: number; // 0-1, higher = more movement + className?: string; +} + +export function ParallaxLayer({ + children, + speed = 0.5, + className = "", +}: ParallaxLayerProps) { + const layerRef = useRef(null); + const [isClient, setIsClient] = useState(false); + + useEffect(() => { + setIsClient(true); + }, []); + + useEffect(() => { + if (!isClient || !layerRef.current) return; + + const ctx = gsap.context(() => { + const moveDistance = speed * 150; // max 150px movement + + gsap.to(layerRef.current, { + y: -moveDistance, + ease: "none", + scrollTrigger: { + trigger: layerRef.current, + start: "top bottom", + end: "bottom top", + scrub: 1, + }, + }); + }, layerRef); + + return () => ctx.revert(); + }, [isClient, speed]); + + if (!isClient) { + return
{children}
; + } + + return ( +
+ {children} +
+ ); +} + +// ───────────────────────────────────────────────────────────────────────────── +// PROGRESS INDICATOR - Shows scroll progress through section +// ───────────────────────────────────────────────────────────────────────────── +interface ProgressIndicatorProps { + className?: string; + color?: string; +} + +export function ProgressIndicator({ + className = "", + color = "#10b981", +}: ProgressIndicatorProps) { + const containerRef = useRef(null); + const [progress, setProgress] = useState(0); + + useEffect(() => { + if (typeof window === "undefined" || !containerRef.current) return; + + const ctx = gsap.context(() => { + ScrollTrigger.create({ + trigger: containerRef.current, + start: "top top", + end: "bottom bottom", + scrub: true, + onUpdate: (self) => { + setProgress(self.progress); + }, + }); + }, containerRef); + + return () => ctx.revert(); + }, []); + + return ( +
+
+
+
+
+
+
+ ); +} + +// ───────────────────────────────────────────────────────────────────────────── +// FADE ON SCROLL - Elements that fade/scale as user scrolls +// ───────────────────────────────────────────────────────────────────────────── +interface FadeOnScrollProps { + children: React.ReactNode; + className?: string; + from?: "top" | "bottom" | "left" | "right"; + distance?: number; + duration?: number; +} + +export function FadeOnScroll({ + children, + className = "", + from = "bottom", + distance = 100, + duration = 1, +}: FadeOnScrollProps) { + const elementRef = useRef(null); + + useEffect(() => { + if (typeof window === "undefined" || !elementRef.current) return; + + const ctx = gsap.context(() => { + let fromProps: gsap.TweenVars = { opacity: 0 }; + let fromY = 0; + let fromX = 0; + + switch (from) { + case "top": + fromY = distance; + break; + case "bottom": + fromY = -distance; + break; + case "left": + fromX = distance; + break; + case "right": + fromX = -distance; + break; + } + + fromProps.y = fromY; + fromProps.x = fromX; + + gsap.fromTo( + elementRef.current, + fromProps, + { + opacity: 1, + x: 0, + y: 0, + duration, + ease: "power3.out", + scrollTrigger: { + trigger: elementRef.current, + start: "top 85%", + toggleActions: "play none none reverse", + }, + } + ); + }, elementRef); + + return () => ctx.revert(); + }, [from, distance, duration]); + + return ( +
+ {children} +
+ ); +} \ No newline at end of file