diff --git a/server/src/emailTemplate.js b/server/src/emailTemplate.js
index d7271a7..953781a 100644
--- a/server/src/emailTemplate.js
+++ b/server/src/emailTemplate.js
@@ -49,7 +49,9 @@ export function buildInvitationEmail({ rencontre, baseUrl, texts = {} }) {
year: 'numeric',
});
const restantes = Math.max(0, r.places - (r.inscrits ?? 0));
- const inscriptionUrl = `${baseUrl}/?inscription=${r.id}`;
+ // A dedicated route is more reliable from Outlook than a homepage query
+ // string: redirects, tracking filters and cached homepages can drop it.
+ const inscriptionUrl = `${baseUrl}/inscription/${r.id}`;
const logoUrl = `${baseUrl}/assets/logo.jpg`;
const subject = `Invitation — ${r.titre} · ${dateStr}`;
@@ -71,21 +73,17 @@ export function buildInvitationEmail({ rencontre, baseUrl, texts = {} }) {
? `
`
: '';
- // "Bulletproof" CTA button. Outlook (Word engine) ignores padding and
- // display:inline-block on , collapsing the button to a plain link, so
- // it needs a VML roundrect. Other clients get the styled , sized with
- // a fixed width + line-height (not padding) so both paths match. The
- // conditional comments are inert HTML comments in normal renderers.
+ // A table-backed CTA survives rich-text paste into Outlook. In particular,
+ // the visible label remains a normal , whereas VML hyperlinks are
+ // commonly stripped when Outlook receives them through the clipboard.
const ctaButton = (url, label) => `
-
-
- ${label}
- `;
+ `;
const html = `
diff --git a/server/src/routes/public.js b/server/src/routes/public.js
index 9e05497..5bf6c67 100644
--- a/server/src/routes/public.js
+++ b/server/src/routes/public.js
@@ -56,6 +56,26 @@ publicRouter.get('/bootstrap', async (_req, res, next) => {
}
});
+// Direct event lookup used by links from invitation emails. It deliberately
+// does not depend on the homepage bootstrap payload, so the registration page
+// can always resolve a valid upcoming event by id.
+publicRouter.get('/rencontres/:id', validate(idParam, 'params'), async (req, res, next) => {
+ try {
+ const result = await query(
+ `SELECT r.id, r.titre, r.date_renc, r.heure, r.lieu, r.description, r.places,
+ r.participants_par_compte, r.image_path, COUNT(i.id)::int AS inscrits
+ FROM rencontres r LEFT JOIN inscriptions i ON i.rencontre_id = r.id
+ WHERE r.id = $1 AND r.date_renc >= CURRENT_DATE
+ GROUP BY r.id`,
+ [req.params.id]
+ );
+ if (result.rowCount === 0) return res.status(404).json({ error: 'Rencontre introuvable' });
+ res.json({ rencontre: result.rows[0] });
+ } catch (err) {
+ next(err);
+ }
+});
+
// Membership request (home page form)
publicRouter.post(
'/demandes-adhesion',
diff --git a/web/src/App.jsx b/web/src/App.jsx
index 094b668..2b5bf06 100644
--- a/web/src/App.jsx
+++ b/web/src/App.jsx
@@ -3,6 +3,7 @@ import { useEffect } from 'react';
import Header from './components/Header.jsx';
import Footer from './components/Footer.jsx';
import Home from './pages/Home.jsx';
+import Inscription from './pages/Inscription.jsx';
import Annuaire from './pages/Annuaire.jsx';
import Association from './pages/Association.jsx';
import RencontresPassees from './pages/RencontresPassees.jsx';
@@ -33,6 +34,7 @@ export default function App() {
} />
+ } />
} />
} />
} />
diff --git a/web/src/pages/Home.jsx b/web/src/pages/Home.jsx
index e202602..f97989c 100644
--- a/web/src/pages/Home.jsx
+++ b/web/src/pages/Home.jsx
@@ -6,7 +6,7 @@ import { api, dateParts } from '../lib/api.js';
import Modal from '../components/Modal.jsx';
import PastEventCard from '../components/PastEventCard.jsx';
-function InscriptionModal({ rencontre, onClose, onDone }) {
+export function InscriptionModal({ rencontre, onClose, onDone }) {
const { user, login } = useAuth();
const [context, setContext] = useState(null);
const [participants, setParticipants] = useState(['']);
diff --git a/web/src/pages/Inscription.jsx b/web/src/pages/Inscription.jsx
new file mode 100644
index 0000000..6be9b56
--- /dev/null
+++ b/web/src/pages/Inscription.jsx
@@ -0,0 +1,37 @@
+import { useEffect, useState } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import { api } from '../lib/api.js';
+import { InscriptionModal } from './Home.jsx';
+
+export default function Inscription() {
+ const { id } = useParams();
+ const navigate = useNavigate();
+ const [rencontre, setRencontre] = useState(null);
+ const [error, setError] = useState('');
+
+ useEffect(() => {
+ api.get(`/api/public/rencontres/${id}`)
+ .then((data) => setRencontre(data.rencontre))
+ .catch((err) => setError(err.message));
+ }, [id]);
+
+ const close = () => navigate('/', { replace: true });
+
+ if (error) {
+ return (
+
+ Inscription indisponible
+ {error}
+
+
+ );
+ }
+
+ if (!rencontre) return ;
+
+ return (
+
+
+
+ );
+}