Email d'invitation HTML généré depuis l'administration des rencontres

Chaque rencontre dispose d'un bouton « ✉ Email » dans l'administration
(admins et modérateurs) qui génère un email HTML prêt à envoyer, aux
couleurs du site avec le logo du club : titre, description, encadré
date/heure/lieu/places restantes et bouton d'inscription.

- Nouvelle route GET /api/admin/rencontres/:id/email : construit l'email
  (mise en page en tables + styles inline, compatible clients mail) avec
  des URL absolues dérivées de l'hôte de la requête.
- Le bouton de l'email pointe vers /?inscription=<id> : la page d'accueil
  ouvre alors automatiquement la modale d'inscription de la rencontre.
- Modale d'aperçu dans l'admin : copie de l'email (HTML enrichi presse-
  papiers pour coller directement dans un logiciel de messagerie), copie
  de l'objet et du lien d'inscription, téléchargement du fichier .html.
- Les images publiques (logo, uploads) sont maintenant servies avec
  Cross-Origin-Resource-Policy: cross-origin pour que le logo se charge
  dans les webmails et l'aperçu.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fkg15RCxNgUys4ru73He2o
This commit is contained in:
Claude committed 2026-07-14 07:11:34 +00:00
1 parent 9cfd4e4e93
commit 02cfa8f8bc
5 files changed
+235 -4

No files matched your search

+18 -3
View File
@@ -1,5 +1,5 @@
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { Link, useSearchParams } from 'react-router-dom';
import { usePublicData } from '../lib/usePublic.js';
import { api, dateParts } from '../lib/api.js';
import Modal from '../components/Modal.jsx';
@@ -75,11 +75,26 @@ function InscriptionModal({ rencontre, onClose, onDone }) {
export default function Home() {
const { data, reload } = usePublicData();
const [searchParams, setSearchParams] = useSearchParams();
const [inscrRenc, setInscrRenc] = useState(null);
const [homeDone, setHomeDone] = useState(false);
const [homeError, setHomeError] = useState('');
const [form, setForm] = useState({ nom: '', fonction: '', entreprise: '', email: '' });
// Registration links in invitation emails point to /?inscription=<id> :
// once the public data is loaded, open the matching inscription modal.
useEffect(() => {
const id = Number(searchParams.get('inscription'));
if (!id || !data) return;
const renc = (data.rencontres || []).find((r) => r.id === id);
if (renc) setInscrRenc(renc);
}, [data, searchParams]);
const closeInscription = () => {
setInscrRenc(null);
if (searchParams.has('inscription')) setSearchParams({}, { replace: true });
};
const onChange = (e) => setForm({ ...form, [e.target.name]: e.target.value });
const submitHome = async (e) => {
@@ -329,7 +344,7 @@ export default function Home() {
</section>
{inscrRenc && (
<InscriptionModal rencontre={inscrRenc} onClose={() => setInscrRenc(null)} onDone={reload} />
<InscriptionModal rencontre={inscrRenc} onClose={closeInscription} onDone={reload} />
)}
</main>
);