Retour au catalogue

Video Comparison

Comparaison video cote a cote avec controles de lecture synchronises. Ideal pour avant/apres ou comparaisons de produits.

videomedium Both Responsive a11y
minimalcorporatesaasbeautyuniversalsplit
Theme
"use client";

import { useState } from "react";
import { motion } from "framer-motion";
import { Play, Pause, RotateCcw } from "lucide-react";

interface ComparisonVideo {
  title: string;
  label: string;
  thumbnail: string;
  duration: string;
}

interface VideoComparisonProps {
  title?: string;
  subtitle?: string;
  leftVideo?: ComparisonVideo;
  rightVideo?: ComparisonVideo;
}

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

const defaultVideo: ComparisonVideo = {
  title: "Video",
  label: "Video",
  thumbnail: "/api/placeholder/800/500",
  duration: "0:00",
};

export default function VideoComparison({
  title = "Comparez les resultats",
  subtitle = "Avant / Apres",
  leftVideo = defaultVideo,
  rightVideo = defaultVideo,
}: VideoComparisonProps) {
  const [isPlaying, setIsPlaying] = useState(false);
  const [progress, setProgress] = useState(0);

  const handlePlay = () => {
    if (isPlaying) {
      setIsPlaying(false);
      return;
    }
    setIsPlaying(true);
    setProgress(0);
    const interval = setInterval(() => {
      setProgress((p) => {
        if (p >= 100) { clearInterval(interval); setIsPlaying(false); return 100; }
        return p + 0.3;
      });
    }, 50);
  };

  const handleReset = () => {
    setProgress(0);
    setIsPlaying(false);
  };

  return (
    <section style={{ paddingTop: "var(--section-padding-y, 6rem)", paddingBottom: "var(--section-padding-y, 6rem)", background: "var(--color-background)" }}>
      <div style={{ maxWidth: "var(--container-max-width, 72rem)", margin: "0 auto", padding: "0 var(--container-padding-x, 1.5rem)" }}>
        <motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.6, ease: EASE }} style={{ textAlign: "center", marginBottom: "3rem" }}>
          <p style={{ fontSize: "0.8125rem", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.1em", color: "var(--color-accent)", marginBottom: "0.5rem" }}>{subtitle}</p>
          <h2 style={{ fontFamily: "var(--font-sans)", fontSize: "clamp(2rem, 4vw, 3rem)", fontWeight: 700, letterSpacing: "-0.03em", color: "var(--color-foreground)" }}>{title}</h2>
        </motion.div>

        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1.5rem" }}>
          {[leftVideo, rightVideo].map((video, idx) => (
            <motion.div
              key={idx}
              initial={{ opacity: 0, x: idx === 0 ? -20 : 20 }}
              whileInView={{ opacity: 1, x: 0 }}
              viewport={{ once: true }}
              transition={{ duration: 0.6, delay: idx * 0.1, ease: EASE }}
              style={{ borderRadius: "var(--radius-xl, 1.5rem)", overflow: "hidden", border: "1px solid var(--color-border)", background: "var(--color-background-card)" }}
            >
              {/* Label badge */}
              <div style={{ padding: "0.75rem 1.25rem", borderBottom: "1px solid var(--color-border)", display: "flex", alignItems: "center", justifyContent: "space-between" }}>
                <span style={{ fontSize: "0.75rem", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.05em", color: idx === 0 ? "var(--color-foreground-muted)" : "var(--color-accent)" }}>{video.label}</span>
                <span style={{ fontSize: "0.6875rem", color: "var(--color-foreground-muted)" }}>{video.duration}</span>
              </div>

              <div style={{ position: "relative", aspectRatio: "16/10" }}>
                <img src={video.thumbnail} alt={video.title} style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }} />
                {/* Simulated progress overlay */}
                <div style={{ position: "absolute", bottom: 0, left: 0, right: 0, height: "3px", background: "var(--color-border)" }}>
                  <motion.div
                    animate={{ width: `${progress}%` }}
                    style={{ height: "100%", background: "var(--color-accent)" }}
                  />
                </div>
              </div>

              <div style={{ padding: "1rem 1.25rem" }}>
                <h3 style={{ fontSize: "1rem", fontWeight: 600, color: "var(--color-foreground)" }}>{video.title}</h3>
              </div>
            </motion.div>
          ))}
        </div>

        {/* Synchronized controls */}
        <motion.div
          initial={{ opacity: 0, y: 10 }}
          whileInView={{ opacity: 1, y: 0 }}
          viewport={{ once: true }}
          transition={{ delay: 0.3 }}
          style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: "1rem", marginTop: "2rem" }}
        >
          <motion.button
            whileHover={{ scale: 1.05 }}
            whileTap={{ scale: 0.95 }}
            onClick={handlePlay}
            style={{ display: "inline-flex", alignItems: "center", gap: "0.5rem", padding: "0.75rem 2rem", borderRadius: "var(--radius-full, 9999px)", background: "var(--color-accent)", border: "none", cursor: "pointer", fontSize: "0.875rem", fontWeight: 600, color: "var(--color-background)" }}
          >
            {isPlaying ? <Pause style={{ width: 16, height: 16 }} /> : <Play style={{ width: 16, height: 16 }} />}
            {isPlaying ? "Pause" : "Lire les deux"}
          </motion.button>
          <button
            onClick={handleReset}
            style={{ width: "40px", height: "40px", borderRadius: "50%", border: "1px solid var(--color-border)", background: "var(--color-background-card)", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}
          >
            <RotateCcw style={{ width: 16, height: 16, color: "var(--color-foreground)" }} />
          </button>
        </motion.div>
      </div>
    </section>
  );
}

Avis

Video Comparison — React Video Section — Incubator