From 59d3677ab54863cb7b45dadc8678fcd3bc16b1e0 Mon Sep 17 00:00:00 2001 From: laveshparyani Date: Fri, 27 Mar 2026 07:49:51 +0530 Subject: [PATCH 1/6] fix(ui): center CurvedCarousel active item This commit fixes the alignment issue where the 3D curved carousel was skewed to the left. It implements an absolute centering strategy that ensures the active video card is always perfectly centered in the viewport. --- apps/web/components/ui/curved-carousel.tsx | 30 ++++++++++++++-------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/apps/web/components/ui/curved-carousel.tsx b/apps/web/components/ui/curved-carousel.tsx index 9a7c1ab..ddbbaaa 100644 --- a/apps/web/components/ui/curved-carousel.tsx +++ b/apps/web/components/ui/curved-carousel.tsx @@ -28,7 +28,11 @@ export function CurvedCarousel({ items, title, subtitle }: CurvedCarouselProps) // 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 + const gap = 32; // gap-8 = 2rem = 32px + // Center offset to align the active item's center with the viewport's center + const centerOffset = -itemWidth / 2; + const handleDragEnd = (_: any, info: any) => { // Determine the closest index based on the drag offset const velocity = info.velocity.x; @@ -38,14 +42,15 @@ export function CurvedCarousel({ items, title, subtitle }: CurvedCarouselProps) 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); + const currentX = x.get() - centerOffset; + const index = Math.round(-currentX / (itemWidth + gap)); setActive(Math.max(0, Math.min(items.length - 1, index))); } }; useEffect(() => { - x.set(-active * itemWidth); - }, [active, x]); + x.set(-(active * (itemWidth + gap)) + centerOffset); + }, [active, x, centerOffset, gap]); return (
@@ -60,31 +65,36 @@ export function CurvedCarousel({ items, title, subtitle }: CurvedCarouselProps) > {items.map((item, index) => { + const itemOffset = index * (itemWidth + gap); + // 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], + [-itemOffset + centerOffset - (itemWidth + gap), -itemOffset + centerOffset, -itemOffset + centerOffset + (itemWidth + gap)], [-45, 0, 45] ); // eslint-disable-next-line react-hooks/rules-of-hooks const itemScale = useTransform(springX, - [- (index + 1) * itemWidth, - index * itemWidth, - (index - 1) * itemWidth], + [-itemOffset + centerOffset - (itemWidth + gap), -itemOffset + centerOffset, -itemOffset + centerOffset + (itemWidth + gap)], [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], + [-itemOffset + centerOffset - (itemWidth + gap), -itemOffset + centerOffset, -itemOffset + centerOffset + (itemWidth + gap)], [-200, 0, -200] ); // eslint-disable-next-line react-hooks/rules-of-hooks const itemOpacity = useTransform(springX, - [- (index + 2) * itemWidth, - index * itemWidth, - (index - 2) * itemWidth], + [-itemOffset + centerOffset - 2 * (itemWidth + gap), -itemOffset + centerOffset, -itemOffset + centerOffset + 2 * (itemWidth + gap)], [0, 1, 0] ); From 7a8febb227f56d3e594d3ac522133cb6aebf1f13 Mon Sep 17 00:00:00 2001 From: laveshparyani Date: Fri, 27 Mar 2026 08:02:14 +0530 Subject: [PATCH 2/6] feat(ui): enhance video carousel with infinite loop, scroll-sync, and smart-click This commit adds advanced interactive features to the CurvedCarousel: triplicated items for infinite wrapping, useScroll-based page scroll synchronization, and a drag-threshold check to prevent accidental video launches during navigation. --- apps/web/components/ui/curved-carousel.tsx | 113 +++++++++++++-------- 1 file changed, 69 insertions(+), 44 deletions(-) diff --git a/apps/web/components/ui/curved-carousel.tsx b/apps/web/components/ui/curved-carousel.tsx index ddbbaaa..26cdb39 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"; @@ -18,42 +18,70 @@ interface CurvedCarouselProps { } 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 - const gap = 32; // gap-8 = 2rem = 32px - - // Center offset to align the active item's center with the viewport's center + // Constants + const itemWidth = 300; + 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 + const scrollOffset = useTransform(scrollYProgress, [0, 1], [itemTotalWidth, -itemTotalWidth]); + + const handleDragStart = () => setIsDragging(true); + const handleDragEnd = (_: any, info: any) => { - // Determine the closest index based on the drag offset + setTimeout(() => setIsDragging(false), 50); // Small delay to prevent immediate click + 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 currentX = x.get() - centerOffset; - const index = Math.round(-currentX / (itemWidth + gap)); - setActive(Math.max(0, Math.min(items.length - 1, index))); + newIndex = velocity > 0 ? active - 1 : active + 1; } + + // Wrap index within the displayItems range if needed + // But setActive should really be based on the middle set for stability + setActive(newIndex); }; + // Sync scroll and handle infinite wrapping + useEffect(() => { + // Initial position: center of the middle set + x.set(-(middleIndexOffset * itemTotalWidth) + centerOffset); + setActive(middleIndexOffset); + }, []); + useEffect(() => { - x.set(-(active * (itemWidth + gap)) + centerOffset); - }, [active, x, centerOffset, gap]); + 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}

} @@ -65,42 +93,41 @@ export function CurvedCarousel({ items, title, subtitle }: CurvedCarouselProps) > (sx as number) + (so as number)), left: "50%" }} + onDragStart={handleDragStart} onDragEnd={handleDragEnd} className="absolute flex gap-8 items-center" > - {items.map((item, index) => { - const itemOffset = index * (itemWidth + gap); + {displayItems.map((item, index) => { + const itemOffset = index * itemTotalWidth; + + // Transform logic relative to the combined springX + scrollOffset + const combinedPos = useTransform([springX, scrollOffset], ([sx, so]) => (sx as number) + (so as number)); - // Map the global x motion value to local item transforms // eslint-disable-next-line react-hooks/rules-of-hooks - const itemRotation = useTransform(springX, - [-itemOffset + centerOffset - (itemWidth + gap), -itemOffset + centerOffset, -itemOffset + centerOffset + (itemWidth + gap)], + const itemRotation = useTransform(combinedPos, + [-itemOffset + centerOffset - itemTotalWidth, -itemOffset + centerOffset, -itemOffset + centerOffset + itemTotalWidth], [-45, 0, 45] ); // eslint-disable-next-line react-hooks/rules-of-hooks - const itemScale = useTransform(springX, - [-itemOffset + centerOffset - (itemWidth + gap), -itemOffset + centerOffset, -itemOffset + centerOffset + (itemWidth + gap)], + const itemScale = useTransform(combinedPos, + [-itemOffset + centerOffset - itemTotalWidth, -itemOffset + centerOffset, -itemOffset + centerOffset + itemTotalWidth], [0.8, 1, 0.8] ); // eslint-disable-next-line react-hooks/rules-of-hooks - const itemZ = useTransform(springX, - [-itemOffset + centerOffset - (itemWidth + gap), -itemOffset + centerOffset, -itemOffset + centerOffset + (itemWidth + gap)], + const itemZ = useTransform(combinedPos, + [-itemOffset + centerOffset - itemTotalWidth, -itemOffset + centerOffset, -itemOffset + centerOffset + itemTotalWidth], [-200, 0, -200] ); // eslint-disable-next-line react-hooks/rules-of-hooks - const itemOpacity = useTransform(springX, - [-itemOffset + centerOffset - 2 * (itemWidth + gap), -itemOffset + centerOffset, -itemOffset + centerOffset + 2 * (itemWidth + gap)], + const itemOpacity = useTransform(combinedPos, + [-itemOffset + centerOffset - 2 * itemTotalWidth, -itemOffset + centerOffset, -itemOffset + centerOffset + 2 * itemTotalWidth], [0, 1, 0] ); return ( window.open(`https://youtube.com/watch?v=${item.videoId}`, '_blank')} + onClick={() => handleCardClick(item.videoId)} > - {/* Overlay */}
- {/* Glassmorphic Reflection Overlay */}
); @@ -145,8 +170,8 @@ export function CurvedCarousel({ items, title, subtitle }: CurvedCarouselProps) {items.map((_, i) => (
From 4555926ed07d5c60e3be0f9dc627fbfa0570be5b Mon Sep 17 00:00:00 2001 From: laveshparyani Date: Fri, 27 Mar 2026 08:07:34 +0530 Subject: [PATCH 3/6] style(ui): change video carousel cards to horizontal 16:9 This commit restyles the CurvedCarousel video cards to a horizontal 16:9 aspect ratio (480x270), providing a more cinematic look. Centering and 3D transforms have been recalibrated for the new dimensions. --- apps/web/components/ui/curved-carousel.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/web/components/ui/curved-carousel.tsx b/apps/web/components/ui/curved-carousel.tsx index 26cdb39..a3eb35e 100644 --- a/apps/web/components/ui/curved-carousel.tsx +++ b/apps/web/components/ui/curved-carousel.tsx @@ -29,7 +29,8 @@ export function CurvedCarousel({ items, title, subtitle }: CurvedCarouselProps) const middleIndexOffset = items.length; // Constants - const itemWidth = 300; + const itemWidth = 480; + const itemHeight = 270; const gap = 32; const itemTotalWidth = itemWidth + gap; const centerOffset = -itemWidth / 2; @@ -130,7 +131,7 @@ export function CurvedCarousel({ items, title, subtitle }: CurvedCarouselProps) key={`${item.id}-${index}`} style={{ width: itemWidth, - height: 400, + height: itemHeight, rotateY: itemRotation, scale: itemScale, z: itemZ, From 3f2cd7acd4ad06af2df0ecf7ba756df89760db62 Mon Sep 17 00:00:00 2001 From: laveshparyani Date: Fri, 27 Mar 2026 08:13:25 +0530 Subject: [PATCH 4/6] fix(ui): resolve carousel vanishing and update video content This commit refactors the CurvedCarousel to use a dedicated CarouselCard component, fixing a React hook violation that caused rendering instability. It also replaces the 'Pulled profit' video with a high-definition alternative for a better visual experience. --- apps/web/app/partners/[slug]/page.tsx | 2 +- apps/web/components/ui/curved-carousel.tsx | 171 ++++++++++++--------- 2 files changed, 99 insertions(+), 74 deletions(-) diff --git a/apps/web/app/partners/[slug]/page.tsx b/apps/web/app/partners/[slug]/page.tsx index 446f0ef..6a08a8d 100644 --- a/apps/web/app/partners/[slug]/page.tsx +++ b/apps/web/app/partners/[slug]/page.tsx @@ -34,7 +34,7 @@ const PARTNERS_DATA: Record = { 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: "3", videoId: "JvA-MhX9W-Y", title: "How I Made $20,000 in 1 Day | Full Breakdown" }, { 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 a3eb35e..d6261f6 100644 --- a/apps/web/components/ui/curved-carousel.tsx +++ b/apps/web/components/ui/curved-carousel.tsx @@ -17,6 +17,85 @@ 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] + ); + + return ( + onClick(item.videoId)} + > + {item.title} + +
+ + + +

+ {item.title} +

+
+ +
+ + ); +} + export function CurvedCarousel({ items, title, subtitle }: CurvedCarouselProps) { const containerRef = useRef(null); const sectionRef = useRef(null); @@ -47,10 +126,13 @@ export function CurvedCarousel({ items, title, subtitle }: CurvedCarouselProps) // Map scroll progress to a rotational offset const scrollOffset = useTransform(scrollYProgress, [0, 1], [itemTotalWidth, -itemTotalWidth]); + // 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) => { - setTimeout(() => setIsDragging(false), 50); // Small delay to prevent immediate click + setTimeout(() => setIsDragging(false), 50); const velocity = info.velocity.x; const currentX = x.get() - centerOffset; @@ -59,18 +141,13 @@ export function CurvedCarousel({ items, title, subtitle }: CurvedCarouselProps) if (Math.abs(velocity) > 500) { newIndex = velocity > 0 ? active - 1 : active + 1; } - - // Wrap index within the displayItems range if needed - // But setActive should really be based on the middle set for stability setActive(newIndex); }; - // Sync scroll and handle infinite wrapping useEffect(() => { - // Initial position: center of the middle set x.set(-(middleIndexOffset * itemTotalWidth) + centerOffset); setActive(middleIndexOffset); - }, []); + }, [middleIndexOffset, itemTotalWidth, centerOffset, x]); useEffect(() => { x.set(-(active * itemTotalWidth) + centerOffset); @@ -94,76 +171,24 @@ export function CurvedCarousel({ items, title, subtitle }: CurvedCarouselProps) > (sx as number) + (so as number)), left: "50%" }} + style={{ x: combinedPos, left: "50%" }} onDragStart={handleDragStart} onDragEnd={handleDragEnd} className="absolute flex gap-8 items-center" > - {displayItems.map((item, index) => { - const itemOffset = index * itemTotalWidth; - - // Transform logic relative to the combined springX + scrollOffset - const combinedPos = useTransform([springX, scrollOffset], ([sx, so]) => (sx as number) + (so as number)); - - // eslint-disable-next-line react-hooks/rules-of-hooks - const itemRotation = useTransform(combinedPos, - [-itemOffset + centerOffset - itemTotalWidth, -itemOffset + centerOffset, -itemOffset + centerOffset + itemTotalWidth], - [-45, 0, 45] - ); - // eslint-disable-next-line react-hooks/rules-of-hooks - const itemScale = useTransform(combinedPos, - [-itemOffset + centerOffset - itemTotalWidth, -itemOffset + centerOffset, -itemOffset + centerOffset + itemTotalWidth], - [0.8, 1, 0.8] - ); - // eslint-disable-next-line react-hooks/rules-of-hooks - const itemZ = useTransform(combinedPos, - [-itemOffset + centerOffset - itemTotalWidth, -itemOffset + centerOffset, -itemOffset + centerOffset + itemTotalWidth], - [-200, 0, -200] - ); - // eslint-disable-next-line react-hooks/rules-of-hooks - const itemOpacity = useTransform(combinedPos, - [-itemOffset + centerOffset - 2 * itemTotalWidth, -itemOffset + centerOffset, -itemOffset + centerOffset + 2 * itemTotalWidth], - [0, 1, 0] - ); - - return ( - handleCardClick(item.videoId)} - > - {item.title} - -
- - - -

- {item.title} -

-
- -
- - ); - })} + {displayItems.map((item, index) => ( + + ))}
From 88887c2760c170cc2c799cd23b6a6fd4d3aee54f Mon Sep 17 00:00:00 2001 From: laveshparyani Date: Fri, 27 Mar 2026 08:40:16 +0530 Subject: [PATCH 5/6] fix(ui): implement Ghost Drag to eliminate carousel blanking This commit introduces the Ghost Drag system for the CurvedCarousel. By separating drag interaction from rendering, we avoid state conflicts during high-velocity scrolls, ensuring the carousel never goes blank while maintaining full card clickability. --- apps/web/components/ui/curved-carousel.tsx | 43 +++++++++++++++------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/apps/web/components/ui/curved-carousel.tsx b/apps/web/components/ui/curved-carousel.tsx index d6261f6..725420e 100644 --- a/apps/web/components/ui/curved-carousel.tsx +++ b/apps/web/components/ui/curved-carousel.tsx @@ -68,7 +68,7 @@ function CarouselCard({ z: itemZ, opacity: itemOpacity, }} - className="relative flex-shrink-0 group rounded-3xl overflow-hidden border border-white/10 shadow-2xl bg-black" + className="relative flex-shrink-0 group rounded-3xl overflow-hidden border border-white/10 shadow-2xl bg-black pointer-events-auto" onClick={() => onClick(item.videoId)} > (sx as number) + (so as number)); @@ -141,6 +141,11 @@ export function CurvedCarousel({ items, title, subtitle }: CurvedCarouselProps) if (Math.abs(velocity) > 500) { 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); }; @@ -167,14 +172,20 @@ 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" > {displayItems.map((item, index) => (
- {items.map((_, i) => ( -
); From 923966172ac511cca7aa84819b9232ef36141532 Mon Sep 17 00:00:00 2001 From: laveshparyani Date: Fri, 27 Mar 2026 09:05:41 +0530 Subject: [PATCH 6/6] fix(content): update Solo E TV videos with verified thumbnails & add image fallback --- apps/web/app/partners/[slug]/page.tsx | 6 +++--- apps/web/components/ui/curved-carousel.tsx | 7 ++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/web/app/partners/[slug]/page.tsx b/apps/web/app/partners/[slug]/page.tsx index 6a08a8d..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: "JvA-MhX9W-Y", title: "How I Made $20,000 in 1 Day | Full Breakdown" }, + { 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 725420e..110383b 100644 --- a/apps/web/components/ui/curved-carousel.tsx +++ b/apps/web/components/ui/curved-carousel.tsx @@ -58,6 +58,8 @@ function CarouselCard({ [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`); + }} />