Retour au catalogue

Contact Booking

Section reservation d'appel avec calendrier placeholder et choix de creneaux horaires.

contactmedium Both Responsive a11y
corporateminimalsaasagencymedicalsplit
Theme
"use client";

import { useState } from "react";
import { motion } from "framer-motion";
import { Calendar, Clock, Check } from "lucide-react";

interface TimeSlot {
  id: string;
  label: string;
}

interface ContactBookingProps {
  badge?: string;
  title?: string;
  subtitle?: string;
  slots?: TimeSlot[];
  ctaLabel?: string;
  calendarLabel?: string;
}

const ease: [number, number, number, number] = [0.16, 1, 0.3, 1];

export default function ContactBooking({
  badge = "Rendez-vous",
  title = "Reservez un appel decouverte",
  subtitle = "Choisissez un creneau qui vous convient et discutons de votre projet.",
  slots = [],
  ctaLabel = "Confirmer le creneau",
  calendarLabel = "Choisissez une date",
}: ContactBookingProps) {
  const [selectedSlot, setSelectedSlot] = useState<string | null>(null);
  const [booked, setBooked] = useState(false);

  return (
    <section
      className="py-20 lg:py-28"
      style={{ background: "var(--color-background)" }}
    >
      <div className="mx-auto max-w-5xl px-6">
        <motion.div
          initial={{ opacity: 0, y: 20 }}
          whileInView={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.6, ease }}
          viewport={{ once: true }}
          className="text-center mb-14"
        >
          {badge && (
            <span
              className="inline-block mb-4 text-xs font-medium tracking-widest uppercase"
              style={{ color: "var(--color-accent)" }}
            >
              {badge}
            </span>
          )}
          <h2
            className="text-3xl md:text-4xl lg:text-5xl font-bold"
            style={{ color: "var(--color-foreground)" }}
          >
            {title}
          </h2>
          {subtitle && (
            <p className="mt-3 text-base" style={{ color: "var(--color-foreground-muted)" }}>
              {subtitle}
            </p>
          )}
        </motion.div>

        {booked ? (
          <motion.div
            initial={{ opacity: 0, scale: 0.95 }}
            animate={{ opacity: 1, scale: 1 }}
            transition={{ duration: 0.4, ease }}
            className="rounded-xl p-12 text-center mx-auto max-w-md"
            style={{ background: "var(--color-background-alt)", border: "1px solid var(--color-border)" }}
          >
            <Check size={40} style={{ color: "var(--color-accent)", margin: "0 auto" }} />
            <p className="text-xl font-semibold mt-4" style={{ color: "var(--color-foreground)" }}>
              Creneau reserve !
            </p>
            <p className="mt-2 text-sm" style={{ color: "var(--color-foreground-muted)" }}>
              Vous recevrez un email de confirmation.
            </p>
          </motion.div>
        ) : (
          <div className="grid lg:grid-cols-2 gap-8">
            {/* Calendar placeholder */}
            <motion.div
              initial={{ opacity: 0, x: -20 }}
              whileInView={{ opacity: 1, x: 0 }}
              transition={{ duration: 0.5, ease, delay: 0.05 }}
              viewport={{ once: true }}
              className="rounded-xl p-8 flex flex-col items-center justify-center min-h-[320px]"
              style={{ background: "var(--color-background-alt)", border: "1px solid var(--color-border)" }}
            >
              <Calendar size={40} style={{ color: "var(--color-accent)", opacity: 0.5 }} />
              <p className="mt-4 text-sm font-medium" style={{ color: "var(--color-foreground)" }}>
                {calendarLabel}
              </p>
              {/* Placeholder grid */}
              <div className="grid grid-cols-7 gap-2 mt-6 w-full max-w-[280px]">
                {["L", "M", "M", "J", "V", "S", "D"].map((d, i) => (
                  <span key={i} className="text-[10px] font-medium text-center" style={{ color: "var(--color-foreground-muted)" }}>
                    {d}
                  </span>
                ))}
                {Array.from({ length: 28 }, (_, i) => (
                  <div
                    key={i}
                    className="aspect-square rounded-md flex items-center justify-center text-xs cursor-pointer transition-colors hover:opacity-80"
                    style={{
                      background: i === 14 ? "var(--color-accent)" : "var(--color-background)",
                      color: i === 14 ? "var(--color-background)" : "var(--color-foreground-muted)",
                      border: "1px solid var(--color-border)",
                    }}
                  >
                    {i + 1}
                  </div>
                ))}
              </div>
            </motion.div>

            {/* Time slots */}
            <motion.div
              initial={{ opacity: 0, x: 20 }}
              whileInView={{ opacity: 1, x: 0 }}
              transition={{ duration: 0.5, ease, delay: 0.15 }}
              viewport={{ once: true }}
              className="flex flex-col gap-4"
            >
              <div className="flex items-center gap-2 mb-2">
                <Clock size={16} style={{ color: "var(--color-accent)" }} />
                <p className="text-sm font-semibold" style={{ color: "var(--color-foreground)" }}>
                  Creneaux disponibles
                </p>
              </div>
              <div className="flex flex-col gap-3">
                {slots.map((slot) => (
                  <button
                    key={slot.id}
                    onClick={() => setSelectedSlot(slot.id)}
                    className="w-full text-left px-5 py-4 rounded-lg text-sm font-medium transition-all cursor-pointer"
                    style={{
                      background: selectedSlot === slot.id ? "var(--color-accent)" : "var(--color-background-alt)",
                      color: selectedSlot === slot.id ? "var(--color-background)" : "var(--color-foreground)",
                      border: `1px solid ${selectedSlot === slot.id ? "var(--color-accent)" : "var(--color-border)"}`,
                    }}
                  >
                    {slot.label}
                  </button>
                ))}
              </div>
              <button
                onClick={() => selectedSlot && setBooked(true)}
                disabled={!selectedSlot}
                className="mt-4 w-full py-3 rounded-lg text-sm font-semibold transition-opacity hover:opacity-90 cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
                style={{ background: "var(--color-accent)", color: "var(--color-background)" }}
              >
                {ctaLabel}
              </button>
            </motion.div>
          </div>
        )}
      </div>
    </section>
  );
}

Avis

Contact Booking — React Contact Section — Incubator