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