Galerie photos illimitée pour les rencontres passées + page publique dédiée + carrousel

Photos :
- Nouvelle table rencontre_passee_photos (photos illimitées par rencontre
  passée, avec position). Migration idempotente au démarrage des anciennes
  colonnes image_path/_2/_3 vers cette table (puis colonnes nullées)
- Le nombre de photos affiché est désormais celui réellement uploadé
  (calculé), le champ manuel nb_photos n'est plus utilisé
- Gestion admin : ajout illimité, choix de la photo « principale » (mise
  en avant), suppression réservée aux admins (les modérateurs ajoutent et
  réordonnent mais ne suppriment pas)

Participants auto :
- rencontres_passees.rencontre_id (FK, ON DELETE SET NULL) ; quand une
  rencontre passée est liée à une rencontre, le nombre de participants est
  calculé depuis ses inscriptions ; sinon valeur manuelle de repli. Le
  formulaire admin propose la liaison et affiche le compte auto

Public :
- Nouvelle page /rencontres-passees (menu « Rencontres passées ») listant
  toutes les rencontres passées
- Carrousel plein écran (composant Carousel) montrant TOUTES les photos
  (flèches, clavier ←/→/Échap, compteur, miniatures) ; ouvert au clic sur
  une carte. La grille d'accueil montre 3 aperçus avec badge « +N » et un
  lien « Voir toutes les rencontres → »
- Composant PastEventCard partagé entre l'accueil et la page dédiée

Vérifié : 34 + 21 + 26 tests existants au vert, 19 nouveaux tests
(participants auto liés/déliés, upload illimité, nb_photos calculé,
principale, restrictions modérateur sur suppression de photo, migration
legacy des colonnes inline), parcours navigateur complet (galerie admin,
page publique, carrousel).

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-13 18:13:32 +00:00
1 parent 214250bc39
commit 298c3becd5
13 files changed
+476 -139

No files matched your search

+2
View File
@@ -5,6 +5,7 @@ import Footer from './components/Footer.jsx';
import Home from './pages/Home.jsx';
import Annuaire from './pages/Annuaire.jsx';
import Association from './pages/Association.jsx';
import RencontresPassees from './pages/RencontresPassees.jsx';
import Espace from './pages/Espace.jsx';
import Admin from './pages/Admin.jsx';
import { useAuth } from './lib/AuthContext.jsx';
@@ -33,6 +34,7 @@ export default function App() {
<Routes>
<Route path="/" element={<Home />} />
<Route path="/annuaire" element={<Annuaire />} />
<Route path="/rencontres-passees" element={<RencontresPassees />} />
<Route path="/association" element={<Association />} />
<Route path="/espace-membre" element={<Espace />} />
<Route path="/admin/*" element={<Admin />} />
+74
View File
@@ -0,0 +1,74 @@
import { useCallback, useEffect, useRef, useState } from 'react';
// Full-screen photo carousel for a past event. `photos` is an array of
// { id, path }. Scrollable with arrows, keyboard (←/→/Esc), a counter and
// a thumbnail strip.
export default function Carousel({ photos, title, startIndex = 0, onClose }) {
const [i, setI] = useState(startIndex);
const n = photos.length;
const stripRef = useRef(null);
const go = useCallback((d) => setI((prev) => (prev + d + n) % n), [n]);
useEffect(() => {
const onKey = (e) => {
if (e.key === 'Escape') onClose();
else if (e.key === 'ArrowLeft') go(-1);
else if (e.key === 'ArrowRight') go(1);
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [go, onClose]);
useEffect(() => {
stripRef.current?.children[i]?.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
}, [i]);
if (n === 0) return null;
const arrow = {
position: 'absolute', top: '50%', transform: 'translateY(-50%)',
width: 52, height: 52, borderRadius: '50%', border: 'none', cursor: 'pointer',
background: 'rgba(255,255,255,.14)', color: '#fff', fontSize: 24, display: 'flex',
alignItems: 'center', justifyContent: 'center',
};
return (
<div
onClick={onClose}
style={{ position: 'fixed', inset: 0, zIndex: 200, background: 'rgba(10,9,8,.94)', display: 'flex', flexDirection: 'column', padding: '24px 24px 18px' }}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', color: '#fff', flexShrink: 0 }}>
<div>
<div className="serif" style={{ fontSize: 20, fontWeight: 600 }}>{title}</div>
<div style={{ fontSize: 13, color: '#B7AFA6', marginTop: 2 }}>{i + 1} / {n}</div>
</div>
<button onClick={onClose} aria-label="Fermer" style={{ background: 'rgba(255,255,255,.14)', border: 'none', cursor: 'pointer', width: 40, height: 40, borderRadius: '50%', fontSize: 20, color: '#fff' }}>×</button>
</div>
<div onClick={(e) => e.stopPropagation()} style={{ flex: 1, position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: 0, margin: '14px 0' }}>
<img src={photos[i].path} alt="" style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain', borderRadius: 4 }} />
{n > 1 && (
<>
<button onClick={() => go(-1)} aria-label="Précédente" style={{ ...arrow, left: 8 }}>‹</button>
<button onClick={() => go(1)} aria-label="Suivante" style={{ ...arrow, right: 8 }}>›</button>
</>
)}
</div>
{n > 1 && (
<div ref={stripRef} onClick={(e) => e.stopPropagation()} style={{ display: 'flex', gap: 8, overflowX: 'auto', flexShrink: 0, padding: '4px 2px' }}>
{photos.map((p, idx) => (
<button
key={p.id ?? idx}
onClick={() => setI(idx)}
style={{ flexShrink: 0, width: 76, height: 54, borderRadius: 4, overflow: 'hidden', border: idx === i ? '2px solid #fff' : '2px solid transparent', padding: 0, cursor: 'pointer', background: 'none', opacity: idx === i ? 1 : 0.55 }}
>
<img src={p.path} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
</button>
))}
</div>
)}
</div>
);
}
+1
View File
@@ -27,6 +27,7 @@ export default function Header() {
<nav style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
<NavLink to="/" end className={navLinkClass}>Accueil</NavLink>
<NavLink to="/annuaire" className={navLinkClass}>Annuaire</NavLink>
<NavLink to="/rencontres-passees" className={navLinkClass}>Rencontres passées</NavLink>
<NavLink to="/association" className={navLinkClass}>L'association</NavLink>
<NavLink to="/espace-membre" className={navLinkClass}>Espace membre</NavLink>
<Link to="/" style={{ marginLeft: 10, textDecoration: 'none' }}>
+68
View File
@@ -0,0 +1,68 @@
import { useState } from 'react';
import Carousel from './Carousel.jsx';
// Editorial card for one past event: text on the left, a 1-large-+-2-small
// photo grid on the right. Clicking any photo (or "voir les N photos")
// opens the full carousel with every photo.
export default function PastEventCard({ event }) {
const [carouselAt, setCarouselAt] = useState(null); // null | index
const photos = event.photos || [];
const hasPhotos = photos.length > 0;
const cell = (idx, extra = {}) => {
const photo = photos[idx];
return (
<div
className={photo ? '' : 'placeholder-pattern'}
style={{ borderRadius: 3, overflow: 'hidden', cursor: photo ? 'pointer' : 'default', ...extra }}
onClick={photo ? () => setCarouselAt(idx) : undefined}
>
{photo && <img src={photo.path} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />}
</div>
);
};
return (
<>
<article className="card hero-grid" style={{ display: 'grid', gridTemplateColumns: '1.15fr .85fr', gap: 40, alignItems: 'center', padding: 36 }}>
<div>
<div style={{ fontSize: 12, letterSpacing: '.14em', textTransform: 'uppercase', color: 'var(--gray-light)', fontWeight: 600, marginBottom: 12 }}>
{event.date_label} · {event.lieu}
</div>
<h3 className="serif" style={{ fontSize: 30, fontWeight: 600, lineHeight: 1.12, marginBottom: 16 }}>{event.titre}</h3>
<p style={{ fontSize: 15.5, lineHeight: 1.7, color: 'var(--gray)', marginBottom: 20 }}>{event.texte}</p>
<div style={{ display: 'flex', gap: 20, fontSize: 13, color: 'var(--gray-light)', alignItems: 'center', flexWrap: 'wrap' }}>
<span>👥 {event.participants} participants</span>
<span>📷 {event.nb_photos} photo{event.nb_photos > 1 ? 's' : ''}</span>
{hasPhotos && (
<button className="btn-link" style={{ fontSize: 13 }} onClick={() => setCarouselAt(0)}>
Voir les photos →
</button>
)}
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gridTemplateRows: '1fr 1fr', gap: 10, height: 240 }}>
{cell(0, { gridRow: 'span 2' })}
{cell(1)}
{photos.length > 3 ? (
<div
style={{ borderRadius: 3, overflow: 'hidden', cursor: 'pointer', position: 'relative' }}
onClick={() => setCarouselAt(2)}
>
<img src={photos[2].path} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
<div style={{ position: 'absolute', inset: 0, background: 'rgba(15,14,13,.55)', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: "'Newsreader',serif", fontSize: 22, fontWeight: 600 }}>
+{photos.length - 3}
</div>
</div>
) : (
cell(2)
)}
</div>
</article>
{carouselAt !== null && (
<Carousel photos={photos} title={event.titre} startIndex={carouselAt} onClose={() => setCarouselAt(null)} />
)}
</>
);
}
+145 -70
View File
@@ -4,26 +4,111 @@ import { useAuth } from '../../lib/AuthContext.jsx';
import Modal from '../Modal.jsx';
import ImageSlot from '../ImageSlot.jsx';
function PastEventFormModal({ event, onClose, onSaved }) {
// Manages the gallery of one past event: unlimited photos, add / delete /
// set-as-principal. Only available once the event exists (has an id).
function PhotoManager({ eventId, photos, setPhotos }) {
const { user } = useAuth();
const [error, setError] = useState('');
const add = async (path, id) => {
// ImageSlot uploads then hands us the stored path; the route also
// returns the new photo id, but ImageSlot only forwards the path, so we
// reload the event's photos to stay in sync.
setError('');
try {
const d = await api.get(`/api/admin/rencontres-passees`);
const ev = d.rencontresPassees.find((e) => e.id === eventId);
if (ev) setPhotos(ev.photos || []);
} catch (e) {
setError(e.message);
}
};
const remove = async (photo) => {
setError('');
try {
await api.del(`/api/admin/rencontres-passees/${eventId}/photos/${photo.id}`);
setPhotos((prev) => prev.filter((p) => p.id !== photo.id));
} catch (e) {
setError(e.message);
}
};
const setPrincipal = async (photo) => {
setError('');
try {
await api.post(`/api/admin/rencontres-passees/${eventId}/photos/${photo.id}/principale`);
setPhotos((prev) => [photo, ...prev.filter((p) => p.id !== photo.id)]);
} catch (e) {
setError(e.message);
}
};
return (
<div>
<div className="field" style={{ marginBottom: 8 }}>Photos ({photos.length})</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(96px, 1fr))', gap: 10 }}>
{photos.map((p, idx) => (
<div key={p.id} style={{ position: 'relative', aspectRatio: '4/3', borderRadius: 4, overflow: 'hidden', border: idx === 0 ? '2px solid var(--red)' : '1px solid var(--border)' }}>
<img src={p.path} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
{idx === 0 && (
<span style={{ position: 'absolute', top: 4, left: 4, background: 'var(--red)', color: '#fff', fontSize: 9, fontWeight: 700, padding: '2px 6px', borderRadius: 3, letterSpacing: '.04em' }}>PRINCIPALE</span>
)}
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, display: 'flex', justifyContent: 'space-between', gap: 4, padding: 4, background: 'linear-gradient(transparent, rgba(0,0,0,.55))' }}>
{idx !== 0 && (
<button type="button" title="Définir comme principale" onClick={() => setPrincipal(p)} style={{ background: 'rgba(255,255,255,.9)', border: 'none', cursor: 'pointer', borderRadius: 3, fontSize: 11, padding: '2px 6px' }}>★</button>
)}
{user.role === 'admin' && (
<button type="button" title="Supprimer" onClick={() => remove(p)} style={{ marginLeft: 'auto', background: 'rgba(255,255,255,.9)', border: 'none', cursor: 'pointer', borderRadius: 3, fontSize: 11, padding: '2px 6px', color: 'var(--red)' }}>✕</button>
)}
</div>
</div>
))}
<div style={{ aspectRatio: '4/3' }}>
<ImageSlot endpoint={`/api/admin/rencontres-passees/${eventId}/photos`} placeholder="+ Ajouter une photo" onUploaded={add} />
</div>
</div>
{error && <p className="error-text" style={{ marginTop: 10 }}>{error}</p>}
</div>
);
}
function PastEventFormModal({ event, rencontres, onClose, onSaved }) {
const { user } = useAuth();
const [savedId, setSavedId] = useState(event?.id ?? null);
const [form, setForm] = useState(
event
? { date_label: event.date_label, lieu: event.lieu, titre: event.titre, texte: event.texte, participants: event.participants, nb_photos: event.nb_photos }
: { date_label: '', lieu: '', titre: '', texte: '', participants: 0, nb_photos: 0 }
? { date_label: event.date_label, lieu: event.lieu, titre: event.titre, texte: event.texte, participants: event.participants, rencontre_id: event.rencontre_id || '' }
: { date_label: '', lieu: '', titre: '', texte: '', participants: 0, rencontre_id: '' }
);
const [images, setImages] = useState([event?.image_path || '', event?.image_path_2 || '', event?.image_path_3 || '']);
const [photos, setPhotos] = useState(event?.photos || []);
const [error, setError] = useState('');
const [savedMsg, setSavedMsg] = useState(false);
const onChange = (e) => setForm({ ...form, [e.target.name]: e.target.value });
const setImageAt = (i) => (path) => setImages((prev) => prev.map((p, idx) => (idx === i ? path : p)));
const linked = form.rencontre_id ? rencontres.find((r) => r.id === Number(form.rencontre_id)) : null;
const submit = async (e) => {
e.preventDefault();
setError('');
const body = {
date_label: form.date_label,
lieu: form.lieu,
titre: form.titre,
texte: form.texte,
participants: Number(form.participants) || 0,
rencontre_id: form.rencontre_id ? Number(form.rencontre_id) : null,
};
try {
if (event) await api.put(`/api/admin/rencontres-passees/${event.id}`, form);
else await api.post('/api/admin/rencontres-passees', form);
onSaved();
if (savedId) {
await api.put(`/api/admin/rencontres-passees/${savedId}`, body);
setSavedMsg(true);
setTimeout(() => setSavedMsg(false), 1500);
} else {
const res = await api.post('/api/admin/rencontres-passees', body);
setSavedId(res.id);
}
} catch (err) {
setError(err.message);
}
@@ -32,7 +117,7 @@ function PastEventFormModal({ event, onClose, onSaved }) {
const remove = async () => {
if (!window.confirm('Supprimer cette rencontre passée et ses photos ?')) return;
try {
await api.del(`/api/admin/rencontres-passees/${event.id}`);
await api.del(`/api/admin/rencontres-passees/${savedId}`);
onSaved();
} catch (err) {
setError(err.message);
@@ -40,37 +125,9 @@ function PastEventFormModal({ event, onClose, onSaved }) {
};
return (
<Modal onClose={onClose} maxWidth={620} header={{ kicker: 'Rencontres passées', title: event ? 'Modifier la rencontre passée' : 'Nouvelle rencontre passée' }}>
<Modal onClose={onSaved} maxWidth={640} header={{ kicker: 'Rencontres passées', title: savedId ? 'Modifier la rencontre passée' : 'Nouvelle rencontre passée' }}>
<form onSubmit={submit} style={{ padding: '26px 30px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div>
<div className="field" style={{ marginBottom: 8 }}>Photos</div>
<div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr', gridTemplateRows: '1fr 1fr', gap: 8, height: 160 }}>
<div style={{ gridRow: 'span 2' }}>
<ImageSlot
endpoint={event ? `/api/admin/rencontres-passees/${event.id}/image/1` : undefined}
value={images[0]}
disabled={!event}
placeholder={event ? 'Photo principale' : "Enregistrez d'abord"}
onUploaded={setImageAt(0)}
/>
</div>
<ImageSlot
endpoint={event ? `/api/admin/rencontres-passees/${event.id}/image/2` : undefined}
value={images[1]}
disabled={!event}
placeholder="Photo"
onUploaded={setImageAt(1)}
/>
<ImageSlot
endpoint={event ? `/api/admin/rencontres-passees/${event.id}/image/3` : undefined}
value={images[2]}
disabled={!event}
placeholder="Photo"
onUploaded={setImageAt(2)}
/>
</div>
</div>
<label className="field">Titre
<input name="titre" value={form.titre} onChange={onChange} required maxLength={200} />
</label>
@@ -85,61 +142,78 @@ function PastEventFormModal({ event, onClose, onSaved }) {
<label className="field">Description
<textarea name="texte" value={form.texte} onChange={onChange} rows={3} required maxLength={2000} />
</label>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<label className="field">Participants
<input name="participants" type="number" min={0} max={100000} value={form.participants} onChange={onChange} required />
</label>
<label className="field">Nombre de photos affiché
<input name="nb_photos" type="number" min={0} max={100000} value={form.nb_photos} onChange={onChange} required />
</label>
</div>
<label className="field">Rencontre liée (participants automatiques)
<select name="rencontre_id" value={form.rencontre_id} onChange={onChange}>
<option value="">Aucune — saisir le nombre à la main</option>
{rencontres.map((r) => <option key={r.id} value={r.id}>{r.titre}</option>)}
</select>
</label>
<label className="field">Participants
{linked ? (
<input value={`${linked.inscrits} (depuis « ${linked.titre} »)`} disabled />
) : (
<input name="participants" type="number" min={0} max={100000} value={form.participants} onChange={onChange} />
)}
</label>
</div>
{error && <p className="error-text" style={{ marginTop: 12 }}>{error}</p>}
<div style={{ display: 'flex', gap: 12, marginTop: 24, alignItems: 'center' }}>
<button type="submit" className="btn btn-red btn-sm" style={{ flex: 1, fontSize: 14.5, padding: 13 }}>
{event ? 'Enregistrer' : 'Créer'}
<div style={{ display: 'flex', gap: 12, marginTop: 20, alignItems: 'center' }}>
<button type="submit" className="btn btn-red btn-sm" style={{ fontSize: 14.5, padding: '13px 24px' }}>
{savedId ? 'Enregistrer' : 'Créer et ajouter des photos'}
</button>
{event && (
<PastDeleteButton onDelete={remove} />
{savedMsg && <span className="success-text">✓ Enregistré</span>}
{savedId && user.role === 'admin' && (
<button type="button" className="btn-link-gray" style={{ fontSize: 13.5, padding: '13px 10px' }} onClick={remove}>
Supprimer
</button>
)}
<button type="button" className="btn btn-outline-soft btn-sm" style={{ fontSize: 14.5, padding: '13px 22px' }} onClick={onClose}>
Annuler
<button type="button" className="btn btn-outline-soft btn-sm" style={{ marginLeft: 'auto', fontSize: 14.5, padding: '13px 22px' }} onClick={onSaved}>
Fermer
</button>
</div>
</form>
</Modal>
);
}
// Only admins can delete — moderators manage this section but can't
// destroy data (same rule as members/rencontres/inscriptions).
function PastDeleteButton({ onDelete }) {
const { user } = useAuth();
if (user.role !== 'admin') return null;
return (
<button type="button" className="btn-link-gray" style={{ fontSize: 13.5, padding: '13px 10px' }} onClick={onDelete}>
Supprimer
</button>
{savedId && (
<div style={{ padding: '4px 30px 30px', borderTop: '1px solid var(--border-soft)', marginTop: 6 }}>
<div style={{ paddingTop: 20 }}>
<PhotoManager eventId={savedId} photos={photos} setPhotos={setPhotos} />
<p style={{ fontSize: 12.5, color: 'var(--gray-light)', lineHeight: 1.55, marginTop: 12 }}>
La première photo (★ principale) est mise en avant sur la page d'accueil. Le nombre de photos
affiché sur le site correspond automatiquement aux photos ci-dessus.
</p>
</div>
</div>
)}
</Modal>
);
}
export function PastEventsTab() {
const [events, setEvents] = useState([]);
const [rencontres, setRencontres] = useState([]);
const [modal, setModal] = useState(null); // null | 'new' | event
const reload = () => api.get('/api/admin/rencontres-passees').then((d) => setEvents(d.rencontresPassees)).catch(() => {});
const reload = () =>
Promise.all([api.get('/api/admin/rencontres-passees'), api.get('/api/admin/rencontres')])
.then(([p, r]) => {
setEvents(p.rencontresPassees);
setRencontres(r.rencontres);
})
.catch(() => {});
useEffect(() => {
reload();
}, []);
const cover = (ev) => (ev.photos && ev.photos[0]) || null;
return (
<>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 22 }}>
<div>
<h3 className="serif" style={{ fontSize: 20, fontWeight: 600 }}>Rencontres passées</h3>
<div style={{ fontSize: 12.5, color: 'var(--gray-light)', marginTop: 3 }}>
Section « Ils y étaient » de la page d'accueil.
Section « Ils y étaient » de la page d'accueil et page « Rencontres passées ».
</div>
</div>
<button className="btn btn-red btn-sm" style={{ fontSize: 13, padding: '10px 18px' }} onClick={() => setModal('new')}>
@@ -149,13 +223,13 @@ export function PastEventsTab() {
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
{events.map((ev) => (
<div key={ev.id} className="card" style={{ borderRadius: 6, padding: '18px 20px', display: 'grid', gridTemplateColumns: 'auto 1fr auto', gap: 20, alignItems: 'center' }}>
<div className={ev.image_path ? '' : 'placeholder-pattern'} style={{ width: 84, height: 60, borderRadius: 4, overflow: 'hidden', flexShrink: 0 }}>
{ev.image_path && <img src={ev.image_path} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />}
<div className={cover(ev) ? '' : 'placeholder-pattern'} style={{ width: 84, height: 60, borderRadius: 4, overflow: 'hidden', flexShrink: 0 }}>
{cover(ev) && <img src={cover(ev).path} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />}
</div>
<div style={{ minWidth: 0 }}>
<div style={{ fontSize: 16, fontWeight: 600 }}>{ev.titre}</div>
<div style={{ fontSize: 13, color: 'var(--gray-light)', marginTop: 4 }}>
{ev.date_label} · {ev.lieu} · {ev.participants} participants
{ev.date_label} · {ev.lieu} · {ev.participants_display} participants · {ev.nb_photos} photo{ev.nb_photos > 1 ? 's' : ''}
</div>
</div>
<button className="btn btn-sm" style={{ background: 'var(--admin-bg)', fontSize: 13, padding: '9px 16px' }} onClick={() => setModal(ev)}>
@@ -172,6 +246,7 @@ export function PastEventsTab() {
{modal && (
<PastEventFormModal
event={modal === 'new' ? null : modal}
rencontres={rencontres}
onClose={() => setModal(null)}
onSaved={() => { setModal(null); reload(); }}
/>
+11 -28
View File
@@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
import { usePublicData } from '../lib/usePublic.js';
import { api, dateParts } from '../lib/api.js';
import Modal from '../components/Modal.jsx';
import PastEventCard from '../components/PastEventCard.jsx';
function InscriptionModal({ rencontre, onClose, onDone }) {
const [form, setForm] = useState({ nom: '', entreprise: '', email: '', tel: '' });
@@ -246,36 +247,18 @@ export default function Home() {
{/* PAST */}
<section style={{ background: 'var(--beige)', marginTop: 56, padding: '96px 0' }}>
<div className="container">
<div style={{ marginBottom: 52 }}>
<div className="kicker" style={{ marginBottom: 14 }}>Ils y étaient</div>
<h2 className="serif" style={{ fontWeight: 500, fontSize: 44, lineHeight: 1.08 }}>Rencontres passées</h2>
<div style={{ marginBottom: 52, display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', gap: 24, flexWrap: 'wrap' }}>
<div>
<div className="kicker" style={{ marginBottom: 14 }}>Ils y étaient</div>
<h2 className="serif" style={{ fontWeight: 500, fontSize: 44, lineHeight: 1.08 }}>Rencontres passées</h2>
</div>
<Link to="/rencontres-passees" style={{ textDecoration: 'none' }}>
<span className="btn-link" style={{ fontSize: 14, whiteSpace: 'nowrap' }}>Voir toutes les rencontres →</span>
</Link>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 34 }}>
{passees.map((p) => (
<article key={p.id} className="card hero-grid" style={{ display: 'grid', gridTemplateColumns: '1.15fr .85fr', gap: 40, alignItems: 'center', padding: 36 }}>
<div>
<div style={{ fontSize: 12, letterSpacing: '.14em', textTransform: 'uppercase', color: 'var(--gray-light)', fontWeight: 600, marginBottom: 12 }}>
{p.date_label} · {p.lieu}
</div>
<h3 className="serif" style={{ fontSize: 30, fontWeight: 600, lineHeight: 1.12, marginBottom: 16 }}>{p.titre}</h3>
<p style={{ fontSize: 15.5, lineHeight: 1.7, color: 'var(--gray)', marginBottom: 20 }}>{p.texte}</p>
<div style={{ display: 'flex', gap: 20, fontSize: 13, color: 'var(--gray-light)' }}>
<span>👥 {p.participants} participants</span>
<span>📷 {p.nb_photos} photos</span>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gridTemplateRows: '1fr 1fr', gap: 10, height: 240 }}>
<div className={p.image_path ? '' : 'placeholder-pattern'} style={{ gridRow: 'span 2', borderRadius: 3, overflow: 'hidden' }}>
{p.image_path && <img src={p.image_path} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />}
</div>
<div className={p.image_path_2 ? '' : 'placeholder-pattern'} style={{ borderRadius: 3, overflow: 'hidden' }}>
{p.image_path_2 && <img src={p.image_path_2} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />}
</div>
<div className={p.image_path_3 ? '' : 'placeholder-pattern'} style={{ borderRadius: 3, overflow: 'hidden' }}>
{p.image_path_3 && <img src={p.image_path_3} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />}
</div>
</div>
</article>
{passees.slice(0, 3).map((p) => (
<PastEventCard key={p.id} event={p} />
))}
</div>
</div>
+41
View File
@@ -0,0 +1,41 @@
import { usePublicData } from '../lib/usePublic.js';
import PastEventCard from '../components/PastEventCard.jsx';
export default function RencontresPassees() {
const { data } = usePublicData();
const passees = data?.rencontresPassees ?? [];
return (
<main>
<section style={{ background: '#fff', borderBottom: '1px solid var(--border)' }}>
<div className="container" style={{ padding: '70px 32px 60px' }}>
<div className="kicker" style={{ marginBottom: 16 }}>Ils y étaient</div>
<h1 className="serif" style={{ fontWeight: 500, fontSize: 'clamp(36px, 5vw, 54px)', lineHeight: 1.05, marginBottom: 18 }}>
Nos rencontres passées
</h1>
<p style={{ fontSize: 17, lineHeight: 1.6, color: 'var(--gray)', maxWidth: 600 }}>
Revivez en images les temps forts du Business Club : galas, visites privées, tables rondes
et afterworks. Cliquez sur une rencontre pour parcourir toutes ses photos.
</p>
</div>
</section>
<section style={{ background: 'var(--beige)', padding: '64px 0 96px' }}>
<div className="container">
{passees.length === 0 ? (
<div style={{ textAlign: 'center', padding: '80px 20px', color: 'var(--gray-light)' }}>
<div className="serif" style={{ fontSize: 26, color: 'var(--ink)', marginBottom: 10 }}>Aucune rencontre passée</div>
<p style={{ fontSize: 15 }}>Les prochains événements apparaîtront ici une fois terminés.</p>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 34 }}>
{passees.map((p) => (
<PastEventCard key={p.id} event={p} />
))}
</div>
)}
</div>
</section>
</main>
);
}