Skip to content

Commit 410a92c

Browse files
authored
Merge pull request #6 from stackified/dev
Dev
2 parents f9f882e + f1eb0a3 commit 410a92c

2 files changed

Lines changed: 166 additions & 1 deletion

File tree

apps/web/app/partners/[slug]/page.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Footer } from "@/components/footer";
44
import { PartnerHero } from "@/components/sections/partner-hero";
55
import { PartnerBio } from "@/components/sections/partner-bio";
66
import { PartnerVideo } from "@/components/sections/partner-video";
7+
import { CurvedCarousel } from "@/components/ui/curved-carousel";
78
import { PartnerCTA } from "@/components/sections/partner-cta";
89
import { PartnerFeatures } from "@/components/sections/partner-features";
910
import { TrustMetricsSection } from "@/components/sections/trust-metrics-section";
@@ -18,6 +19,7 @@ interface PartnerData {
1819
ctaMessage?: string;
1920
youtubeId?: string;
2021
ctaUrl?: string;
22+
featuredVideos?: { id: string; videoId: string; title: string }[];
2123
}
2224

2325
const PARTNERS_DATA: Record<string, PartnerData> = {
@@ -28,7 +30,15 @@ const PARTNERS_DATA: Record<string, PartnerData> = {
2830
quote: "Life is short and working for other people sucks",
2931
ctaMessage: "Trade with the broker I trust. Join me at RestroFX and experience trading the way it was meant to be. Raw spreads, lightning-fast execution, and a platform that puts you first.",
3032
youtubeId: "01loBLlZRHw",
31-
ctaUrl: "https://portal.restrofx.com/r/glaPWwHQ"
33+
ctaUrl: "https://portal.restrofx.com/r/glaPWwHQ",
34+
featuredVideos: [
35+
{ id: "1", videoId: "R2djd5ACzPM", title: "i'm finally buying my dream car" },
36+
{ id: "2", videoId: "_QmCh4dNVGE", title: "Don't Trade Every Pair | Here's What Actually Works" },
37+
{ id: "3", videoId: "DV6cte3H9rc", title: "Pulled $15k profit - here's every single trade" },
38+
{ id: "4", videoId: "KhLUPlL777U", title: "Why You Should Reconsider Trading This year" },
39+
{ id: "5", videoId: "SyC37iKc2wE", title: "I Made $20k Trading Silver | Here's My Exact Strategy" },
40+
{ id: "6", videoId: "rExdi9Vzkxk", title: "Is Trading Really Worth It? My 6 Years of Results" }
41+
]
3242
},
3343
"default": {
3444
name: "Our Global Partner",
@@ -76,6 +86,16 @@ export default function PartnerProfilePage({ params }: { params: { slug: string
7686
</ScrollReveal>
7787
)}
7888

89+
{partner.featuredVideos && (
90+
<ScrollReveal>
91+
<CurvedCarousel
92+
items={partner.featuredVideos}
93+
title="Our Latest Insights"
94+
subtitle="Watch the journey unfold. From strategy deep dives to lifestyle updates, stay connected with our partner's latest content."
95+
/>
96+
</ScrollReveal>
97+
)}
98+
7999
<div className="py-4 sm:py-6">
80100
<ScrollReveal>
81101
<div className="text-center mb-12">
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"use client";
2+
3+
import { motion, useMotionValue, useSpring, useTransform } from "framer-motion";
4+
import { useState, useRef, useEffect } from "react";
5+
import Image from "next/image";
6+
import { Play } from "lucide-react";
7+
8+
interface CarouselItem {
9+
id: string;
10+
title: string;
11+
videoId: string;
12+
}
13+
14+
interface CurvedCarouselProps {
15+
items: CarouselItem[];
16+
title?: string;
17+
subtitle?: string;
18+
}
19+
20+
export function CurvedCarousel({ items, title, subtitle }: CurvedCarouselProps) {
21+
const [active, setActive] = useState(0);
22+
const containerRef = useRef<HTMLDivElement>(null);
23+
const x = useMotionValue(0);
24+
25+
// Spring for smooth dragging
26+
const springX = useSpring(x, { stiffness: 300, damping: 30 });
27+
28+
// Calculate the rotation based on the number of items
29+
// We want to map the drag distance to an index
30+
const itemWidth = 300; // estimated width of a card
31+
32+
const handleDragEnd = (_: any, info: any) => {
33+
// Determine the closest index based on the drag offset
34+
const velocity = info.velocity.x;
35+
36+
// Simple snapping
37+
if (Math.abs(velocity) > 500) {
38+
if (velocity > 0) setActive((prev) => Math.max(0, prev - 1));
39+
else setActive((prev) => Math.min(items.length - 1, prev + 1));
40+
} else {
41+
const index = Math.round(-x.get() / itemWidth);
42+
setActive(Math.max(0, Math.min(items.length - 1, index)));
43+
}
44+
};
45+
46+
useEffect(() => {
47+
x.set(-active * itemWidth);
48+
}, [active, x]);
49+
50+
return (
51+
<section className="py-24 bg-transparent overflow-hidden perspective-1000">
52+
<div className="container px-4 md:px-6 mb-12 text-center">
53+
{title && <h2 className="text-4xl md:text-6xl font-bold font-heading mb-4">{title}</h2>}
54+
{subtitle && <p className="text-muted-foreground text-lg md:text-xl max-w-2xl mx-auto">{subtitle}</p>}
55+
</div>
56+
57+
<div
58+
ref={containerRef}
59+
className="relative h-[500px] flex items-center justify-center cursor-grab active:cursor-grabbing"
60+
>
61+
<motion.div
62+
drag="x"
63+
dragConstraints={{ left: -(items.length - 1) * itemWidth, right: 0 }}
64+
style={{ x }}
65+
onDragEnd={handleDragEnd}
66+
className="flex gap-8 items-center"
67+
>
68+
{items.map((item, index) => {
69+
// Map the global x motion value to local item transforms
70+
// eslint-disable-next-line react-hooks/rules-of-hooks
71+
const itemRotation = useTransform(springX,
72+
[- (index + 1) * itemWidth, - index * itemWidth, - (index - 1) * itemWidth],
73+
[-45, 0, 45]
74+
);
75+
// eslint-disable-next-line react-hooks/rules-of-hooks
76+
const itemScale = useTransform(springX,
77+
[- (index + 1) * itemWidth, - index * itemWidth, - (index - 1) * itemWidth],
78+
[0.8, 1, 0.8]
79+
);
80+
// eslint-disable-next-line react-hooks/rules-of-hooks
81+
const itemZ = useTransform(springX,
82+
[- (index + 1) * itemWidth, - index * itemWidth, - (index - 1) * itemWidth],
83+
[-200, 0, -200]
84+
);
85+
// eslint-disable-next-line react-hooks/rules-of-hooks
86+
const itemOpacity = useTransform(springX,
87+
[- (index + 2) * itemWidth, - index * itemWidth, - (index - 2) * itemWidth],
88+
[0, 1, 0]
89+
);
90+
91+
return (
92+
<motion.div
93+
key={item.id}
94+
style={{
95+
width: itemWidth,
96+
height: 400,
97+
rotateY: itemRotation,
98+
scale: itemScale,
99+
z: itemZ,
100+
opacity: itemOpacity,
101+
}}
102+
className="relative flex-shrink-0 group rounded-3xl overflow-hidden border border-white/10 shadow-2xl bg-black"
103+
onClick={() => window.open(`https://youtube.com/watch?v=${item.videoId}`, '_blank')}
104+
>
105+
<Image
106+
src={`https://i.ytimg.com/vi/${item.videoId}/maxresdefault.jpg`}
107+
alt={item.title}
108+
fill
109+
className="object-cover opacity-60 group-hover:opacity-80 transition-opacity duration-500"
110+
/>
111+
112+
{/* Overlay */}
113+
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent flex flex-col justify-end p-6">
114+
<motion.div
115+
initial={{ y: 20, opacity: 0 }}
116+
whileInView={{ y: 0, opacity: 1 }}
117+
className="p-4 rounded-full bg-primary/20 backdrop-blur-md border border-primary/40 w-fit mb-4 group-hover:scale-110 transition-transform duration-300"
118+
>
119+
<Play className="w-6 h-6 text-primary fill-primary" />
120+
</motion.div>
121+
<h3 className="text-white font-bold text-lg leading-tight group-hover:text-primary transition-colors duration-300">
122+
{item.title}
123+
</h3>
124+
</div>
125+
126+
{/* Glassmorphic Reflection Overlay */}
127+
<div className="absolute inset-x-0 bottom-0 h-1/3 bg-gradient-to-t from-white/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-700 pointer-events-none" />
128+
</motion.div>
129+
);
130+
})}
131+
</motion.div>
132+
</div>
133+
134+
<div className="flex justify-center gap-2 mt-8">
135+
{items.map((_, i) => (
136+
<button
137+
key={i}
138+
onClick={() => setActive(i)}
139+
className={`h-2 rounded-full transition-all duration-300 ${active === i ? 'w-8 bg-primary' : 'w-2 bg-neutral-800'}`}
140+
/>
141+
))}
142+
</div>
143+
</section>
144+
);
145+
}

0 commit comments

Comments
 (0)