diff --git a/apps/web/app/partners/[slug]/page.tsx b/apps/web/app/partners/[slug]/page.tsx index 446f0ef..0ba6d1d 100644 --- a/apps/web/app/partners/[slug]/page.tsx +++ b/apps/web/app/partners/[slug]/page.tsx @@ -32,9 +32,9 @@ const PARTNERS_DATA: Record = { youtubeId: "01loBLlZRHw", ctaUrl: "https://portal.restrofx.com/r/glaPWwHQ", featuredVideos: [ - { id: "1", videoId: "R2djd5ACzPM", title: "i'm finally buying my dream car" }, - { id: "2", videoId: "_QmCh4dNVGE", title: "Don't Trade Every Pair | Here's What Actually Works" }, - { id: "3", videoId: "DV6cte3H9rc", title: "Pulled $15k profit - here's every single trade" }, + { id: "1", videoId: "LdjfeDLhRiM", title: "$38k withdrawal in brought a lamborghini Then Went Left!" }, + { id: "2", videoId: "R2djd5ACzPM", title: "i'm finally buying my dream car" }, + { id: "3", videoId: "_QmCh4dNVGE", title: "Don't Trade Every Pair | Here's What Actually Works" }, { id: "4", videoId: "KhLUPlL777U", title: "Why You Should Reconsider Trading This year" }, { id: "5", videoId: "SyC37iKc2wE", title: "I Made $20k Trading Silver | Here's My Exact Strategy" }, { id: "6", videoId: "rExdi9Vzkxk", title: "Is Trading Really Worth It? My 6 Years of Results" } diff --git a/apps/web/components/ui/curved-carousel.tsx b/apps/web/components/ui/curved-carousel.tsx index 9a7c1ab..110383b 100644 --- a/apps/web/components/ui/curved-carousel.tsx +++ b/apps/web/components/ui/curved-carousel.tsx @@ -1,7 +1,7 @@ "use client"; -import { motion, useMotionValue, useSpring, useTransform } from "framer-motion"; -import { useState, useRef, useEffect } from "react"; +import { motion, useMotionValue, useSpring, useTransform, useScroll } from "framer-motion"; +import { useState, useRef, useEffect, useMemo } from "react"; import Image from "next/image"; import { Play } from "lucide-react"; @@ -17,38 +17,159 @@ interface CurvedCarouselProps { subtitle?: string; } +interface CarouselCardProps { + item: CarouselItem; + index: number; + combinedPos: any; // MotionValue + itemWidth: number; + itemHeight: number; + itemTotalWidth: number; + centerOffset: number; + onClick: (videoId: string) => void; +} + +function CarouselCard({ + item, + index, + combinedPos, + itemWidth, + itemHeight, + itemTotalWidth, + centerOffset, + onClick +}: CarouselCardProps) { + const itemOffset = index * itemTotalWidth; + + // Transform logic relative to the combined position + const itemRotation = useTransform(combinedPos, + [-itemOffset + centerOffset - itemTotalWidth, -itemOffset + centerOffset, -itemOffset + centerOffset + itemTotalWidth], + [-45, 0, 45] + ); + const itemScale = useTransform(combinedPos, + [-itemOffset + centerOffset - itemTotalWidth, -itemOffset + centerOffset, -itemOffset + centerOffset + itemTotalWidth], + [0.8, 1, 0.8] + ); + const itemZ = useTransform(combinedPos, + [-itemOffset + centerOffset - itemTotalWidth, -itemOffset + centerOffset, -itemOffset + centerOffset + itemTotalWidth], + [-200, 0, -200] + ); + const itemOpacity = useTransform(combinedPos, + [-itemOffset + centerOffset - 2 * itemTotalWidth, -itemOffset + centerOffset, -itemOffset + centerOffset + 2 * itemTotalWidth], + [0, 1, 0] + ); + + const [imgSrc, setImgSrc] = useState(`https://i.ytimg.com/vi/${item.videoId}/maxresdefault.jpg`); + + return ( + onClick(item.videoId)} + > + {item.title} { + setImgSrc(`https://i.ytimg.com/vi/${item.videoId}/hqdefault.jpg`); + }} + /> + +
+ + + +

+ {item.title} +

+
+ +
+ + ); +} + export function CurvedCarousel({ items, title, subtitle }: CurvedCarouselProps) { - const [active, setActive] = useState(0); const containerRef = useRef(null); + const sectionRef = useRef(null); + const [active, setActive] = useState(0); const x = useMotionValue(0); + const [isDragging, setIsDragging] = useState(false); - // Spring for smooth dragging - const springX = useSpring(x, { stiffness: 300, damping: 30 }); + // Triplicate items for infinite loop + const displayItems = useMemo(() => [...items, ...items, ...items], [items]); + const middleIndexOffset = items.length; - // Calculate the rotation based on the number of items - // We want to map the drag distance to an index - const itemWidth = 300; // estimated width of a card + // Constants + const itemWidth = 480; + const itemHeight = 270; + const gap = 32; + const itemTotalWidth = itemWidth + gap; + const centerOffset = -itemWidth / 2; + + // Spring for smooth dragging and scroll + const springX = useSpring(x, { stiffness: 150, damping: 25 }); + + // Scroll Sync + const { scrollYProgress } = useScroll({ + target: sectionRef, + offset: ["start end", "end start"] + }); + + // Map scroll progress to a rotational offset - reduced for subtler effect + const scrollOffset = useTransform(scrollYProgress, [0, 1], [itemTotalWidth * 0.4, -itemTotalWidth * 0.4]); + + // Combined position for all items to reactive to + const combinedPos = useTransform([springX, scrollOffset], ([sx, so]) => (sx as number) + (so as number)); + + const handleDragStart = () => setIsDragging(true); const handleDragEnd = (_: any, info: any) => { - // Determine the closest index based on the drag offset + setTimeout(() => setIsDragging(false), 50); + const velocity = info.velocity.x; + const currentX = x.get() - centerOffset; + let newIndex = Math.round(-currentX / itemTotalWidth); - // Simple snapping if (Math.abs(velocity) > 500) { - if (velocity > 0) setActive((prev) => Math.max(0, prev - 1)); - else setActive((prev) => Math.min(items.length - 1, prev + 1)); - } else { - const index = Math.round(-x.get() / itemWidth); - setActive(Math.max(0, Math.min(items.length - 1, index))); + newIndex = velocity > 0 ? active - 1 : active + 1; } + + // Keep active within reasonable bounds of the triple list + if (newIndex < 1) newIndex = middleIndexOffset; + if (newIndex > displayItems.length - 2) newIndex = middleIndexOffset + items.length - 1; + + setActive(newIndex); }; useEffect(() => { - x.set(-active * itemWidth); - }, [active, x]); + x.set(-(middleIndexOffset * itemTotalWidth) + centerOffset); + setActive(middleIndexOffset); + }, [middleIndexOffset, itemTotalWidth, centerOffset, x]); + + useEffect(() => { + x.set(-(active * itemTotalWidth) + centerOffset); + }, [active, x, itemTotalWidth, centerOffset]); + + const handleCardClick = (videoId: string) => { + if (isDragging) return; + window.open(`https://youtube.com/watch?v=${videoId}`, '_blank'); + }; return ( -
+
{title &&

{title}

} {subtitle &&

{subtitle}

} @@ -56,89 +177,51 @@ export function CurvedCarousel({ items, title, subtitle }: CurvedCarouselProps)
{ + x.set(x.get() + info.delta.x); + }} + style={{ x: combinedPos, left: "50%" }} + className="absolute flex gap-8 items-center cursor-grab active:cursor-grabbing" > - {items.map((item, index) => { - // Map the global x motion value to local item transforms - // eslint-disable-next-line react-hooks/rules-of-hooks - const itemRotation = useTransform(springX, - [- (index + 1) * itemWidth, - index * itemWidth, - (index - 1) * itemWidth], - [-45, 0, 45] - ); - // eslint-disable-next-line react-hooks/rules-of-hooks - const itemScale = useTransform(springX, - [- (index + 1) * itemWidth, - index * itemWidth, - (index - 1) * itemWidth], - [0.8, 1, 0.8] - ); - // eslint-disable-next-line react-hooks/rules-of-hooks - const itemZ = useTransform(springX, - [- (index + 1) * itemWidth, - index * itemWidth, - (index - 1) * itemWidth], - [-200, 0, -200] - ); - // eslint-disable-next-line react-hooks/rules-of-hooks - const itemOpacity = useTransform(springX, - [- (index + 2) * itemWidth, - index * itemWidth, - (index - 2) * itemWidth], - [0, 1, 0] - ); - - return ( - window.open(`https://youtube.com/watch?v=${item.videoId}`, '_blank')} - > - {item.title} - - {/* Overlay */} -
- - - -

- {item.title} -

-
- - {/* Glassmorphic Reflection Overlay */} -
- - ); - })} + {displayItems.map((item, index) => ( + + ))}
- {items.map((_, i) => ( -
);