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

+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>
);
}