Personnages: portrait image, scene list moved to modal, card shows count only
- Add portrait upload (reuses ressource-upload endpoint) - Card shows portrait image, truncated description, scene count badge - Modal shows full detail + scene list grouped by acte with links - Removes long scene list from card preview Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+122
-60
@@ -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'));
|
||||
<div class="modal-body">
|
||||
<div class="field field-title"><input type="text" id="fp-nom" placeholder="Nom du personnage..."></div>
|
||||
<div class="field"><label>Rôle</label><input type="text" id="fp-role" placeholder="Héros, antagoniste, allié..." style="width:100%;padding:8px 12px;border:1px solid var(--border);border-radius:8px;font-size:14px;font-family:'Lora',serif;color:var(--text);background:var(--bg-input);"></div>
|
||||
<div class="field"><label>Portrait</label><input type="file" id="fp-image-input" accept="image/*" onchange="previewPersoImage(this)" style="font-family:'Lora',serif;font-size:13px;"><div id="fp-image-preview" style="margin-top:8px;"></div></div>
|
||||
<div class="field"><label>Description</label><textarea id="fp-desc" placeholder="Personnalité, arc, motivations..." rows="4"></textarea></div>
|
||||
<div class="field"><label>Origines et influences</label><textarea id="fp-origines" placeholder="D'où vient le personnage, quels personnages de fiction l'ont inspiré..." rows="3"></textarea></div>
|
||||
<div class="field"><label>Backstory</label><textarea id="fp-backstory" placeholder="Son passé, ce qui l'a façonné avant le début de l'histoire..." rows="4"></textarea></div>
|
||||
<div class="field"><label>Tags</label><input type="text" id="fp-tags" placeholder="Tags séparés par des virgules..." style="width:100%;padding:8px 12px;border:1px solid var(--border);border-radius:8px;font-size:14px;font-family:'Lora',serif;color:var(--text);background:var(--bg-input);"></div>
|
||||
<div id="fp-scenes-container"></div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn-delete" id="btn-delete-perso" onclick="deletePerso()" style="display:none;">Supprimer</button>
|
||||
<button class="btn-save" onclick="savePerso()">Enregistrer</button>
|
||||
@@ -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)
|
||||
).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);
|
||||
});
|
||||
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);
|
||||
}
|
||||
|
||||
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-card-scenes-list';
|
||||
linkedScenes.forEach(scene => {
|
||||
list.className = 'perso-modal-scenes-list';
|
||||
grouped[acte].forEach(scene => {
|
||||
const link = document.createElement('a');
|
||||
link.textContent = scene.Title;
|
||||
link.onclick = (e) => { e.stopPropagation(); switchView('frise'); setTimeout(() => openEdit(scene.Id), 200); };
|
||||
link.onclick = (e) => { e.stopPropagation(); closePersoModal(); switchView('frise'); setTimeout(() => openEdit(scene.Id), 200); };
|
||||
list.appendChild(link);
|
||||
});
|
||||
scenesEl.appendChild(list);
|
||||
card.appendChild(scenesEl);
|
||||
}
|
||||
|
||||
grid.appendChild(card);
|
||||
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 ? `<img src="${esc(p.Image)}" style="max-width:200px;max-height:150px;border-radius:8px;">` : '';
|
||||
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 = `<img src="${e.target.result}" style="max-width:200px;max-height:150px;border-radius:8px;">`;
|
||||
};
|
||||
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',
|
||||
|
||||
Reference in New Issue
Block a user