mirror of
https://github.com/R0m1k3/PleinR.git
synced 2026-10-14 18:59:41 +02:00
Logo annuaire + modals devenir membre & contact
- Logo en haut a gauche de la page Annuaire (prop logo sur SiteHeader) - Modal "Devenir adherent" sur les CTA accueil + annuaire : cree une demande d'adhesion (membership_requests) et alerte les admins via le journal d'activite (compteur tableau de bord deja cable) - Formulaire de contact (modal depuis le pied de page) : nouvelle table contact_messages + alerte admin - Composant Modal partage, server actions publiques (sans auth) - Migration drizzle 0002 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
e2cf03c9f3
commit
141acffc1c
12 files changed
+1057
-17
No files matched your search
@@ -0,0 +1,115 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Modal } from "./Modal";
|
||||
import { submitContactMessage } from "@/app/actions/public";
|
||||
|
||||
export function ContactModalButton({
|
||||
label,
|
||||
className,
|
||||
style,
|
||||
}: {
|
||||
label: string;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [pending, setPending] = useState(false);
|
||||
const [done, setDone] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
function close() {
|
||||
setOpen(false);
|
||||
setTimeout(() => {
|
||||
setDone(false);
|
||||
setError(null);
|
||||
}, 200);
|
||||
}
|
||||
|
||||
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
setPending(true);
|
||||
setError(null);
|
||||
const res = await submitContactMessage(new FormData(e.currentTarget));
|
||||
setPending(false);
|
||||
if (res.ok) setDone(true);
|
||||
else setError(res.error ?? "Une erreur est survenue.");
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button type="button" className={className} style={{ cursor: "pointer", ...style }} onClick={() => setOpen(true)}>
|
||||
{label}
|
||||
</button>
|
||||
|
||||
<Modal open={open} onClose={close} title="Nous contacter">
|
||||
{done ? (
|
||||
<div style={{ textAlign: "center", padding: "10px 0 6px" }}>
|
||||
<div style={{ fontSize: 40, marginBottom: 8 }}>✓</div>
|
||||
<p style={{ margin: "0 0 18px", fontSize: 15, color: "#4a4030" }}>
|
||||
Message envoyé ! Nous vous répondrons dès que possible.
|
||||
</p>
|
||||
<button type="button" className="lift-cta" onClick={close} style={primaryBtn}>
|
||||
Fermer
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={onSubmit}>
|
||||
<p style={{ margin: "0 0 18px", fontSize: 14, color: "#6c6150" }}>
|
||||
Une question, une suggestion ? Écrivez-nous.
|
||||
</p>
|
||||
|
||||
<Field label="Nom *">
|
||||
<input name="name" required maxLength={200} className="field" placeholder="Votre nom" />
|
||||
</Field>
|
||||
<Field label="E-mail *">
|
||||
<input name="email" type="email" required maxLength={200} className="field" placeholder="vous@exemple.fr" />
|
||||
</Field>
|
||||
<Field label="Objet">
|
||||
<input name="subject" maxLength={200} className="field" placeholder="Objet du message" />
|
||||
</Field>
|
||||
<Field label="Message *">
|
||||
<textarea name="message" required rows={5} maxLength={4000} className="field" placeholder="Votre message…" style={{ resize: "vertical" }} />
|
||||
</Field>
|
||||
|
||||
{error && <div style={errorBox}>{error}</div>}
|
||||
|
||||
<button type="submit" className="lift-cta" disabled={pending} style={{ ...primaryBtn, opacity: pending ? 0.6 : 1 }}>
|
||||
{pending ? "Envoi…" : "Envoyer le message"}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Field({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div style={{ marginBottom: 14 }}>
|
||||
<label className="field-label">{label}</label>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const primaryBtn: React.CSSProperties = {
|
||||
width: "100%",
|
||||
border: "none",
|
||||
background: "#13324F",
|
||||
color: "#fff",
|
||||
fontWeight: 700,
|
||||
fontSize: 15,
|
||||
padding: "13px 18px",
|
||||
borderRadius: 12,
|
||||
cursor: "pointer",
|
||||
};
|
||||
|
||||
const errorBox: React.CSSProperties = {
|
||||
background: "#fbe8e6",
|
||||
color: "#a3372e",
|
||||
fontSize: 13,
|
||||
padding: "10px 12px",
|
||||
borderRadius: 10,
|
||||
marginBottom: 14,
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Modal } from "./Modal";
|
||||
import { submitMembershipRequest } from "@/app/actions/public";
|
||||
|
||||
export function MembershipModalButton({
|
||||
label,
|
||||
className,
|
||||
style,
|
||||
}: {
|
||||
label: string;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [pending, setPending] = useState(false);
|
||||
const [done, setDone] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
function close() {
|
||||
setOpen(false);
|
||||
// Réinitialise après la fermeture (laisse l'animation finir).
|
||||
setTimeout(() => {
|
||||
setDone(false);
|
||||
setError(null);
|
||||
}, 200);
|
||||
}
|
||||
|
||||
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
setPending(true);
|
||||
setError(null);
|
||||
const res = await submitMembershipRequest(new FormData(e.currentTarget));
|
||||
setPending(false);
|
||||
if (res.ok) setDone(true);
|
||||
else setError(res.error ?? "Une erreur est survenue.");
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button type="button" className={className} style={{ cursor: "pointer", ...style }} onClick={() => setOpen(true)}>
|
||||
{label}
|
||||
</button>
|
||||
|
||||
<Modal open={open} onClose={close} title="Devenir adhérent">
|
||||
{done ? (
|
||||
<div style={{ textAlign: "center", padding: "10px 0 6px" }}>
|
||||
<div style={{ fontSize: 40, marginBottom: 8 }}>✓</div>
|
||||
<p style={{ margin: "0 0 18px", fontSize: 15, color: "#4a4030" }}>
|
||||
Demande envoyée ! L'équipe Plein R vous recontactera rapidement.
|
||||
</p>
|
||||
<button type="button" className="lift-cta" onClick={close} style={primaryBtn}>
|
||||
Fermer
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={onSubmit}>
|
||||
<p style={{ margin: "0 0 18px", fontSize: 14, color: "#6c6150" }}>
|
||||
Laissez vos coordonnées : nous revenons vers vous pour finaliser votre adhésion.
|
||||
</p>
|
||||
|
||||
<Field label="Nom / Raison sociale *">
|
||||
<input name="name" required maxLength={200} className="field" placeholder="Votre nom ou entreprise" />
|
||||
</Field>
|
||||
<Field label="E-mail">
|
||||
<input name="email" type="email" maxLength={200} className="field" placeholder="vous@exemple.fr" />
|
||||
</Field>
|
||||
<Field label="Téléphone">
|
||||
<input name="phone" maxLength={40} className="field" placeholder="06 12 34 56 78" />
|
||||
</Field>
|
||||
<Field label="Message">
|
||||
<textarea name="message" rows={4} maxLength={2000} className="field" placeholder="Votre activité, vos questions…" style={{ resize: "vertical" }} />
|
||||
</Field>
|
||||
|
||||
{error && <div style={errorBox}>{error}</div>}
|
||||
|
||||
<button type="submit" className="lift-cta" disabled={pending} style={{ ...primaryBtn, opacity: pending ? 0.6 : 1 }}>
|
||||
{pending ? "Envoi…" : "Envoyer ma demande"}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Field({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div style={{ marginBottom: 14 }}>
|
||||
<label className="field-label">{label}</label>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const primaryBtn: React.CSSProperties = {
|
||||
width: "100%",
|
||||
border: "none",
|
||||
background: "#9a6638",
|
||||
color: "#fff",
|
||||
fontWeight: 700,
|
||||
fontSize: 15,
|
||||
padding: "13px 18px",
|
||||
borderRadius: 12,
|
||||
cursor: "pointer",
|
||||
};
|
||||
|
||||
const errorBox: React.CSSProperties = {
|
||||
background: "#fbe8e6",
|
||||
color: "#a3372e",
|
||||
fontSize: 13,
|
||||
padding: "10px 12px",
|
||||
borderRadius: 10,
|
||||
marginBottom: 14,
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
export function Modal({
|
||||
open,
|
||||
onClose,
|
||||
title,
|
||||
children,
|
||||
maxWidth = 480,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
maxWidth?: number;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") onClose();
|
||||
};
|
||||
document.addEventListener("keydown", onKey);
|
||||
const prev = document.body.style.overflow;
|
||||
document.body.style.overflow = "hidden";
|
||||
return () => {
|
||||
document.removeEventListener("keydown", onKey);
|
||||
document.body.style.overflow = prev;
|
||||
};
|
||||
}, [open, onClose]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={title}
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: "fixed",
|
||||
inset: 0,
|
||||
zIndex: 1000,
|
||||
background: "rgba(25,20,12,0.55)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: 20,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
background: "#fff",
|
||||
borderRadius: 18,
|
||||
border: "1px solid #e6dcc6",
|
||||
width: "100%",
|
||||
maxWidth,
|
||||
maxHeight: "90vh",
|
||||
overflowY: "auto",
|
||||
boxShadow: "0 24px 60px rgba(25,20,12,0.28)",
|
||||
padding: "26px 28px",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 16, marginBottom: 16 }}>
|
||||
<h2 className="font-display" style={{ fontWeight: 800, fontSize: 22, margin: 0, color: "#26201a" }}>
|
||||
{title}
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
aria-label="Fermer"
|
||||
style={{
|
||||
border: "none",
|
||||
background: "#f1ead9",
|
||||
color: "#6f6450",
|
||||
width: 34,
|
||||
height: 34,
|
||||
borderRadius: 10,
|
||||
fontSize: 18,
|
||||
cursor: "pointer",
|
||||
lineHeight: 1,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ContactModalButton } from "./ContactModalButton";
|
||||
|
||||
export function SiteFooter() {
|
||||
return (
|
||||
<footer style={{ background: "#EFE9DA", borderTop: "1px solid #e2d6bd", padding: "28px 56px" }}>
|
||||
@@ -6,7 +8,19 @@ export function SiteFooter() {
|
||||
style={{ display: "flex", justifyContent: "flex-end", alignItems: "center" }}
|
||||
>
|
||||
<div style={{ fontSize: 13, color: "#8c8068", textAlign: "right", lineHeight: 1.7 }}>
|
||||
contact@plein-r.fr · Bassin de Pompey
|
||||
<ContactModalButton
|
||||
label="Nous contacter"
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
padding: 0,
|
||||
font: "inherit",
|
||||
color: "#9a6638",
|
||||
fontWeight: 700,
|
||||
textDecoration: "underline",
|
||||
}}
|
||||
/>
|
||||
{" · Bassin de Pompey"}
|
||||
<br />
|
||||
Réseau · Rencontre · Réussite
|
||||
</div>
|
||||
|
||||
@@ -7,18 +7,24 @@ const NAV = [
|
||||
{ key: "association", label: "L'association", href: "#" },
|
||||
];
|
||||
|
||||
export function SiteHeader({ active }: { active?: string }) {
|
||||
export function SiteHeader({ active, logo = false }: { active?: string; logo?: boolean }) {
|
||||
return (
|
||||
<header
|
||||
className="container"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
justifyContent: logo ? "space-between" : "flex-end",
|
||||
padding: "18px 56px",
|
||||
gap: 20,
|
||||
}}
|
||||
>
|
||||
{logo && (
|
||||
<Link href="/" aria-label="Plein R — accueil" style={{ display: "inline-flex", alignItems: "center" }}>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src="/assets/logo.png" alt="Plein R" style={{ height: 46, width: "auto", display: "block" }} />
|
||||
</Link>
|
||||
)}
|
||||
<nav style={{ display: "flex", alignItems: "center", gap: 30, flexWrap: "wrap" }}>
|
||||
{NAV.map((n) => {
|
||||
const isActive = active === n.key;
|
||||
|
||||
Reference in new issue
Block a user