const ACTES_FRISE = ["Prologue", "Acte 1", "Acte 2", "Acte 3", "Épilogue"]; let records = []; let columns = {}; let editingId = null; // ── View switching ── function switchView(name) { document.querySelectorAll('.view').forEach(v => v.classList.remove('active')); document.getElementById('view-' + name).classList.add('active'); document.querySelectorAll('.nav-item[data-view]').forEach(n => n.classList.toggle('active', n.dataset.view === name)); try { localStorage.setItem('tdb-rdb-view', name); } catch(e) {} const topTitle = document.querySelector('.topbar-title'); const btnNew = document.querySelector('.btn-new'); // Hide topbar new button (each page has its own) const topBtnNew = document.querySelector('.topbar-right > .btn-new'); if (topBtnNew) topBtnNew.style.display = 'none'; if (name === 'vision') { topTitle.textContent = 'Vision'; } else if (name === 'redaction') { topTitle.textContent = 'Rédaction'; buildBinder(); } else if (name === 'personnages') { topTitle.textContent = 'Personnages'; loadPersonnages(); } else if (name === 'ressources') { topTitle.textContent = 'Ressources'; loadRessources(); } else { topTitle.textContent = 'Frise'; if (topBtnNew) topBtnNew.style.display = ''; } } // ── Dark mode ── function toggleDark() { document.body.classList.toggle('dark'); localStorage.setItem('tdb-dark', document.body.classList.contains('dark') ? '1' : '0'); } if (localStorage.getItem('tdb-dark') === '1') document.body.classList.add('dark'); // ── Filters ── function populateFilterOptions() { // Collect unique personnages from data const persoSet = new Set(); records.forEach(r => { if (r.Personnage) r.Personnage.split(',').forEach(p => persoSet.add(p.trim())); }); const persoSel = document.getElementById('filter-perso'); const curPerso = persoSel.value; persoSel.innerHTML = ''; [...persoSet].sort().forEach(p => { const opt = document.createElement('option'); opt.value = p; opt.textContent = p; persoSel.appendChild(opt); }); persoSel.value = curPerso; // Tags from data (free text, not from column options) const tagSet = new Set(); records.forEach(r => { if (r.Tags) r.Tags.split(',').forEach(t => { const trimmed = t.trim(); if (trimmed) tagSet.add(trimmed); }); }); const tagSel = document.getElementById('filter-tag'); const curTag = tagSel.value; tagSel.innerHTML = ''; [...tagSet].sort((a, b) => a.localeCompare(b, 'fr')).forEach(t => { const opt = document.createElement('option'); opt.value = t; opt.textContent = t; tagSel.appendChild(opt); }); tagSel.value = curTag; // Populate Rédaction filters (same data) const redPersoSel = document.getElementById('red-filter-perso'); if (redPersoSel) { const curRP = redPersoSel.value; while (redPersoSel.options.length > 1) redPersoSel.remove(1); [...persoSet].sort().forEach(p => { const opt = document.createElement('option'); opt.value = p; opt.textContent = p; redPersoSel.appendChild(opt); }); redPersoSel.value = curRP; } const redTagSel = document.getElementById('red-filter-tag'); if (redTagSel) { const curRT = redTagSel.value; while (redTagSel.options.length > 1) redTagSel.remove(1); [...tagSet].sort((a, b) => a.localeCompare(b, 'fr')).forEach(t => { const opt = document.createElement('option'); opt.value = t; opt.textContent = t; redTagSel.appendChild(opt); }); redTagSel.value = curRT; } } function applyFilters() { // Re-render frise if sort changed (sort is applied during render) renderFrise(); const perso = document.getElementById('filter-perso').value; const tag = document.getElementById('filter-tag').value; const search = document.getElementById('filter-search').value.toLowerCase().trim(); document.querySelectorAll('.card').forEach(card => { const id = Number(card.dataset.id); const r = records.find(rec => rec.Id === id); if (!r) return; let visible = true; if (perso && !(r.Personnage || '').split(',').map(s => s.trim()).includes(perso)) visible = false; if (tag && !(r.Tags || '').split(',').map(s => s.trim()).includes(tag)) visible = false; if (search) { const haystack = [r.Title, r['Résumé'], r.Personnage, r['Intérêt narratif'], r.Implications, r.Origine].filter(Boolean).join(' ').toLowerCase(); if (!haystack.includes(search)) visible = false; } card.classList.toggle('filtered-out', !visible); }); // Resize acte sections based on visible cards document.querySelectorAll('.acte-section').forEach(section => { const visibleCards = section.querySelectorAll('.card:not(.filtered-out)').length; const minW = Math.max(280, visibleCards * 274 + 20); section.style.width = minW + 'px'; }); } function clearFilters() { document.getElementById('filter-perso').value = ''; document.getElementById('filter-tag').value = ''; document.getElementById('filter-search').value = ''; document.getElementById('filter-sort').value = 'ordre'; applyFilters(); } const STATUS_TAGS = { 'À étudier': 'badge-etudier', 'Validée': 'badge-validee', 'Abandonnée': 'badge-abandonnee', 'En conflit': 'badge-conflit' }; const STATUS_COLORS = { 'À étudier': '#c4a030', 'Validée': '#5a7a3a', 'Abandonnée': '#a08a76', 'En conflit': '#8b3030' }; function tagBadgeClass(tagName) { return STATUS_TAGS[tagName] || 'badge-tag'; } function makeCard(r) { const card = document.createElement('div'); card.className = 'card'; card.dataset.id = r.Id; card.dataset.acte = r.Acte || 'En attente'; card.onclick = () => openEdit(r.Id); const persos = r.Personnage ? r.Personnage.split(',').map(p => `${esc(p.trim())}` ).join('') : ''; const likeE = r.LikeE ? 'liked' : ''; const likeM = r.LikeM ? 'liked' : ''; const comments = parseComments(r.Commentaires); const commentBadge = comments.length > 0 ? `${comments.length}` : ''; card.innerHTML = `
${esc(r.Title || '')}
${r['Résumé'] ? `
${renderMarkdown(r['Résumé'])}
` : ''}
${(r.Tags || '').split(',').filter(t => t.trim()).map(t => `${esc(t.trim())}`).join('')} ${persos} ${commentBadge}
`; return card; } async function toggleLike(e, id, who) { e.stopPropagation(); const r = records.find(r => r.Id === id); if (!r) return; const field = who === 'E' ? 'LikeE' : 'LikeM'; const newVal = r[field] ? 0 : 1; r[field] = newVal; renderFrise(); await fetch(`/api/records/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ [field]: newVal }), }); } // ── Card comments ── let commentAuthor = 'E'; function parseComments(raw) { if (!raw) return []; try { return JSON.parse(raw); } catch { return []; } } function renderCardComments(comments) { const list = document.getElementById('card-comment-list'); list.innerHTML = comments.map((c, i) => `
${c.auteur === 'M' ? 'Myriam' : 'Etienne'} ${esc(c.date || '')}
${esc(c.texte || '')}
`).join(''); } async function addCardComment() { if (!editingId) return; const input = document.getElementById('card-comment-input'); const texte = input.value.trim(); if (!texte) return; const r = records.find(r => r.Id === editingId); if (!r) return; const comments = parseComments(r.Commentaires); const now = new Date().toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' }); comments.push({ auteur: commentAuthor, date: now, texte }); const json = JSON.stringify(comments); r.Commentaires = json; renderCardComments(comments); input.value = ''; await fetch(`/api/records/${editingId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ Commentaires: json }), }); renderFrise(); } async function deleteCardComment(index) { if (!editingId) return; const r = records.find(r => r.Id === editingId); if (!r) return; const comments = parseComments(r.Commentaires); comments.splice(index, 1); const json = comments.length > 0 ? JSON.stringify(comments) : null; r.Commentaires = json; renderCardComments(comments); await fetch(`/api/records/${editingId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ Commentaires: json || '' }), }); renderFrise(); } function toggleCommentAuthor() { commentAuthor = commentAuthor === 'E' ? 'M' : 'E'; const btn = document.getElementById('comment-author-btn'); btn.textContent = commentAuthor === 'E' ? 'Etienne' : 'Myriam'; } function sortRecords(a, b, mode) { if (mode === 'date-desc') return new Date(b.CreatedAt || 0) - new Date(a.CreatedAt || 0); if (mode === 'date-asc') return new Date(a.CreatedAt || 0) - new Date(b.CreatedAt || 0); if (mode === 'alpha') return (a.Title || '').localeCompare(b.Title || '', 'fr'); return (a.Ordre || 0) - (b.Ordre || 0); } function renderFrise() { const area = document.getElementById('timeline-area'); area.innerHTML = ''; // Build horizontal timeline const container = document.createElement('div'); container.style.cssText = 'display:flex; min-width:min-content;'; const sortMode = document.getElementById('filter-sort')?.value || 'ordre'; ACTES_FRISE.forEach(acte => { const acteRecords = records .filter(r => (r.Acte || 'En attente') === acte) .sort((a, b) => sortRecords(a, b, sortMode)); const section = document.createElement('div'); section.className = 'acte-section'; section.dataset.acte = acte; section.innerHTML = `
${esc(acte)}
`; const row = document.createElement('div'); row.className = 'cards-row'; row.dataset.acte = acte; acteRecords.forEach(r => row.appendChild(makeCard(r))); section.appendChild(row); container.appendChild(section); // SortableJS on horizontal row new Sortable(row, { group: 'frise', animation: 200, direction: 'horizontal', ghostClass: 'sortable-ghost', chosenClass: 'sortable-chosen', onEnd: handleDrop, }); }); area.appendChild(container); // Waiting zone const waitingCards = document.getElementById('waiting-cards'); waitingCards.innerHTML = ''; const waitingRecords = records .filter(r => !r.Acte || r.Acte === 'En attente') .sort((a, b) => sortRecords(a, b, sortMode)); waitingRecords.forEach(r => waitingCards.appendChild(makeCard(r))); new Sortable(waitingCards, { group: 'frise', animation: 200, direction: 'horizontal', ghostClass: 'sortable-ghost', chosenClass: 'sortable-chosen', onEnd: handleDrop, }); } async function handleDrop(evt) { const toContainer = evt.to; const newActe = toContainer.dataset.acte || 'En attente'; // Collect new order for target const updates = []; const cards = toContainer.querySelectorAll('.card'); cards.forEach((c, i) => { updates.push({ id: Number(c.dataset.id), Acte: newActe, Ordre: i + 1 }); }); // Reorder source if different if (evt.from !== evt.to) { const srcActe = evt.from.dataset.acte || 'En attente'; const srcCards = evt.from.querySelectorAll('.card'); srcCards.forEach((c, i) => { updates.push({ id: Number(c.dataset.id), Acte: srcActe, Ordre: i + 1 }); }); } // Update local state updates.forEach(u => { const rec = records.find(r => r.Id === u.id); if (rec) { rec.Acte = u.Acte; rec.Ordre = u.Ordre; } }); await fetch('/api/reorder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updates), }); } function populateSelects() { ['Acte'].forEach(name => { const sel = document.getElementById(`f-${name.toLowerCase()}`); if (!sel || !columns[name]) return; while (sel.options.length > 1) sel.remove(1); columns[name].options.forEach(o => { const opt = document.createElement('option'); opt.value = o; opt.textContent = o; sel.appendChild(opt); }); }); const container = document.getElementById('f-perso'); container.innerHTML = ''; if (columns.Personnage) { columns.Personnage.options.forEach(p => { const tag = document.createElement('span'); tag.className = 'ms-tag'; tag.textContent = p; tag.onclick = () => tag.classList.toggle('selected'); container.appendChild(tag); }); } const addBtn = document.createElement('span'); addBtn.className = 'ms-add'; addBtn.textContent = '+'; addBtn.title = 'Ajouter un personnage'; addBtn.onclick = async () => { const name = prompt('Nom du personnage :'); if (!name || !name.trim()) return; const trimmed = name.trim(); if (columns.Personnage && columns.Personnage.options.includes(trimmed)) { alert('Ce personnage existe déjà.'); return; } const res = await fetch('/api/columns/personnage', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: trimmed }), }); if (res.ok) { if (!columns.Personnage) columns.Personnage = { type: 'MultiSelect', options: [] }; columns.Personnage.options.push(trimmed); const tag = document.createElement('span'); tag.className = 'ms-tag selected'; tag.textContent = trimmed; tag.onclick = () => tag.classList.toggle('selected'); container.insertBefore(tag, addBtn); } else { alert('Erreur lors de l\'ajout.'); } }; container.appendChild(addBtn); } function openNew() { editingId = null; document.getElementById('modal-label').textContent = 'Nouvelle idee'; document.getElementById('f-title').value = ''; renderModalTags(['À étudier']); updateTagSuggestions(); document.getElementById('f-tags-input').value = ''; document.getElementById('f-acte').value = 'En attente'; document.getElementById('f-resume').value = ''; document.getElementById('f-interet').value = ''; document.getElementById('f-implications').value = ''; document.getElementById('f-origine').value = ''; document.getElementById('f-lien').value = ''; document.querySelectorAll('#f-perso .ms-tag').forEach(t => t.classList.remove('selected')); document.getElementById('btn-delete').style.display = 'none'; document.getElementById('btn-history-frise').style.display = 'none'; document.getElementById('btn-rediger').style.display = 'none'; document.getElementById('card-comments-section').style.display = 'none'; document.getElementById('card-comment-list').innerHTML = ''; document.getElementById('card-comment-input').value = ''; document.getElementById('modal').classList.add('open'); } function openEdit(id) { const r = records.find(rec => rec.Id === id); if (!r) return; editingId = id; document.getElementById('modal-label').textContent = 'Modifier'; document.getElementById('f-title').value = r.Title || ''; renderModalTags((r.Tags || '').split(',').map(s => s.trim()).filter(Boolean)); updateTagSuggestions(); document.getElementById('f-tags-input').value = ''; document.getElementById('f-acte').value = r.Acte || ''; document.getElementById('f-resume').value = r['Résumé'] || ''; document.getElementById('f-interet').value = r['Intérêt narratif'] || ''; document.getElementById('f-implications').value = r.Implications || ''; document.getElementById('f-origine').value = r.Origine || ''; document.getElementById('f-lien').value = r['Lien rédaction'] || ''; const selected = r.Personnage ? r.Personnage.split(',').map(s => s.trim()) : []; document.querySelectorAll('#f-perso .ms-tag').forEach(t => { t.classList.toggle('selected', selected.includes(t.textContent)); }); document.getElementById('btn-delete').style.display = 'block'; document.getElementById('btn-history-frise').style.display = ''; document.getElementById('btn-rediger').style.display = ''; // Comments const comments = parseComments(r.Commentaires); document.getElementById('card-comments-section').style.display = 'block'; renderCardComments(comments); document.getElementById('card-comment-input').value = ''; document.getElementById('modal').classList.add('open'); } function closeModal() { document.getElementById('modal').classList.remove('open'); editingId = null; } async function saveCard() { const persos = []; document.querySelectorAll('#f-perso .ms-tag.selected').forEach(t => persos.push(t.textContent)); const data = { Title: document.getElementById('f-title').value, Tags: getModalTags().join(', ') || null, Acte: document.getElementById('f-acte').value || 'En attente', Personnage: persos.join(',') || null, 'Résumé': document.getElementById('f-resume').value || null, 'Intérêt narratif': document.getElementById('f-interet').value || null, Implications: document.getElementById('f-implications').value || null, Origine: document.getElementById('f-origine').value || null, 'Lien rédaction': document.getElementById('f-lien').value || null, }; if (editingId) { await fetch(`/api/records/${editingId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); } else { data.Ordre = records.filter(r => r.Acte === data.Acte).length + 1; await fetch('/api/records', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); } closeModal(); await loadData(); } async function deleteCard() { if (!editingId) return; if (!confirm('Supprimer cette idee ?')) return; await fetch(`/api/records/${editingId}`, { method: 'DELETE' }); closeModal(); await loadData(); } function goToRedaction(id) { const scene = records.find(r => r.Id === id); if (!scene) return; closeModal(); switchView('redaction'); setTimeout(() => openScene(scene), 300); } async function loadData() { const [recRes, colRes] = await Promise.all([ fetch('/api/records'), fetch('/api/columns'), ]); if (recRes.status === 401) { window.location.reload(); return; } const recData = await recRes.json(); records = recData.list || []; columns = await colRes.json(); populateSelects(); populateFilterOptions(); applyFilters(); // Restore last active view try { const saved = localStorage.getItem('tdb-rdb-view'); if (saved && document.getElementById('view-' + saved)) switchView(saved); } catch(e) {} } function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; } // Sanitize HTML string and return a DocumentFragment (safe for appendChild) function createSanitizedFragment(html) { const clean = typeof DOMPurify !== 'undefined' ? DOMPurify.sanitize(html) : esc(html); const tpl = document.createElement('template'); tpl.innerHTML = clean; return tpl.content; } // Render Markdown to sanitized HTML string function renderMarkdown(text) { if (!text) return ''; const raw = typeof marked !== 'undefined' ? marked.parse(text) : esc(text); return typeof DOMPurify !== 'undefined' ? DOMPurify.sanitize(raw) : esc(text); } // ── Tags input ── function getModalTags() { return [...document.querySelectorAll('#f-tags .tag-chip')].map(c => c.dataset.tag); } function collectExistingTags() { const tagSet = new Set(); records.forEach(r => { if (r.Tags) r.Tags.split(',').forEach(t => { const trimmed = t.trim(); if (trimmed) tagSet.add(trimmed); }); }); return [...tagSet].sort((a, b) => a.localeCompare(b, 'fr')); } function updateTagSuggestions() { const select = document.getElementById('f-tags-select'); const current = getModalTags(); const existing = collectExistingTags().filter(t => !current.includes(t)); select.innerHTML = ''; existing.forEach(t => { const opt = document.createElement('option'); opt.value = t; opt.textContent = t; select.appendChild(opt); }); } function addExistingTag() { const select = document.getElementById('f-tags-select'); const val = select.value; if (!val) return; const current = getModalTags(); if (!current.includes(val)) { renderModalTags([...current, val]); } select.value = ''; updateTagSuggestions(); } function renderModalTags(tags) { const container = document.getElementById('f-tags'); container.querySelectorAll('.tag-chip').forEach(c => c.remove()); const input = document.getElementById('f-tags-input'); (tags || []).forEach(tag => { const chip = document.createElement('span'); chip.className = 'tag-chip'; chip.dataset.tag = tag; chip.innerHTML = `${esc(tag)}×`; container.insertBefore(chip, input); }); } function addTag() { const input = document.getElementById('f-tags-input'); const val = input.value.trim(); if (!val) return; const current = getModalTags(); if (!current.includes(val)) { renderModalTags([...current, val]); } input.value = ''; updateTagSuggestions(); } document.getElementById('modal').addEventListener('click', e => { if (e.target === document.getElementById('modal')) closeModal(); }); document.addEventListener('keydown', e => { if (e.key === 'Escape') { closeModal(); if (typeof closePersoModal === 'function') closePersoModal(); if (typeof closeResModal === 'function') closeResModal(); } }); loadData(); // ══════════════════════════════════════ // ══ VISION — Editable sections ══ // ══════════════════════════════════════ let visionData = {}; // section -> {Id, Contenu} const PLACEHOLDERS = { 'trois-mots': 'Les trois mots qui définissent votre univers...', 'hero-sous-titre': 'Sous-titre ou accroche...', 'phrase': 'Cliquez pour écrire la phrase qui résume votre histoire...', 'phrase-detail': 'Un détail, une nuance, une précision...', 'synopsis': 'Développez ici le résumé de votre histoire en quelques paragraphes...', }; const FIELD_MAP = { 'trois-mots': 'v-trois-mots', 'hero-sous-titre': 'v-hero-sub', 'phrase': 'v-phrase', 'phrase-detail': 'v-phrase-detail', 'synopsis': 'v-synopsis', }; function showSaved() { const el = document.getElementById('vision-saving'); el.classList.add('show'); setTimeout(() => el.classList.remove('show'), 1200); } async function saveVisionSection(section, contenu) { const row = visionData[section]; if (!row) return; row.Contenu = contenu; // update local cache immediately fetch(`/api/vision/${row.Id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ Contenu: contenu }), }).then(() => showSaved()); } function setupEditable(elId, section) { const el = document.getElementById(elId); if (!el) return; const placeholder = PLACEHOLDERS[section] || ''; function updatePlaceholder() { const text = el.textContent.trim(); if (!text || text === placeholder) { el.classList.add('placeholder'); if (!text) el.textContent = placeholder; } else { el.classList.remove('placeholder'); } } el.addEventListener('focus', () => { if (el.classList.contains('placeholder')) { el.textContent = ''; el.classList.remove('placeholder'); } }); el.addEventListener('blur', () => { const text = el.textContent.trim(); if (text && text !== placeholder) { const row = visionData[section]; if (row && row.Contenu !== text) { saveVisionSection(section, text); } } else { el.textContent = placeholder; el.classList.add('placeholder'); const row = visionData[section]; if (row && row.Contenu) { saveVisionSection(section, ''); } } }); // Prevent Enter creating divs in single-line fields if (section !== 'synopsis') { el.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); el.blur(); } }); } updatePlaceholder(); } // ── Tags ── function renderTags() { const container = document.getElementById('v-tags'); const row = visionData['tags']; let tags = []; try { tags = JSON.parse(row?.Contenu || '[]'); } catch { tags = []; } // Normalize: old format {label, type} → just strings tags = tags.map(t => typeof t === 'string' ? t : t.label || ''); container.innerHTML = ''; tags.forEach((label, i) => { const span = document.createElement('span'); span.className = 'tag ambiance'; span.innerHTML = `${esc(label)}×`; container.appendChild(span); }); const addBtn = document.createElement('button'); addBtn.className = 'tag-add'; addBtn.textContent = '+ Ajouter'; addBtn.onclick = () => { const label = prompt('Nouveau tag :'); if (!label) return; tags.push(label); saveVisionSection('tags', JSON.stringify(tags)); renderTags(); }; container.appendChild(addBtn); } function removeTag(index) { const row = visionData['tags']; let tags = []; try { tags = JSON.parse(row?.Contenu || '[]'); } catch { return; } tags = tags.map(t => typeof t === 'string' ? t : t.label || ''); tags.splice(index, 1); saveVisionSection('tags', JSON.stringify(tags)); renderTags(); } // ── Lists (on-veut / on-ne-veut-pas) — items with title + optional description ── function renderList(section, containerId, cssClass) { const container = document.getElementById(containerId); const row = visionData[section]; let items = []; try { items = JSON.parse(row?.Contenu || '[]'); } catch { items = []; } // Normalize: old format was plain strings, new format is {title, desc} items = items.map(it => typeof it === 'string' ? { title: it, desc: '' } : it); container.innerHTML = ''; items.forEach((item, i) => { const hasDesc = !!item.desc; const div = document.createElement('div'); div.className = `list-item ${cssClass}${hasDesc ? ' expanded' : ''}`; const content = document.createElement('div'); content.className = 'list-item-text'; content.style.flex = '1'; const titleEl = document.createElement('div'); titleEl.className = 'list-item-title'; titleEl.contentEditable = 'true'; titleEl.textContent = item.title; titleEl.addEventListener('blur', () => { const v = titleEl.textContent.trim(); if (v && v !== items[i].title) { items[i].title = v; saveVisionSection(section, JSON.stringify(items)); } }); titleEl.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); titleEl.blur(); } }); content.appendChild(titleEl); const descEl = document.createElement('div'); descEl.className = 'list-item-desc'; descEl.contentEditable = 'true'; descEl.setAttribute('data-placeholder', 'Développer... (Markdown : - item pour les listes)'); descEl.style.display = 'none'; // Render Markdown preview (sanitized) const descPreview = document.createElement('div'); descPreview.className = 'list-item-desc-preview'; descPreview.style.display = 'none'; if (hasDesc) { const rawHtml = typeof marked !== 'undefined' ? marked.parse(item.desc) : esc(item.desc); descPreview.textContent = ''; descPreview.appendChild(createSanitizedFragment(rawHtml)); descPreview.style.display = 'block'; } descPreview.onclick = () => { descPreview.style.display = 'none'; descEl.style.display = 'block'; descEl.textContent = items[i].desc || ''; descEl.focus(); }; descEl.addEventListener('blur', () => { const v = descEl.innerText.trim(); if (v !== (items[i].desc || '')) { items[i].desc = v; saveVisionSection(section, JSON.stringify(items)); } descEl.style.display = 'none'; if (v) { const rawHtml = typeof marked !== 'undefined' ? marked.parse(v) : esc(v); descPreview.textContent = ''; descPreview.appendChild(createSanitizedFragment(rawHtml)); descPreview.style.display = 'block'; } else { descPreview.style.display = 'none'; div.classList.remove('expanded'); expandBtn.textContent = '▸'; expandBtn.title = 'Développer'; } }); content.appendChild(descPreview); content.appendChild(descEl); const bullet = document.createElement('span'); bullet.className = 'bullet'; const actions = document.createElement('span'); actions.style.cssText = 'display:flex; gap:6px; align-items:center;'; const expandBtn = document.createElement('span'); expandBtn.className = 'list-item-expand'; expandBtn.textContent = hasDesc ? '▾' : '▸'; expandBtn.title = hasDesc ? 'Réduire' : 'Développer'; expandBtn.style.cursor = 'pointer'; expandBtn.onclick = () => { if (div.classList.contains('expanded')) { div.classList.remove('expanded'); descPreview.style.display = 'none'; descEl.style.display = 'none'; expandBtn.textContent = '▸'; expandBtn.title = 'Développer'; } else { div.classList.add('expanded'); if (items[i].desc) { const rawHtml = typeof marked !== 'undefined' ? marked.parse(items[i].desc) : esc(items[i].desc); descPreview.textContent = ''; descPreview.appendChild(createSanitizedFragment(rawHtml)); descPreview.style.display = 'block'; } else { descEl.style.display = 'block'; descEl.textContent = ''; descEl.focus(); } expandBtn.textContent = '▾'; expandBtn.title = 'Réduire'; } }; if (hasDesc) div.classList.add('expanded'); const removeBtn = document.createElement('span'); removeBtn.className = 'list-item-remove'; removeBtn.innerHTML = '×'; removeBtn.onclick = () => { items.splice(i, 1); saveVisionSection(section, JSON.stringify(items)); renderList(section, containerId, cssClass); }; actions.appendChild(expandBtn); actions.appendChild(removeBtn); div.appendChild(bullet); div.appendChild(content); div.appendChild(actions); container.appendChild(div); }); const addEl = document.createElement('div'); addEl.className = 'list-add'; addEl.innerHTML = '+ Ajouter un élément'; addEl.onclick = () => { const text = prompt('Nouvel élément :'); if (!text) return; items.push({ title: text, desc: '' }); saveVisionSection(section, JSON.stringify(items)); renderList(section, containerId, cssClass); }; container.appendChild(addEl); } function removeListItem(section, containerId, cssClass, index) { const row = visionData[section]; let items = []; try { items = JSON.parse(row?.Contenu || '[]'); } catch { return; } items.splice(index, 1); saveVisionSection(section, JSON.stringify(items)); renderList(section, containerId, cssClass); } // ── Moodboard (images + free text captions) ── function renderMoodboard() { const container = document.getElementById('v-moodboard'); const row = visionData['moodboard']; let items = []; try { items = JSON.parse(row?.Contenu || '[]'); } catch { items = []; } container.innerHTML = ''; items.forEach((item, i) => { const card = document.createElement('div'); card.className = 'mood-card'; const imgDiv = document.createElement('div'); const hasImg = !!item.image; imgDiv.className = 'mood-card-img' + (hasImg ? ' has-image' : ' empty'); if (hasImg) { const zoom = item.zoom ?? 100; imgDiv.style.backgroundImage = `url(${item.image})`; imgDiv.style.backgroundSize = `${zoom}%`; imgDiv.style.backgroundPosition = `${item.posX ?? 50}% ${item.posY ?? 50}%`; } if (!hasImg) { imgDiv.onclick = () => { moodUploadTarget = i; document.getElementById('mood-file-input').click(); }; } else { // Drag to reposition (2D) — double-click to replace image let startX, startY, startPosX, startPosY, didDrag; const onMove = (e) => { const dx = e.clientX - startX, dy = e.clientY - startY; if (!didDrag && Math.abs(dx) < 4 && Math.abs(dy) < 4) return; didDrag = true; const rect = imgDiv.getBoundingClientRect(); const scale = (item.zoom ?? 100) / 100; item.posX = Math.max(0, Math.min(100, startPosX + (dx / rect.width) * -80 / scale)); item.posY = Math.max(0, Math.min(100, startPosY + (dy / rect.height) * -80 / scale)); imgDiv.style.backgroundPosition = `${item.posX}% ${item.posY}%`; }; const onUp = () => { imgDiv.classList.remove('dragging'); window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); if (didDrag) saveVisionSection('moodboard', JSON.stringify(items)); }; imgDiv.addEventListener('mousedown', (e) => { if (e.button !== 0) return; e.preventDefault(); didDrag = false; imgDiv.classList.add('dragging'); startX = e.clientX; startY = e.clientY; startPosX = item.posX ?? 50; startPosY = item.posY ?? 50; window.addEventListener('mousemove', onMove); window.addEventListener('mouseup', onUp); }); // Ctrl+scroll to zoom (prevents accidental zoom) imgDiv.addEventListener('wheel', (e) => { if (!e.ctrlKey && !e.metaKey) return; e.preventDefault(); const delta = e.deltaY > 0 ? -10 : 10; item.zoom = Math.max(100, Math.min(400, (item.zoom ?? 100) + delta)); imgDiv.style.backgroundSize = `${item.zoom}%`; saveVisionSection('moodboard', JSON.stringify(items)); }, { passive: false }); imgDiv.addEventListener('dblclick', () => { moodUploadTarget = i; document.getElementById('mood-file-input').click(); }); } const caption = document.createElement('div'); caption.className = 'mood-card-caption'; caption.contentEditable = 'true'; caption.textContent = item.caption || ''; caption.setAttribute('placeholder', 'Légende...'); caption.addEventListener('blur', () => { const v = caption.textContent.trim(); if (v !== (items[i].caption || '')) { items[i].caption = v; saveVisionSection('moodboard', JSON.stringify(items)); } }); const removeBtn = document.createElement('button'); removeBtn.className = 'mood-card-remove'; removeBtn.innerHTML = '×'; removeBtn.onclick = (e) => { e.stopPropagation(); removeMoodItem(i); }; // Drag handle for reordering const handle = document.createElement('div'); handle.className = 'mood-card-handle'; handle.innerHTML = '⠿'; if (hasImg) { card.draggable = false; handle.addEventListener('mousedown', () => { card.draggable = true; }); window.addEventListener('mouseup', () => { card.draggable = false; }); card.addEventListener('dragstart', (e) => { moodDragIndex = i; card.classList.add('dragging-card'); e.dataTransfer.effectAllowed = 'move'; }); card.addEventListener('dragend', () => { card.classList.remove('dragging-card'); card.draggable = false; moodDragIndex = -1; container.querySelectorAll('.mood-card').forEach(c => c.classList.remove('drag-over')); }); card.addEventListener('dragover', (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; card.classList.add('drag-over'); }); card.addEventListener('dragleave', () => { card.classList.remove('drag-over'); }); card.addEventListener('drop', (e) => { e.preventDefault(); card.classList.remove('drag-over'); if (moodDragIndex < 0 || moodDragIndex === i) return; const moved = items.splice(moodDragIndex, 1)[0]; items.splice(i, 0, moved); saveVisionSection('moodboard', JSON.stringify(items)); renderMoodboard(); }); } card.appendChild(removeBtn); card.appendChild(handle); card.appendChild(imgDiv); card.appendChild(caption); container.appendChild(card); }); const addCard = document.createElement('div'); addCard.className = 'mood-add'; addCard.innerHTML = '
+
'; addCard.onclick = () => { items.push({ caption: '', image: '' }); saveVisionSection('moodboard', JSON.stringify(items)); renderMoodboard(); }; container.appendChild(addCard); } let moodUploadTarget = -1; let moodDragIndex = -1; document.addEventListener('DOMContentLoaded', () => { const moodInput = document.getElementById('mood-file-input'); if (moodInput) moodInput.addEventListener('change', function() { const file = this.files[0]; if (!file || moodUploadTarget < 0) return; const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = async () => { const maxW = 1200; let w = img.width, h = img.height; if (w > maxW) { h = Math.round(h * maxW / w); w = maxW; } const canvas = document.createElement('canvas'); canvas.width = w; canvas.height = h; canvas.getContext('2d').drawImage(img, 0, 0, w, h); const dataUrl = canvas.toDataURL('image/jpeg', 0.8); try { const res = await fetch('/api/mood-upload', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dataUrl, index: moodUploadTarget }), }); const data = await res.json(); if (data.path) { const row = visionData['moodboard']; let items = []; try { items = JSON.parse(row?.Contenu || '[]'); } catch { items = []; } if (items[moodUploadTarget]) { items[moodUploadTarget].image = data.path; items[moodUploadTarget].posX = 50; items[moodUploadTarget].posY = 50; items[moodUploadTarget].zoom = 100; saveVisionSection('moodboard', JSON.stringify(items)); renderMoodboard(); } } } catch (err) { console.error('Mood upload error:', err); } }; img.src = e.target.result; }; reader.readAsDataURL(file); this.value = ''; }); }); function removeMoodItem(index) { const row = visionData['moodboard']; let items = []; try { items = JSON.parse(row?.Contenu || '[]'); } catch { return; } items.splice(index, 1); saveVisionSection('moodboard', JSON.stringify(items)); renderMoodboard(); } // ── Hero image with drag-to-reposition ── let heroMeta = { posY: 50, height: 400 }; // posY in %, height in px function applyHeroImage(dataUrl) { const banner = document.getElementById('hero-banner'); if (dataUrl) { banner.style.backgroundImage = `url(${dataUrl})`; banner.classList.add('has-image'); } else { banner.style.backgroundImage = ''; banner.classList.remove('has-image'); } } function applyHeroMeta() { const banner = document.getElementById('hero-banner'); banner.style.backgroundPosition = `center ${heroMeta.posY}%`; banner.style.height = heroMeta.height + 'px'; } function saveHeroMeta() { saveVisionSection('hero-meta', JSON.stringify({ posY: heroMeta.posY, height: heroMeta.height })); } // Drag to reposition (function() { const banner = document.getElementById('hero-banner'); let dragging = false, startY = 0, startPos = 0; banner.addEventListener('mousedown', (e) => { if (!banner.classList.contains('has-image')) return; if (e.target.closest('[contenteditable]') || e.target.closest('.hero-controls') || e.target.closest('button')) return; dragging = true; startY = e.clientY; startPos = heroMeta.posY; banner.classList.add('dragging'); e.preventDefault(); }); window.addEventListener('mousemove', (e) => { if (!dragging) return; const delta = e.clientY - startY; const sensitivity = 0.15; heroMeta.posY = Math.max(0, Math.min(100, startPos + delta * sensitivity)); banner.style.backgroundPosition = `center ${heroMeta.posY}%`; }); window.addEventListener('mouseup', () => { if (!dragging) return; dragging = false; banner.classList.remove('dragging'); saveHeroMeta(); }); })(); async function uploadHeroImage(input) { const file = input.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = async (e) => { const img = new Image(); img.onload = async () => { const maxW = 1600; let w = img.width, h = img.height; if (w > maxW) { h = Math.round(h * maxW / w); w = maxW; } const canvas = document.createElement('canvas'); canvas.width = w; canvas.height = h; canvas.getContext('2d').drawImage(img, 0, 0, w, h); const dataUrl = canvas.toDataURL('image/jpeg', 0.85); // Upload file to server (too large for NocoDB LongText) try { const res = await fetch('/api/hero-upload', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dataUrl }), }); const data = await res.json(); if (data.path) { applyHeroImage(data.path); heroMeta.posY = 50; applyHeroMeta(); saveVisionSection('hero-image', data.path); saveHeroMeta(); } } catch (err) { console.error('Hero upload error:', err); } }; img.src = e.target.result; }; reader.readAsDataURL(file); input.value = ''; } // ── Load Vision data ── async function loadVision() { try { const res = await fetch('/api/vision'); if (res.status === 401) return; const data = await res.json(); const rows = data.list || []; // Index by section visionData = {}; rows.forEach(r => { visionData[r.Section] = r; }); // Populate text fields for (const [section, elId] of Object.entries(FIELD_MAP)) { const el = document.getElementById(elId); if (!el) continue; const contenu = visionData[section]?.Contenu || ''; el.textContent = contenu || PLACEHOLDERS[section] || ''; setupEditable(elId, section); } // Hero image (base64 brut) + meta (position/hauteur séparées) applyHeroImage(visionData['hero-image']?.Contenu || ''); try { const meta = JSON.parse(visionData['hero-meta']?.Contenu || '{}'); heroMeta.posY = meta.posY ?? 50; heroMeta.height = meta.height ?? 400; } catch { /* defaults */ } applyHeroMeta(); // Render structured sections renderTags(); renderList('on-veut', 'v-on-veut', 'yes'); renderList('on-ne-veut-pas', 'v-on-ne-veut-pas', 'no'); renderMoodboard(); } catch (e) { console.error('Vision load error:', e); } } loadVision(); // ══════════════════════════════════════════════════════ // ══ PERSONNAGES ══ // ══════════════════════════════════════════════════════ let personnages = []; let editingPersoId = null; let pendingPersoImage = null; async function loadPersonnages() { try { const res = await fetch('/api/personnages'); if (res.status === 401) return; const data = await res.json(); personnages = data.list || []; renderPersonnages(); } catch (e) { console.error('Personnages load error:', e); } } function renderPersonnages() { const grid = document.getElementById('perso-grid'); grid.textContent = ''; personnages.forEach(p => { const card = document.createElement('div'); card.className = 'perso-card'; card.onclick = () => openPersoEdit(p.Id); // Portrait if (p.Image) { const img = document.createElement('img'); img.className = 'perso-card-portrait'; img.src = p.Image; img.alt = p.Nom || ''; card.appendChild(img); } const body = document.createElement('div'); body.className = 'perso-card-body'; // Nom const nameEl = document.createElement('div'); nameEl.className = 'perso-card-name'; nameEl.textContent = p.Nom || ''; body.appendChild(nameEl); // Rôle if (p.Role) { const roleEl = document.createElement('div'); roleEl.className = 'perso-card-role'; roleEl.textContent = p.Role; body.appendChild(roleEl); } // Description (tronquée) if (p.Description) { const descEl = document.createElement('div'); descEl.className = 'perso-card-desc perso-card-text'; descEl.appendChild(createSanitizedFragment(renderMarkdown(p.Description))); body.appendChild(descEl); } // Tags const tagList = (p.Tags || '').split(',').filter(t => t.trim()); if (tagList.length) { const tagsEl = document.createElement('div'); tagsEl.className = 'perso-card-tags'; tagList.forEach(t => { const badge = document.createElement('span'); badge.className = `card-badge ${tagBadgeClass(t.trim())}`; badge.textContent = t.trim(); tagsEl.appendChild(badge); }); body.appendChild(tagsEl); } // Compteur scènes const sceneCount = records.filter(r => r.Personnage && r.Personnage.split(',').map(s => s.trim()).includes(p.Nom) ).length; if (sceneCount) { const countEl = document.createElement('div'); countEl.className = 'perso-card-scene-count'; countEl.textContent = `${sceneCount} scène${sceneCount > 1 ? 's' : ''}`; body.appendChild(countEl); } card.appendChild(body); grid.appendChild(card); }); } function getLinkedScenes(nom) { return records.filter(r => r.Personnage && r.Personnage.split(',').map(s => s.trim()).includes(nom) ).sort((a, b) => { const ai = ACTE_ORDER.indexOf(a.Acte), bi = ACTE_ORDER.indexOf(b.Acte); if (ai !== bi) return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi); return (a.Ordre || 0) - (b.Ordre || 0); }); } function buildModalScenes(nom) { const container = document.getElementById('fp-scenes-container'); container.textContent = ''; const scenes = getLinkedScenes(nom); if (!scenes.length) return; const wrapper = document.createElement('div'); wrapper.className = 'perso-modal-scenes'; const title = document.createElement('div'); title.className = 'perso-modal-scenes-title'; title.textContent = `Scènes (${scenes.length})`; wrapper.appendChild(title); // Group by acte const grouped = {}; scenes.forEach(s => { const acte = s.Acte || 'Sans acte'; if (!grouped[acte]) grouped[acte] = []; grouped[acte].push(s); }); ACTE_ORDER.forEach(acte => { if (!grouped[acte] || !grouped[acte].length) return; const acteTitle = document.createElement('div'); acteTitle.className = 'perso-modal-scenes-acte'; acteTitle.textContent = acte; wrapper.appendChild(acteTitle); const list = document.createElement('div'); list.className = 'perso-modal-scenes-list'; grouped[acte].forEach(scene => { const link = document.createElement('a'); link.textContent = scene.Title; link.onclick = (e) => { e.stopPropagation(); closePersoModal(); switchView('frise'); setTimeout(() => openEdit(scene.Id), 200); }; list.appendChild(link); }); wrapper.appendChild(list); }); container.appendChild(wrapper); } function openPersoModal() { editingPersoId = null; pendingPersoImage = null; document.getElementById('perso-modal-label').textContent = 'Nouveau personnage'; document.getElementById('fp-nom').value = ''; document.getElementById('fp-role').value = ''; document.getElementById('fp-desc').value = ''; document.getElementById('fp-origines').value = ''; document.getElementById('fp-backstory').value = ''; document.getElementById('fp-tags').value = ''; document.getElementById('fp-image-input').value = ''; document.getElementById('fp-image-preview').innerHTML = ''; document.getElementById('fp-scenes-container').textContent = ''; document.getElementById('btn-delete-perso').style.display = 'none'; document.getElementById('perso-modal').classList.add('open'); } function openPersoEdit(id) { const p = personnages.find(x => x.Id === id); if (!p) return; editingPersoId = id; pendingPersoImage = null; document.getElementById('perso-modal-label').textContent = 'Modifier'; document.getElementById('fp-nom').value = p.Nom || ''; document.getElementById('fp-role').value = p.Role || ''; document.getElementById('fp-desc').value = p.Description || ''; document.getElementById('fp-origines').value = p.Origines || ''; document.getElementById('fp-backstory').value = p.Backstory || ''; document.getElementById('fp-tags').value = p.Tags || ''; document.getElementById('fp-image-input').value = ''; document.getElementById('fp-image-preview').innerHTML = p.Image ? `` : ''; buildModalScenes(p.Nom); document.getElementById('btn-delete-perso').style.display = 'block'; document.getElementById('perso-modal').classList.add('open'); } function closePersoModal() { document.getElementById('perso-modal').classList.remove('open'); editingPersoId = null; pendingPersoImage = null; } function previewPersoImage(input) { const file = input.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = e => { pendingPersoImage = e.target.result; document.getElementById('fp-image-preview').innerHTML = ``; }; reader.readAsDataURL(file); } async function savePerso() { let imagePath = null; if (pendingPersoImage) { const upRes = await fetch('/api/ressource-upload', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dataUrl: pendingPersoImage }), }); if (upRes.ok) { const upData = await upRes.json(); imagePath = upData.path; } } const data = { Nom: document.getElementById('fp-nom').value, Role: document.getElementById('fp-role').value || null, Description: document.getElementById('fp-desc').value || null, Origines: document.getElementById('fp-origines').value || null, Backstory: document.getElementById('fp-backstory').value || null, Tags: document.getElementById('fp-tags').value || null, }; if (imagePath) data.Image = imagePath; if (editingPersoId) { await fetch(`/api/personnages/${editingPersoId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); } else { await fetch('/api/personnages', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); } closePersoModal(); await loadPersonnages(); } async function deletePerso() { if (!editingPersoId || !confirm('Supprimer ce personnage ?')) return; await fetch(`/api/personnages/${editingPersoId}`, { method: 'DELETE' }); closePersoModal(); await loadPersonnages(); } document.getElementById('perso-modal').addEventListener('click', e => { if (e.target === document.getElementById('perso-modal')) closePersoModal(); }); // ══════════════════════════════════════════════════════ // ══ RESSOURCES ══ // ══════════════════════════════════════════════════════ let ressources = []; let editingResId = null; let pendingResImage = null; // dataUrl for new upload function videoThumbnail(url) { if (!url) return null; // YouTube: various URL formats let m = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/); if (m) return `https://img.youtube.com/vi/${m[1]}/hqdefault.jpg`; // Vimeo m = url.match(/vimeo\.com\/(\d+)/); if (m) return `https://vumbnail.com/${m[1]}.jpg`; return null; } function extractDomain(url) { try { return new URL(url).hostname.replace('www.', ''); } catch { return url; } } async function loadRessources() { try { const res = await fetch('/api/ressources'); if (res.status === 401) return; const data = await res.json(); ressources = data.list || []; populateResCatFilter(); renderRessources(); } catch (e) { console.error('Ressources load error:', e); } } function populateResCatFilter() { const catSet = new Set(); ressources.forEach(r => { if (r.Categorie) catSet.add(r.Categorie); }); const sel = document.getElementById('res-filter-cat'); const cur = sel.value; sel.innerHTML = ''; [...catSet].sort().forEach(c => { const opt = document.createElement('option'); opt.value = c; opt.textContent = c; sel.appendChild(opt); }); sel.value = cur; } function renderRessources() { const grid = document.getElementById('res-grid'); grid.innerHTML = ''; const filterCat = document.getElementById('res-filter-cat')?.value || ''; const filterSearch = (document.getElementById('res-filter-search')?.value || '').toLowerCase().trim(); ressources.forEach(r => { if (filterCat && r.Categorie !== filterCat) return; if (filterSearch) { const hay = [r.Titre, r.Description, r.Tags, r.Categorie, r.Lien].filter(Boolean).join(' ').toLowerCase(); if (!hay.includes(filterSearch)) return; } const card = document.createElement('div'); card.className = 'res-card'; card.onclick = () => openResEdit(r.Id); // Image: uploaded > video thumbnail > nothing const imgSrc = r.Image || videoThumbnail(r.Lien); if (imgSrc) { const img = document.createElement('img'); img.className = 'res-card-img'; img.src = imgSrc; img.alt = ''; card.appendChild(img); } const body = document.createElement('div'); body.className = 'res-card-body'; const titleEl = document.createElement('div'); titleEl.className = 'res-card-title'; titleEl.textContent = r.Titre || ''; body.appendChild(titleEl); if (r.Categorie) { const catEl = document.createElement('div'); catEl.className = 'res-card-cat'; catEl.textContent = r.Categorie; body.appendChild(catEl); } if (r.Description) { const descEl = document.createElement('div'); descEl.className = 'res-card-desc'; descEl.textContent = r.Description; body.appendChild(descEl); } // Lien cliquable if (r.Lien) { const linkDiv = document.createElement('div'); linkDiv.className = 'res-card-link'; linkDiv.innerHTML = ''; const a = document.createElement('a'); a.href = r.Lien; a.target = '_blank'; a.rel = 'noopener'; a.textContent = extractDomain(r.Lien); a.onclick = (e) => e.stopPropagation(); linkDiv.appendChild(a); body.appendChild(linkDiv); } const tags = (r.Tags || '').split(',').filter(t => t.trim()); if (tags.length) { const tagsEl = document.createElement('div'); tagsEl.className = 'res-card-tags'; tags.forEach(t => { const badge = document.createElement('span'); badge.className = 'card-badge badge-tag'; badge.style.cssText = 'font-size:11px;padding:2px 6px;'; badge.textContent = t.trim(); tagsEl.appendChild(badge); }); body.appendChild(tagsEl); } card.appendChild(body); grid.appendChild(card); }); } function updateResUrlLink(url) { const container = document.getElementById('fr-url-link'); container.innerHTML = ''; if (url && url.startsWith('http')) { const a = document.createElement('a'); a.href = url; a.target = '_blank'; a.rel = 'noopener'; a.textContent = 'Ouvrir le lien'; container.appendChild(a); } } function openResModal() { editingResId = null; pendingResImage = null; document.getElementById('res-modal-label').textContent = 'Nouvelle ressource'; document.getElementById('fr-titre').value = ''; document.getElementById('fr-cat').value = ''; document.getElementById('fr-tags').value = ''; document.getElementById('fr-url').value = ''; document.getElementById('fr-desc').value = ''; document.getElementById('fr-image-input').value = ''; document.getElementById('fr-image-preview').innerHTML = ''; updateResUrlLink(''); document.getElementById('btn-delete-res').style.display = 'none'; document.getElementById('res-modal').classList.add('open'); } function openResEdit(id) { const r = ressources.find(x => x.Id === id); if (!r) return; editingResId = id; pendingResImage = null; document.getElementById('res-modal-label').textContent = 'Modifier'; document.getElementById('fr-titre').value = r.Titre || ''; document.getElementById('fr-cat').value = r.Categorie || ''; document.getElementById('fr-tags').value = r.Tags || ''; document.getElementById('fr-url').value = r.Lien || ''; document.getElementById('fr-desc').value = r.Description || ''; document.getElementById('fr-image-input').value = ''; document.getElementById('fr-image-preview').innerHTML = r.Image ? `` : ''; updateResUrlLink(r.Lien || ''); document.getElementById('btn-delete-res').style.display = 'block'; document.getElementById('res-modal').classList.add('open'); } function closeResModal() { document.getElementById('res-modal').classList.remove('open'); editingResId = null; pendingResImage = null; } function previewResImage(input) { const file = input.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = e => { pendingResImage = e.target.result; document.getElementById('fr-image-preview').innerHTML = ``; }; reader.readAsDataURL(file); } async function saveRes() { let imagePath = null; // Upload image if new one selected if (pendingResImage) { const upRes = await fetch('/api/ressource-upload', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dataUrl: pendingResImage }), }); if (upRes.ok) { const upData = await upRes.json(); imagePath = upData.path; } } const data = { Titre: document.getElementById('fr-titre').value, Categorie: document.getElementById('fr-cat').value || null, Tags: document.getElementById('fr-tags').value || null, Lien: document.getElementById('fr-url').value || null, Description: document.getElementById('fr-desc').value || null, }; if (imagePath) data.Image = imagePath; if (editingResId) { await fetch(`/api/ressources/${editingResId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); } else { await fetch('/api/ressources', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); } closeResModal(); await loadRessources(); } async function deleteRes() { if (!editingResId || !confirm('Supprimer cette ressource ?')) return; await fetch(`/api/ressources/${editingResId}`, { method: 'DELETE' }); closeResModal(); await loadRessources(); } document.getElementById('res-modal').addEventListener('click', e => { if (e.target === document.getElementById('res-modal')) closeResModal(); }); // ══════════════════════════════════════════════════════ // ══ RÉDACTION — Binder + TipTap Editor ══ // ══════════════════════════════════════════════════════ let tiptapEditor = null; let currentSceneId = null; let saveTimeout = null; let wordCounts = {}; // sceneId → word count (cached) const ACTE_ORDER = ["Prologue", 'Acte 1', 'Acte 2', 'Acte 3', "Épilogue"]; function buildBinder() { const binder = document.getElementById('binder'); // Group scenes by acte (exclude "En attente", only include Scène and Événement types relevant to writing) const grouped = {}; ACTE_ORDER.forEach(a => { grouped[a] = []; }); const redPerso = document.getElementById('red-filter-perso')?.value || ''; const redTag = document.getElementById('red-filter-tag')?.value || ''; const redSearch = (document.getElementById('red-filter-search')?.value || '').toLowerCase().trim(); (records || []).forEach(r => { const acte = r.Acte; if (!acte || acte === 'En attente') return; if (redPerso && !(r.Personnage || '').split(',').map(s => s.trim()).includes(redPerso)) return; if (redTag && !(r.Tags || '').split(',').map(s => s.trim()).includes(redTag)) return; if (redSearch) { const haystack = [r.Title, r['Résumé'], r.Personnage].filter(Boolean).join(' ').toLowerCase(); if (!haystack.includes(redSearch)) return; } if (grouped[acte]) grouped[acte].push(r); }); binder.innerHTML = ''; // Legend at top const legend = document.createElement('div'); legend.className = 'binder-legend'; const legendTitle = document.createElement('div'); legendTitle.className = 'binder-legend-title'; legendTitle.textContent = 'Tags statut'; legend.appendChild(legendTitle); Object.entries(STATUS_COLORS).forEach(([label, color]) => { const item = document.createElement('div'); item.className = 'binder-legend-item'; const dot = document.createElement('span'); dot.className = 'binder-legend-dot'; dot.style.background = color; const txt = document.createElement('span'); txt.textContent = label; item.appendChild(dot); item.appendChild(txt); legend.appendChild(item); }); binder.appendChild(legend); let totalWords = 0; ACTE_ORDER.forEach(acte => { const scenes = grouped[acte]; if (!scenes || scenes.length === 0) return; const acteDiv = document.createElement('div'); acteDiv.className = 'binder-acte'; const title = document.createElement('div'); title.className = 'binder-acte-title'; title.textContent = acte; acteDiv.appendChild(title); scenes.sort((a, b) => (a.Ordre || 0) - (b.Ordre || 0)); scenes.forEach(scene => { const el = document.createElement('div'); el.className = 'binder-scene' + (scene.Id === currentSceneId ? ' active' : ''); const sceneTags = (scene.Tags || '').split(',').map(t => t.trim()).filter(Boolean); const statusDots = sceneTags .filter(t => STATUS_COLORS[t]) .map(t => ``) .join(''); const wc = wordCounts[scene.Id] || 0; totalWords += wc; el.innerHTML = `${statusDots || ''}${esc(scene.Title)}${wc ? `${wc}` : ''}`; el.onclick = () => openScene(scene); acteDiv.appendChild(el); }); binder.appendChild(acteDiv); }); // Total word count at bottom const totalDiv = document.createElement('div'); totalDiv.style.cssText = 'padding: 16px 20px; margin-top: 12px; border-top: 1px solid var(--border); font-size: 12px; color: var(--text-hint);'; totalDiv.textContent = totalWords > 0 ? `Total : ${totalWords.toLocaleString('fr-FR')} mots` : ''; binder.appendChild(totalDiv); } async function openScene(scene) { currentSceneId = scene.Id; // Update binder selection document.querySelectorAll('.binder-scene').forEach(el => el.classList.remove('active')); event?.target?.closest?.('.binder-scene')?.classList.add('active'); // Show editor document.getElementById('editor-empty').style.display = 'none'; const active = document.getElementById('editor-active'); active.style.display = 'flex'; // Header document.getElementById('editor-title').textContent = scene.Title; const meta = []; if (scene.Acte) meta.push(scene.Acte); if (scene.Tags) meta.push(scene.Tags); if (scene.Personnage) meta.push(scene.Personnage); document.getElementById('editor-meta').textContent = meta.join(' · '); document.getElementById('btn-history-redaction').style.display = ''; // Panneau fiche (intention) buildFichePanel(scene); // Load content from API try { const res = await fetch(`/api/contenu/${scene.Id}`); const row = await res.json(); const html = row?.Texte || ''; initEditor(html); updateWordCount(); } catch (e) { console.error('Load content error:', e); initEditor(''); } } // ── Panneau fiche (intention) ── let ficheOpen = false; try { ficheOpen = localStorage.getItem('tdb-fiche-open') === '1'; } catch(e) {} function toggleFiche() { ficheOpen = !ficheOpen; document.getElementById('editor-fiche-content').classList.toggle('open', ficheOpen); document.getElementById('fiche-toggle-icon').classList.toggle('open', ficheOpen); try { localStorage.setItem('tdb-fiche-open', ficheOpen ? '1' : '0'); } catch(e) {} } function buildFichePanel(scene) { const container = document.getElementById('editor-fiche-content'); container.textContent = ''; const fields = [ { label: 'Résumé', value: scene['Résumé'] }, { label: 'Intérêt narratif', value: scene['Intérêt narratif'] }, { label: 'Implications', value: scene.Implications }, { label: 'Origine', value: scene.Origine }, { label: 'Lien rédaction', value: scene['Lien rédaction'] }, ]; let hasContent = false; fields.forEach(f => { if (!f.value) return; hasContent = true; const field = document.createElement('div'); field.className = 'editor-fiche-field'; const label = document.createElement('div'); label.className = 'editor-fiche-field-label'; label.textContent = f.label; field.appendChild(label); const val = document.createElement('div'); val.className = 'editor-fiche-field-value'; val.appendChild(createSanitizedFragment(renderMarkdown(f.value))); field.appendChild(val); container.appendChild(field); }); // Tags const tagList = (scene.Tags || '').split(',').map(t => t.trim()).filter(Boolean); if (tagList.length) { hasContent = true; const field = document.createElement('div'); field.className = 'editor-fiche-field'; const label = document.createElement('div'); label.className = 'editor-fiche-field-label'; label.textContent = 'Tags'; field.appendChild(label); const tagsEl = document.createElement('div'); tagsEl.className = 'editor-fiche-tags'; tagList.forEach(t => { const badge = document.createElement('span'); badge.className = `card-badge ${tagBadgeClass(t)}`; badge.textContent = t; tagsEl.appendChild(badge); }); field.appendChild(tagsEl); container.appendChild(field); } // Personnages const persos = (scene.Personnage || '').split(',').map(p => p.trim()).filter(Boolean); if (persos.length) { hasContent = true; const field = document.createElement('div'); field.className = 'editor-fiche-field'; const label = document.createElement('div'); label.className = 'editor-fiche-field-label'; label.textContent = 'Personnages'; field.appendChild(label); const persosEl = document.createElement('div'); persosEl.className = 'editor-fiche-persos'; persos.forEach(p => { const badge = document.createElement('span'); badge.className = 'card-badge badge-perso'; badge.textContent = p; persosEl.appendChild(badge); }); field.appendChild(persosEl); container.appendChild(field); } // Commentaires de la fiche const comments = parseComments(scene.Commentaires); if (comments.length) { hasContent = true; const field = document.createElement('div'); field.className = 'editor-fiche-field'; const label = document.createElement('div'); label.className = 'editor-fiche-field-label'; label.textContent = `Commentaires (${comments.length})`; field.appendChild(label); const list = document.createElement('div'); comments.forEach(c => { const item = document.createElement('div'); item.style.cssText = 'margin-bottom:6px;'; const author = document.createElement('span'); author.style.cssText = 'font-weight:600;font-size:12px;'; author.textContent = (c.auteur === 'M' ? 'Myriam' : 'Etienne') + ' — '; item.appendChild(author); const text = document.createElement('span'); text.textContent = c.texte || ''; item.appendChild(text); list.appendChild(item); }); field.appendChild(list); container.appendChild(field); } // Afficher/masquer le panneau selon l'état mémorisé const ficheEl = document.getElementById('editor-fiche'); ficheEl.style.display = hasContent ? '' : 'none'; container.classList.toggle('open', ficheOpen && hasContent); document.getElementById('fiche-toggle-icon').classList.toggle('open', ficheOpen && hasContent); } function initEditor(html) { // Destroy previous editor if (tiptapEditor) { tiptapEditor.destroy(); tiptapEditor = null; } const Editor = window.TiptapEditor; const StarterKit = window.TiptapStarterKit; const CommentMark = window.TiptapComment; if (!Editor || !StarterKit) { // Fallback: contenteditable div if TipTap CDN not loaded const el = document.getElementById('editor-content'); el.innerHTML = html || '

'; el.contentEditable = 'true'; el.classList.add('tiptap'); el.addEventListener('input', () => { clearTimeout(saveTimeout); saveTimeout = setTimeout(saveContent, 1500); updateWordCount(); }); buildToolbarFallback(); return; } const extensions = [StarterKit]; if (CommentMark) extensions.push(CommentMark); tiptapEditor = new Editor({ element: document.getElementById('editor-content'), extensions, content: html || '

', onUpdate: () => { clearTimeout(saveTimeout); saveTimeout = setTimeout(saveContent, 1500); updateWordCount(); }, }); // Listen for clicks on comment marks document.getElementById('editor-content').addEventListener('click', (e) => { const mark = e.target.closest('.comment-mark'); if (mark) showCommentPopup(mark, e); }); buildToolbar(); } function buildToolbar() { const bar = document.getElementById('editor-toolbar'); if (!tiptapEditor) { buildToolbarFallback(); return; } const btns = [ { label: 'G', cmd: () => tiptapEditor.chain().focus().toggleBold().run(), active: () => tiptapEditor.isActive('bold') }, { label: 'I', cmd: () => tiptapEditor.chain().focus().toggleItalic().run(), active: () => tiptapEditor.isActive('italic'), style: 'font-style:italic;' }, { label: 'H2', cmd: () => tiptapEditor.chain().focus().toggleHeading({ level: 2 }).run(), active: () => tiptapEditor.isActive('heading', { level: 2 }) }, { label: 'H3', cmd: () => tiptapEditor.chain().focus().toggleHeading({ level: 3 }).run(), active: () => tiptapEditor.isActive('heading', { level: 3 }) }, { label: '•', cmd: () => tiptapEditor.chain().focus().toggleBulletList().run(), active: () => tiptapEditor.isActive('bulletList') }, { label: '1.', cmd: () => tiptapEditor.chain().focus().toggleOrderedList().run(), active: () => tiptapEditor.isActive('orderedList') }, { label: '❝', cmd: () => tiptapEditor.chain().focus().toggleBlockquote().run(), active: () => tiptapEditor.isActive('blockquote') }, { label: '—', cmd: () => tiptapEditor.chain().focus().setHorizontalRule().run(), active: () => false }, ]; bar.innerHTML = ''; btns.forEach(b => { const btn = document.createElement('button'); btn.innerHTML = b.label; if (b.style) btn.style.cssText = b.style; btn.onclick = b.cmd; bar.appendChild(btn); }); // Separator + Comment button const sep = document.createElement('span'); sep.className = 'toolbar-sep'; bar.appendChild(sep); const commentBtn = document.createElement('button'); commentBtn.innerHTML = 'Commenter'; commentBtn.title = 'Ajouter un commentaire sur la sélection'; commentBtn.style.cssText = 'font-size:13px;'; commentBtn.onclick = () => addComment(); bar.appendChild(commentBtn); // Update active states on selection change tiptapEditor.on('selectionUpdate', () => { bar.querySelectorAll('button').forEach((btn, i) => { btn.classList.toggle('is-active', btns[i].active()); }); }); tiptapEditor.on('update', () => { bar.querySelectorAll('button').forEach((btn, i) => { btn.classList.toggle('is-active', btns[i].active()); }); }); } function buildToolbarFallback() { const bar = document.getElementById('editor-toolbar'); bar.innerHTML = 'Éditeur simplifié — TipTap non chargé'; } function updateWordCount() { let text = ''; if (tiptapEditor) { text = tiptapEditor.getText(); } else { const el = document.getElementById('editor-content'); text = el?.textContent || ''; } const count = text.trim() ? text.trim().split(/\s+/).length : 0; document.getElementById('word-count').textContent = `${count.toLocaleString('fr-FR')} mots`; if (currentSceneId) wordCounts[currentSceneId] = count; } async function saveContent() { if (!currentSceneId) return; let html = ''; let wordCount = 0; if (tiptapEditor) { html = tiptapEditor.getHTML(); const text = tiptapEditor.getText(); wordCount = text.trim() ? text.trim().split(/\s+/).length : 0; } else { const el = document.getElementById('editor-content'); html = el?.innerHTML || ''; const text = el?.textContent || ''; wordCount = text.trim() ? text.trim().split(/\s+/).length : 0; } try { await fetch(`/api/contenu/${currentSceneId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ Texte: html, Mots: wordCount }), }); wordCounts[currentSceneId] = wordCount; // Show saved indicator const el = document.getElementById('editor-saving'); el.classList.add('show'); setTimeout(() => el.classList.remove('show'), 1500); // Update binder word count buildBinder(); } catch (e) { console.error('Save error:', e); } } // Load word counts on init (for binder display) async function loadWordCounts() { try { const res = await fetch('/api/contenu'); if (res.status === 401) return; const data = await res.json(); (data.list || []).forEach(r => { if (r.SceneId && r.Mots) wordCounts[r.SceneId] = r.Mots; }); } catch (e) { /* silent */ } } loadWordCounts(); // ── Comments ── function addComment() { if (!tiptapEditor) return; const { from, to } = tiptapEditor.state.selection; if (from === to) { alert('Sélectionnez du texte pour commenter.'); return; } const author = prompt('Auteur (E ou M) :', 'E'); if (!author) return; const text = prompt('Commentaire :'); if (!text) return; const now = new Date().toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' }); tiptapEditor.chain().focus() .setMark('comment', { text, author: author.toUpperCase(), date: now }) .run(); } let activeCommentMark = null; function showCommentPopup(markEl, event) { activeCommentMark = markEl; const popup = document.getElementById('comment-popup'); const author = markEl.getAttribute('data-author') || 'E'; const authorName = author === 'M' ? 'Myriam' : 'Etienne'; document.getElementById('comment-popup-author').textContent = authorName; document.getElementById('comment-popup-author').className = `comment-popup-author ${author.toLowerCase()}`; document.getElementById('comment-popup-date').textContent = markEl.getAttribute('data-date') || ''; document.getElementById('comment-popup-text').textContent = markEl.getAttribute('data-comment') || ''; // Position near click const rect = markEl.getBoundingClientRect(); popup.style.display = 'block'; popup.style.top = (rect.bottom + 8) + 'px'; popup.style.left = Math.min(rect.left, window.innerWidth - 340) + 'px'; } function closeCommentPopup() { document.getElementById('comment-popup').style.display = 'none'; activeCommentMark = null; } function deleteComment() { if (!tiptapEditor || !activeCommentMark) return; // Find the position of this comment span in the document const commentText = activeCommentMark.getAttribute('data-comment'); const commentDate = activeCommentMark.getAttribute('data-date'); const doc = tiptapEditor.state.doc; let markFrom = null, markTo = null; doc.descendants((node, pos) => { if (markFrom !== null) return false; if (node.isText) { const mark = node.marks.find(m => m.type.name === 'comment' && m.attrs.text === commentText && m.attrs.date === commentDate); if (mark) { markFrom = pos; markTo = pos + node.nodeSize; } } }); if (markFrom !== null) { tiptapEditor.chain().focus().setTextSelection({ from: markFrom, to: markTo }).unsetMark('comment').run(); } closeCommentPopup(); clearTimeout(saveTimeout); saveTimeout = setTimeout(saveContent, 500); } // Close popup on click outside document.addEventListener('click', (e) => { const popup = document.getElementById('comment-popup'); if (popup.style.display === 'block' && !popup.contains(e.target) && !e.target.closest('.comment-mark')) { closeCommentPopup(); } }); // ── Version history ── let versionType = null; let versionRecordId = null; let versionList = []; let selectedVersionId = null; async function openVersionPanel(type, recordId) { if (!recordId) return; versionType = type; versionRecordId = recordId; selectedVersionId = null; document.getElementById('version-panel-title').textContent = type === 'frise' ? 'Historique — Fiche' : 'Historique — Texte'; document.getElementById('version-list').innerHTML = '
Chargement...
'; document.getElementById('version-preview').style.display = 'none'; document.getElementById('version-actions').style.display = 'none'; document.getElementById('version-panel').classList.add('open'); try { const r = await fetch(`/api/versions/${type}/${recordId}`); const data = await r.json(); versionList = data.list || []; renderVersionList(); } catch (e) { document.getElementById('version-list').innerHTML = '
Erreur de chargement
'; } } function closeVersionPanel() { document.getElementById('version-panel').classList.remove('open'); versionType = null; versionRecordId = null; selectedVersionId = null; } function renderVersionList() { const container = document.getElementById('version-list'); if (versionList.length === 0) { container.innerHTML = '
Aucune version enregistrée pour le moment.
Les versions seront créées automatiquement à chaque modification.
'; return; } container.innerHTML = ''; for (const v of versionList) { const div = document.createElement('div'); div.className = 'version-item'; div.dataset.id = v.Id; const date = new Date(v.CreatedAt); const dateStr = date.toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' }); let preview = ''; try { const snap = JSON.parse(v.Snapshot); if (versionType === 'frise') { preview = [snap.Title, snap['Résumé']].filter(Boolean).join(' — '); } else { const tmp = document.createElement('div'); tmp.innerHTML = snap.Texte || ''; preview = tmp.textContent.substring(0, 150); } } catch (e) { preview = ''; } const label = v.Label || ''; div.innerHTML = `
${dateStr}
` + (label ? `
${label.replace(/` : '') + (preview ? `
${preview.replace(/` : ''); div.onclick = () => selectVersion(v.Id); container.appendChild(div); } } function selectVersion(id) { selectedVersionId = id; document.querySelectorAll('.version-item').forEach(el => el.classList.toggle('selected', Number(el.dataset.id) === id)); const v = versionList.find(x => x.Id === id); if (!v) return; try { const snap = JSON.parse(v.Snapshot); const previewPane = document.getElementById('version-preview'); const content = document.getElementById('version-preview-content'); if (versionType === 'frise') { const parts = []; if (snap.Title) parts.push(`${snap.Title.replace(/`); if (snap.Acte) parts.push(`${snap.Acte}`); if (snap['Résumé']) parts.push(snap['Résumé'].replace(/Intérêt : ' + snap['Intérêt narratif'].replace(/Tags : ' + snap.Tags.replace(/'); content.innerHTML = parts.join('
'); } else { content.innerHTML = DOMPurify.sanitize(snap.Texte || 'Texte vide'); } previewPane.style.display = ''; document.getElementById('version-actions').style.display = ''; } catch (e) { document.getElementById('version-preview').style.display = 'none'; } } async function restoreVersion() { if (!selectedVersionId) return; const isArchive = versionType === 'archive'; const msg = isArchive ? 'Restaurer cette fiche supprimée ? Elle sera recréée dans la frise.' : 'Restaurer cette version ? L\u2019état actuel sera sauvegardé avant la restauration.'; if (!confirm(msg)) return; try { if (isArchive) { // Re-create the card from snapshot const v = versionList.find(x => x.Id === selectedVersionId); if (!v) return; const snap = JSON.parse(v.Snapshot); const { Title, Acte, Ordre, Personnage, Tags, Notes } = snap; const data = { Title, Acte: 'En attente', Ordre: 999, Personnage, Tags, Notes, 'Résumé': snap['Résumé'], 'Intérêt narratif': snap['Intérêt narratif'], Implications: snap.Implications, Origine: snap.Origine, 'Lien rédaction': snap['Lien rédaction'] }; const r = await fetch('/api/records', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); if (r.ok) { closeVersionPanel(); await loadData(); } else { alert('Erreur lors de la restauration'); } } else { const r = await fetch(`/api/versions/${selectedVersionId}/restore`, { method: 'POST' }); if (r.ok) { closeVersionPanel(); if (versionType === 'frise') { await loadData(); if (editingId) openEdit(editingId); } else if (versionType === 'redaction' && currentSceneId) { const scene = records.find(rec => rec.Id === currentSceneId); if (scene) openScene(scene); } } else { alert('Erreur lors de la restauration'); } } } catch (e) { alert('Erreur : ' + e.message); } } // ── Archives (deleted cards) ── async function openArchives() { versionType = 'archive'; versionRecordId = null; selectedVersionId = null; document.getElementById('version-panel-title').textContent = 'Archives — Fiches supprimées'; document.getElementById('version-list').innerHTML = '
Chargement...
'; document.getElementById('version-preview').style.display = 'none'; document.getElementById('version-actions').style.display = 'none'; document.getElementById('version-panel').classList.add('open'); try { const r = await fetch('/api/archives'); const data = await r.json(); versionList = data.list || []; renderArchiveList(); } catch (e) { document.getElementById('version-list').innerHTML = '
Erreur de chargement
'; } } function renderArchiveList() { const container = document.getElementById('version-list'); if (versionList.length === 0) { container.innerHTML = '
Aucune fiche supprimée.
'; return; } container.innerHTML = ''; for (const v of versionList) { const div = document.createElement('div'); div.className = 'version-item'; div.dataset.id = v.Id; const date = new Date(v.CreatedAt); const dateStr = date.toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' }); let title = '', preview = ''; try { const snap = JSON.parse(v.Snapshot); title = snap.Title || snap.Titre || 'Sans titre'; preview = [snap.Acte, snap['Résumé']].filter(Boolean).join(' — '); } catch (e) { title = 'Fiche'; } div.innerHTML = `
${dateStr}
` + `
${title.replace(/` + (preview ? `
${preview.replace(/` : ''); div.onclick = () => selectArchive(v.Id); container.appendChild(div); } } function selectArchive(id) { selectedVersionId = id; versionType = 'archive'; document.querySelectorAll('.version-item').forEach(el => el.classList.toggle('selected', Number(el.dataset.id) === id)); const v = versionList.find(x => x.Id === id); if (!v) return; try { const snap = JSON.parse(v.Snapshot); const content = document.getElementById('version-preview-content'); const parts = []; if (snap.Title) parts.push(`${snap.Title.replace(/`); if (snap.Acte) parts.push(`${snap.Acte}`); if (snap.Personnage) parts.push('Personnages : ' + snap.Personnage.replace(/Intérêt : ' + snap['Intérêt narratif'].replace(/Tags : ' + snap.Tags.replace(/'); if (snap.Notes) parts.push('Notes : ' + snap.Notes.replace(/'); document.getElementById('version-preview').style.display = ''; document.getElementById('version-actions').style.display = ''; } catch (e) { document.getElementById('version-preview').style.display = 'none'; } } // Close popup on version panel click outside document.addEventListener('click', (e) => { const panel = document.getElementById('version-panel'); if (panel.classList.contains('open') && !panel.contains(e.target) && !e.target.closest('.btn-history')) { closeVersionPanel(); } });