From 3b630bb68c4f76e29963b2689ed4aa3efea22080 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Math=C3=A9o=20GUILBERT?=
Date: Tue, 2 Jun 2026 18:49:24 +0200
Subject: [PATCH] add contact form modal and email sending functionality
---
app/[locale]/freelance/_client.tsx | 18 ++--
app/actions/sendContactMail.ts | 39 +++++++
components/ui/ContactFormModal.tsx | 160 +++++++++++++++++++++++++++++
3 files changed, 210 insertions(+), 7 deletions(-)
create mode 100644 app/actions/sendContactMail.ts
create mode 100644 components/ui/ContactFormModal.tsx
diff --git a/app/[locale]/freelance/_client.tsx b/app/[locale]/freelance/_client.tsx
index c71cbe8..981dce0 100644
--- a/app/[locale]/freelance/_client.tsx
+++ b/app/[locale]/freelance/_client.tsx
@@ -1,8 +1,9 @@
'use client';
-import { useEffect, useRef } from 'react';
+import { useEffect, useRef, useState } from 'react';
import { useTranslations } from 'next-intl';
import { ProjectCard } from '@/components/ui/ProjectCard';
+import { ContactFormModal } from '@/components/ui/ContactFormModal';
import { Project } from '@/lib/projects/types';
type props = {
@@ -13,6 +14,8 @@ type props = {
export default function FreelanceClient({ projects, locale }: props) {
const t = useTranslations('freelancePage');
+ const [contactOpen, setContactOpen] = useState(false);
+
const heroRef = useRef(null);
const belowFoldRef = useRef(null);
@@ -102,6 +105,7 @@ export default function FreelanceClient({ projects, locale }: props) {
return (
+ setContactOpen(false)} />
{/* ── Hero ── */}
-
setContactOpen(true)}
className="inline-flex items-center gap-2.5 font-sans text-[13px] tracking-wide text-brand-beige bg-brand-brown rounded-lg px-8 py-3.5 hover:bg-brand-brown/85 transition-colors duration-200 cursor-pointer"
>
{t('cta.button')}
-
+
{t('cta.note1')}
{t('cta.note2')}
diff --git a/app/actions/sendContactMail.ts b/app/actions/sendContactMail.ts
new file mode 100644
index 0000000..d63de7d
--- /dev/null
+++ b/app/actions/sendContactMail.ts
@@ -0,0 +1,39 @@
+'use server';
+
+import { sendMail } from '@/lib/mail';
+
+export type ContactFormState = { success: true } | { success: false; error: string } | null;
+
+export async function sendContactMail(
+ _: ContactFormState,
+ formData: FormData,
+): Promise
{
+ const name = (formData.get('name') as string)?.trim();
+ const email = (formData.get('email') as string)?.trim();
+ const message = (formData.get('message') as string)?.trim();
+
+ if (!name || !email || !message) {
+ return { success: false, error: 'Tous les champs sont requis.' };
+ }
+
+ const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailRe.test(email)) {
+ return { success: false, error: 'Adresse email invalide.' };
+ }
+
+ try {
+ await sendMail({
+ from: 'Formulaire de contact ',
+ subject: `Message de ${name}`,
+ html: `
+ Nom : ${name}
+ Email : ${email}
+
+ ${message.replace(/\n/g, '
')}
+ `,
+ });
+ return { success: true };
+ } catch {
+ return { success: false, error: 'Une erreur est survenue. Réessaie plus tard.' };
+ }
+}
diff --git a/components/ui/ContactFormModal.tsx b/components/ui/ContactFormModal.tsx
new file mode 100644
index 0000000..d6b91a7
--- /dev/null
+++ b/components/ui/ContactFormModal.tsx
@@ -0,0 +1,160 @@
+'use client';
+
+import { useEffect, useRef, useState, useActionState } from 'react';
+import { createPortal } from 'react-dom';
+import { sendContactMail, type ContactFormState } from '@/app/actions/sendContactMail';
+
+type Props = {
+ isOpen: boolean;
+ onClose: () => void;
+};
+
+type FieldProps = {
+ label: string;
+ name: string;
+ type?: string;
+ required?: boolean;
+ textarea?: boolean;
+ inputRef?: React.RefObject;
+};
+
+function Field({ label, name, type = 'text', required, textarea, inputRef }: FieldProps) {
+ const base =
+ 'w-full font-sans text-sm text-brand-brown bg-white/60 border border-brand-brown/12 rounded-lg px-4 py-3 placeholder:text-brand-brown/25 focus:outline-none focus:border-brand-brown/35 focus:bg-white/80 transition-colors duration-150';
+
+ return (
+
+
+ {textarea ? (
+
+ ) : (
+ }
+ name={name}
+ type={type}
+ required={required}
+ className={base}
+ />
+ )}
+
+ );
+}
+
+export function ContactFormModal({ isOpen, onClose }: Props) {
+ const [mounted, setMounted] = useState(false);
+ const [state, action, pending] = useActionState(
+ sendContactMail,
+ null,
+ );
+ const firstInputRef = useRef(null);
+ const formRef = useRef(null);
+
+ useEffect(() => setMounted(true), []);
+
+ useEffect(() => {
+ if (isOpen) {
+ setTimeout(() => firstInputRef.current?.focus(), 50);
+ document.body.style.overflow = 'hidden';
+ } else {
+ document.body.style.overflow = '';
+ }
+ return () => { document.body.style.overflow = ''; };
+ }, [isOpen]);
+
+ useEffect(() => {
+ const handleKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
+ if (isOpen) document.addEventListener('keydown', handleKey);
+ return () => document.removeEventListener('keydown', handleKey);
+ }, [isOpen, onClose]);
+
+ useEffect(() => {
+ if (!isOpen) formRef.current?.reset();
+ }, [isOpen]);
+
+ if (!mounted || !isOpen) return null;
+
+ return createPortal(
+
+
+ {/* Backdrop */}
+
+
+ {/* Panel */}
+
+
+ {/* Header */}
+
+
+
+ Démarrons un projet
+
+
+ Je réponds sous 48h.
+
+
+
+
+
+ {/* Success */}
+ {state?.success ? (
+
+
Message envoyé.
+
Je vous reviens très vite.
+
+
+ ) : (
+
+ )}
+
+
,
+ document.body,
+ );
+}