Retour au catalogue

Product Showcase Carousel

Carousel de produits avec image large, navigation fleches et dots. Transition animee entre produits.

product-showcasemedium Both Responsive a11y
minimalelegantecommerceuniversalcarousel
Theme
"use client";

import React, { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { ChevronLeft, ChevronRight } from "lucide-react";

interface Product {
  id: string;
  name: string;
  price: string;
  imageSrc?: string;
  imageAlt?: string;
  tag?: string;
}

interface ProductShowcaseCarouselProps {
  badge?: string;
  title?: string;
  subtitle?: string;
  products?: Product[];
}

const EASE = [0.16, 1, 0.3, 1] as const;

export default function ProductShowcaseCarousel({
  badge = "Collection",
  title = "Nos produits",
  subtitle = "Decouvrez notre selection",
  products = [],
}: ProductShowcaseCarouselProps) {
  const [current, setCurrent] = useState(0);

  const prev = () => setCurrent((c) => (c === 0 ? products.length - 1 : c - 1));
  const next = () => setCurrent((c) => (c === products.length - 1 ? 0 : c + 1));

  if (products.length === 0) return null;

  return (
    <section
      style={{
        padding: "var(--section-padding-y-lg) 0",
        background: "var(--color-background)",
        overflow: "hidden",
      }}
    >
      <div
        style={{
          maxWidth: "var(--container-max-width)",
          margin: "0 auto",
          padding: "0 var(--container-padding-x)",
        }}
      >
        {/* Header */}
        <motion.div
          initial={{ opacity: 0, y: 20 }}
          whileInView={{ opacity: 1, y: 0 }}
          viewport={{ once: true, margin: "-80px" }}
          transition={{ duration: 0.5, ease: EASE }}
          style={{
            display: "flex",
            justifyContent: "space-between",
            alignItems: "flex-end",
            marginBottom: "3rem",
            flexWrap: "wrap",
            gap: "1rem",
          }}
        >
          <div>
            {badge && (
              <span
                style={{
                  display: "inline-block",
                  fontSize: "0.75rem",
                  fontWeight: 600,
                  textTransform: "uppercase",
                  letterSpacing: "0.1em",
                  color: "var(--color-accent)",
                  marginBottom: "0.5rem",
                }}
              >
                {badge}
              </span>
            )}
            <h2
              style={{
                fontFamily: "var(--font-sans)",
                fontSize: "clamp(1.75rem, 3vw, 2.75rem)",
                fontWeight: 700,
                letterSpacing: "-0.02em",
                color: "var(--color-foreground)",
                marginBottom: "0.5rem",
              }}
            >
              {title}
            </h2>
            <p style={{ fontSize: "1rem", color: "var(--color-foreground-muted)" }}>
              {subtitle}
            </p>
          </div>

          {/* Navigation arrows */}
          <div style={{ display: "flex", gap: "0.5rem" }}>
            <button
              onClick={prev}
              aria-label="Produit precedent"
              style={{
                width: 44,
                height: 44,
                borderRadius: "var(--radius-full)",
                border: "1px solid var(--color-border)",
                background: "var(--color-background)",
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                cursor: "pointer",
                color: "var(--color-foreground-muted)",
                transition: "all var(--duration-normal) var(--ease-out)",
              }}
            >
              <ChevronLeft style={{ width: 20, height: 20 }} />
            </button>
            <button
              onClick={next}
              aria-label="Produit suivant"
              style={{
                width: 44,
                height: 44,
                borderRadius: "var(--radius-full)",
                border: "1px solid var(--color-accent)",
                background: "var(--color-accent)",
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                cursor: "pointer",
                color: "var(--color-foreground)",
                transition: "all var(--duration-normal) var(--ease-out)",
              }}
            >
              <ChevronRight style={{ width: 20, height: 20 }} />
            </button>
          </div>
        </motion.div>

        {/* Carousel */}
        <div style={{ position: "relative", minHeight: "380px" }}>
          <AnimatePresence mode="wait">
            <motion.div
              key={current}
              initial={{ opacity: 0, x: 60 }}
              animate={{ opacity: 1, x: 0 }}
              exit={{ opacity: 0, x: -60 }}
              transition={{ duration: 0.45, ease: EASE }}
              style={{
                display: "grid",
                gridTemplateColumns: "1fr",
                gap: "2rem",
                alignItems: "center",
              }}
              className="md:!grid-cols-[1.2fr_1fr]"
            >
              {/* Image */}
              <div
                style={{
                  borderRadius: "var(--radius-xl)",
                  overflow: "hidden",
                  aspectRatio: "4/3",
                  background: "var(--color-background-alt)",
                  position: "relative",
                }}
              >
                {products[current].imageSrc ? (
                  <img
                    src={products[current].imageSrc}
                    alt={products[current].imageAlt || products[current].name}
                    style={{ width: "100%", height: "100%", objectFit: "cover" }}
                  />
                ) : (
                  <div
                    style={{
                      width: "100%",
                      height: "100%",
                      display: "flex",
                      alignItems: "center",
                      justifyContent: "center",
                      background: `radial-gradient(circle, color-mix(in srgb, var(--color-accent) 8%, transparent), transparent 60%)`,
                      color: "var(--color-foreground-muted)",
                      fontSize: "0.875rem",
                    }}
                  >
                    {products[current].name}
                  </div>
                )}
                {products[current].tag && (
                  <span
                    style={{
                      position: "absolute",
                      top: "1rem",
                      left: "1rem",
                      padding: "0.35rem 0.75rem",
                      borderRadius: "var(--radius-full)",
                      background: "var(--color-accent)",
                      color: "var(--color-foreground)",
                      fontSize: "0.75rem",
                      fontWeight: 600,
                    }}
                  >
                    {products[current].tag}
                  </span>
                )}
              </div>

              {/* Info */}
              <div>
                <h3
                  style={{
                    fontFamily: "var(--font-sans)",
                    fontSize: "clamp(1.5rem, 2.5vw, 2.25rem)",
                    fontWeight: 700,
                    color: "var(--color-foreground)",
                    marginBottom: "0.75rem",
                  }}
                >
                  {products[current].name}
                </h3>
                <span
                  style={{
                    fontSize: "1.5rem",
                    fontWeight: 700,
                    color: "var(--color-accent)",
                  }}
                >
                  {products[current].price}
                </span>
              </div>
            </motion.div>
          </AnimatePresence>
        </div>

        {/* Dots */}
        <div
          style={{
            display: "flex",
            justifyContent: "center",
            gap: "0.5rem",
            marginTop: "2rem",
          }}
        >
          {products.map((_, i) => (
            <button
              key={i}
              onClick={() => setCurrent(i)}
              aria-label={`Voir produit ${i + 1}`}
              style={{
                width: i === current ? 32 : 8,
                height: 8,
                borderRadius: "var(--radius-full)",
                background: i === current ? "var(--color-accent)" : "var(--color-border)",
                border: "none",
                cursor: "pointer",
                transition: "all var(--duration-normal) var(--ease-out)",
              }}
            />
          ))}
        </div>
      </div>
    </section>
  );
}

Avis

Product Showcase Carousel — React Product-showcase Section — Incubator