Retour au catalogue

Portfolio Carousel

Carousel horizontal de projets avec grandes images et overlay d'infos.

portfoliomedium Both Responsive a11y
boldelegantuniversalagencyuniversalcarousel
Theme
"use client";

import React, { useRef } from "react";
import { motion } from "framer-motion";
import { ArrowLeft, ArrowRight } from "lucide-react";

interface ProjectItem {
  id: string;
  title: string;
  category: string;
  image?: string;
  description?: string;
}

interface PortfolioCarouselProps {
  badge?: string;
  title?: string;
  subtitle?: string;
  projects: ProjectItem[];
}

export default function PortfolioCarousel({ badge, title, subtitle, projects }: PortfolioCarouselProps) {
  const scrollRef = useRef<HTMLDivElement>(null);

  function scroll(dir: "left" | "right") {
    if (!scrollRef.current) return;
    const amount = scrollRef.current.clientWidth * 0.7;
    scrollRef.current.scrollBy({ left: dir === "left" ? -amount : amount, behavior: "smooth" });
  }

  return (
    <section className="py-[var(--section-padding-y,6rem)]" style={{ backgroundColor: "var(--color-background)" }}>
      <div className="mx-auto max-w-7xl px-[var(--container-padding-x,1.5rem)]">
        <div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
          <motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-80px" }} transition={{ duration: 0.5 }}>
            {badge && <span className="inline-block text-xs font-medium tracking-wider uppercase px-3 py-1 rounded-full border" style={{ color: "var(--color-accent)", borderColor: "var(--color-border)" }}>{badge}</span>}
            {title && <h2 className="mt-4 text-3xl font-bold tracking-tight md:text-4xl" style={{ color: "var(--color-foreground)" }}>{title}</h2>}
            {subtitle && <p className="mt-2 text-sm" style={{ color: "var(--color-foreground-muted)" }}>{subtitle}</p>}
          </motion.div>
          <div className="flex gap-2">
            <button onClick={() => scroll("left")} className="flex h-10 w-10 items-center justify-center rounded-full border transition-colors" style={{ borderColor: "var(--color-border)", color: "var(--color-foreground)" }} aria-label="Precedent"><ArrowLeft className="h-4 w-4" /></button>
            <button onClick={() => scroll("right")} className="flex h-10 w-10 items-center justify-center rounded-full border transition-colors" style={{ borderColor: "var(--color-border)", color: "var(--color-foreground)" }} aria-label="Suivant"><ArrowRight className="h-4 w-4" /></button>
          </div>
        </div>

        <div ref={scrollRef} className="mt-10 flex gap-6 overflow-x-auto pb-4 snap-x snap-mandatory" style={{ scrollbarWidth: "none" }}>
          {projects.map((project, i) => (
            <motion.div key={project.id} initial={{ opacity: 0, x: 40 }} whileInView={{ opacity: 1, x: 0 }} viewport={{ once: true, margin: "-40px" }} transition={{ delay: i * 0.08, duration: 0.5, ease: [0.16, 1, 0.3, 1] }} className="group flex-shrink-0 w-[min(80vw,500px)] snap-start rounded-[var(--radius-xl,1.5rem)] overflow-hidden border" style={{ backgroundColor: "var(--color-background-card)", borderColor: "var(--color-border)" }}>
              <div className="relative aspect-[16/10] overflow-hidden" style={{ backgroundColor: "var(--color-background-alt)", backgroundImage: project.image ? `url(${project.image})` : undefined, backgroundSize: "cover", backgroundPosition: "center" }}>
                <div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors duration-300" />
              </div>
              <div className="p-5">
                <span className="text-xs font-medium" style={{ color: "var(--color-accent)" }}>{project.category}</span>
                <h3 className="mt-1 text-lg font-bold tracking-tight" style={{ color: "var(--color-foreground)" }}>{project.title}</h3>
                {project.description && <p className="mt-1.5 text-sm line-clamp-2" style={{ color: "var(--color-foreground-muted)" }}>{project.description}</p>}
              </div>
            </motion.div>
          ))}
        </div>
      </div>
    </section>
  );
}

Avis

Portfolio Carousel — React Portfolio Section — Incubator