diff --git a/public/index.html b/public/index.html
index 5d2fef7..3ea67c8 100644
--- a/public/index.html
+++ b/public/index.html
@@ -233,10 +233,6 @@ window.dispatchEvent(new Event('tiptap-loaded'));
.perso-card-text { font-size: 14px; color: var(--text-muted); line-height: 1.5; }
.perso-card-text p { margin: 0 0 4px; }
.perso-card-text ul, .perso-card-text ol { margin: 0 0 4px; padding-left: 18px; }
- .perso-card-scenes { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--border-light); }
- .perso-card-scenes a { font-size: 13px; color: var(--accent); text-decoration: none; cursor: pointer; }
- .perso-card-scenes a:hover { text-decoration: underline; color: var(--accent-hover); }
- .perso-card-scenes-list { display: flex; flex-wrap: wrap; gap: 4px 10px; }
/* Tags input */
.tags-input-container { display: flex; flex-wrap: wrap; gap: 6px; padding: 6px 8px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-input); align-items: center; min-height: 36px; }
@@ -491,13 +487,22 @@ window.dispatchEvent(new Event('tiptap-loaded'));
.page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px; }
.page-header h2 { font-family: 'Playfair Display', serif; font-size: 28px; font-weight: 600; margin: 0; }
- .perso-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; }
- .perso-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; padding: 20px; cursor: pointer; transition: box-shadow 0.15s; }
+ .perso-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 20px; }
+ .perso-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; cursor: pointer; transition: box-shadow 0.15s; }
.perso-card:hover { box-shadow: 0 4px 14px var(--shadow-hover); }
+ .perso-card-portrait { width: 100%; height: 200px; object-fit: cover; background: var(--bg-alt); }
+ .perso-card-body { padding: 16px 20px; }
.perso-card-name { font-family: 'Playfair Display', serif; font-size: 20px; font-weight: 600; margin-bottom: 4px; }
.perso-card-role { font-size: 14px; color: var(--accent); margin-bottom: 8px; font-weight: 500; }
- .perso-card-desc { font-size: 14px; color: var(--text-muted); line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; }
+ .perso-card-desc { font-size: 14px; color: var(--text-muted); line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.perso-card-tags { margin-top: 10px; display: flex; gap: 6px; flex-wrap: wrap; }
+ .perso-card-scene-count { margin-top: 10px; font-size: 12px; color: var(--text-hint); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; }
+ .perso-modal-scenes { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border); }
+ .perso-modal-scenes-title { font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-hint); font-weight: 600; margin-bottom: 8px; }
+ .perso-modal-scenes-acte { font-size: 11px; color: var(--accent); font-weight: 600; margin-top: 8px; margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.3px; }
+ .perso-modal-scenes-list { display: flex; flex-direction: column; gap: 2px; }
+ .perso-modal-scenes-list a { font-size: 13px; color: var(--text); text-decoration: none; cursor: pointer; padding: 2px 0; }
+ .perso-modal-scenes-list a:hover { color: var(--accent); }
.res-filters { display: flex; gap: 12px; align-items: center; margin-bottom: 20px; flex-wrap: wrap; }
.res-filters select, .res-filters input { padding: 6px 10px; border: 1px solid var(--border); border-radius: 8px; font-size: 13px; font-family: 'Lora', serif; color: var(--text); background: var(--bg-input); outline: none; }
@@ -797,10 +802,12 @@ window.dispatchEvent(new Event('tiptap-loaded'));
+
+
@@ -2049,6 +2056,7 @@ loadVision();
let personnages = [];
let editingPersoId = null;
+let pendingPersoImage = null;
async function loadPersonnages() {
try {
@@ -2068,55 +2076,38 @@ function renderPersonnages() {
card.className = 'perso-card';
card.onclick = () => openPersoEdit(p.Id);
- // Header
+ // 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 || '';
- card.appendChild(nameEl);
+ body.appendChild(nameEl);
+ // Rôle
if (p.Role) {
const roleEl = document.createElement('div');
roleEl.className = 'perso-card-role';
roleEl.textContent = p.Role;
- card.appendChild(roleEl);
+ body.appendChild(roleEl);
}
- // Description (markdown)
+ // 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)));
- card.appendChild(descEl);
- }
-
- // Origines et influences (markdown)
- if (p.Origines) {
- const section = document.createElement('div');
- section.className = 'perso-card-section';
- const title = document.createElement('div');
- title.className = 'perso-card-section-title';
- title.textContent = 'Origines et influences';
- section.appendChild(title);
- const text = document.createElement('div');
- text.className = 'perso-card-text';
- text.appendChild(createSanitizedFragment(renderMarkdown(p.Origines)));
- section.appendChild(text);
- card.appendChild(section);
- }
-
- // Backstory (markdown)
- if (p.Backstory) {
- const section = document.createElement('div');
- section.className = 'perso-card-section';
- const title = document.createElement('div');
- title.className = 'perso-card-section-title';
- title.textContent = 'Backstory';
- section.appendChild(title);
- const text = document.createElement('div');
- text.className = 'perso-card-text';
- text.appendChild(createSanitizedFragment(renderMarkdown(p.Backstory)));
- section.appendChild(text);
- card.appendChild(section);
+ body.appendChild(descEl);
}
// Tags
@@ -2130,42 +2121,79 @@ function renderPersonnages() {
badge.textContent = t.trim();
tagsEl.appendChild(badge);
});
- card.appendChild(tagsEl);
+ body.appendChild(tagsEl);
}
- // Liens scènes automatiques (triés dans l'ordre narratif : acte puis ordre)
- const linkedScenes = records.filter(r =>
+ // Compteur scènes
+ const sceneCount = records.filter(r =>
r.Personnage && r.Personnage.split(',').map(s => s.trim()).includes(p.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);
- });
- if (linkedScenes.length) {
- const scenesEl = document.createElement('div');
- scenesEl.className = 'perso-card-scenes';
- const scenesTitle = document.createElement('div');
- scenesTitle.className = 'perso-card-section-title';
- scenesTitle.textContent = `Scènes (${linkedScenes.length})`;
- scenesEl.appendChild(scenesTitle);
- const list = document.createElement('div');
- list.className = 'perso-card-scenes-list';
- linkedScenes.forEach(scene => {
- const link = document.createElement('a');
- link.textContent = scene.Title;
- link.onclick = (e) => { e.stopPropagation(); switchView('frise'); setTimeout(() => openEdit(scene.Id), 200); };
- list.appendChild(link);
- });
- scenesEl.appendChild(list);
- card.appendChild(scenesEl);
+ ).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 = '';
@@ -2173,6 +2201,9 @@ function openPersoModal() {
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');
}
@@ -2181,6 +2212,7 @@ 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 || '';
@@ -2188,6 +2220,9 @@ function openPersoEdit(id) {
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');
}
@@ -2195,9 +2230,34 @@ function openPersoEdit(id) {
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,
@@ -2206,6 +2266,8 @@ async function savePerso() {
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',