PageSourceSearch

https://lecture-lab-a.netlify.app/js/index.js

js lecture-lab-a.netlify.app collected 2026-10-03 10:59:54 UTC 132,820 bytes, 2,819 lines download raw bytes

1// ══════════════════════════════════════════════════════════
2//  js/index.js — Module principal de la page d'accueil
3// ══════════════════════════════════════════════════════════
4
5// ── Imports ──────────────────────────────────────────────
6import { doc, getDoc, setDoc, addDoc, updateDoc, deleteDoc,
7         collection, query, where, orderBy, onSnapshot, serverTimestamp }
8  from 'https://www.gstatic.com/firebasejs/10.12.2/firebase-firestore.js';
9import { CONFIG } from './config.js';
10import { db } from './firebase-init.js';
11import { userState, getDisplayName, getUserPhoto,
12         renderNavUser, initAuthUI, initAuthStateListener } from './auth.js';
13import { showToast, initHamburger, initAvatarDropdownClose, initScrollReveal, escapeHtml } from './utils.js';
14import { logActivity, listenActivity, initNotificationBell, initBugReport } from './activity.js';
15import { registerRoute, processCurrentHash, setHash, clearHash, onDataReady, waitForData } from './router.js';
16
17// ── State (page-specific) ────────────────────────────────
18const state = {
19  members: [],
20  sessions: [],
21  pastSessions: [],
22  proposals: [],
23  selectedBook: null,
24  searchResults: [],
25  expandedSessions: new Set(),
26  expandedPastSessions: new Set(),
27  themes: [],
28  tips: [],
29  tipSelectedBook: null,
30  limits: { monthlyProposals: 15, monthlyTips: 15, maxThemes: 20, freeThemes: 2, maxProposalsTotal: 100, anonProposals: 2 },
31  currentBook: null,
32  currentComments: [],
33  currentCritiques: [],
34  pendingCritiques: [],
35};
36
37// ── Types de session ─────────────────────────────────────
38const SESSION_TYPES = {
39  theme:              { label: 'Thème libre',       icon: '📚' },
40  'lecture-commune':  { label: 'Lecture commune',   icon: '📖' },
41  'rencontre-auteur': { label: 'Rencontre auteur', icon: '✍️' },
42  debat:              { label: 'Débat',             icon: '⚔️' }
43};
44
45function getSessionType(s) { return s.type || 'theme'; }
46
47function sessionTypeBadge(type) {
48  if (type === 'theme') return '';
49  const t = SESSION_TYPES[type];
50  return t ? `<span class="session-type-badge type-${type}">${t.icon} ${t.label}</span>` : '';
51}
52
53function getSessionTitle(s) {
54  const type = getSessionType(s);
55  switch (type) {
56    case 'lecture-commune': {
57      const b = (s.books || [])[0];
58      return b ? b.title : (s.theme || 'Lecture commune');
59    }
60    case 'rencontre-auteur': return s.authorName || s.theme || 'Rencontre auteur';
61    case 'debat': return s.debateQuestion || s.theme || 'Débat';
62    default: return s.theme || '';
63  }
64}
65
66function getSessionSubtitle(s) {
67  const type = getSessionType(s);
68  switch (type) {
69    case 'lecture-commune': {
70      const b = (s.books || [])[0];
71      return b ? b.author : (s.themeDescription || '');
72    }
73    case 'rencontre-auteur': return s.authorBio ? (s.authorBio.length > 150 ? s.authorBio.substring(0, 150) + '…' : s.authorBio) : '';
74    case 'debat': return s.debateDescription || s.themeDescription || '';
75    default: return s.themeDescription || '';
76  }
77}
78
79function heroLabel(s) {
80  const type = getSessionType(s);
81  switch (type) {
82    case 'lecture-commune': return 'Prochaine lecture commune…';
83    case 'rencontre-auteur': return 'Prochaine rencontre avec…';
84    case 'debat': return 'Prochain débat…';
85    default: return s.date ? 'Prochaine session sur le thème…' : 'Prochaine session';
86  }
87}
88
89function heroThemeText(s) {
90  const type = getSessionType(s);
91  const title = getSessionTitle(s);
92  if (!title) return '';
93  if (type === 'rencontre-auteur') return title;
94  return `« ${title} »`;
95}
96
97// Contenu de la zone livres pour la session principale (fond sombre)
98function renderFirstSessionBooks(s) {
99  const type = getSessionType(s);
100  const books = s.books || [];
101
102  if (type === 'lecture-commune') {
103    const b = books[0];
104    if (!b) return `<p style="color:rgba(255,255,255,0.35);font-style:italic;
104font-family:'Playfair Display',serif;">Le livre sera annoncé bientôt.</p>`;
105    return `
106      <div class="lc-featured">
107        <div class="lc-book-main clickable" data-sid="${s.id}" data-stype="sessions" data-bi="0">
108          <div class="lc-cover">${b.coverUrl ? `<img src="${b.coverUrl}" alt="">` : '<span class="lc-cover-ph">📖</span>'}</div>
109          <div class="lc-details">
110            <div class="lc-title">${escapeHtml(b.title)}</div>
111            <div class="lc-author">${escapeHtml(b.author)}</div>
112            ${b.presenter ? `<div class="book-presenter">Présenté par ${escapeHtml(b.presenter)}</div>` : ''}
113            ${b.description ? `<div class="lc-desc">${escapeHtml(b.description).substring(0, 300)}${b.description.length > 300 ? '…' : ''}</div>` : ''}
114          </div>
115        </div>
116        ${s.discussionGuide ? `
117          <div class="lc-guide">
118            <div class="lc-guide-label">Guide de discussion</div>
119            <div class="lc-guide-text">${escapeHtml(s.discussionGuide)}</div>
120          </div>` : ''}
121      </div>`;
122  }
123
124  if (type === 'rencontre-auteur') {
125    return `
126      <div class="ra-featured">
127        <div class="ra-author-card">
128          ${s.authorPhoto ? `<img class="ra-photo" src="${s.authorPhoto}" alt="">` : `<div class="ra-photo-ph">✍️</div>`}
129          <div class="ra-info">
130            <div class="ra-name">${escapeHtml(s.authorName || '')}</div>
131            ${s.authorBio ? `<div class="ra-bio">${escapeHtml(s.authorBio)}</div>` : ''}
132          </div>
133        </div>
134        ${books.length > 0 ? `
135          <div class="ra-bib-label">Bibliographie sélective</div>
136          <div class="books-grid">
137            ${books.map((b, i) => `
138              <div class="book-card clickable" data-sid="${s.id}" data-stype="sessions" data-bi="${i}">
139                <div class="book-card-content">
140                  <div class="book-cover">${b.coverUrl ? `<img src="${b.coverUrl}" alt="">` : '📖'}</div>
141                  <div class="book-info">
142                    <div class="book-title">${escapeHtml(b.title)}</div>
143                    <div class="book-author">${escapeHtml(b.author)}</div>
144                  </div>
145                </div>
146              </div>`).join('')}
147          </div>` : ''}
148      </div>`;
149  }
150
151  if (type === 'debat') {
152    const pour = books[0];
153    const contre = books[1];
154    return `
155      <div class="debat-arena">
156        <div class="debat-side debat-pour">
157          <div class="debat-side-label">Pour</div>
158          ${pour ? `
159            <div class="book-card clickable" data-sid="${s.id}" data-stype="sessions" data-bi="0">
160              <div class="book-card-content">
161                <div class="book-cover">${pour.coverUrl ? `<img src="${pour.coverUrl}" alt="">` : '📖'}</div>
162                <div class="book-info">
163                  <div class="book-title">${escapeHtml(pour.title)}</div>
164                  <div class="book-author">${escapeHtml(pour.author)}</div>
165                  ${pour.presenter ? `<div class="book-presenter">Présenté par ${escapeHtml(pour.presenter)}</div>` : ''}
166                </div>
167              </div>
168            </div>` : `<div class="debat-empty">Livre à annoncer</div>`}
169        </div>
170        <div class="debat-vs">VS</div>
171        <div class="debat-side debat-contre">
172          <div class="debat-side-label">Contre</div>
173          ${contre ? `
174            <div class="book-card clickable" data-sid="${s.id}" data-stype="sessions" data-bi="1">
175              <div class="book-card-content">
176                <div class="book-cover">${contre.coverUrl ? `<img src="${contre.coverUrl}" alt="">` : '📖'}</div>
177                <div class="book-info">
178                  <div class="book-title">${escapeHtml(contre.title)}</div>
179                  <div class="book-author">${escapeHtml(contre.author)}</div>
180                  ${contre.presenter ? `<div class="book-presenter">Présenté par ${escapeHtml(contre.presenter)}</div>` : ''}
181                </div>
182              </div>
183            </div>` : `<div class="debat-empty">Livre à annoncer</div>`}
184        </div>
185      </div>
186      ${s.debateDescription ? `<div class="debat-desc">${escapeHtml(s.debateDescription)}</div>` : ''}`;
187  }
188
189  // type === 'theme' : retourne null → le code existant gère l'affichage
190  return null;
191}
192
193// Contenu carte compacte pour sessions suivantes (fond sombre)
194function renderUpcomingCardContent(s) {
195  const type = getSessionType(s);
196  const books = s.books || [];
197
198  if (type === 'lecture-commune') {
199    const b = books[0];
200    return `
201      ${sessionTypeBadge(type)}
202      <div class="upcoming-theme">
202${escapeHtml(b ? b.title : 'Livre à annoncer')}</div>
203      ${b ? `<div class="upcoming-desc">${escapeHtml(b.author)}</div>` : ''}`;
204  }
205
206  if (type === 'rencontre-auteur') {
207    return `
208      ${sessionTypeBadge(type)}
209      <div class="upcoming-theme">${escapeHtml(s.authorName || 'Auteur à annoncer')}</div>
210      ${books.length > 0 ? `<div class="upcoming-desc">${books.length} livre${books.length > 1 ? 's' : ''} à découvrir</div>` : ''}`;
211  }
212
213  if (type === 'debat') {
214    const pour = books[0];
215    const contre = books[1];
216    return `
217      ${sessionTypeBadge(type)}
218      <div class="upcoming-theme">${escapeHtml(s.debateQuestion || 'Question à annoncer')}</div>
219      ${pour && contre ? `<div class="upcoming-debat-mini"><span>${escapeHtml(pour.title)}</span> <span class="upcoming-vs">vs</span> <span>${escapeHtml(contre.title)}</span></div>` : ''}`;
220  }
221
222  return null; // theme : code existant
223}
224
225// Contenu pour la dernière session featured (fond clair)
226function renderFeaturedPastContent(s) {
227  const type = getSessionType(s);
228  const books = s.books || [];
229
230  if (type === 'lecture-commune') {
231    const b = books[0];
232    return `
233      ${b ? `
234        <div class="lc-featured lc-featured-past">
235          <div class="lc-book-main clickable" data-pid="${s.id}" data-bi="0">
236            <div class="lc-cover">${b.coverUrl ? `<img src="${b.coverUrl}" alt="">` : '<span class="lc-cover-ph">📖</span>'}</div>
237            <div class="lc-details">
238              <div class="lc-title">${escapeHtml(b.title)}</div>
239              <div class="lc-author">${escapeHtml(b.author)}</div>
240              ${b.presenter ? `<div class="book-presenter" style="color:var(--green);">Présenté par ${escapeHtml(b.presenter)}</div>` : ''}
241            </div>
242          </div>
243          ${s.discussionGuide ? `
244            <div class="lc-guide lc-guide-past">
245              <div class="lc-guide-label">Guide de discussion</div>
246              <div class="lc-guide-text">${escapeHtml(s.discussionGuide)}</div>
247            </div>` : ''}
248        </div>` : ''}`;
249  }
250
251  if (type === 'rencontre-auteur') {
252    return `
253      <div class="ra-featured ra-featured-past">
254        <div class="ra-author-card">
255          ${s.authorPhoto ? `<img class="ra-photo" src="${s.authorPhoto}" alt="">` : `<div class="ra-photo-ph">✍️</div>`}
256          <div class="ra-info">
257            <div class="ra-name" style="color:var(--text);">${escapeHtml(s.authorName || '')}</div>
258            ${s.authorBio ? `<div class="ra-bio" style="color:var(--text-mid);">${escapeHtml(s.authorBio)}</div>` : ''}
259          </div>
260        </div>
261        ${books.length > 0 ? `
262          <div class="ra-bib-label" style="color:var(--text-mid);">Bibliographie sélective</div>
263          <div class="last-session-books">
264            ${books.map((b, i) => `
265              <div class="book-card clickable" data-pid="${s.id}" data-bi="${i}">
266                <div class="book-card-content">
267                  <div class="book-cover">${b.coverUrl ? `<img src="${b.coverUrl}" alt="">` : '📖'}</div>
268                  <div class="book-info">
269                    <div class="book-title">${escapeHtml(b.title)}</div>
270                    <div class="book-author">${escapeHtml(b.author)}</div>
271                  </div>
272                </div>
273              </div>`).join('')}
274          </div>` : ''}
275      </div>`;
276  }
277
278  if (type === 'debat') {
279    const pour = books[0];
280    const contre = books[1];
281    return `
282      <div class="debat-arena debat-arena-past">
283        <div class="debat-side debat-pour">
284          <div class="debat-side-label" style="color:var(--green);">Pour</div>
285          ${pour ? `
286            <div class="book-card clickable" data-pid="${s.id}" data-bi="0">
287              <div class="book-card-content">
288                <div class="book-cover">${pour.coverUrl ? `<img src="${pour.coverUrl}" alt="">` : '📖'}</div>
289                <div class="book-info">
290                  <div class="book-title">${escapeHtml(pour.title)}</div>
291                  <div class="book-author">${escapeHtml(pour.author)}</div>
292                </div>
293              </div>
294            </div>` : ''}
295        </div>
296        <div class="debat-vs" style="color:var(--terra);">VS</div>
297        <div class="debat-side debat-contre">
298          <div class="debat-side-label" style="color:var(--terra);">Contre</div>
299          ${contre ? `
300            <div class="book-card clickable" data-pid="${s.id}" data-bi="1">
301              <div class="book-card-content">
302                <div class="book-cover">${contre.coverUrl ? `<img src="${contre.coverUrl}" alt="">` : '📖'}</div>
303                <div class="book-info">
304                  <div class="book-title">${escapeHtml(contre.title)}</div>
305                  <div class="book-author">${escapeHtml(contre.author)}</div>
306                </div>
307              </div>
308            </div>` : ''}
309        </div>
310      </div>
311      ${s.debateDescription ? `<div class="debat-desc" style="color:var(--text-mid);">${escapeHtml(s.debateDescription)}</div>` : ''}`;
312  }
313
314  return null; // theme : code existant
315}
316
317// Contenu carte compacte passée (fond clair)
318function renderPastCardContent(s) {
319  const type = getSessionType(s);
320  const books = s.books || [];
321
322  if (type === 'lecture-commune') {
323    const b = books[0];
324    return `
325      <div class="past-date">${escapeHtml(s.date)}</div>
326      ${sessionTypeBadge(type)}
327      <div class="past-theme">${escapeHtml(b ? b.title : 'Lecture commune')}</div>
328      ${b ? `<div class="past-theme-desc">${escapeHtml(b.author)}</div>` : ''}
329      ${s.recap ? `<div class="past-recap">${s.recap}</div>` : ''}`;
330  }
331
332  if (type === 'rencontre-auteur') {
333    return `
334      <div class="past-date">${escapeHtml(s.date)}</div>
335      ${sessionTypeBadge(type)}
336      <div class="past-theme">${escapeHtml(s.authorName || 'Rencontre auteur')}</div>
337      ${books.length > 0 ? `<ul class="past-books">${books.map((b, i) =>
338        `<li class="clickable" data-pid="${s.id}" data-bi="${i}"><span><strong>${escapeHtml(b.title)}</strong> — ${escapeHtml(b.author)}</span></li>`
339      ).join('')}</ul>` : ''}
340      ${s.recap ? `<div class="past-recap">${s.recap}</div>` : ''}`;
341  }
342
343  if (type === 'debat') {
344    const pour = books[0];
345    const contre = books[1];
346    return `
347      <div class="past-date">${escapeHtml(s.date)}</div>
348      ${sessionTypeBadge(type)}
349      <div class="past-theme">${escapeHtml(s.debateQuestion || 'Débat')}</div>
350      ${pour && contre ? `<div class="past-debat-mini"><span class="past-debat-pour">${escapeHtml(pour.title)}</span> <span class="past-debat-vs">vs</span> <span class="past-debat-contre">${escapeHtml(contre.title)}</span></div>` : ''}
351      ${s.recap ? `<div class="past-recap">${s.recap}</div>` : ''}`;
352  }
353
354  return null; // theme : code existant
355}
356
357// ── Limites configurables ────────────────────────────────
358async function loadLimits() {
359  try {
360    const snap = await getDoc(doc(db, 'settings', 'limits'));
361    if (snap.exists()) Object.assign(state.limits, snap.data());
362  } catch(e) { /* garde les défauts */ }
363}
364async function saveLimits(data) {
365  await setDoc(doc(db, 'settings', 'limits'), data, { merge: true });
366  Object.assign(state.limits, data);
367  showToast('Limites mises à jour !');
368}
369
370// ── Firestore : membres officiels ────────────────────────
371function listenMembers() {
372  return onSnapshot(doc(db, 'settings', 'members'), snap => {
373    state.members = snap.exists() ? (snap.data().emails || []) : [];
374    userState.isMember = !!(userState.user && (state.members.includes(userState.user.email) || userState.isAdmin));
375  }, () => { state.members = []; userState.isMember = false; showToast('Erreur de chargement des membres.'); });
376}
377
378async function addMember(email) {
379  const members = [...new Set([...state.members, email.toLowerCase().trim()])];
380  await setDoc(doc(db, 'settings', 'members'), { emails: members });
381  showToast('Membre ajouté !');
382}
383
384async function removeMember(email) {
385  const members = state.members.filter(e => e !== email);
386  await setDoc(doc(db, 'settings', 'members'), { emails: members });
387  showToast('Membre retiré.');
388}
389
390// ── Firestore : sessions à venir ─────────────────────────
391function listenSessions() {
392  const q = query(collection(db, 'sessions'), orderBy('order', 'asc'));
393  return onSnapshot(q, snap => {
394    state.sessions = snap.docs.map(d => ({ id: d.id, ...d.data() }));
395    renderHero();
396    renderAllSessions();
397    renderSuggestSection();
398    // Re-render admin si le panel est ouvert
399    if (!document.getElementById('admin-overlay').classList.contains('hidden')) {
400      renderAdminSessions();
401    }
402    onDataReady('sessions');
403  }, () => {
404    state.sessions = [];
405    renderHero();
406    renderAllSessions();
407    showToast('Erreur de chargement des sessions.');
408  });
409}
410
411async function saveSession(id, data) {
412  await setDoc(doc(db, 'sessions', id), data, { merge: true });
413}
414
415async function addSession(type = 'theme') {
416  const maxOrder = state.sessions.reduce((m, s) => Math.max(m, s.order || 0), 0);
417  const base = { type, date: '—', heure: '—', lieu: CONFIG.club.lieu, books: [], order: maxOrder + 1 };
418  switch (type) {
419    case 'lecture-commune':
420      Object.assign(base, { theme: 'Lecture commune', themeDescription: '', discussionGuide: '' });
421      break;
422    case 'rencontre-auteur':
423      Object.assign(base, { theme: 'Rencontre auteur', themeDescription: '', authorName: '', authorBio: '', authorPhoto: '' });
424      break;
425    case 'debat':
426      Object.assign(base, { theme: 'Débat', themeDescription: '', debateQuestion: '', debateDescription: '' });
427      break;
428    default:
429      Object.assign(base, { theme: 'À définir', themeDescription: '' });
430  }
431  await addDoc(collection(db, 'sessions'), base);
432  showToast('Nouvelle session créée !');
433}
434
435async function archiveSession(id) {
436  const s = state.sessions.find(s => s.id === id);
437  if (!s || !confirm('Archiver cette session ?')) return;
438  const { id: _, ...data } = s;
439  await addDoc(collection(db, 'pastSessions'), { ...data, archivedAt: serverTimestamp() });
440  await deleteDoc(doc(db, 'sessions', id));
441  showToast('Session archivée !');
442}
443
444async function deleteSession(id) {
445  if (!confirm('Supprimer cette session ? Cette action est irréversible.')) return;
446  await deleteDoc(doc(db, 'sessions', id));
447  showToast('Session supprimée.');
448}
449
450async function removeBookFromSession(sessionId, bookIndex) {
451  const s = state.sessions.find(s => s.id === sessionId);
452  if (!s) return;
453  const books = [...(s.books || [])];
454  books.splice(bookIndex, 1);
455  await saveSession(sessionId, { books });
456}
457
458async function swapSessionOrder(id1, id2) {
459  const s1 = state.sessions.find(s => s.id === id1);
460  const s2 = state.sessions.find(s => s.id === id2);
461  if (!s1 || !s2) return;
462  await saveSession(id1, { order: s2.order });
463  await saveSession(id2, { order: s1.order });
464}
465
466// ── Firestore : sessions passées ─────────────────────────
467function listenPastSessions() {
468  const q = query(collection(db, 'pastSessions'), orderBy('archivedAt', 'desc'));
469  return onSnapshot(q, snap => {
470    state.pastSessions = snap.docs.map(d => ({ id: d.id, ...d.data() }));
471    renderPastSessions();
472    if (!document.getElementById('admin-overlay').classList.contains('hidden')) {
473      renderAdminPastSessions();
474    }
475    onDataReady('pastSessions');
476  }, () => {
477    state.pastSessions = [];
478    renderPastSessions();
479    showToast('Erreur de chargement des sessions passées.');
480  });
481}
482
483async function savePastSession(id, data) {
484  await setDoc(doc(db, 'pastSessions', id), data, { merge: true });
485}
486
487async function deletePastSession(id) {
488  if (!confirm('Supprimer cette session archivée ?')) return;
489  await deleteDoc(doc(db, 'pastSessions', id));
490  showToast('Session archivée supprimée.');
491}
492
493async function addBookToPastSession(id, book) {
494  const s = state.pastSessions.find(s => s.id === id);
495  if (!s) return;
496  const books = [...(s.books || [])];
497  books.push(book);
498  await savePastSession(id, { books });
499  showToast('Livre ajouté !');
500}
501
502async function removeBookFromPastSession(id, bookIndex) {
503  const s = state.pastSessions.find(s => s.id === id);
504  if (!s) return;
505  const books = [...(s.books || [])];
506  books.splice(bookIndex, 1);
507  await savePastSession(id, { books });
508}
509
510async function swapPastSessionOrder(id1, id2) {
511  const s1 = state.pastSessions.find(s => s.id === id1);
512  const s2 = state.pastSessions.find(s => s.id === id2);
513  if (!s1 || !s2) return;
514  await savePastSession(id1, { archivedAt: s2.archivedAt });
515  await savePastSession(id2, { archivedAt: s1.archivedAt });
516}
517
518// ── Firestore : thèmes / sondage ─────────────────────────
519function listenThemes() {
520  const q = query(collection(db, 'themes'), orderBy('votes', 'desc'));
521  return onSnapshot(q, snap => {
522    state.themes = snap.docs.map(d => ({ id: d.id, ...d.data() }));
523    renderPoll();
524  }, err => {
525    console.error('Erreur chargement thèmes :', err);
526    state.themes = [];
527    renderPoll();
528  });
529}
530
531// Helper : normalise voters (supporte l'ancien format array d'UIDs et le nouveau format map {uid: name})
532function getVotersMap(theme) {
533  const v = theme.voters || {};
534  if (Array.isArray(v)) {
535    const map = {};
536    v.forEach(uid => { map[uid] = 'Membre'; });
537    return map;
538  }
539  return v;
540}
541function getVoteCount(theme) {
542  return Object.keys(getVotersMap(theme)).length;
543}
544
545async function voteTheme(id) {
546  if (!userState.user) { showToast('Connecte-toi pour voter.'); return; }
547  const theme = state.themes.find(t => t.id === id);
548  if (!theme) return;
549  const voters = getVotersMap(theme);
550  if (voters[userState.user.uid]) { showToast('Tu as déjà voté pour ce thème.'); return; }
551  voters[userState.user.uid] = getDisplayName();
552  await updateDoc(doc(db, 'themes', id), { votes: Object.keys(voters).length, voters });
553}
554
555async function unvoteTheme(id) {
556  if (!userState.user) return;
557  const theme = state.themes.find(t => t.id === id);
558  if (!theme) return;
559  const voters = getVotersMap(theme);
560  delete voters[userState.user.uid];
561  await updateDoc(doc(db, 'themes', id), { votes: Object.keys(voters).length, voters });
562}
563
564async function suggestTheme(name) {
565  if (!userState.user) { showToast('Connecte-toi pour proposer un thème.'); return; }
566  if (state.themes.length >= state.limits.maxThemes) { showToast(`Le nombre maximum de thèmes (${state.limits.maxThemes}) est atteint.`); return; }
567  const myThemes = state.themes.filter(t => t.proposedBy === userState.user.uid);
568  if (myThemes.length >= state.limits.freeThemes) {
569    // Au-delà de 2, passage en validation admin
570    await addDoc(collection(db, 'themes'), { name, votes: 0, voters: {}, proposedBy: userState.user.uid, status: 'pending' });
571    showToast('Thème soumis à validation admin !');
572  } else {
573    await addDoc(collection(db, 'themes'), { name, votes: 0, voters: {}, proposedBy: userState.user.uid, status: 'approved' });
574    showToast('Thème proposé !');
575  }
576}
577
578async function addTheme(name) {
579  // Admin : ajout direct
580  await addDoc(collection(db, 'themes'), { name, votes: 0, voters: {}, proposedBy: userState.user?.uid || null, status: 'approved' });
581  showToast('Thème ajouté !');
582}
583
584async function deleteTheme(id) {
585  if (!confirm('Supprimer ce thème ?')) return;
586  await deleteDoc(doc(db, 'themes', id));
587  showToast('Thème supprimé.');
588}
589
590async function resetAllVotes() {
591  if (!confirm('Remettre tous les votes à zéro ?')) return;
592  for (const t of state.themes) {
593    await updateDoc(doc(db, 'themes', t.id), { votes: 0, voters: {} });
594  }
595  showToast('Votes remis à zéro !');
596}
597
598// ── Firestore : propositions ─────────────────────────────
599let unsubProposals = null;
600let unsubPendingCritiques = null;
601function listenProposals() {
602  const q = query(collection(db, 'proposals'), where('status', '==', 'pending'), orderBy('proposedAt', 'desc'));
603  return onSnapshot(q, snap => {
604    state.proposals = snap.docs.map(d => ({ id: d.id, ...d.data() }));
605    renderAdminProposals();
606  }, err => {
607    console.error('Erreur chargement propositions :', err);
608    state.proposals = [];
609    renderAdminProposals();
610  });
611}
612
613async function submitProposal(data) {
614  await addDoc(collection(db, 'proposals'), {
615    ...data,
616    proposedBy: {
617      uid: userState.user?.uid || null,
618      name: data.proposerName || (userState.user ? getDisplayName() : 'Anonyme'),
619      email: userState.user?.email || null,
620      photo: userState.user ? getUserPhoto() : null
621    },
622    proposedAt: serverTimestamp(),
623    status: 'pending'
624  });
625}
626
627async function approveProposal(p, sessionId) {
628  const session = state.sessions.find(s => s.id === sessionId);
629  if (!session) { showToast('Aucune session sélectionnée.'); return; }
630  const { id, title, author, description, coverUrl, googleBooksId } = p;
631  const books = [...(session.books || [])];
632  books.push({ title, author, description: description || '', coverUrl: coverUrl || '', googleBooksId: googleBooksId || '', presenter: p.proposedBy?.name || '', note: p.note || '' });
633  await saveSession(sessionId, { books });
634  await updateDoc(doc(db, 'proposals', id), { status: 'approved' });
635  showToast(`✓ « ${title} » ajouté à la session !`);
636  logActivity('proposal', `« ${title} » ajouté à la prochaine session`, `#book/${sessionId}/${books.length - 1}`);
637}
638
639async function rejectProposal(id) {
640  await updateDoc(doc(db, 'proposals', id), { status: 'rejected' });
641  showToast('Proposition refusée.');
642}
643
644// ── Recherche de livres (Google Books + Open Library) ────
645let searchTimer = null;
646async function searchBooks(q) {
647  const key = CONFIG.googleBooksApiKey ? `&key=${CONFIG.googleBooksApiKey}` : '';
648  const url  = `https://www.googleapis.com/books/v1/volumes?q=${encodeURIComponent(q)}&maxResults=6${key}`;
649  try {
650    const res  = await fetch(url);
651    const data = await res.json();
652    return (data.items || []).map(item => {
653      const v = item.volumeInfo;
654      return {
655        googleBooksId: item.id,
656        title:         v.title || '',
657        author:        (v.authors || []).join(', '),
658        description:   v.description || '',
659        coverUrl:      (v.imageLinks?.thumbnail || '').replace('http://', 'https://')
660      };
661    });
662  } catch { return []; }
663}
664
665async function searchOpenLibrary(q) {
666  const url = `https://openlibrary.org/search.json?q=${encodeURIComponent(q)}&limit=6&fields=key,title,author_name,cover_i,first_publish_year`;
667  try {
668    const res  = await fetch(url);
669    const data = await res.json();
670    return (data.docs || []).map(d => ({
671      openLibraryId: d.key || '',
672      title:         d.title || '',
673      author:        (d.author_name || []).join(', '),
674      description:   '',
675      coverUrl:      d.cover_i ? `https://covers.openlibrary.org/b/id/${d.cover_i}-M.jpg` : ''
676    }));
677  } catch { return []; }
678}
679
680function renderSearchResults(results, dropdown, query, onSelect) {
681  function bindResults(items) {
682    dropdown.querySelectorAll('.search-result-item').forEach(el => {
683      el.addEventListener('click', () => onSelect(items[+el.dataset.i]));
684    });
685  }
686
687  function renderItems(items) {
688    dropdown.innerHTML = items.map((r, i) => `
689      <div class="search-result-item" data-i="${i}">
690        ${r.coverUrl ? `<img src="${r.coverUrl}" alt="">` : '<div class="no-cover">📖</div>'}
691        <div>
692          <div class="result-title">${escapeHtml(r.title)}</div>
693          <div class="result-author">${escapeHtml(r.author || 'Auteur inconnu')}</div>
694        </div>
695      </div>`).join('');
696    bindResults(items);
697  }
698
699  function addOpenLibraryLink(label) {
700    const altLink = document.createElement('div');
701    altLink.className = 'search-alt-link';
702    altLink.textContent = label;
703    altLink.addEventListener('click', async (e) => {
704      e.stopPropagation();
705      dropdown.innerHTML = '<div class="search-loading">Recherche sur Open Library…</div>';
706      const olResults = await searchOpenLibrary(query);
707      if (!olResults.length) { dropdown.innerHTML = '<div class="search-loading">Aucun résultat.</div>'; return; }
708      renderItems(olResults);
709    });
710    dropdown.appendChild(altLink);
711  }
712
713  if (!results.length) {
714    dropdown.innerHTML = '<div class="search-loading">Aucun résultat sur Google Books.</div>';
715    addOpenLibraryLink('📚 Cliquer ici pour effectuer la recherche sur Open Library');
716    return;
717  }
718  renderItems(results);
719  addOpenLibraryLink('📚 Cliquer ici pour effectuer la recherche sur Open Library');
720}
721
722// ── Firestore : conseils lecture ─────────────────────────
723function listenTips() {
724  const q = query(collection(db, 'tips'), orderBy('createdAt', 'desc'));
725  return onSnapshot(q, snap => {
726    state.tips = snap.docs.map(d => ({ id: d.id, ...d.data() }));
727    renderTips();
728    onDataReady('tips');
729  }, () => { state.tips = []; renderTips(); showToast('Erreur de chargement des conseils.'); });
730}
731
732async function addTip(data) {
733  const isAdm = userState.isAdmin;
734  const docRef = await addDoc(collection(db, 'tips'), {
735    ...data,
736    recommendedBy: {
737      uid: userState.user?.uid || null,
738      name: data.recommenderName || getDisplayName(),
739      isAdmin: isAdm
740    },
741    status: isAdm ? 'approved' : 'pending',
742    createdAt: serverTimestamp()
743  });
744  showToast(isAdm ? 'Conseil ajouté !' : 'Conseil envoyé, en attente de validation !');
745  if (isAdm) logActivity('tip', `Nouveau conseil : « ${data.title || 'un livre'} »`, `#tip/${docRef.id}`);
746}
747
748async function approveTip(id) {
749  await setDoc(doc(db, 'tips', id), { status: 'approved' }, { merge: true });
750  showToast('Conseil approuvé !');
751  const tip = state.tips.find(t => t.id === id);
752  logActivity('tip', `Nouveau conseil : « ${tip?.title || 'un livre'} »`, `#tip/${id}`);
753}
754
755async function rejectTip(id) {
756  if (!confirm('Refuser ce conseil ?')) return;
757  await deleteDoc(doc(db, 'tips', id));
758  showToast('Conseil refusé.');
759}
760
761async function deleteTip(id) {
762  if (!confirm('Supprimer ce conseil ?')) return;
763  await deleteDoc(doc(db, 'tips', id));
764  showToast('Conseil supprimé.');
765}
766
767// ── Firestore : commentaires ─────────────────────────────
768let unsubComments = null;
769
770function listenComments(sessionId, sessionType, bookIndex) {
771  if (unsubComments) { unsubComments(); unsubComments = null; }
772  const q = query(
773    collection(db, 'comments'),
774    where('sessionId', '==', sessionId),
775    orderBy('createdAt', 'asc')
776  );
777  unsubComments = onSnapshot(q, snap => {
778    // Filtrer côté client par sessionType et bookIndex
779    state.currentComments = snap.docs
780      .map(d => ({ id: d.id, ...d.data() }))
781      .filter(c => c.sessionType === sessionType && c.bookIndex === bookIndex);
782    renderModalComments();
783  }, err => {
784    console.error('Erreur chargement commentaires :', err);
785    state.currentComments = [];
786    renderModalComments();
787  });
788}
789
790async function addComment(sessionId, sessionType, bookIndex, text) {
791  await addDoc(collection(db, 'comments'), {
792    sessionId, sessionType, bookIndex, text,
793    author: { uid: userState.user.uid, name: getDisplayName(), photo: getUserPhoto() },
794    createdAt: serverTimestamp()
795  });
796  const book = state.currentBook;
797  const deepLink = `#${sessionType === 'pastSessions' ? 'pastbook' : 'book'}/${sessionId}/${bookIndex}`;
798  logActivity('comment', `Commentaire sur « ${book?.title || 'un livre'} »`, deepLink);
799}
800
801async function deleteComment(commentId) {
802  await deleteDoc(doc(db, 'comments', commentId));
803}
804
805// ── Firestore : critiques ────────────────────────────────
806let unsubCritiques = null;
807
808function listenCritiques(sessionId, sessionType, bookIndex) {
809  if (unsubCritiques) { unsubCritiques(); unsubCritiques = null; }
810  if (!sessionId) { state.currentCritiques = []; return; }
811  // Query simple par sessionId, filtre côté client pour éviter un index composite complexe
812  const q = query(
813    collection(db, 'critiques'),
814    where('sessionId', '==', sessionId),
815    orderBy('createdAt', 'asc')
816  );
817  unsubCritiques = onSnapshot(q, snap => {
818    state.currentCritiques = snap.docs
819      .map(d => ({ id: d.id, ...d.data() }))
820      .filter(c => c.sessionType === sessionType && c.bookIndex === bookIndex && c.status === 'approved');
821    renderModalCritiques();
822  }, err => {
823    console.error('Erreur critiques (index manquant ?) :', err);
824    state.currentCritiques = [];
825    renderModalCritiques();
826  });
827}
828
829async function submitCritique(sessionId, sessionType, bookIndex, authorName, text) {
830  const directPublish = userState.isAdmin || userState.isMember;
831  const data = {
832    sessionId, sessionType, bookIndex, authorName, text,
833    status: directPublish ? 'approved' : 'pending',
834    createdAt: serverTimestamp()
835  };
836  if (userState.user) data.authorUid = userState.user.uid;
837  await addDoc(collection(db, 'critiques'), data);
838  if (directPublish) {
839    const book = state.currentBook;
840    const deepLink = `#${sessionType === 'pastSessions' ? 'pastbook' : 'book'}/${sessionId}/${bookIndex}`;
841    logActivity('critique', `Critique de « ${book?.title || 'un livre'} » par ${authorName}`, deepLink);
842  }
843}
844
845async function updateCritique(id, data) {
846  await setDoc(doc(db, 'critiques', id), data, { merge: true });
847}
848
849async function deleteCritique(id) {
850  if (!confirm('Supprimer cette critique ?')) return;
851  await deleteDoc(doc(db, 'critiques', id));
852  showToast('Critique supprimée.');
853}
854
855function listenPendingCritiques() {
856  const q = query(collection(db, 'critiques'), where('status', '==', 'pending'), orderBy('createdAt', 'asc'));
857  return onSnapshot(q, snap => {
858    state.pendingCritiques = snap.docs.map(d => ({ id: d.id, ...d.data() }));
859    const badge = document.getElementById('critiques-pending-count');
860    if (badge) badge.textContent = state.pendingCritiques.length > 0 ? `(${state.pendingCritiques.length})` : '';
861    if (!document.getElementById('admin-overlay').classList.contains('hidden')) {
862      renderAdminCritiques();
863    }
864  }, err => {
865    console.error('Erreur critiques pending (index manquant ?) :', err);
866    state.pendingCritiques = [];
867  });
868}
869
870async function rejectCritique(id) {
871  if (!confirm('Refuser cette critique ?')) return;
872  await deleteDoc(doc(db, 'critiques', id));
873  showToast('Critique refusée.');
874}
875
876// ── Firestore : réactions ────────────────────────────────
877const REACTION_TYPES = [
878  { key: 'loved',    icon: '❤️',  label: "J'ai adoré" },
879  { key: 'wantRead', icon: '📖', label: 'Envie de le lire' },
880  { key: 'notForMe', icon: '🙅', label: 'Pas pour moi' }
881];
882
883// Chaque réaction est stockée comme un objet { uid: displayName }
884async function toggleReaction(sessionId, bookIndex, reactionKey) {
885  if (!userState.user) return;
886  const uid = userState.user.uid;
887  const name = getDisplayName();
888  const docRef = doc(db, 'reactions', `${sessionId}_${bookIndex}`);
889  const snap = await getDoc(docRef);
890  const data = snap.exists() ? snap.data() : {};
891  // Construire le document complet avec les 3 types de réactions
892  const fullDoc = {};
893  for (const rt of REACTION_TYPES) {
894    fullDoc[rt.key] = { ...(data[rt.key] || {}) };
895    // Retirer l'utilisateur de ce type
896    delete fullDoc[rt.key][uid];
897  }
898  // Si c'est un toggle-on (l'utilisateur n'était pas sur cette réaction), l'ajouter
899  const wasActive = !!(data[reactionKey] || {})[uid];
900  if (!wasActive) {
901    fullDoc[reactionKey][uid] = name;
902  }
903  // Écriture complète (pas de merge) pour éviter les doublons
904  await setDoc(docRef, fullDoc);
905}
906
907let unsubReactions = null;
908let currentReactionsData = {};
909
910function listenReactions(sessionId, bookIndex) {
911  if (unsubReactions) { unsubReactions(); unsubReactions = null; }
912  currentReactionsData = {};
913  if (!sessionId) { renderModalReactions(); return; }
914  const docRef = doc(db, 'reactions', `${sessionId}_${bookIndex}`);
915  unsubReactions = onSnapshot(docRef, snap => {
916    currentReactionsData = snap.exists() ? snap.data() : {};
917    renderModalReactions();
918  }, () => {
919    currentReactionsData = {};
920    renderModalReactions();
921    showToast('Erreur de chargement des réactions.');
922  });
923}
924
925// ── Modale livre ─────────────────────────────────────────
926function openBookModal(book, sessionId, sessionType, bookIndex) {
927  state.currentBook = { ...book, sessionId, sessionType, bookIndex };
928  if (sessionId) {
929    setHash(`${sessionType === 'pastSessions' ? 'pastbook' : 'book'}/${sessionId}/${bookIndex}`);
930  }
931  const modal = document.getElementById('book-modal');
932  const cover = document.getElementById('book-modal-cover');
933  if (book.coverUrl) { cover.src = book.coverUrl; cover.style.display = ''; }
934  else { cover.style.display = 'none'; }
935  document.getElementById('book-modal-title').textContent = book.title;
936  document.getElementById('book-modal-author').textContent = book.author;
937  document.getElementById('book-modal-presenter').textContent = book.presenter ? `Présenté par ${book.presenter}` : '';
938  const hasDesc = !!book.description;
939  document.getElementById('book-modal-desc-label').style.display = hasDesc ? '' : 'none';
940  document.getElementById('book-modal-desc').textContent = hasDesc ? book.description : 'Aucun résumé éditeur disponible.';
941  modal.classList.remove('hidden');
942  const isPast = sessionType === 'pastSessions';
943  listenComments(sessionId, sessionType, bookIndex);
944  renderModalCommentForm();
945  if (isPast && sessionId) {
946    listenCritiques(sessionId, sessionType, bookIndex);
947    listenReactions(sessionId, bookIndex);
948    renderModalCritiqueForm();
949  } else {
950    document.getElementById('book-modal-reactions').innerHTML = '';
951    document.getElementById('book-modal-critiques').innerHTML = '';
952    document.getElementById('book-modal-critique-form').innerHTML = '';
953  }
954}
955
956function closeBookModal() {
957  document.getElementById('book-modal').classList.add('hidden');
958  if (unsubComments) { unsubComments(); unsubComments = null; }
959  if (unsubCritiques) { unsubCritiques(); unsubCritiques = null; }
960  if (unsubReactions) { unsubReactions(); unsubReactions = null; }
961  clearHash();
962  state.currentBook = null;
963  state.currentComments = [];
964  state.currentCritiques = [];
965  currentReactionsData = {};
966}
967
968function renderModalComments() {
969  const el = document.getElementById('book-modal-comments');
970  if (state.currentComments.length === 0) {
971    el.innerHTML = '<p class="no-comments">Aucun commentaire pour le moment.</p>';
972    return;
973  }
974  el.innerHTML = state.currentComments.map(c => {
975    const date = c.createdAt?.toDate ? c.createdAt.toDate().toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' }) : '';
976    const canDelete = userState.user && (userState.user.uid === c.author?.uid || userState.isAdmin);
977    return `
978      <div class="comment-item">
979        ${c.author?.photo
980          ? `<img class="comment-avatar" src="${c.author.photo}" alt="">`
981          : `<div class="comment-avatar-ph">👤</div>`}
982        <div class="comment-body">
983          <div class="comment-meta">
984            <strong>${escapeHtml(c.author?.name || 'Anonyme')}</strong> · ${date}
985            ${canDelete ? `<button class="comment-delete" data-cid="${c.id}" title="Supprimer">✕</button>` : ''}
986          </div>
987          <div class="comment-text">${escapeHtml(c.text)}</div>
988        </div>
989      </div>`;
990  }).join('');
991
992  el.querySelectorAll('.comment-delete').forEach(btn => {
993    btn.addEventListener('click', () => {
994      if (confirm('Supprimer ce commentaire ?')) deleteComment(btn.dataset.cid);
995    });
996  });
997}
998
999function renderModalCommentForm() {
1000  const el = document.getElementById('book-modal-comment-form');
1001  if (!userState.user) {
1002    el.innerHTML = '<p class="comment-login">Connecte-toi pour laisser un commentaire.</p>';
1003    return;
1004  }
1005  el.innerHTML = `
1006    <div class="comment-form">
1007      <textarea id="comment-input" placeholder="Ton commentaire…"></textarea>
1008      <button id="btn-send-comment">Envoyer</button>
1009    </div>`;
1010  document.getElementById('btn-send-comment').addEventListener('click', async () => {
1011    const text = document.getElementById('comment-input').value.trim();
1012    if (!text) return;
1013    const b = state.currentBook;
1014    await addComment(b.sessionId, b.sessionType, b.bookIndex, text);
1015    document.getElementById('comment-input').value = '';
1016  });
1017}
1018
1019// ── Render : réactions (modale) ──────────────────────────
1020function renderModalReactions() {
1021  const el = document.getElementById('book-modal-reactions');
1022  const b = state.currentBook;
1023  if (!b || b.sessionType !== 'pastSessions' || !b.sessionId) { el.innerHTML = ''; return; }
1024  const uid = userState.user?.uid;
1025  if (!uid) {
1026    el.innerHTML = `<p class="reactions-login">Connecte-toi pour réagir à ce livre.</p>`;
1027    return;
1028  }
1029  el.innerHTML = `<div class="reactions-bar">
1030    ${REACTION_TYPES.map(rt => {
1031      const voters = currentReactionsData[rt.key] || {};
1032      const names = Object.values(voters);
1033      const count = names.length;
1034      const active = !!voters[uid];
1035      const tooltip = count > 0 ? names.join(', ') : '';
1036      return `<button class="reaction-btn ${active ? 'active' : ''}" data-rk="${rt.key}" ${tooltip ? `title="${escapeHtml(tooltip)}"` : ''}>
1037        <span class="reaction-icon">${rt.icon}</span>
1038        <span>${rt.label}</span>
1039        ${count >
1039 0 ? `<span class="reaction-count">${count}</span>` : ''}
1040      </button>`;
1041    }).join('')}
1042  </div>`;
1043  el.querySelectorAll('.reaction-btn').forEach(btn => {
1044    btn.addEventListener('click', async () => {
1045      btn.disabled = true;
1046      await toggleReaction(b.sessionId, b.bookIndex, btn.dataset.rk);
1047      btn.disabled = false;
1048    });
1049  });
1050}
1051
1052// ── Render : critiques (modale) ──────────────────────────
1053function renderModalCritiques() {
1054  const el = document.getElementById('book-modal-critiques');
1055  if (state.currentCritiques.length === 0) { el.innerHTML = ''; return; }
1056  const uid = userState.user?.uid;
1057  el.innerHTML = `
1058    <div class="critiques-section">
1059      <p class="critiques-title">Critiques & présentations</p>
1060      ${state.currentCritiques.map(c => {
1061        const isAuthor = uid && c.authorUid && c.authorUid === uid;
1062        const canManage = userState.isAdmin || isAuthor;
1063        return `
1064        <div class="critique-item" data-cid="${c.id}">
1065          <div class="critique-header">
1066            <div class="critique-author">${escapeHtml(c.authorName || 'Anonyme')}</div>
1067            ${canManage ? `<div class="critique-actions">
1068              <button class="critique-action-btn critique-edit-btn" data-cid="${c.id}" title="Modifier">✎</button>
1069              <button class="critique-action-btn critique-delete-btn" data-cid="${c.id}" title="Supprimer">✕</button>
1070            </div>` : ''}
1071          </div>
1072          <div class="critique-text" id="critique-text-${c.id}">${escapeHtml(c.text)}</div>
1073          <div class="critique-edit-form hidden" id="critique-edit-${c.id}">
1074            <textarea class="critique-edit-textarea">${escapeHtml(c.text)}</textarea>
1075            <div class="critique-edit-btns">
1076              <button class="critique-save-btn" data-cid="${c.id}">Enregistrer</button>
1077              <button class="critique-cancel-btn" data-cid="${c.id}">Annuler</button>
1078            </div>
1079            ${isAuthor && !userState.isAdmin && !userState.isMember ? `<p class="critique-hint">La modification sera re-soumise à validation.</p>` : ''}
1080          </div>
1081        </div>`;
1082      }).join('')}
1083    </div>`;
1084
1085  // Supprimer
1086  el.querySelectorAll('.critique-delete-btn').forEach(btn => {
1087    btn.addEventListener('click', () => deleteCritique(btn.dataset.cid));
1088  });
1089  // Ouvrir l'édition
1090  el.querySelectorAll('.critique-edit-btn').forEach(btn => {
1091    btn.addEventListener('click', () => {
1092      const cid = btn.dataset.cid;
1093      document.getElementById(`critique-text-${cid}`).classList.add('hidden');
1094      document.getElementById(`critique-edit-${cid}`).classList.remove('hidden');
1095      btn.closest('.critique-actions').classList.add('hidden');
1096    });
1097  });
1098  // Annuler l'édition
1099  el.querySelectorAll('.critique-cancel-btn').forEach(btn => {
1100    btn.addEventListener('click', () => {
1101      const cid = btn.dataset.cid;
1102      document.getElementById(`critique-text-${cid}`).classList.remove('hidden');
1103      document.getElementById(`critique-edit-${cid}`).classList.add('hidden');
1104      btn.closest('.critique-item')?.querySelector('.critique-actions')?.classList.remove('hidden');
1105    });
1106  });
1107  // Sauvegarder
1108  el.querySelectorAll('.critique-save-btn').forEach(btn => {
1109    btn.addEventListener('click', async () => {
1110      const cid = btn.dataset.cid;
1111      const textarea = document.getElementById(`critique-edit-${cid}`).querySelector('textarea');
1112      const newText = textarea.value.trim();
1113      if (!newText) return;
1114      const c = state.currentCritiques.find(x => x.id === cid);
1115      const isAuthor = uid && c?.authorUid && c.authorUid === uid;
1116      if (userState.isAdmin || userState.isMember) {
1117        // Admin ou membre officiel : modification directe, reste approved
1118        await updateCritique(cid, { text: newText });
1119        showToast('Critique modifiée.');
1120      } else if (isAuthor) {
1121        // Auteur non-membre : re-soumis à validation
1122        await updateCritique(cid, { text: newText, status: 'pending' });
1123        showToast('Modification soumise à validation.');
1124      }
1125    });
1126  });
1127}
1128
1129function renderModalCritiqueForm() {
1130  const el = document.getElementById('book-modal-critique-form');
1131  const b = state.currentBook;
1132  if (!b || b.sessionType !== 'pastSessions' || !b.sessionId) { el.innerHTML = ''; return; }
1133  el.innerHTML = `
1134    <div class="critique-form">
1135      <p class="critique-form-title">Soumettre une critique ou présentation</p>
1136      <input type="text" id="critique-author" placeholder="Votre nom" value="${userState.user ? escapeHtml(getDisplayName()) : ''}">
1137      <textarea id="critique-text" placeholder="Votre critique ou présentation du livre…"></textarea>
1138      ${userState.isAdmin || userState.isMember
1139        ? `<p class="critique-hint">En tant que membre, votre texte sera publié directement.</p>`
1140        : `<p class="critique-hint">Votre texte sera soumis à validation avant publication.</p>`}
1141      <button id="btn-send-critique">${userState.isAdmin || userState.isMember ? 'Publier' : 'Soumettre'}</button>
1142    </div>`;
1143  document.getElementById('btn-send-critique').addEventListener('click', async () => {
1144    const author = document.getElementById('critique-author').value.trim();
1145    const text = document.getElementById('critique-text').value.trim();
1146    if (!author || !text) { showToast('Veuillez remplir tous les champs.'); return; }
1147    try {
1148      await submitCritique(b.sessionId, b.sessionType, b.bookIndex, author, text);
1149      document.getElementById('critique-text').value = '';
1150      showToast(userState.isAdmin || userState.isMember ? 'Critique publiée !' : 'Critique soumise 
1150! Elle sera visible après validation.');
1151    } catch (err) {
1152      console.error('Erreur soumission critique :', err);
1153      showToast('Erreur lors de la soumission. Vérifiez la console.');
1154    }
1155  });
1156}
1157
1158// ── Render : admin critiques ─────────────────────────────
1159function renderAdminCritiques() {
1160  const el = document.getElementById('admin-critiques-list');
1161  const pending = state.pendingCritiques;
1162  if (pending.length === 0) {
1163    el.innerHTML = `<p style="font-size:0.85rem;color:var(--text-light);font-style:italic;text-align:center;padding:1.5rem;">Aucune critique en attente.</p>`;
1164    return;
1165  }
1166  el.innerHTML = pending.map(c => {
1167    const sessionLabel = findSessionLabel(c.sessionId, c.bookIndex);
1168    return `
1169      <div class="admin-critique-card" data-cid="${c.id}">
1170        <div class="admin-critique-meta"><strong>${escapeHtml(c.authorName || 'Anonyme')}</strong> — ${escapeHtml(sessionLabel)}</div>
1171        <textarea class="admin-critique-edit" data-cid="${c.id}">${escapeHtml(c.text)}</textarea>
1172        <div class="admin-critique-actions">
1173          <button class="btn-approve-critique" data-cid="${c.id}">✓ Approuver</button>
1174          <button class="btn-reject-critique" data-cid="${c.id}">✕ Refuser</button>
1175        </div>
1176      </div>`;
1177  }).join('');
1178  el.querySelectorAll('.btn-approve-critique').forEach(btn => {
1179    btn.addEventListener('click', async () => {
1180      const cid = btn.dataset.cid;
1181      const textarea = el.querySelector(`.admin-critique-edit[data-cid="${cid}"]`);
1182      const newText = textarea.value.trim();
1183      if (!newText) return;
1184      const critiqueData = state.pendingCritiques.find(c => c.id === cid);
1185      await updateCritique(cid, { text: newText, status: 'approved' });
1186      showToast('Critique approuvée !');
1187      if (critiqueData) {
1188        const label = findSessionLabel(critiqueData.sessionId, critiqueData.bookIndex);
1189        logActivity('critique', `Critique approuvée : ${label}`, `#${critiqueData.sessionType === 'pastSessions' ? 'pastbook' : 'book'}/${critiqueData.sessionId}/${critiqueData.bookIndex}`);
1190      }
1191    });
1192  });
1193  el.querySelectorAll('.btn-reject-critique').forEach(btn => {
1194    btn.addEventListener('click', () => rejectCritique(btn.dataset.cid));
1195  });
1196}
1197
1198function findSessionLabel(sessionId, bookIndex) {
1199  const p = state.pastSessions.find(s => s.id === sessionId);
1200  if (!p) return 'Session inconnue';
1201  const book = (p.books || [])[bookIndex];
1202  return `${p.theme || '?'} → ${book ? book.title : `Livre #${bookIndex + 1}`}`;
1203}
1204
1205// ── Render : hero ────────────────────────────────────────
1206function renderHero() {
1207  const s = state.sessions[0];
1208  if (!s) {
1209    document.getElementById('hero-next-label').textContent = '';
1210    document.getElementById('hero-theme').textContent = '';
1211    document.getElementById('hero-desc').textContent  = 'Le prochain thème sera annoncé bientôt.';
1212    document.getElementById('hero-meta').innerHTML    = '';
1213    return;
1214  }
1215  document.getElementById('hero-next-label').textContent = heroLabel(s);
1216  document.getElementById('hero-theme').textContent = heroThemeText(s);
1217  document.getElementById('hero-desc').textContent  = getSessionSubtitle(s);
1218  document.getElementById('hero-meta').innerHTML    =
1219    `<span class="badge badge-green">📅 ${escapeHtml(s.date)}</span>
1220     <span class="badge badge-terra">🕒 ${escapeHtml(s.heure)}</span>
1221     <span class="badge badge-dark">📍 ${escapeHtml(s.lieu || CONFIG.club.lieu)}</span>`;
1222}
1223
1224// ── Render : sessions à venir ────────────────────────────
1225function renderAllSessions() {
1226  const first = state.sessions[0];
1227  // Première session : affichage principal
1228  if (!first) {
1229    document.getElementById('session-theme-title').textContent = 'Prochain thème';
1230    document.getElementById('session-desc-text').textContent   = '';
1231    document.getElementById('session-date').textContent        = '';
1232    document.getElementById('session-time').textContent        = '';
1233    document.getElementById('session-location').textContent    = '';
1234    document.getElementById('books-grid').innerHTML = `<p style="color:rgba(255,255,255,0.35);font-style:italic;
1234font-family:'Playfair Display',serif;">Aucune session prévue pour le moment.</p>`;
1235    document.getElementById('upcoming-sessions').innerHTML = '';
1236    return;
1237  }
1238  const firstType = getSessionType(first);
1239  // En-tête : adapté au type
1240  const titlePrefix = firstType === 'theme' ? 'Thème : ' : (firstType === 'debat' ? '' : '');
1241  document.getElementById('session-theme-title').innerHTML = `${sessionTypeBadge(firstType)} ${titlePrefix}${escapeHtml(getSessionTitle(first))}`;
1242  document.getElementById('session-desc-text').textContent = getSessionSubtitle(first);
1243  document.getElementById('session-date').textContent        = `📅 ${first.date}`;
1244  document.getElementById('session-time').textContent        = `🕒 ${first.heure}`;
1245  document.getElementById('session-location').textContent    = `📍 ${first.lieu || CONFIG.club.lieu}`;
1246
1247  const books = first.books || [];
1248  const booksGrid = document.getElementById('books-grid');
1249  const revealContainer = document.getElementById('books-reveal-all');
1250  if (revealContainer) revealContainer.remove();
1251
1252  // Contenu : type-specific ou thème par défaut
1253  const customContent = renderFirstSessionBooks(first);
1254  if (customContent !== null) {
1255    booksGrid.innerHTML = customContent;
1256    // Events clic sur les book-card et lc-book-main
1257    booksGrid.querySelectorAll('.book-card.clickable, .lc-book-main.clickable').forEach(card => {
1258      card.addEventListener('click', () => {
1259        const sid = card.dataset.sid;
1260        const bi = +card.dataset.bi;
1261        const s = state.sessions.find(s => s.id === sid);
1262        if (s) openBookModal(s.books[bi], sid, card.dataset.stype || 'sessions', bi);
1263      });
1264    });
1265  } else {
1266    // Type thème : affichage original avec spoilers
1267    booksGrid.innerHTML = books.length === 0
1268      ? `<p style="color:rgba(255,255,255,0.35);font-style:italic;font-family:'Playfair Display',serif;">Aucun livre annoncé pour le moment.</p>`
1269      : books.map((b, i) => `
1270          <div class="book-card clickable spoiler" data-sid="${first.id}" data-stype="sessions" data-bi="${i}">
1271            <div class="book-card-spoiler-overlay">
1272              ${b.note ? `<div class="spoiler-teaser">"${escapeHtml(b.note)}"</div>` : `<div class="spoiler-teaser">Un livre à découvrir…</div>`}
1273              <div class="spoiler-hint">Cliquer pour révéler</div>
1274            </div>
1275            <div class="book-card-content">
1276              <div class="book-cover">
1277                ${b.coverUrl ? `<img src="${b.coverUrl}" alt="">` : '📖'}
1278              </div>
1279              <div class="book-info">
1280                <div class="book-title">${escapeHtml(b.title)}</div>
1281                <div class="book-author">${escapeHtml(b.author)}</div>
1282                ${b.presenter ? `<div class="book-presenter">Présenté par ${escapeHtml(b.presenter)}</div>` : ''}
1283              </div>
1284            </div>
1285          </div>`).join('');
1286    if (books.length > 0) {
1287      const revealBtn = document.createElement('div');
1288      revealBtn.id = 'books-reveal-all';
1289      revealBtn.style.textAlign = 'center';
1290      revealBtn.style.marginTop = '0.75rem';
1291      revealBtn.innerHTML = `<button class="btn-reveal-all">Tout révéler</button>`;
1292      booksGrid.after(revealBtn);
1293      revealBtn.querySelector('.btn-reveal-all').addEventListener('click', () => {
1294        booksGrid.querySelectorAll('.book-card.spoiler').forEach(c => c.classList.remove('spoiler'));
1295        revealBtn.style.display = 'none';
1296      });
1297    }
1298    booksGrid.querySelectorAll('.book-card.clickable').forEach(card => {
1299      card.addEventListener('click', () => {
1300        if (card.classList.contains('spoiler')) { card.classList.remove('spoiler'); return; }
1301        const s = state.sessions.find(s => s.id === card.dataset.sid);
1302        if (s) openBookModal(s.books[+card.dataset.bi], card.dataset.sid, card.dataset.stype, +card.dataset.bi);
1303      });
1304    });
1305  }
1306
1307  // CTA : seulement pour les sessions thème
1308  const cta = document.getElementById('session-cta');
1309  if (firstType === 'theme') {
1310    cta.innerHTML = `<a href="#" id="btn-open-propose-modal">📚 Proposer un livre pour cette session</a>`;
1311    document.getElementById('btn-open-propose-modal').addEventListener('click', (e) => {
1312      e.preventDefault();
1313      openProposeModal();
1314    });
1315  } else {
1316    cta.innerHTML = '';
1317  }
1318
1319  // Sessions suivantes
1320  const rest = state.sessions.slice(1);
1321  const upEl = document.getElementById('upcoming-sessions');
1322  upEl.innerHTML = rest.length === 0 ? '' : `
1323    <div class="upcoming-separator">
1324      <p class="section-label">Sessions suivantes</p>
1325      <div class="upcoming-grid">
1326        ${rest.map(s => {
1327          const customCard = renderUpcomingCardContent(s);
1328          if (customCard !== null) {
1329            return `<div class="upcoming-card">
1330              <div class="upcoming-date">📅 ${escapeHtml(s.date)} · 🕒 ${escapeHtml(s.heure)} · 📍 ${escapeHtml(s.lieu || CONFIG.club.lieu)}</div>
1331              ${customCard}
1332            </div>`;
1333          }
1334          // Type thème : affichage original
1335          return `<div class="upcoming-card">
1336            <div class="upcoming-date">📅 ${escapeHtml(s.date)} · 🕒 ${escapeHtml(s.heure)} · 📍 ${escapeHtml(s.lieu || CONFIG.club.lieu)}</div>
1337            <div class="upcoming-theme">${escapeHtml(s.theme)}</div>
1338            ${s.themeDescription ? `<div class="upcoming-desc">${escapeHtml(s.themeDescription)}</div>` : ''}
1339            ${(s.books || []).length > 0 ? `
1340              <ul class="upcoming-books">
1341                ${s.books.map((b, i) => `<li class="clickable spoiler-li" data-sid="${s.id}" data-bi="${i}">
1342                  <span class="upcoming-spoiler-tag">Cliquer pour révéler</span>
1343                  <span class="upcoming-book-content">
1344                    <span class="upcoming-book-cover">${b.coverUrl ? `<img src="${b.coverUrl}" alt="">` : '📖'}</span>
1345                    <span class="upcoming-book-info"><strong>${escapeHtml(b.title)}</strong><span>${escapeHtml(b.author)}${b.presenter ? ` · ${escapeHtml(b.presenter)}` : ''}</span></span>
1346                  </span>
1347                </li>`).join('')}
1348              </ul>
1349              <button class="btn-reveal-all" data-sid="${s.id}">Tout révéler</button>` : ''}
1350          </div>`;
1351        }).join('')}
1352      </div>
1353    </div>`;
1354  upEl.querySelectorAll('.upcoming-books li.clickable').forEach(li => {
1355    li.addEventListener('click', () => {
1356      if (li.classList.contains('spoiler-li')) {
1357        li.classList.remove('spoiler-li');
1358        return;
1359      }
1360      const s = state.sessions.find(s => s.id === li.dataset.sid);
1361      if (s) openBookModal(s.books[+li.dataset.bi], li.dataset.sid, 'sessions', +li.dataset.bi);
1362    });
1363  });
1364  // Bouton "Tout révéler" dans les sessions suivantes
1365  upEl.querySelectorAll('.btn-reveal-all').forEach(btn => {
1366    btn.addEventListener('click', () => {
1367      const card = btn.closest('.upcoming-card');
1368      card.querySelectorAll('.spoiler-li').forEach(li => li.classList.remove('spoiler-li'));
1369      btn.style.display = 'none';
1370    });
1371  });
1372}
1373
1374// ── Render : sessions passées ────────────────────────────
1375
1376
1377function renderPastSessions() {
1378  const featured = document.getElementById('last-session-featured');
1379  const grid = document.getElementById('past-grid');
1380  const separatorContainer = document.getElementById('history-separator-container');
1381  const olderWrapper = document.getElementById('older-sessions-wrapper');
1382
1383  if (state.pastSessions.length === 0) {
1384    featured.innerHTML = '';
1385    separatorContainer.innerHTML = '';
1386    olderWrapper.style.display = 'none';
1387    grid.innerHTML = `<p class="empty-sessions">Les sessions passées apparaîtront ici après chaque rencontre.</p>`;
1388    return;
1389  }
1390
1391  // ── Dernière session : hero avec tuiles livres ──
1392  const last = state.pastSessions[0];
1393  const lastType = getSessionType(last);
1394  const lastBooks = last.books || [];
1395  const lastLabel = lastType === 'theme' ? 'Les découvertes de la dernière session'
1396    : lastType === 'lecture-commune' ? 'Dernière lecture commune'
1397    : lastType === 'rencontre-auteur' ? 'Dernière rencontre auteur'
1398    : lastType === 'debat' ? 'Dernier débat' : 'Dernière session';
1399
1400  const customFeatured = renderFeaturedPastContent(last);
1401  featured.innerHTML = `
1402    <div class="last-session-hero" data-pid="${last.id}">
1403      <div class="last-session-header">
1404        <div class="last-session-label">${lastLabel}</div>
1405        ${sessionTypeBadge(lastType)}
1406        <div class="last-session-theme">${escapeHtml(getSessionTitle(last))}</div>
1407        <div class="last-session-meta">${escapeHtml(last.date)}</div>
1408        ${lastType === 'theme' && last.themeDescription ? `<div class="last-session-desc">${escapeHtml(last.themeDescription)}</div>` : ''}
1409      </div>
1410      ${customFeatured !== null ? customFeatured : (lastBooks.length > 0 ? `
1411        <div class="last-session-books">
1412          ${lastBooks.map((b, i) => `
1413            <div class="book-card clickable" data-pid="${last.id}" data-bi="${i}">
1414              <div class="book-card-content">
1415                <div class="book-cover">
1416                  ${b.coverUrl ? `<img src="${b.coverUrl}" alt="">` : '📖'}
1417                </div>
1418                <div class="book-info">
1419                  <div class="book-title">${escapeHtml(b.title)}</div>
1420                  <div class="book-author">${escapeHtml(b.author)}</div>
1421                  ${b.presenter ? `<div class="book-presenter">Présenté par ${escapeHtml(b.presenter)}</div>` : ''}
1422                </div>
1423              </div>
1424            </div>`).join('')}
1425        </div>` : `<p style="color:var(--text-light);font-style:italic;">Aucun livre pour cette session.</p>`)}
1426      ${last.recap ? `<div class="last-session-recap">${last.recap}</div>` : ''}
1427    </div>`;
1428
1429  // Events sur la section featured (book-card + lc-book-main)
1430  featured.querySelectorAll('.book-card.clickable, .lc-book-main.clickable').forEach(card => {
1431    card.addEventListener('click', () => {
1432      const p = state.pastSessions.find(s => s.id === card.dataset.pid);
1433      if (p) openBookModal(p.books[+card.dataset.bi], card.dataset.pid, 'pastSessions', +card.dataset.bi);
1434    });
1435  });
1436
1437  // ── Anciennes sessions : grille compacte ──
1438  const older = state.pastSessions.slice(1);
1439  if (older.length === 0) {
1440    separatorContainer.innerHTML = '';
1441    olderWrapper.style.display = 'none';
1442  } else {
1443    separatorContainer.innerHTML = `<div class="history-separator"><span>Sessions précédentes</span></div>`;
1444    olderWrapper.style.display = '';
1445    grid.innerHTML = older.map(p => {
1446      const customCard = renderPastCardContent(p);
1447      if (customCard !== null) {
1448        return `<div class="past-card" data-pid="${p.id}">${customCard}</div>`;
1449      }
1450      // Type thème : affichage original
1451      return `<div class="past-card" data-pid="${p.id}">
1452          <div class="past-date">${escapeHtml(p.date)}</div>
1453          <div class="past-theme">${escapeHtml(p.theme)}</div>
1454          ${p.themeDescription ? `<div class="past-theme-desc">${escapeHtml(p.themeDescription)}</div>` : ''}
1455          <ul class="past-books">
1456            ${(p.books || []).map((b, i) =>
1457              `<li class="clickable" data-pid="${p.id}" data-bi="${i}"><span><strong>${escapeHtml(b.title)}</strong> — ${escapeHtml(b.author)}${b.presenter ? ` <span class="book-presenter-inline">(${escapeHtml(b.presenter)})</span>` : ''}</span></li>`
1458            ).join('')}
1459          </ul>
1460          ${p.recap ? `<div class="past-recap">${p.recap}</div>` : ''}
1461        </div>`;
1462    }).join('');
1463  }
1464
1465  // ── Events : clic livre → modale ──
1466  const container = document.getElementById('passees');
1467  container.querySelectorAll('.past-books li.clickable, .lc-book-main.clickable').forEach(el => {
1468    el.addEventListener('click', () => {
1469      const pid = el.dataset.pid;
1470      const bi = +el.dataset.bi;
1471      const p = state.pastSessions.find(s => s.id === pid);
1472      if (p) openBookModal(p.books[bi], pid, 'pastSessions', bi);
1473    });
1474  });
1475  // Events clic sur book-card dans les past cards (debat, rencontre-auteur)
1476  container.querySelectorAll('.past-card .book-card.clickable, .debat-arena-past .book-card.clickable').forEach(card => {
1477    card.addEventListener('click', () => {
1478      const pid = card.dataset.pid;
1479      const bi = +card.dataset.bi;
1480      const p = state.pastSessions.find(s => s.id === pid);
1481      if (p) openBookModal(p.books[bi], pid, 'pastSessions', bi);
1482    });
1483  });
1484}
1485
1486// ── Render : section suggestion ──────────────────────────
1487function getAnonProposalCount() {
1488  try {
1489    const d = JSON.parse(localStorage.getItem('anonProposals') || '{}');
1490    const month = new Date().toISOString().slice(0, 7);
1491    return d[month] || 0;
1492  } catch { return 0; }
1493}
1494function incrementAnonProposalCount() {
1495  try {
1496    const d = JSON.parse(localStorage.getItem('anonProposals') || '{}');
1497    const month = new Date().toISOString().slice(0, 7);
1498    d[month] = (d[month] || 0) + 1;
1499    localStorage.setItem('anonProposals', JSON.stringify(d));
1500  } catch {}
1501}
1502
1503function openProposeModal() {
1504  const modal = document.getElementById('propose-modal');
1505  const body = document.getElementById('propose-modal-body');
1506  modal.classList.remove('hidden');
1507  renderSuggestForm(body);
1508}
1509
1510function renderSuggestSection() {
1511  renderSuggestForm(document.getElementById('suggest-content'));
1512}
1513
1514function renderSuggestForm(el) {
1515  const themeSessions = state.sessions.filter(s => getSessionType(s) === 'theme');
1516  const sessionsOptions = themeSessions.map((s, i) =>
1517    `<option value="${s.id}">${i === 0 ? '▸ ' : ''}${escapeHtml(s.theme)} (${escapeHtml(s.date)})</option>`
1518  ).join('');
1519
1520  state.selectedBook = null;
1521  el.innerHTML = `
1522      <div class="proposal-form">
1523        <div class="form-group">
1524          <label>Ton prénom / nom / pseudo *</label>
1525          <input type="text" class="proposer-name" placeholder="Ex : Marie Dupont" value="${userState.user ? escapeHtml(getDisplayName()) : ''}">
1526        </div>
1527        ${themeSessions.length > 0 ? `
1528        <div class="form-group">
1529          <label>Pour quelle session ?</label>
1530          <select class="proposal-session">${sessionsOptions}</select>
1531        </div>` : ''}
1532        <div class="form-group book-search-wrapper">
1533          <label>Rechercher un livre *</label>
1534          <input type="text" class="book-search-input" placeholder="Tape un titre ou un auteur…" autocomplete="off">
1535          <div class="book-search-dropdown search-dropdown"></div>
1536        </div>
1537        <div class="book-preview propose-preview">
1538          <img class="preview-cover" src="" alt="">
1539          <div class="book-preview-info">
1540            <div class="preview-title"></div>
1541            <div class="preview-author"></div>
1542            <button class="btn-change-book">✕ Changer de livre</button>
1543          </div>
1544        </div>
1545        <div class="form-group">
1546          <label>Pourquoi ce livre ? (optionnel) <span style="font-weight:400;font-size:0.78rem;color:var(--text-light);">— ce texte sera affiché en teaser anti-spoil sur la carte du livre</span></label>
1547          <textarea class="proposal-note" placeholder="Donne envie sans trop en dire… ce texte apparaîtra sur la carte du livre avant qu'on ne le révèle."></textarea>
1548        </div>
1549        <button class="btn-submit btn-submit-proposal">✉ Envoyer ma proposition</button>
1550      </div>
1551      <div class="form-success proposal-success">
1552        <div class="success-icon">📬</div>
1553        <h3>Merci !</h3>
1554        <p>Ta proposition a été transmise aux organisateurs.</p>
1555        <button class="btn-reset btn-reset-proposal">Faire une autre suggestion</button>
1556      </div>`;
1557
1558  const $ = (sel) => el.querySelector(sel);
1559  const input    = $('.book-search-input');
1560  const dropdown = $('.search-dropdown');
1561  const preview  = $('.propose-preview');
1562
1563  input.addEventListener('input', () => {
1564    clearTimeout(searchTimer);
1565    const q = input.value.trim();
1566    if (q.length < 2) { dropdown.classList.remove('open'); return; }
1567    dropdown.innerHTML = `<div class="search-loading">Recherche en cours…</div>`;
1568    dropdown.classList.add('open');
1569    searchTimer = setTimeout(async () => {
1570      const results = await searchBooks(q);
1571      state.searchResults = results;
1572      renderSearchResults(results, dropdown, q, (book) => {
1573        state.selectedBook = book;
1574        dropdown.classList.remove('open');
1575        input.closest('.form-group').style.display = 'none';
1576        $('.preview-cover').src  = book.coverUrl || '';
1577        $('.preview-cover').style.display = book.coverUrl ? '' : 'none';
1578        $('.preview-title').textContent  = book.title;
1579        $('.preview-author').textContent = book.author;
1580        preview.classList.add('show');
1581      });
1582    }, 450);
1583  });
1584
1585  document.addEventListener('click', e => {
1586    if (!e.target.closest('.book-search-wrapper')) dropdown.classList.remove('open');
1587  });
1588
1589  $('.btn-change-book').addEventListener('click', () => {
1590    state.selectedBook = null;
1591    input.value = '';
1592    input.closest('.form-group').style.display = '';
1593    preview.classList.remove('show');
1594    input.focus();
1595  });
1596
1597  $('.btn-submit-proposal').addEventListener('click', async () => {
1598    if (!state.selectedBook) { showToast('Sélectionne d\'abord un livre dans la liste.'); return; }
1599    const proposerName = $('.proposer-name').value.trim();
1600    if (!proposerName) { showToast('Indique ton prénom/nom.'); return; }
1601    const note = $('.proposal-note').value.trim();
1602    const sessionSelect = $('.proposal-session');
1603    const targetSessionId = sessionSelect ? sessionSelect.value : null;
1604    try {
1605      await submitProposal({ ...state.selectedBook, note, proposerName, targetSessionId });
1606      $('.proposal-form').style.display    = 'none';
1607      $('.proposal-success').classList.add('show');
1608    } catch(e) { showToast('Erreur lors de l\'envoi. Réessaie.'); console.error(e); }
1609  });
1610
1611  $('.btn-reset-proposal').addEventListener('click', () => renderSuggestForm(el));
1612}
1613
1614// ── Render : admin — propositions ────────────────────────
1615function renderAdminProposals() {
1616  const count = state.proposals.length;
1617  const makeSessionOptions = (targetId) => state.sessions.map((s, i) => {
1618    const typeInfo = SESSION_TYPES[getSessionType(s)] || SESSION_TYPES.theme;
1619    const label = getSessionType(s) !== 'theme' ? `${typeInfo.icon} ${escapeHtml(getSessionTitle(s))}` : escapeHtml(s.theme);
1620    return `<option value="${s.id}" ${s.id === targetId ? 'selected' : ''}>${i === 0 ? '▸ ' : ''}${label} (${escapeHtml(s.date)})</option>`;
1621  }).join('');
1622  document.getElementById('proposals-count').textContent = count > 0 ? `(${count})` : '';
1623  document.getElementById('proposals-list').innerHTML = count === 0
1624    ? `<p class="empty-proposals">Aucune proposition en attente.</p>`
1625    : state.proposals.map(p => `
1626        <div class="proposal-card">
1627          <div class="proposal-card-header">
1628            ${p.coverUrl
1629              ? `<img class="proposal-cover" src="${p.coverUrl}" alt="">`
1630              : `<div class="proposal-cover-ph">📖</div>`}
1631            <div>
1632              <div class="proposal-title">${escapeHtml(p.title)}</div>
1633              <div class="proposal-author">${escapeHtml(p.author || '—')}</div>
1634              <div class="proposal-proposer">Proposé par ${escapeHtml(p.proposedBy?.name || 'Anonyme')}${p.targetSessionId ? ` · pour <strong>${escapeHtml(state.sessions.find(s => s.id === p.targetSessionId)?.theme || 'session inconnue')}</strong>` : ''}</div>
1635            </div>
1636          </div>
1637          ${p.note ? `<div class="proposal-note">"${escapeHtml(p.note)}"</div>` : ''}
1638          ${state.sessions.length > 0
1639            ? `<select class="proposal-session-select" data-proposal-id="${p.id}">${makeSessionOptions(p.targetSessionId)}</select>`
1640            : `<p style="font-size:0.78rem;color:var(--text-light);font-style:italic;margin-bottom:0.5rem;">Créez d'abord une session pour pouvoir approuver.</p>`}
1641          <div class="proposal-actions">
1642            <button class="btn-approve" data-id="${p.id}" ${state.sessions.length === 0 ? 'disabled style="opacity:0.5;cursor:not-allowed;"' : ''}>✓ Ajouter</button>
1643            <button class="btn-reject"  data-id="${p.id}">✕ Refuser</button>
1644          </div>
1645        </div>`).join('');
1646
1647  document.querySelectorAll('.btn-approve').forEach(btn => {
1648    btn.addEventListener('click', async () => {
1649      const proposal = state.proposals.find(p => p.id === btn.dataset.id);
1650      const select = document.querySelector(`.proposal-session-select[data-proposal-id="${btn.dataset.id}"]`);
1651      if (proposal && select) await approveProposal(proposal, select.value);
1652    });
1653  });
1654  document.querySelectorAll('.btn-reject').forEach(btn => {
1655    btn.addEventListener('click', () => rejectProposal(btn.dataset.id));
1656  });
1657}
1658
1659// ── Helper : champs admin spécifiques au type ───────────
1660function adminTypeFields(s) {
1661  const type = getSessionType(s);
1662  switch (type) {
1663    case 'lecture-commune':
1664      return `
1665        <div class="sform-group">
1666          <label>Guide de discussion</label>
1667          <textarea class="edit-discussion-guide" rows="3" placeholder="Questions, axes de réflexion…">${escapeHtml(s.discussionGuide || '')}</textarea>
1668        </div>`;
1669    case 'rencontre-auteur':
1670      return `
1671        <div class="sform-group">
1672          <label>Nom de l'auteur</label>
1673          <input type="text" class="edit-author-name" value="${escapeHtml(s.authorName || '')}" placeholder="Ex : Victor Hugo">
1674        </div>
1675        <div class="sform-group">
1676          <label>Biographie</label>
1677          <textarea class="edit-author-bio" rows="3" placeholder="Courte biographie de l'auteur…">${escapeHtml(s.authorBio || '')}</textarea>
1678        </div>
1679        <div class="sform-group">
1680          <label>Photo (URL)</label>
1681          <input type="text" class="edit-author-photo" value="${escapeHtml(s.authorPhoto || '')}" placeholder="https://...">
1682        </div>`;
1683    case 'debat':
1684      return `
1685        <div class="sform-group">
1686          <label>Question du débat</label>
1687          <input type="text" class="edit-debate-question" value="${escapeHtml(s.debateQuestion || '')}" placeholder="Ex : Faut-il séparer l'œuvre de l'artiste ?">
1688        </div>
1689        <div class="sform-group">
1690          <label>Description du débat</label>
1691          <textarea class="edit-debate-desc" rows="2" placeholder="Contexte, enjeux…">${escapeHtml(s.debateDescription || '')}</textarea>
1692        </div>`;
1693    default: return '';
1694  }
1695}
1696
1697function adminBooksLabel(s) {
1698  const type = getSessionType(s);
1699  const books = s.books || [];
1700  if (type === 'lecture-commune') return `Livre (${books.length}/1)`;
1701  if (type === 'debat') return `Livres — Pour / Contre (${books.length}/2)`;
1702  if (type === 'rencontre-auteur') return `Bibliographie (${books.length})`;
1703  return `Livres (${books.length})`;
1704}
1705
1706function adminBookItemHTML(b, i, sid, type) {
1707  const label = type === 'debat' ? (i === 0 ? '🟢 Pour' : '🔴 Contre') : '';
1708  return `
1709    <div class="admin-book-item">
1710      ${b.coverUrl ? `<img src="${b.coverUrl}" style="width:28px;height:38px;object-fit:cover;border-radius:3px;">` : '📖'}
1711      <div class="book-info">
1712        ${label ? `<span class="admin-book-label">${label}</span>` : ''}
1713        <strong>${escapeHtml(b.title)}</strong>
1714        <span>${escapeHtml(b.author)}</span>
1715        <input type="text" class="admin-book-presenter" data-sid="${sid}" data-bi="${i}" value="${escapeHtml(b.presenter || '')}" placeholder="Présenté par…">
1716        ${type === 'theme' ? `<input type="text" class="admin-book-note" data-sid="${sid}" data-bi="${i}" value="${escapeHtml(b.note || '')}" placeholder="Texte anti-spoil (donne l'eau à la bouche…)">` : ''}
1717      </div>
1718      <button class="btn-remove-book" data-sid="${sid}" data-i="${i}" title="Retirer">🗑</button>
1719    </div>`;
1720}
1721
1722// ── Render : admin — sessions ────────────────────────────
1723function renderAdminSessions() {
1724  const el = document.getElementById('session-editor');
1725  const sessions = state.sessions;
1726  const typeOptions = Object.entries(SESSION_TYPES).map(([k, v]) => `<option value="${k}">${v.icon} ${v.label}</option>`).join('');
1727
1728  el.innerHTML = `
1729    <div class="admin-add-session-row">
1730      <div class="admin-type-select-wrapper">
1731        <label class="admin-type-select-label">Type de session</label>
1732        <select class="admin-type-select" id="new-session-type">${typeOptions}</select>
1733      </div>
1734      <button class="btn-add-session" id="btn-add-session">+ Créer</button>
1735    </div>
1736    ${sessions.length === 0
1737      ? `<p style="font-size:0.85rem;color:var(--text-light);font-style:italic;text-align:center;padding:1.5rem;">Aucune session. Créez-en une !</p>`
1738      : sessions.map((s, idx) => {
1739        const type = getSessionType(s);
1740        const typeInfo = SESSION_TYPES[type] || SESSION_TYPES.theme;
1741        const books = s.books || [];
1742        const expanded = state.expandedSessions.has(s.id);
1743        return `
1744        <div class="admin-session-card ${expanded ? '' : 'collapsed'}" data-sid="${s.id}">
1745          <div class="admin-session-card-header" data-sid="${s.id}">
1746            <h3><span class="collapse-chevron">▼</span> ${type !== 'theme' ? `<span class="admin-type-tag type-${type}">${typeInfo.icon}</span> ` : ''}${escapeHtml(getSessionTitle(s) || 'Sans titre')} <span class="header-date">${escapeHtml(s.date || '')}</span></h3>
1747            ${idx === 0 ? '<span class="admin-session-badge">Prochaine</span>' : ''}
1748            <div class="header-move-btns">
1749              ${idx > 0 ? `<button class="btn-move-header" data-sid="${s.id}" data-dir="up" title="Monter">▲</button>` : ''}
1750              ${idx < sessions.length - 1 ? `<button class="btn-move-header" data-sid="${s.id}" data-dir="down" title="Descendre">▼</button>` : ''}
1751            </div>
1752          </div>
1753          <div class="session-editor">
1754            <div class="admin-type-display"><span class="session-type-badge type-${type}">${typeInfo.icon} ${typeInfo.label}</span></div>
1755            ${type === 'theme' ? `
1756              <div class="sform-group">
1757                <label>Thème</label>
1758                <input type="text" class="edit-theme" value="${escapeHtml(s.theme || '')}" placeholder="Ex : Féminisme">
1759              </div>
1760              <div class="sform-group">
1761                <label>Description</label>
1762                <textarea class="edit-desc" rows="2">${escapeHtml(s.themeDescription || '')}</textarea>
1763              </div>` : ''}
1764            ${adminTypeFields(s)}
1765            <div class="sform-row">
1766              <div class="sform-group">
1767                <label>Date (affichage)</label>
1768                <input type="text" class="edit-date" value="${escapeHtml(s.date || '')}" placeholder="Ex : Samedi 26 avril">
1769              </div>
1770              <div class="sform-group">
1771                <label>Date (calendrier)</label>
1772                <input type="date" class="edit-date-iso" value="${escapeHtml(s.dateISO || '')}">
1773              </div>
1774              <div class="sform-group">
1775                <label>Heure</label>
1776                <input type="text" class="edit-heure" value="${escapeHtml(s.heure || '')}" placeholder="Ex : 15h00">
1777              </div>
1778            </div>
1779            <div class="sform-group">
1780              <label>Lieu</label>
1781              <input type="text" class="edit-lieu" value="${escapeHtml(s.lieu || CONFIG.club.lieu)}" placeholder="Ex : Lab-A, 11 Av. Louis Pasteur">
1782            </div>
1783            <button class="btn-save btn-save-session" data-sid="${s.id}">💾 Enregistrer</button>
1784            <hr class="admin-sep">
1785            <p class="admin-sublabel">${adminBooksLabel(s)}</p>
1786            <div>
1787              ${books.length === 0
1788                ? `<p style="font-size:0.8rem;color:var(--text-light);font-style:italic;">Validez des propositions pour ajouter des livres.</p>`
1789                : books.map((b, i) => adminBookItemHTML(b, i, s.id, type)).join('')}
1790            </div>
1791            <hr class="admin-sep">
1792            <div class="admin-session-actions">
1793              <button class="btn-archive" data-sid="${s.id}">📦 Archiver</button>
1794              <button class="btn-delete-session" data-sid="${s.id}">🗑 Supprimer</button>
1795            </div>
1796          </div>
1797        </div>`;
1798      }).join('')}`;
1799
1800  // Nouvelle session avec type
1801  document.getElementById('btn-add-session').addEventListener('click', () => {
1802    const typeSelect = document.getElementById('new-session-type');
1803    addSession(typeSelect.value);
1804  });
1805
1806  // Toggle collapse
1807  el.querySelectorAll('.admin-session-card-header').forEach(header => {
1808    header.addEventListener('click', (e) => {
1809      if (e.target.closest('.header-move-btns')) return;
1810      const sid = header.dataset.sid;
1811      const card = header.closest('.admin-session-card');
1812      card.classList.toggle('collapsed');
1813      if (!card.classList.contains('collapsed')) state.expandedSessions.add(sid);
1814      else state.expandedSessions.delete(sid);
1815    });
1816  });
1817
1818  // Monter/descendre
1819  el.querySelectorAll('.btn-move-header').forEach(btn => {
1820    btn.addEventListener('click', async (e) => {
1821      e.stopPropagation();
1822      const idx = sessions.findIndex(s => s.id === btn.dataset.sid);
1823      const targetIdx = btn.dataset.dir === 'up' ? idx - 1 : idx + 1;
1824      if (targetIdx >= 0 && targetIdx < sessions.length) {
1825        await swapSessionOrder(sessions[idx].id, sessions[targetIdx].id);
1826      }
1827    });
1828  });
1829
1830  // Sauvegarder chaque session (type-aware)
1831  el.querySelectorAll('.btn-save-session').forEach(btn => {
1832    btn.addEventListener('click', async () => {
1833      const card = btn.closest('.admin-session-card');
1834      const sid = btn.dataset.sid;
1835      const s = state.sessions.find(s => s.id === sid);
1836      const type = getSessionType(s);
1837      const updatedBooks = (s?.books || []).map((b, i) => {
1838        const presInput = card.querySelector(`.admin-book-presenter[data-sid="${sid}"][data-bi="${i}"]`);
1839        const noteInput = card.querySelector(`.admin-book-note[data-sid="${sid}"][data-bi="${i}"]`);
1840        return { ...b, presenter: presInput ? presInput.value.trim() : (b.presenter || ''), note: n
1840oteInput ? noteInput.value.trim() : (b.note || '') };
1841      });
1842      const data = {
1843        date:    card.querySelector('.edit-date').value.trim(),
1844        dateISO: card.querySelector('.edit-date-iso').value.trim(),
1845        heure:   card.querySelector('.edit-heure').value.trim(),
1846        lieu:    card.querySelector('.edit-lieu').value.trim(),
1847        books:   updatedBooks
1848      };
1849      // Champs spécifiques au type
1850      if (type === 'theme') {
1851        data.theme = card.querySelector('.edit-theme').value.trim();
1852        data.themeDescription = card.querySelector('.edit-desc').value.trim();
1853      } else if (type === 'lecture-commune') {
1854        data.discussionGuide = card.querySelector('.edit-discussion-guide').value.trim();
1855        // Le thème est le titre du livre
1856        data.theme = updatedBooks[0]?.title || 'Lecture commune';
1857      } else if (type === 'rencontre-auteur') {
1858        data.authorName = card.querySelector('.edit-author-name').value.trim();
1859        data.authorBio = card.querySelector('.edit-author-bio').value.trim();
1860        data.authorPhoto = card.querySelector('.edit-author-photo').value.trim();
1861        data.theme = data.authorName || 'Rencontre auteur';
1862      } else if (type === 'debat') {
1863        data.debateQuestion = card.querySelector('.edit-debate-question').value.trim();
1864        data.debateDescription = card.querySelector('.edit-debate-desc').value.trim();
1865        data.theme = data.debateQuestion || 'Débat';
1866      }
1867      await saveSession(sid, data);
1868      showToast('Session mise à jour !');
1869    });
1870  });
1871
1872  // Retirer un livre
1873  el.querySelectorAll('.btn-remove-book').forEach(btn => {
1874    btn.addEventListener('click', async () => {
1875      if (confirm('Retirer ce livre ?')) {
1876        await removeBookFromSession(btn.dataset.sid, +btn.dataset.i);
1877        showToast('Livre retiré.');
1878      }
1879    });
1880  });
1881
1882  // Archiver
1883  el.querySelectorAll('.btn-archive').forEach(btn => {
1884    btn.addEventListener('click', () => archiveSession(btn.dataset.sid));
1885  });
1886
1887  // Supprimer
1888  el.querySelectorAll('.btn-delete-session').forEach(btn => {
1889    btn.addEventListener('click', () => deleteSession(btn.dataset.sid));
1890  });
1891}
1892
1893// ── Render : admin sessions passées ──────────────────────
1894function renderAdminPastSessions() {
1895  const el = document.getElementById('past-session-editor');
1896  const sessions = state.pastSessions;
1897
1898  el.innerHTML = `
1899    ${sessions.length === 0
1900      ? `<p style="font-size:0.85rem;color:var(--text-light);font-style:italic;text-align:center;padding:1.5rem;">Aucune session archivée.</p>`
1901      : sessions.map((s, idx) => {
1902        const type = getSessionType(s);
1903        const typeInfo = SESSION_TYPES[type] || SESSION_TYPES.theme;
1904        const books = s.books || [];
1905        const expanded = state.expandedPastSessions.has(s.id);
1906        return `
1907        <div class="admin-session-card ${expanded ? '' : 'collapsed'}" data-pid="${s.id}">
1908          <div class="admin-past-card-header" data-pid="${s.id}">
1909            <h3><span class="collapse-chevron">▼</span> ${type !== 'theme' ? `<span class="admin-type-tag type-${type}">${typeInfo.icon}</span> ` : ''}${escapeHtml(getSessionTitle(s) || 'Sans titre')} <span class="header-date">${escapeHtml(s.date || '')}</span></h3>
1910            <div class="header-move-btns">
1911              ${idx > 0 ? `<button class="btn-move-past-header" data-pid="${s.id}" data-dir="up" title="Monter">▲</button>` : ''}
1912              ${idx < sessions.length - 1 ? `<button class="btn-move-past-header" data-pid="${s.id}" data-dir="down" title="Descendre">▼</button>` : ''}
1913            </div>
1914          </div>
1915          <div class="session-editor">
1916            <div class="admin-type-display"><span class="session-type-badge type-${type}">${typeInfo.icon} ${typeInfo.label}</span></div>
1917            ${type === 'theme' ? `
1918              <div class="sform-group">
1919                <label>Thème</label>
1920                <input type="text" class="edit-past-theme" value="${escapeHtml(s.theme || '')}" placeholder="Ex : Féminisme">
1921              </div>
1922              <div class="sform-group">
1923                <label>Description</label>
1924                <textarea class="edit-past-desc" rows="2">${escapeHtml(s.themeDescription || '')}</textarea>
1925              </div>` : ''}
1926            ${type === 'lecture-commune' ? `
1927              <div class="sform-group">
1928                <label>Guide de discussion</label>
1929                <textarea class="edit-past-discussion-guide" rows="3" placeholder="Questions, axes de réflexion…">${escapeHtml(s.discussionGuide || '')}</textarea>
1930              </div>` : ''}
1931            ${type === 'rencontre-auteur' ? `
1932              <div class="sform-group">
1933                <label>Nom de l'auteur</label>
1934                <input type="text" class="edit-past-author-name" value="${escapeHtml(s.authorName || '')}" placeholder="Ex : Victor Hugo">
1935              </div>
1936              <div class="sform-group">
1937                <label>Biographie</label>
1938                <textarea class="edit-past-author-bio" rows="3" placeholder="Courte biographie…">${escapeHtml(s.authorBio || '')}</textarea>
1939              </div>
1940              <div class="sform-group">
1941                <label>Photo (URL)</label>
1942                <input type="text" class="edit-past-author-photo" value="${escapeHtml(s.authorPhoto || '')}" placeholder="https://...">
1943              </div>` : ''}
1944            ${type === 'debat' ? `
1945              <div class="sform-group">
1946                <label>Question du débat</label>
1947                <input type="text" class="edit-past-debate-question" value="${escapeHtml(s.debateQuestion || '')}" placeholder="Ex : Faut-il séparer l'œuvre de l'artiste ?">
1948              </div>
1949              <div class="sform-group">
1950                <label>Description du débat</label>
1951                <textarea class="edit-past-debate-desc" rows="2" placeholder="Contexte, enjeux…">${escapeHtml(s.debateDescription || '')}</textarea>
1952              </div>` : ''}
1953            <div class="sform-row">
1954              <div class="sform-group">
1955                <label>Date (affichage)</label>
1956                <input type="text" class="edit-past-date" value="${escapeHtml(s.date || '')}" placeholder="Ex : Samedi 26 avril">
1957              </div>
1958              <div class="sform-group">
1959                <label>Date (calendrier)</label>
1960                <input type="date" class="edit-past-date-iso" value="${escapeHtml(s.dateISO || '')}">
1961              </div>
1962              <div class="sform-group">
1963                <label>Heure</label>
1964                <input type="text" class="edit-past-heure" value="${escapeHtml(s.heure || '')}" placeholder="Ex : 15h00">
1965              </div>
1966            </div>
1967            <div class="sform-group">
1968              <label>Compte-rendu</label>
1969              <textarea class="edit-past-recap" rows="3" placeholder="Compte-rendu de la séance…">${escapeHtml(s.recap || '')}</textarea>
1970            </div>
1971            <button class="btn-save btn-save-past-session" data-pid="${s.id}">💾 Enregistrer</button>
1972            <hr class="admin-sep">
1973            <p class="admin-sublabel">${adminBooksLabel(s)}</p>
1974            <div>
1975              ${books.map((b, i) => `
1976                <div class="admin-book-item">
1977                  ${b.coverUrl ? `<img src="${b.coverUrl}" style="width:28px;height:38px;object-fit:cover;border-radius:3px;">` : '📖'}
1978                  <div class="book-info">
1979                    ${type === 'debat' ? `<span class="admin-book-label">${i === 0 ? '🟢 Pour' : '🔴 Contre'}</span>` : ''}
1980                    <strong>${escapeHtml(b.title)}</strong>
1981                    <span>${escapeHtml(b.author)}</span>
1982                    <input type="text" class="admin-book-presenter" data-pid="${s.id}" data-bi="${i}" value="${escapeHtml(b.presenter || '')}" placeholder="Présenté par…">
1983                  </div>
1984                  <button class="btn-remove-past-book-admin" data-pid="${s.id}" data-i="${i}" title="Retirer">🗑</button>
1985                </div>`).join('')}
1986              <div class="admin-past-add-book" style="margin-top:0.5rem;">
1987                <div class="book-search-wrapper" style="position:relative;">
1988                  <input type="text" class="admin-past-book-search" data-pid="${s.id}" placeholder="Ajouter un livre…" autocomplete="off"
1989                    style="width:100%;padding:0.45rem 0.7rem;border:1px solid var(--border);border-radius:6px;font-family:'DM Sans',sans-serif;font-size:0.82rem;background:var(--bg);color:var(--text);outline:none;">
1990                  <div class="book-search-dropdown" id="admin-past-dd-${s.id}"></div>
1991                </div>
1992              </div>
1993            </div>
1994            <hr class="admin-sep">
1995            <div class="admin-session-actions">
1996              <button class="btn-delete-past-admin" data-pid="${s.id}">🗑 Supprimer</button>
1997            </div>
1998          </div>
1999        </div>`;
2000      }).join('')}`;
2001
2002  // Toggle collapse
2003  el.querySelectorAll('.admin-past-card-header').forEach(header => {
2004    header.addEventListener('click', (e) => {
2005      if (e.target.closest('.header-move-btns')) return;
2006      const pid = header.dataset.pid;
2007      const card = header.closest('.admin-session-card');
2008      card.classList.toggle('collapsed');
2009      if (!card.classList.contains('collapsed')) state.expandedPastSessions.add(pid);
2010      else state.expandedPastSessions.delete(pid);
2011    });
2012  });
2013
2014  // Monter/descendre
2015  el.querySelectorAll('.btn-move-past-header').forEach(btn => {
2016    btn.addEventListener('click', async (e) => {
2017      e.stopPropagation();
2018      const idx = sessions.findIndex(s => s.id === btn.dataset.pid);
2019      const targetIdx = btn.dataset.dir === 'up' ? idx - 1 : idx + 1;
2020      if (targetIdx >= 0 && targetIdx < sessions.length) {
2021        await swapPastSessionOrder(sessions[idx].id, sessions[targetIdx].id);
2022      }
2023    });
2024  });
2025
2026  // Sauvegarder
2027  el.querySelectorAll('.btn-save-past-session').forEach(btn => {
2028    btn.addEventListener('click', async () => {
2029      const card = btn.closest('.admin-session-card');
2030      const pid = btn.dataset.pid;
2031      const s = state.pastSessions.find(s => s.id === pid);
2032      const type = getSessionType(s);
2033      const updatedBooks = (s?.books || []).map((b, i) => {
2034        const presInput = card.querySelector(`.admin-book-presenter[data-pid="${pid}"][data-bi="${i}"]`);
2035        return { ...b, presenter: presInput ? presInput.value.trim() : (b.presenter || '') };
2036      });
2037      const data = {
2038        date:    card.querySelector('.edit-past-date').value.trim(),
2039        dateISO: card.querySelector('.edit-past-date-iso').value.trim(),
2040        heure:   card.querySelector('.edit-past-heure').value.trim(),
2041        recap:   card.querySelector('.edit-past-recap').value.trim(),
2042        books:   updatedBooks
2043      };
2044      if (type === 'theme') {
2045        data.theme = card.querySelector('.edit-past-theme').value.trim();
2046        data.themeDescription = card.querySelector('.edit-past-desc').value.trim();
2047      } else if (type === 'lecture-commune') {
2048        data.discussionGuide = card.querySelector('.edit-past-discussion-guide').value.trim();
2049        data.theme = updatedBooks[0]?.title || 'Lecture commune';
2050      } else if (type === 'rencontre-auteur') {
2051        data.authorName = card.querySelector('.edit-past-author-name').value.trim();
2052        data.authorBio = card.querySelector('.edit-past-author-bio').value.trim();
2053        data.authorPhoto = card.querySelector('.edit-past-author-photo').value.trim();
2054        data.theme = data.authorName || 'Rencontre auteur';
2055      } else if (type === 'debat') {
2056        data.debateQuestion = card.querySelector('.edit-past-debate-question').value.trim();
2057        data.debateDescription = card.querySelector('.edit-past-debate-desc').value.trim();
2058        data.theme = data.debateQuestion || 'Débat';
2059      }
2060      await savePastSession(pid, data);
2061      showToast('Session mise à jour !');
2062    });
2063  });
2064
2065  // Retirer un livre
2066  el.querySelectorAll('.btn-remove-past-book-admin').forEach(btn => {
2067    btn.addEventListener('click', async () => {
2068      if (confirm('Retirer ce livre ?')) {
2069        await removeBookFromPastSession(btn.dataset.pid, +btn.dataset.i);
2070        showToast('Livre retiré.');
2071      }
2072    });
2073  });
2074
2075  // Recherche livre pour ajouter
2076  let pastAdminSearchTimer = null;
2077  el.querySelectorAll('.admin-past-book-search').forEach(input => {
2078    const pid = input.dataset.pid;
2079    const dropdown = document.getElementById(`admin-past-dd-${pid}`);
2080    input.addEventListener('input', () => {
2081      clearTimeout(pastAdminSearchTimer);
2082      const q = input.value.trim();
2083      if (q.length < 2) { dropdown.classList.remove('open'); return; }
2084      dropdown.innerHTML = '<div class="search-loading">Recherche…</div>';
2085      dropdown.classList.add('open');
2086      pastAdminSearchTimer = setTimeout(async () => {
2087        const results = await searchBooks(q);
2088        renderSearchResults(results, dropdown, q, async (book) => {
2089          await addBookToPastSession(pid, { title: book.title, author: book.author, description: book.description || '', coverUrl: book.coverUrl || '', presenter: '' });
2090          dropdown.classList.remove('open');
2091          input.value = '';
2092        });
2093      }, 450);
2094    });
2095  });
2096
2097  // Supprimer
2098  el.querySelectorAll('.btn-delete-past-admin').forEach(btn => {
2099    btn.addEventListener('click', () => deletePastSession(btn.dataset.pid));
2100  });
2101}
2102
2103// ── Render : conseils lecture ─────────────────────────────
2104function tipCardHTML(t) {
2105  const date = t.createdAt?.toDate ? t.createdAt.toDate().toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' }) : '';
2106  return `
2107    <div class="tip-card" data-tip-id="${t.id}">
2108      ${t.coverUrl
2109        ? `<img class="tip-card-cover" src="${t.coverUrl}" alt="">`
2110        : `<div class="tip-card-cover-ph">📖</div>`}
2111      <div class="tip-card-body">
2112        <div class="tip-card-title">${escapeHtml(t.title)}</div>
2113        <div class="tip-card-author">${escapeHtml(t.author || '')}</div>
2114        ${t.note ? `<div class="tip-card-note">"${escapeHtml(t.note)}"</div>` : ''}
2115        <div class="tip-card-by">
2115${t.recommendedBy?.isAdmin ? '📌 ' : ''}Recommandé par ${escapeHtml(t.recommendedBy?.name || 'Anonyme')}${date ? ` · ${date}` : ''}</div>
2116        ${userState.isAdmin ? `<button class="btn-delete-theme" style="margin-top:0.3rem;" data-tip-id="${t.id}" title="Supprimer">🗑</button>` : ''}
2117      </div>
2118    </div>`;
2119}
2120
2121// Stockage des handlers pour nettoyage
2122let _tipsArrowLHandler = null;
2123let _tipsArrowRHandler = null;
2124let _tipsMouseEnter = null;
2125let _tipsMouseLeave = null;
2126
2127function renderTips() {
2128  // Nettoyer l'ancien carousel en le clonant (supprime tous les event listeners d'un coup)
2129  const oldCarousel = document.getElementById('tips-carousel');
2130  const carousel = oldCarousel.cloneNode(false);
2131  oldCarousel.parentNode.replaceChild(carousel, oldCarousel);
2132
2133  // Nettoyer les anciens handlers des flèches
2134  const arrowL = document.getElementById('tips-arrow-left');
2135  const arrowR = document.getElementById('tips-arrow-right');
2136  if (_tipsArrowLHandler) { arrowL.removeEventListener('click', _tipsArrowLHandler); }
2137  if (_tipsArrowRHandler) { arrowR.removeEventListener('click', _tipsArrowRHandler); }
2138
2139  // Nettoyer l'auto-scroll
2140  if (window._tipsAutoScroll) { clearInterval(window._tipsAutoScroll); window._tipsAutoScroll = null; }
2141
2142  const approved = state.tips.filter(t => t.status === 'approved');
2143  const needsLoop = approved.length > 3;
2144
2145  if (approved.length === 0) {
2146    carousel.innerHTML = '<p class="tips-empty">Aucun conseil pour le moment.</p>';
2147  } else {
2148    const cards = approved.map(t => tipCardHTML(t)).join('');
2149    carousel.innerHTML = needsLoop ? cards + cards + cards : cards;
2150  }
2151
2152  arrowL.style.display = needsLoop ? '' : 'none';
2153  arrowR.style.display = needsLoop ? '' : 'none';
2154  const scrollAmt = 240;
2155
2156  // Positionner au milieu du triplement
2157  if (needsLoop) {
2158    carousel.style.scrollBehavior = 'auto';
2159    carousel.scrollLeft = carousel.scrollWidth / 3;
2160    carousel.style.scrollBehavior = 'smooth';
2161  }
2162
2163  // Reset silencieux quand on atteint les bords
2164  const checkLoop = () => {
2165    if (!needsLoop) return;
2166    const third = carousel.scrollWidth / 3;
2167    if (carousel.scrollLeft >= third * 2) {
2168      carousel.style.scrollBehavior = 'auto';
2169      carousel.scrollLeft -= third;
2170      carousel.style.scrollBehavior = 'smooth';
2171    } else if (carousel.scrollLeft <= 0) {
2172      carousel.style.scrollBehavior = 'auto';
2173      carousel.scrollLeft += third;
2174      carousel.style.scrollBehavior = 'smooth';
2175    }
2176  };
2177  carousel.addEventListener('scrollend', checkLoop);
2178  let scrollTimeout;
2179  carousel.addEventListener('scroll', () => {
2180    clearTimeout(scrollTimeout);
2181    scrollTimeout = setTimeout(checkLoop, 150);
2182  });
2183
2184  // Flèches (handlers nettoyables)
2185  _tipsArrowLHandler = () => carousel.scrollBy({ left: -scrollAmt });
2186  _tipsArrowRHandler = () => carousel.scrollBy({ left: scrollAmt });
2187  arrowL.addEventListener('click', _tipsArrowLHandler);
2188  arrowR.addEventListener('click', _tipsArrowRHandler);
2189
2190  // Défilement automatique
2191  if (needsLoop) {
2192    window._tipsAutoScroll = setInterval(() => carousel.scrollBy({ left: scrollAmt }), 4000);
2193    _tipsMouseEnter = () => clearInterval(window._tipsAutoScroll);
2194    _tipsMouseLeave = () => {
2195      window._tipsAutoScroll = setInterval(() => carousel.scrollBy({ left: scrollAmt }), 4000);
2196    };
2197    carousel.addEventListener('mouseenter', _tipsMouseEnter);
2198    carousel.addEventListener('mouseleave', _tipsMouseLeave);
2199  }
2200
2201  // Clic sur carte → modale livre
2202  carousel.querySelectorAll('.tip-card').forEach(card => {
2203    card.addEventListener('click', (e) => {
2204      if (e.target.closest('.btn-delete-theme')) return;
2205      const t = state.tips.find(t => t.id === card.dataset.tipId);
2206      if (t) {
2207        openBookModal({ title: t.title, author: t.author, description: t.description || '', coverUrl: t.coverUrl, presenter: '' }, null, null, null);
2208        setHash(`tip/${t.id}`);
2209      }
2210    });
2211  });
2212
2213  // Admin : supprimer
2214  carousel.querySelectorAll('.btn-delete-theme[data-tip-id]').forEach(btn => {
2215    btn.addEventListener('click', (e) => {
2216      e.stopPropagation();
2217      deleteTip(btn.dataset.tipId);
2218    });
2219  });
2220
2221  // Badge admin pending
2222  const pending = state.tips.filter(t => t.status === 'pending');
2223  const badge = document.getElementById('tips-pending-count');
2224  if (badge) badge.textContent = pending.length > 0 ? `(${pending.length})` : '';
2225
2226  // Formulaire d'ajout
2227  renderTipForm();
2228}
2229
2230function renderTipForm() {
2231  const container = document.getElementById('tip-form-container');
2232  if (!userState.user) {
2233    container.innerHTML = '<p style="color:var(--text-light);font-size:0.82rem;font-style:italic;margin-top:0.75rem;">Connecte-toi pour recommander un livre.</p>';
2234    return;
2235  }
2236  container.innerHTML = `
2237    <div class="tip-form" id="tip-form">
2238      <input type="text" id="tip-recommender-name" placeholder="Ton prénom / nom" value="${userState.user ? escapeHtml(getDisplayName()) : ''}" style="flex:0 0 180px;">
2239      <div class="book-search-wrapper" style="flex:1;min-width:200px;">
2240        <input type="text" id="tip-book-search" placeholder="Rechercher un livre à recommander…" autocomplete="off">
2241        <div class="book-search-dropdown" id="tip-search-dd"></div>
2242      </div>
2243      <div id="tip-selected" style="width:100%;display:none;"></div>
2244      <textarea id="tip-note" placeholder="Pourquoi recommandes-tu ce livre ? (optionnel)" style="display:none;"></textarea>
2245      <button class="btn-submit-tip" id="btn-submit-tip" style="display:none;">Recommander</button>
2246    </div>`;
2247
2248  const input = document.getElementById('tip-book-search');
2249  const dropdown = document.getElementById('tip-search-dd');
2250  let tipTimer = null;
2251
2252  input.addEventListener('input', () => {
2253    clearTimeout(tipTimer);
2254    const q = input.value.trim();
2255    if (q.length < 2) { dropdown.classList.remove('open'); return; }
2256    dropdown.innerHTML = '<div class="search-loading">Recherche…</div>';
2257    dropdown.classList.add('open');
2258    tipTimer = setTimeout(async () => {
2259      const results = await searchBooks(q);
2260      renderSearchResults(results, dropdown, q, (book) => {
2261        state.tipSelectedBook = book;
2262        dropdown.classList.remove('open');
2263        input.closest('.book-search-wrapper').style.display = 'none';
2264        const sel = document.getElementById('tip-selected');
2265        sel.style.display = '';
2266        sel.innerHTML = `
2267          <div class="tip-selected-book">
2268            ${book.coverUrl ? `<img src="${book.coverUrl}" alt="">` : ''}
2269            <strong>${escapeHtml(book.title)}</strong> — ${escapeHtml(book.author || '')}
2270            <button class="tip-remove" id="tip-remove-book">✕</button>
2271          </div>`;
2272        document.getElementById('tip-note').style.display = '';
2273        document.getElementById('btn-submit-tip').style.display = '';
2274        document.getElementById('tip-remove-book').addEventListener('click', () => {
2275          state.tipSelectedBook = null;
2276          input.closest('.book-search-wrapper').style.display = '';
2277          input.value = '';
2278          sel.style.display = 'none';
2279          document.getElementById('tip-note').style.display = 'none';
2280          document.getElementById('btn-submit-tip').style.display = 'none';
2281        });
2282      });
2283    }, 450);
2284  });
2285
2286  document.getElementById('btn-submit-tip').addEventListener('click', async () => {
2287    if (!state.tipSelectedBook) return;
2288    const recommenderName = document.getElementById('tip-recommender-name').value.trim();
2289    if (!recommenderName) { showToast('Indique ton prénom/nom.'); return; }
2290    const note = document.getElementById('tip-note').value.trim();
2291    await addTip({
2292      title: state.tipSelectedBook.title,
2293      author: state.tipSelectedBook.author || '',
2294      description: state.tipSelectedBook.description || '',
2295      coverUrl: state.tipSelectedBook.coverUrl || '',
2296      googleBooksId: state.tipSelectedBook.googleBooksId || '',
2297      note,
2298      recommenderName
2299    });
2300    state.tipSelectedBook = null;
2301    renderTipForm();
2302  });
2303}
2304
2305// ── Render : calendrier ──────────────────────────────────
2306let calendarDate = new Date();
2307
2308function renderCalendar() {
2309  const year = calendarDate.getFullYear();
2310  const month = calendarDate.getMonth();
2311  const monthNames = ['Janvier','Février','Mars','Avril','Mai','Juin','Juillet','Août','Septembre','Octobre','Novembre','Décembre'];
2312  const dayNames = ['Lun','Mar','Mer','Jeu','Ven','Sam','Dim'];
2313
2314  document.getElementById('cal-month').textContent = `${monthNames[month]} ${year}`;
2315
2316  // Collecter les événements du mois (sessions à venir + passées)
2317  const events = {};
2318  [...state.sessions, ...state.pastSessions].forEach(s => {
2319    if (!s.dateISO) return;
2320    const d = new Date(s.dateISO + 'T00:00:00');
2321    if (d.getFullYear() === year && d.getMonth() === month) {
2322      const day = d.getDate();
2323      const isPast = state.pastSessions.some(p => p.id === s.id);
2324      events[day] = { session: s, isPast };
2325    }
2326  });
2327
2328  // Grille
2329  const firstDay = new Date(year, month, 1).getDay();
2330  const offset = firstDay === 0 ? 6 : firstDay - 1; // Lundi = 0
2331  const daysInMonth = new Date(year, month + 1, 0).getDate();
2332  const today = new Date();
2333  const isCurrentMonth = today.getFullYear() === year && today.getMonth() === month;
2334
2335  let html = dayNames.map(d => `<div class="cal-day-name">${d}</div>`).join('');
2336  for (let i = 0; i < offset; i++) html += '<div class="cal-day empty"></div>';
2337  for (let d = 1; d <= daysInMonth; d++) {
2338    const isToday = isCurrentMonth && today.getDate() === d;
2339    const ev = events[d];
2340    let cls = 'cal-day';
2341    if (isToday) cls += ' today';
2342    if (ev) cls += ev.isPast ? ' past-event' : ' upcoming-event';
2343    const tooltip = ev ? escapeHtml(ev.session.theme || '') : '';
2344    html += `<div class="${cls}" ${tooltip ? `title="${tooltip}"` : ''}>${d}</div>`;
2345  }
2346
2347  document.getElementById('cal-grid').innerHTML = html;
2348
2349  // Légende
2350  const legend = document.getElementById('cal-legend');
2351  const evList = Object.entries(events).sort(([a],[b]) => a - b);
2352  legend.innerHTML = evList.length === 0
2353    ? '<p class="cal-no-events">Aucune session ce mois-ci.</p>'
2354    : evList.map(([day, ev]) => {
2355        const s = ev.session;
2356        const dot = ev.isPast ? 'past-dot' : 'upcoming-dot';
2357        return `<div class="cal-event"><span class="cal-dot ${dot}"></span><strong>${day} ${monthNames[month].slice(0,3)}.</strong> ${escapeHtml(s.theme || '')} ${s.heure ? `· ${escapeHtml(s.heure)}` : ''}</div>`;
2358      }).join('');
2359}
2360
2361function initCalendar() {
2362  const modal = document.getElementById('calendar-modal');
2363  document.getElementById('open-calendar').addEventListener('click', () => {
2364    calendarDate = new Date();
2365    renderCalendar();
2366    modal.classList.remove('hidden');
2367  });
2368  document.getElementById('cal-modal-close').addEventListener('click', () => modal.classList.add('hidden'));
2369  modal.addEventListener('click', (e) => { if (e.target === modal) modal.classList.add('hidden'); });
2370  document.getElementById('cal-prev').addEventListener('click', () => {
2371    calendarDate.setMonth(calendarDate.getMonth() - 1);
2372    renderCalendar();
2373  });
2374  document.getElementById('cal-next').addEventListener('click', () => {
2375    calendarDate.setMonth(calendarDate.getMonth() + 1);
2376    renderCalendar();
2377  });
2378}
2379
2380// ── Render : sondage ─────────────────────────────────────
2381function renderPoll() {
2382  const el = document.getElementById('poll-container');
2383  const approvedThemes = state.themes.filter(t => t.status !== 'pending');
2384  const uid = userState.user?.uid;
2385  const myThemeCount = uid ? state.themes.filter(t => t.proposedBy === uid).length : 0;
2386  const totalThemes = state.themes.length;
2387  const canSuggest = uid && totalThemes < state.limits.maxThemes;
2388
2389  if (approvedThemes.length === 0) {
2390    el.innerHTML = `<p style="color:var(--text-light);font-style:italic;">Aucun thème en lice pour le moment.</p>
2391      ${!uid ? '<p class="poll-login-hint">Connecte-toi pour voter et proposer des thèmes.</p>' : ''}
2392      ${canSuggest ? `<div class="poll-suggest"><input type="text" id="suggest-theme-input" placeholder="Proposer un thème…"><button id="btn-suggest-theme">Proposer</button></div>` : ''}`;
2393    document.getElementById('btn-suggest-theme')?.addEventListener('click', () => {
2394      const name = document.getElementById('suggest-theme-input').value.trim();
2395      if (name) { suggestTheme(name); document.getElementById('suggest-theme-input').value = ''; }
2396    });
2397    return;
2398  }
2399
2400  // Trier par nombre de votes décroissant
2401  const sorted = [...approvedThemes].sort((a, b) => getVoteCount(b) - getVoteCount(a));
2402  const maxVotes = getVoteCount(sorted[0]) || 1;
2403  const rankSymbols = ['🥇', '🥈', '🥉'];
2404
2405  // Calcul des rangs par paliers de votes (ex-aequo = même rang)
2406  let currentRank = 0;
2407  let lastCount = -1;
2408  const ranks = sorted.map(t => {
2409    const c = getVoteCount(t);
2410    if (c !== lastCount) { currentRank++; lastCount = c; }
2411    return currentRank;
2412  });
2413
2414  // Séparer top 3 (rangs 1-3) et le reste
2415  const topItems = [];
2416  const otherItems = [];
2417  sorted.forEach((t, idx) => { (ranks[idx] <= 3 ? topItems : otherItems).push({ theme: t, rank: ranks[idx], idx }); });
2418
2419  function renderItem(t, rank, isCompact) {
2420    const voters = getVotersMap(t);
2421    const names = Object.values(voters);
2422    const count = names.length;
2423    const hasVoted = uid && !!voters[uid];
2424    const pct = maxVotes > 0 ? (count / maxVotes) * 100 : 0;
2425    const rankClass = rank <= 3 && count > 0 ? `rank-${rank}` : '';
2426    const rankLabel = rank <= 3 && count > 0 ? rankSymbols[rank - 1] : `${rank}
2426`;
2427    const tooltip = names.length > 0 ? names.join(', ') : 'Aucun vote';
2428    const compact = isCompact ? ' compact' : '';
2429    return `
2430      <div class="poll-item ${rankClass}${compact}" title="${escapeHtml(tooltip)}">
2431        ${!isCompact ? `<div class="poll-bar-bg" style="width:${pct}%"></div>` : ''}
2432        <span class="poll-rank">${rankLabel}</span>
2433        <div class="poll-item-body">
2434          <div class="poll-name">${escapeHtml(t.name)}</div>
2435          ${!isCompact && names.length > 0 ? `<div class="poll-voters-hint">${escapeHtml(names.slice(0, 3).join(', '))}${names.length > 3 ? ` +${names.length - 3}` : ''}</div>` : ''}
2436        </div>
2437        <div class="poll-right">
2438          <span class="poll-votes-count">${count}</span>
2439          ${uid
2440            ? (hasVoted
2441              ? `<button class="btn-vote voted" data-tid="${t.id}" title="Annuler le vote">Voté</button>`
2442              : `<button class="btn-vote" data-tid="${t.id}">Voter</button>`)
2443            : ''}
2444        </div>
2445      </div>`;
2446  }
2447
2448  el.innerHTML = `
2449    <div class="poll-top">
2450      ${topItems.map(({ theme, rank }) => renderItem(theme, rank, false)).join('')}
2451    </div>
2452    ${otherItems.length > 0 ? `<div class="poll-others">${otherItems.map(({ theme, rank }) => renderItem(theme, rank, true)).join('')}</div>` : ''}
2453    ${!uid ? '<p class="poll-login-hint">Connecte-toi pour voter et proposer des thèmes.</p>' : ''}
2454    ${canSuggest ? `
2455      <div class="poll-suggest">
2456        <input type="text" id="suggest-theme-input" placeholder="Proposer un thème…">
2457        <button id="btn-suggest-theme">Proposer</button>
2458        ${myThemeCount >= state.limits.freeThemes ? '<p style="font-size:0.75rem;color:var(--text-light);margin-top:0.3rem;">Tes prochaines propositions seront soumises à validation.</p>' : ''}
2459      </div>` : (uid && totalThemes >= state.limits.maxThemes ? `<p style="font-size:0.8rem;color:var(--text-light);margin-top:0.5rem;">Le nombre maximum de thèmes (${state.limits.maxThemes}) est atteint.</p>` : '')}`;
2460
2461  el.querySelectorAll('.btn-vote:not(.voted)').forEach(btn => {
2462    btn.addEventListener('click', () => voteTheme(btn.dataset.tid));
2463  });
2464  el.querySelectorAll('.btn-vote.voted').forEach(btn => {
2465    btn.addEventListener('click', () => unvoteTheme(btn.dataset.tid));
2466  });
2467  document.getElementById('btn-suggest-theme')?.addEventListener('click', () => {
2468    const name = document.getElementById('suggest-theme-input').value.trim();
2469    if (name) { suggestTheme(name); document.getElementById('suggest-theme-input').value = ''; }
2470  });
2471}
2472
2473// ── Render : admin — thèmes ──────────────────────────────
2474function renderAdminThemes() {
2475  const el = document.getElementById('admin-themes');
2476  const pendingThemes = state.themes.filter(t => t.status === 'pending');
2477  const approvedThemes = state.themes.filter(t => t.status !== 'pending');
2478  el.innerHTML = `
2479    ${pendingThemes.length > 0 ? `
2480      <p class="admin-sublabel">En attente de validation (${pendingThemes.length})</p>
2481      ${pendingThemes.map(t => `
2482          <div class="admin-theme-item">
2483            <span class="theme-name">${escapeHtml(t.name)}</span>
2484            <button class="btn-approve-theme" data-tid="${t.id}" title="Approuver" style="background:var(--green);color:#fff;border:none;padding:0.2rem 0.6rem;border-radius:999px;font-size:0.75rem;cursor:pointer;">✓</button>
2485            <button class="btn-delete-theme" data-tid="${t.id}" title="Refuser">🗑</button>
2486          </div>`).join('')}` : ''}
2487    <p class="admin-sublabel"${pendingThemes.length > 0 ? ' style="margin-top:1rem;"' : ''}>Thèmes du sondage (${approvedThemes.length}) — max ${state.limits.maxThemes} au total</p>
2488    ${approvedThemes.length === 0
2489      ? '<p style="font-size:0.82rem;color:var(--text-light);font-style:italic;">Aucun thème.</p>'
2490      : approvedThemes.map(t => `
2491          <div class="admin-theme-item">
2492            <span class="theme-name">${escapeHtml(t.name)}</span>
2493            <span class="theme-votes">${getVoteCount(t)} votes</span>
2494            <button class="btn-delete-theme" data-tid="${t.id}" title="Supprimer">🗑</button>
2495          </div>`).join('')}
2496    <div class="admin-add-theme">
2497      <input type="text" id="new-theme-name" placeholder="Nouveau thème…">
2498      <button id="btn-add-theme">+</button>
2499    </div>
2500    <button class="btn-reset-votes" id="btn-reset-votes">Remettre tous les votes à zéro</button>`;
2501
2502  el.querySelectorAll('.btn-delete-theme').forEach(btn => {
2503    btn.addEventListener('click', () => deleteTheme(btn.dataset.tid));
2504  });
2505  el.querySelectorAll('.btn-approve-theme').forEach(btn => {
2506    btn.addEventListener('click', async () => {
2507      await setDoc(doc(db, 'themes', btn.dataset.tid), { status: 'approved' }, { merge: true });
2508      showToast('Thème approuvé !');
2509    });
2510  });
2511  document.getElementById('btn-add-theme').addEventListener('click', () => {
2512    const name = document.getElementById('new-theme-name').value.trim();
2513    if (name) { addTheme(name); document.getElementById('new-theme-name').value = ''; }
2514  });
2515  document.getElementById('btn-reset-votes').addEventListener('click', resetAllVotes);
2516}
2517
2518// ── Render : admin — conseils ────────────────────────────
2519function renderAdminTips() {
2520  const el = document.getElementById('admin-tips');
2521  const pending = state.tips.filter(t => t.status === 'pending');
2522  const approved = state.tips.filter(t => t.status === 'approved');
2523  el.innerHTML = `
2524    <p class="admin-sublabel">En attente (${pending.length})</p>
2525    ${pending.length === 0
2526      ? '<p style="font-size:0.82rem;color:var(--text-light);font-style:italic;margin-bottom:1rem;">Aucun conseil en attente.</p>'
2527      : pending.map(t => `
2528          <div class="proposal-card" style="margin-bottom:0.75rem;">
2529            <div class="proposal-card-header">
2530              ${t.coverUrl ? `<img class="proposal-cover" src="${t.coverUrl}" alt="">` : `<div class="proposal-cover-ph">📖</div>`}
2531              <div>
2532                <div class="proposal-title">${escapeHtml(t.title)}</div>
2533                <div class="proposal-author">${escapeHtml(t.author || '—')}</div>
2534                <div class="proposal-proposer">Recommandé par ${escapeHtml(t.recommendedBy?.name || 'Anonyme')}</div>
2535              </div>
2536            </div>
2537            ${t.note ? `<div class="proposal-note">"${escapeHtml(t.note)}"</div>` : ''}
2538            <div class="proposal-actions">
2539              <button class="btn-approve" data-tip-id="${t.id}">✓ Approuver</button>
2540              <button class="btn-edit-tip" data-tip-id="${t.id}">✏️ Modifier</button>
2541              <button class="btn-reject" data-tip-id="${t.id}">✕ Refuser</button>
2542            </div>
2543            <div class="admin-tip-edit-form hidden" id="tip-edit-${t.id}">
2544              <label class="past-edit-label">Titre</label>
2545              <input type="text" class="past-edit-input tip-edit-title" value="${escapeHtml(t.title || '')}">
2546              <label class="past-edit-label">Auteur</label>
2547              <input type="text" class="past-edit-input tip-edit-author" value="${escapeHtml(t.author || '')}">
2548              <label class="past-edit-label">Recommandé par</label>
2549              <input type="text" class="past-edit-input tip-edit-recommender" value="${escapeHtml(t.recommendedBy?.name || '')}">
2550              <label class="past-edit-label">Note</label>
2551              <textarea class="past-edit-input tip-edit-note" rows="2">${escapeHtml(t.note || '')}</textarea>
2552              <button class="btn-save-past-edit btn-save-tip-edit" data-tip-id="${t.id}">Enregistrer</button>
2553            </div>
2554          </div>`).join('')}
2555    <p class="admin-sublabel" style="margin-top:1rem;">Publiés (${approved.length})</p>
2556    ${approved.length === 0
2557      ? '<p style="font-size:0.82rem;color:var(--text-light);font-style:italic;">Aucun conseil publié.</p>'
2558      : approved.map(t => `
2559          <div class="admin-tip-item" data-tip-id="${t.id}">
2560            <div class="admin-tip-header">
2561              <span class="theme-name">${escapeHtml(t.title)} <span style="color:var(--text-light);font-weight:400;">— ${escapeHtml(t.recommendedBy?.name || 'Anonyme')}</span></span>
2562              <span>
2563                <button class="btn-edit-tip" data-tip-id="${t.id}" title="Modifier">✏️</button>
2564                <button class="btn-delete-theme" data-tip-id="${t.id}" title="Supprimer">🗑</button>
2565              </span>
2566            </div>
2567            <div class="admin-tip-edit-form hidden" id="tip-edit-${t.id}">
2568              <label class="past-edit-label">Titre</label>
2569              <input type="text" class="past-edit-input tip-edit-title" value="${escapeHtml(t.title || '')}">
2570              <label class="past-edit-label">Auteur</label>
2571              <input type="text" class="past-edit-input tip-edit-author" value="${escapeHtml(t.author || '')}">
2572              <label class="past-edit-label">Recommandé par</label>
2573              <input type="text" class="past-edit-input tip-edit-recommender" value="${escapeHtml(t.recommendedBy?.name || '')}">
2574              <label class="past-edit-label">Note</label>
2575              <textarea class="past-edit-input tip-edit-note" rows="2">${escapeHtml(t.note || '')}</textarea>
2576              <button class="btn-save-past-edit btn-save-tip-edit" data-tip-id="${t.id}">Enregistrer</button>
2577            </div>
2578          </div>`).join('')}`;
2579
2580  el.querySelectorAll('.btn-approve[data-tip-id]').forEach(btn => {
2581    btn.addEventListener('click', () => approveTip(btn.dataset.tipId));
2582  });
2583  el.querySelectorAll('.btn-reject[data-tip-id]').forEach(btn => {
2584    btn.addEventListener('click', () => rejectTip(btn.dataset.tipId));
2585  });
2586  el.querySelectorAll('.btn-delete-theme[data-tip-id]').forEach(btn => {
2587    btn.addEventListener('click', () => deleteTip(btn.dataset.tipId));
2588  });
2589  el.querySelectorAll('.btn-edit-tip').forEach(btn => {
2590    btn.addEventListener('click', () => {
2591      const form = document.getElementById(`tip-edit-${btn.dataset.tipId}`);
2592      form.classList.toggle('hidden');
2593    });
2594  });
2595  el.querySelectorAll('.btn-save-tip-edit').forEach(btn => {
2596    btn.addEventListener('click', async () => {
2597      const id = btn.dataset.tipId;
2598      const form = document.getElementById(`tip-edit-${id}`);
2599      const title = form.querySelector('.tip-edit-title').value.trim();
2600      const author = form.querySelector('.tip-edit-author').value.trim();
2601      const recommenderName = form.querySelector('.tip-edit-recommender').value.trim();
2602      const note = form.querySelector('.tip-edit-note').value.trim();
2603      if (!title) { showToast('Le titre est obligatoire.'); return; }
2604      const tip = state.tips.find(t => t.id === id);
2605      await setDoc(doc(db, 'tips', id), {
2606        title, author, note,
2607        recommendedBy: { ...tip?.recommendedBy, name: recommenderName }
2608      }, { merge: true });
2609      showToast('Conseil modifié !');
2610    });
2611  });
2612}
2613
2614// ── Render : admin — limites ─────────────────────────────
2615function renderAdminLimits() {
2616  const el = document.getElementById('admin-limits');
2617  const L = state.limits;
2618  el.innerHTML = `
2619    <p class="admin-sublabel">Limites globales</p>
2620    <div class="limits-form">
2621      <label class="past-edit-label">Propositions de livres max au total (toutes personnes confondues)</label>
2622      <input type="number" class="past-edit-input" id="limit-max-proposals-total" value="${L.maxProposalsTotal}" min="1" max="10000">
2623      <label class="past-edit-label">Propositions sans compte (localStorage, par mois) — 0 = connexion obligatoire</label>
2624      <input type="number" class="past-edit-input" id="limit-anon-proposals" value="${L.anonProposals}" min="0" max="50">
2625    </div>
2626    <p class="admin-sublabel" style="margin-top:1rem;">Limites par utilisateur connecté (par mois)</p>
2627    <div class="limits-form">
2628      <label class="past-edit-label">Propositions de livres</label>
2629      <input type="number" class="past-edit-input" id="limit-monthly-proposals" value="${L.monthlyProposals}" min="1" max="100">
2630      <label class="past-edit-label">Conseils de lecture</label>
2631      <input type="number" class="past-edit-input" id="limit-monthly-tips" value="${L.monthlyTips}" min="1" max="100">
2632    </div>
2633    <p class="admin-sublabel" style="margin-top:1rem;">Thèmes du sondage</p>
2634    <div class="limits-form">
2635      <label class="past-edit-label">Nombre max de thèmes</label>
2636      <input type="number" class="past-edit-input" id="limit-max-themes" value="${L.maxThemes}" min="1" max="100">
2637      <label class="past-edit-label">Thèmes gratuits avant validation admin</label>
2638      <input type="number" class="past-edit-input" id="limit-free-themes" value="${L.freeThemes}" min="0" max="50">
2639      <button class="btn-save-past-edit" id="btn-save-limits" style="margin-top:0.75rem;">Enregistrer les limites</button>
2640    </div>
2641    <p class="admin-sublabel" style="margin-top:1.5rem;">Membres officiels (${state.members.length})</p>
2642    <p style="font-size:0.75rem;color:var(--text-light);margin-bottom:0.5rem;">Les membres peuvent publier des critiques sans validation admin.</p>
2643    <div class="members-list">
2644      ${state.members.length === 0
2645        ? '<p style="font-size:0.82rem;color:var(--text-light);font-style:italic;">Aucun membre officiel.</p>'
2646        : state.members.map(email => `
2647          <div class="member-item">
2648            <span class="member-email">${escapeHtml(email)}</span>
2649            <button class="btn-remove-member" data-email="${escapeHtml(email)}" title="Retirer">✕</button>
2650          </div>`).join('')}
2651    </div>
2652    <div class="member-add" style="display:flex;gap:0.35rem;margin-top:0.5rem;">
2653      <input type="email" id="new-member-email" class="past-edit-input" placeholder="[email protected]" style="flex:1;">
2654      <button class="btn-save-past-edit" id="btn-add-member">Ajouter</button>
2655    </div>`;
2656  document.getElementById('btn-save-limits').addEventListener('click', () => {
2657    saveLimits({
2658      maxProposalsTotal: +document.getElementById('limit-max-proposals-total').value || 100,
2659      anonProposals:     +document.getElementById('limit-anon-proposals').value,
2660      monthlyProposals:  +document.getElementById('limit-monthly-proposals').value || 15,
2661      monthlyTips:       +document.getElementById('limit-monthly-tips').value || 15,
2662      maxThemes:         +document.getElementById('limit-max-themes').value || 20,
2663      freeThemes:        +document.getElementById('limit-free-themes').value || 2
2664    });
2665  });
2666  document.getElementById('btn-add-member').addEventListener('click', () => {
2667    const email = document.getElementById('new-member-email').value.trim();
2668    if (email) { addMember(email); document.getElementById('new-member-email').value = ''; }
2669  });
2670  el.querySelectorAll('.btn-remove-member').forEach(btn => {
2671    btn.addEventListener('click', () => {
2672      if (confirm(`Retirer ${btn.dataset.email} des membres ?`)) removeMember(btn.dataset.email);
2673    });
2674  });
2675}
2676
2677// ══════════════════════════════════════════════════════════
2678//  INITIALISATION
2679// ══════════════════════════════════════════════════════════
2680
2681// ── Init modale livre ────────────────────────────────────
2682document.getElementById('book-modal-close').addEventListener('click', closeBookModal);
2683document.getElementById('book-modal').addEventListener('click', e => {
2684  if (e.target === document.getElementById('book-modal')) closeBookModal();
2685});
2686
2687// ── Modale proposition ──────────────────────────────────
2688document.getElementById('propose-modal-close').addEventListener('click', () => {
2689  document.getElementById('propose-modal').classList.add('hidden');
2690});
2691document.getElementById('propose-modal').addEventListener('click', e => {
2692  if (e.target === document.getElementById('propose-modal')) {
2693    document.getElementById('propose-modal').classList.add('hidden');
2694  }
2695});
2696
2697// ── Admin panel ──────────────────────────────────────────
2698document.getElementById('admin-fab').addEventListener('click', () => {
2699  document.getElementById('admin-overlay').classList.remove('hidden');
2700  renderAdminSessions();
2701});
2702document.getElementById('btn-close-admin').addEventListener('click', () => {
2703  document.getElementById('admin-overlay').classList.add('hidden');
2704});
2705document.getElementById('admin-overlay').addEventListener('click', e => {
2706  if (e.target === document.getElementById('admin-overlay'))
2707    document.getElementById('admin-overlay').classList.add('hidden');
2708});
2709document.querySelectorAll('.admin-tab-btn').forEach(btn => {
2710  btn.addEventListener('click', () => {
2711    document.querySelectorAll('.admin-tab-btn').forEach(b => b.classList.remove('active'));
2712    document.querySelectorAll('.admin-tab-content').forEach(c => c.classList.add('hidden'));
2713    btn.classList.add('active');
2714    document.getElementById(`tab-${btn.dataset.tab}`).classList.remove('hidden');
2715    if (btn.dataset.tab === 'session') renderAdminSessions();
2716    if (btn.dataset.tab === 'past') renderAdminPastSessions();
2717    if (btn.dataset.tab === 'critiques') renderAdminCritiques();
2718    if (btn.dataset.tab === 'themes') renderAdminThemes();
2719    if (btn.dataset.tab === 'tips') renderAdminTips();
2720    if (btn.dataset.tab === 'limits') renderAdminLimits();
2721  });
2722});
2723
2724// ── Auth state listener ──────────────────────────────────
2725initAuthStateListener(user => {
2726  // Refresh member status
2727  userState.isMember = !!(user && (state.members.includes(user.email) || userState.isAdmin));
2728  renderHero();
2729  renderAllSessions();
2730  renderPastSessions();
2731  renderSuggestSection();
2732  renderTipForm();
2733  renderTips();
2734  renderPoll();
2735  const fab = document.getElementById('admin-fab');
2736  if (userState.isAdmin) {
2737    fab.classList.remove('hidden');
2738    if (unsubProposals) unsubProposals();
2739    unsubProposals = listenProposals();
2740    if (unsubPendingCritiques) unsubPendingCritiques();
2741    unsubPendingCritiques = listenPendingCritiques();
2742  } else {
2743    fab.classList.add('hidden');
2744    if (unsubProposals) { unsubProposals(); unsubProposals = null; }
2745    if (unsubPendingCritiques) { unsubPendingCritiques(); unsubPendingCritiques = null; }
2746    document.getElementById('admin-overlay').classList.add('hidden');
2747  }
2748});
2749
2750// ── Nav brand click ──────────────────────────────────────
2751document.querySelector('.nav-brand').addEventListener('click', e => {
2752  e.preventDefault();
2753  window.scrollTo({ top: 0, behavior: 'smooth' });
2754  renderHero();
2755  renderAllSessions();
2756  renderPastSessions();
2757  renderSuggestSection();
2758  renderTips();
2759  renderPoll();
2760});
2761
2762// ── Profile update callback ──────────────────────────────
2763window._onProfileUpdate = () => {
2764  renderNavUser();
2765};
2766
2767// ── Init shared UI ───────────────────────────────────────
2768initHamburger();
2769initAvatarDropdownClose();
2770initAuthUI();
2771initNotificationBell();
2772initBugReport();
2773
2774// ── Routes (deep linking) ────────────────────────────────
2775registerRoute('book', async ([sessionId, bookIndex]) => {
2776  await waitForData('sessions');
2777  const s = state.sessions.find(s => s.id === sessionId);
2778  if (s && s.books[+bookIndex]) {
2779    openBookModal(s.books[+bookIndex], sessionId, 'sessions', +bookIndex);
2780    return;
2781  }
2782  // Fallback : chercher dans les sessions passées
2783  await waitForData('pastSessions');
2784  const p = state.pastSessions.find(p => p.id === sessionId);
2785  if (p && p.books[+bookIndex]) {
2786    openBookModal(p.books[+bookIndex], sessionId, 'pastSessions', +bookIndex);
2787  }
2788});
2789
2790registerRoute('pastbook', async ([sessionId, bookIndex]) => {
2791  await waitForData('pastSessions');
2792  const p = state.pastSessions.find(p => p.id === sessionId);
2793  if (p && p.books[+bookIndex]) {
2794    openBookModal(p.books[+bookIndex], sessionId, 'pastSessions', +bookIndex);
2795  }
2796});
2797
2798registerRoute('tip', async ([tipId]) => {
2799  await waitForData('tips');
2800  const t = state.tips.find(t => t.id === tipId);
2801  if (t) openBookModal({ title: t.title, author: t.author, description: t.description || '', coverUrl: t.coverUrl, presenter: '' }, null, null, null);
2802});
2803
2804registerRoute('login', async () => {
2805  const { showLoginModal } = await import('./auth.js');
2806  showLoginModal();
2807});
2808
2809// ── Démarrage ────────────────────────────────────────────
2810loadLimits();
2811listenSessions();
2812listenPastSessions();
2813listenThemes();
2814listenTips();
2815listenMembers();
2816listenActivity();
2817initCalendar();
2818initScrollReveal();
2819processCurrentHash();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.