- Create /api/contact → sends admin notification email on audit request - Create /api/devis → sends admin notification email on macon devis request - Contact.tsx: make inputs controlled, call /api/contact on submit - MaconClient.tsx DevisForm: add controlled state + submit handler calling /api/devis, add success/error states - /api/candidature: add admin notification email alongside candidate confirmation - /api/admin/candidatures/[id]/reject: fetch candidate info + send rejection email All routes read ADMIN_EMAIL env var for admin notifications (fallback to RESEND_FROM_EMAIL). https://claude.ai/code/session_01PzA98VhLMmsHpzs7gnLHGs
287 lines
11 KiB
TypeScript
287 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import MagicReveal from "@/components/ui/MagicReveal";
|
|
import Button from "@/components/ui/Button";
|
|
|
|
interface MaconClientProps {
|
|
type?: "slider" | "form" | "faq" | "floating";
|
|
avantLabel?: string;
|
|
apresLabel?: string;
|
|
avantImage?: string;
|
|
apresImage?: string;
|
|
faqs?: { q: string; a: string }[];
|
|
}
|
|
|
|
export default function MaconClient({
|
|
type,
|
|
avantLabel,
|
|
apresLabel,
|
|
avantImage,
|
|
apresImage,
|
|
faqs,
|
|
}: MaconClientProps) {
|
|
if (type === "slider") {
|
|
return (
|
|
<MagicReveal
|
|
avantLabel={avantLabel || ""}
|
|
apresLabel={apresLabel || ""}
|
|
avantImage={avantImage || ""}
|
|
apresImage={apresImage || ""}
|
|
height="h-64"
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (type === "form") {
|
|
return <DevisForm />;
|
|
}
|
|
|
|
if (type === "faq") {
|
|
return <FaqAccordion faqs={faqs || []} />;
|
|
}
|
|
|
|
if (type === "floating") {
|
|
return <FloatingCTA />;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/* ============================================================
|
|
FAQ ACCORDION
|
|
============================================================ */
|
|
function FaqAccordion({ faqs }: { faqs: { q: string; a: string }[] }) {
|
|
const [openIdx, setOpenIdx] = useState<number | null>(null);
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
{faqs.map((faq, i) => {
|
|
const isOpen = openIdx === i;
|
|
return (
|
|
<div key={i} className="bg-[#f8f6f3] border border-gray-200 rounded-xl overflow-hidden">
|
|
<button
|
|
onClick={() => setOpenIdx(isOpen ? null : i)}
|
|
className="w-full flex items-center justify-between p-5 text-left cursor-pointer"
|
|
>
|
|
<span className="text-navy font-semibold text-sm pr-4">{faq.q}</span>
|
|
<svg
|
|
className={`w-5 h-5 text-orange shrink-0 transition-transform ${isOpen ? "rotate-180" : ""}`}
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</button>
|
|
{isOpen && (
|
|
<div className="px-5 pb-5 -mt-1">
|
|
<p className="text-text-light text-sm leading-relaxed">{faq.a}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/* ============================================================
|
|
SMART DEVIS FORM
|
|
============================================================ */
|
|
function DevisForm() {
|
|
const [step, setStep] = useState<"type" | "details" | "done">("type");
|
|
const [projectType, setProjectType] = useState("");
|
|
const [fields, setFields] = useState({ name: "", phone: "", ville: "", description: "" });
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const updateField = (key: keyof typeof fields, value: string) =>
|
|
setFields((prev) => ({ ...prev, [key]: value }));
|
|
|
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
e.preventDefault();
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const res = await fetch("/api/devis", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ ...fields, projectType }),
|
|
});
|
|
if (!res.ok) {
|
|
const data = await res.json();
|
|
throw new Error(data.error || "Erreur lors de l'envoi");
|
|
}
|
|
setStep("done");
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Erreur inattendue");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
if (step === "done") {
|
|
return (
|
|
<div className="bg-white rounded-2xl p-6 sm:p-8 text-center">
|
|
<div className="w-14 h-14 bg-green-50 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
<svg className="w-7 h-7 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
</div>
|
|
<h3 className="text-navy font-bold text-lg mb-2">Demande envoyée !</h3>
|
|
<p className="text-text-muted text-sm">Nous vous recontactons sous 24h pour votre devis.</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (step === "details") {
|
|
return (
|
|
<div className="bg-white rounded-2xl p-6 sm:p-8">
|
|
<div className="flex items-center gap-2 mb-6">
|
|
<span className="bg-orange text-white text-xs font-bold px-2.5 py-1 rounded-full">2/2</span>
|
|
<h3 className="text-navy font-bold text-lg">Vos coordonnées</h3>
|
|
</div>
|
|
<p className="text-text-muted text-sm mb-5">
|
|
Projet : <strong className="text-navy">{projectType}</strong>
|
|
</p>
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-text mb-1.5">Votre nom</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
placeholder="Marc Dupont"
|
|
value={fields.name}
|
|
onChange={(e) => updateField("name", e.target.value)}
|
|
className="w-full px-4 py-3 bg-[#f8f6f3] border border-gray-200 rounded-xl text-text text-sm placeholder:text-text-muted focus:border-orange focus:ring-1 focus:ring-orange outline-none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-text mb-1.5">Téléphone</label>
|
|
<input
|
|
type="tel"
|
|
required
|
|
placeholder="06 12 34 56 78"
|
|
value={fields.phone}
|
|
onChange={(e) => updateField("phone", e.target.value)}
|
|
className="w-full px-4 py-3 bg-[#f8f6f3] border border-gray-200 rounded-xl text-text text-sm placeholder:text-text-muted focus:border-orange focus:ring-1 focus:ring-orange outline-none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-text mb-1.5">Ville</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
placeholder="Orchies, Cysoing, Saméon..."
|
|
value={fields.ville}
|
|
onChange={(e) => updateField("ville", e.target.value)}
|
|
className="w-full px-4 py-3 bg-[#f8f6f3] border border-gray-200 rounded-xl text-text text-sm placeholder:text-text-muted focus:border-orange focus:ring-1 focus:ring-orange outline-none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-text mb-1.5">Décrivez votre projet (optionnel)</label>
|
|
<textarea
|
|
placeholder="Surface, type de travaux, délais souhaités..."
|
|
rows={3}
|
|
value={fields.description}
|
|
onChange={(e) => updateField("description", e.target.value)}
|
|
className="w-full px-4 py-3 bg-[#f8f6f3] border border-gray-200 rounded-xl text-text text-sm placeholder:text-text-muted focus:border-orange focus:ring-1 focus:ring-orange outline-none resize-none"
|
|
/>
|
|
</div>
|
|
{error && <p className="text-red-600 text-sm">{error}</p>}
|
|
<Button type="submit" size="lg" className="w-full" loading={loading}>
|
|
Envoyer ma demande de devis
|
|
</Button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setStep("type")}
|
|
className="w-full text-text-muted hover:text-text text-sm underline cursor-pointer"
|
|
>
|
|
← Retour
|
|
</button>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="bg-white rounded-2xl p-6 sm:p-8">
|
|
<div className="flex items-center gap-2 mb-6">
|
|
<span className="bg-orange text-white text-xs font-bold px-2.5 py-1 rounded-full">1/2</span>
|
|
<h3 className="text-navy font-bold text-lg">Quel type de projet ?</h3>
|
|
</div>
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
|
{[
|
|
{
|
|
label: "Projet Extension",
|
|
desc: "Agrandissement, garage, sur\u00e9l\u00e9vation",
|
|
icon: (
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
label: "R\u00e9novation",
|
|
desc: "Fa\u00e7ade, rejointoiement, murs",
|
|
icon: (
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
label: "Petits Travaux",
|
|
desc: "Terrasse, muret, dalle",
|
|
icon: (
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
|
|
</svg>
|
|
),
|
|
},
|
|
].map((item) => (
|
|
<button
|
|
key={item.label}
|
|
onClick={() => {
|
|
setProjectType(item.label);
|
|
setStep("details");
|
|
}}
|
|
className="p-5 rounded-xl border-2 border-gray-200 bg-[#f8f6f3] hover:border-orange hover:shadow-md text-center transition-all cursor-pointer group"
|
|
>
|
|
<div className="w-10 h-10 bg-orange/10 rounded-lg flex items-center justify-center mx-auto mb-3 text-orange group-hover:bg-orange group-hover:text-white transition-colors">
|
|
{item.icon}
|
|
</div>
|
|
<p className="font-semibold text-navy text-sm mb-1">{item.label}</p>
|
|
<p className="text-text-muted text-xs">{item.desc}</p>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/* ============================================================
|
|
FLOATING MOBILE CTA
|
|
============================================================ */
|
|
function FloatingCTA() {
|
|
return (
|
|
<div className="fixed bottom-4 left-4 right-4 z-50 md:hidden">
|
|
<a
|
|
href="tel:+33600000000"
|
|
className="flex items-center justify-center gap-2 bg-orange hover:bg-orange-hover text-white font-bold text-sm py-3.5 rounded-xl shadow-lg transition-colors w-full"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
|
|
/>
|
|
</svg>
|
|
Appeler maintenant
|
|
</a>
|
|
</div>
|
|
);
|
|
}
|