Retour au catalogue
Contact 2 Columns
Contact en 2 colonnes equilibrees : infos a gauche, formulaire a droite.
contactmedium Both Responsive a11y
minimalcorporateagencysaasuniversalsplit
Theme
"use client";
import { useState } from "react";
import { motion } from "framer-motion";
import { Mail, Phone, MapPin, ArrowRight } from "lucide-react";
interface ContactInfo {
icon: string;
label: string;
value: string;
href: string;
}
interface FormField {
name: string;
label: string;
type: string;
placeholder?: string;
options?: string[];
required: boolean;
}
interface Contact2ColProps {
badge?: string;
title?: string;
subtitle?: string;
leftColumn?: {
heading: string;
text: string;
contacts: ContactInfo[];
};
rightColumn?: {
fields: FormField[];
ctaLabel: string;
};
}
const ease: [number, number, number, number] = [0.16, 1, 0.3, 1];
const iconMap: Record<string, React.ReactNode> = {
mail: <Mail size={16} />,
phone: <Phone size={16} />,
map: <MapPin size={16} />,
};
export default function Contact2Col({
badge = "Contact",
title = "Discutons ensemble",
subtitle = "",
leftColumn,
rightColumn,
}: Contact2ColProps) {
const [sent, setSent] = useState(false);
return (
<section
className="py-20 lg:py-28"
style={{ background: "var(--color-background)" }}
>
<div className="mx-auto max-w-6xl 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-16"
>
{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>
<div className="grid lg:grid-cols-2 gap-14 lg:gap-20">
{/* Left */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease, delay: 0.05 }}
viewport={{ once: true }}
>
{leftColumn && (
<>
<h3
className="text-xl font-semibold mb-4"
style={{ color: "var(--color-foreground)" }}
>
{leftColumn.heading}
</h3>
<p
className="text-sm leading-relaxed mb-10"
style={{ color: "var(--color-foreground-muted)" }}
>
{leftColumn.text}
</p>
<div className="flex flex-col gap-6">
{leftColumn.contacts.map((c) => (
<div key={c.label} className="flex items-start gap-4">
<div
className="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0"
style={{
background: "var(--color-background-alt)",
color: "var(--color-accent)",
}}
>
{iconMap[c.icon] ?? <Mail size={16} />}
</div>
<div>
<p
className="text-xs font-medium tracking-wide uppercase mb-1"
style={{ color: "var(--color-foreground-muted)" }}
>
{c.label}
</p>
{c.href ? (
<a
href={c.href}
className="text-sm hover:opacity-70 transition-opacity"
style={{ color: "var(--color-foreground)" }}
>
{c.value}
</a>
) : (
<p
className="text-sm"
style={{ color: "var(--color-foreground)" }}
>
{c.value}
</p>
)}
</div>
</div>
))}
</div>
</>
)}
</motion.div>
{/* Right / Form */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease, delay: 0.15 }}
viewport={{ once: true }}
>
{sent ? (
<div
className="rounded-xl p-10 text-center"
style={{
background: "var(--color-background-alt)",
border: "1px solid var(--color-border)",
}}
>
<p
className="text-xl font-semibold"
style={{ color: "var(--color-foreground)" }}
>
Merci !
</p>
<p
className="mt-2 text-sm"
style={{ color: "var(--color-foreground-muted)" }}
>
Nous reviendrons vers vous rapidement.
</p>
</div>
) : (
<form
onSubmit={(e) => {
e.preventDefault();
setSent(true);
}}
className="flex flex-col gap-5 rounded-xl p-8"
style={{
background: "var(--color-background-alt)",
border: "1px solid var(--color-border)",
}}
>
{rightColumn?.fields.map((f) => (
<div key={f.name}>
<label
className="block mb-1.5 text-xs font-medium tracking-wide uppercase"
style={{ color: "var(--color-foreground-muted)" }}
>
{f.label}{f.required ? " *" : ""}
</label>
{f.type === "textarea" ? (
<textarea
name={f.name}
required={f.required}
placeholder={f.placeholder}
rows={4}
className="w-full px-4 py-3 rounded-lg text-sm resize-none outline-none"
style={{
background: "var(--color-background)",
color: "var(--color-foreground)",
border: "1px solid var(--color-border)",
}}
/>
) : f.type === "select" ? (
<select
name={f.name}
required={f.required}
className="w-full px-4 py-3 rounded-lg text-sm outline-none"
style={{
background: "var(--color-background)",
color: "var(--color-foreground)",
border: "1px solid var(--color-border)",
}}
>
<option value="">Choisir...</option>
{f.options?.map((o) => (
<option key={o} value={o}>
{o}
</option>
))}
</select>
) : (
<input
name={f.name}
type={f.type}
required={f.required}
placeholder={f.placeholder}
className="w-full px-4 py-3 rounded-lg text-sm outline-none"
style={{
background: "var(--color-background)",
color: "var(--color-foreground)",
border: "1px solid var(--color-border)",
}}
/>
)}
</div>
))}
<button
type="submit"
className="flex items-center justify-center gap-2 w-full py-3 rounded-lg text-sm font-semibold transition-opacity hover:opacity-90 mt-2"
style={{
background: "var(--color-accent)",
color: "var(--color-background)",
}}
>
{rightColumn?.ctaLabel ?? "Envoyer"}
<ArrowRight size={14} />
</button>
</form>
)}
</motion.div>
</div>
</div>
</section>
);
}