Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion apps/web/app/partners/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Footer } from "@/components/footer";
import { PartnerHero } from "@/components/sections/partner-hero";
import { PartnerBio } from "@/components/sections/partner-bio";
import { PartnerVideo } from "@/components/sections/partner-video";
import { CurvedCarousel } from "@/components/ui/curved-carousel";
import { PartnerCTA } from "@/components/sections/partner-cta";
import { PartnerFeatures } from "@/components/sections/partner-features";
import { TrustMetricsSection } from "@/components/sections/trust-metrics-section";
Expand All @@ -18,6 +19,7 @@ interface PartnerData {
ctaMessage?: string;
youtubeId?: string;
ctaUrl?: string;
featuredVideos?: { id: string; videoId: string; title: string }[];
}

const PARTNERS_DATA: Record<string, PartnerData> = {
Expand All @@ -28,7 +30,15 @@ const PARTNERS_DATA: Record<string, PartnerData> = {
quote: "Life is short and working for other people sucks",
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.",
youtubeId: "01loBLlZRHw",
ctaUrl: "https://portal.restrofx.com/r/glaPWwHQ"
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: "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" }
]
},
"default": {
name: "Our Global Partner",
Expand Down Expand Up @@ -76,6 +86,16 @@ export default function PartnerProfilePage({ params }: { params: { slug: string
</ScrollReveal>
)}

{partner.featuredVideos && (
<ScrollReveal>
<CurvedCarousel
items={partner.featuredVideos}
title="Our Latest Insights"
subtitle="Watch the journey unfold. From strategy deep dives to lifestyle updates, stay connected with our partner's latest content."
/>
</ScrollReveal>
)}

<div className="py-4 sm:py-6">
<ScrollReveal>
<div className="text-center mb-12">
Expand Down
145 changes: 145 additions & 0 deletions apps/web/components/ui/curved-carousel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
"use client";

import { motion, useMotionValue, useSpring, useTransform } from "framer-motion";
import { useState, useRef, useEffect } from "react";
import Image from "next/image";
import { Play } from "lucide-react";

interface CarouselItem {
id: string;
title: string;
videoId: string;
}

interface CurvedCarouselProps {
items: CarouselItem[];
title?: string;
subtitle?: string;
}

export function CurvedCarousel({ items, title, subtitle }: CurvedCarouselProps) {
const [active, setActive] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);
const x = useMotionValue(0);

// Spring for smooth dragging
const springX = useSpring(x, { stiffness: 300, damping: 30 });

// 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 handleDragEnd = (_: any, info: any) => {
// Determine the closest index based on the drag offset
const velocity = info.velocity.x;

// 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)));
}
};

useEffect(() => {
x.set(-active * itemWidth);
}, [active, x]);

return (
<section className="py-24 bg-transparent overflow-hidden perspective-1000">
<div className="container px-4 md:px-6 mb-12 text-center">
{title && <h2 className="text-4xl md:text-6xl font-bold font-heading mb-4">{title}</h2>}
{subtitle && <p className="text-muted-foreground text-lg md:text-xl max-w-2xl mx-auto">{subtitle}</p>}
</div>

<div
ref={containerRef}
className="relative h-[500px] flex items-center justify-center cursor-grab active:cursor-grabbing"
>
<motion.div
drag="x"
dragConstraints={{ left: -(items.length - 1) * itemWidth, right: 0 }}
style={{ x }}
onDragEnd={handleDragEnd}
className="flex gap-8 items-center"
>
{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 (
<motion.div
key={item.id}
style={{
width: itemWidth,
height: 400,
rotateY: itemRotation,
scale: itemScale,
z: itemZ,
opacity: itemOpacity,
}}
className="relative flex-shrink-0 group rounded-3xl overflow-hidden border border-white/10 shadow-2xl bg-black"
onClick={() => window.open(`https://youtube.com/watch?v=${item.videoId}`, '_blank')}
>
<Image
src={`https://i.ytimg.com/vi/${item.videoId}/maxresdefault.jpg`}
alt={item.title}
fill
className="object-cover opacity-60 group-hover:opacity-80 transition-opacity duration-500"
/>

{/* Overlay */}
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent flex flex-col justify-end p-6">
<motion.div
initial={{ y: 20, opacity: 0 }}
whileInView={{ y: 0, opacity: 1 }}
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"
>
<Play className="w-6 h-6 text-primary fill-primary" />
</motion.div>
<h3 className="text-white font-bold text-lg leading-tight group-hover:text-primary transition-colors duration-300">
{item.title}
</h3>
</div>

{/* Glassmorphic Reflection Overlay */}
<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" />
</motion.div>
);
})}
</motion.div>
</div>

<div className="flex justify-center gap-2 mt-8">
{items.map((_, i) => (
<button
key={i}
onClick={() => setActive(i)}
className={`h-2 rounded-full transition-all duration-300 ${active === i ? 'w-8 bg-primary' : 'w-2 bg-neutral-800'}`}
/>
))}
</div>
</section>
);
}
Loading