Files
tdb-rdb/public/index.html
T
Etienne Delvarre 0574b9ab1e Vision éditable — contenu chargé depuis NocoDB
- Nouvelle table NocoDB Vision (9 sections : trois-mots, phrase, synopsis, tags, listes, inspirations)
- Routes API /api/vision GET + PATCH dans server.js
- Frontend : contenteditable sur les champs texte, sauvegarde auto au blur
- Tags, listes et inspirations : ajout/suppression/édition en place
- Indicateur "Enregistré" discret en bas à droite
- Variable NOCODB_VISION_TABLE_ID ajoutée

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-29 08:49:26 +02:00

1094 lines
46 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tableau de bord — RdB</title>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,500;0,600;0,700;1,400&family=Lora:ital,wght@0,400;0,500;0,600;1,400&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.6/Sortable.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
/* ── Light theme (default) ── */
:root {
--bg: #faf6f0;
--bg-alt: #f3ece2;
--bg-card: #fff;
--bg-input: #faf6f0;
--border: #e0d5c7;
--border-light: #f0ebe3;
--text: #2c1810;
--text-muted: #8a7968;
--text-hint: #a08a76;
--text-light: #c4b5a4;
--accent: #8a7060;
--accent-hover: #5c3d2e;
--brown: #5c3d2e;
--terra: #b85c3a;
--olive: #6a7a42;
--blue-grey: #6a7a8a;
--shadow: rgba(92,61,46,0.06);
--shadow-hover: rgba(92,61,46,0.12);
--overlay: rgba(44,24,16,0.4);
}
/* ── Dark theme ── */
.dark {
--bg: #1a1614;
--bg-alt: #231f1c;
--bg-card: #2a2420;
--bg-input: #1a1614;
--border: #3d3530;
--border-light: #332c27;
--text: #e8ddd0;
--text-muted: #a09080;
--text-hint: #807060;
--text-light: #5a4e44;
--accent: #c4a882;
--accent-hover: #e0c8a8;
--brown: #c4a882;
--terra: #d4845a;
--olive: #8aaa5a;
--blue-grey: #8a9aaa;
--shadow: rgba(0,0,0,0.2);
--shadow-hover: rgba(0,0,0,0.35);
--overlay: rgba(0,0,0,0.6);
}
body { font-family: 'Lora', Georgia, serif; background: var(--bg); color: var(--text); min-height: 100vh; transition: background 0.3s, color 0.3s; }
/* Layout */
.app { display: flex; min-height: 100vh; }
/* Sidebar */
.sidebar { width: 220px; background: var(--bg-alt); border-right: 1px solid var(--border); padding: 24px 0; flex-shrink: 0; display: flex; flex-direction: column; }
.sidebar-logo { padding: 0 22px 20px; font-family: 'Playfair Display', serif; font-size: 22px; font-weight: 700; color: var(--brown); }
.sidebar-project { padding: 12px 16px; margin: 0 14px 18px; background: var(--bg-card); border-radius: 10px; font-size: 16px; color: var(--brown); border: 1px solid var(--border); }
.nav-item { padding: 12px 18px; margin: 2px 14px; border-radius: 10px; font-size: 17px; color: var(--text-muted); cursor: pointer; display: flex; align-items: center; gap: 10px; }
.nav-item:hover { background: var(--border); color: var(--text); }
.nav-item.active { background: var(--border); color: var(--text); font-weight: 500; }
.nav-icon { width: 34px; height: 34px; font-size: 17px; background: var(--border); border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; }
.sidebar-footer { margin-top: auto; padding: 16px 22px; font-size: 13px; color: var(--text-hint); }
/* Main */
.main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
/* Topbar */
.topbar { height: 58px; background: var(--bg-card); border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; padding: 0 28px; flex-shrink: 0; }
.topbar-title { font-family: 'Playfair Display', serif; font-size: 26px; font-weight: 600; }
.topbar-right { display: flex; align-items: center; gap: 14px; }
.btn-new { padding: 10px 22px; background: var(--accent); color: var(--bg); border: none; border-radius: 8px; font-size: 16px; font-weight: 600; font-family: 'Lora', serif; cursor: pointer; }
.btn-new:hover { background: var(--accent-hover); }
.dark-toggle { width: 36px; height: 20px; background: var(--border); border-radius: 10px; position: relative; cursor: pointer; border: none; flex-shrink: 0; }
.dark-toggle::after { content: '☀'; font-size: 12px; position: absolute; top: 2px; left: 3px; width: 16px; height: 16px; background: var(--bg-card); border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: left 0.2s; }
.dark .dark-toggle::after { content: '☾'; left: 17px; }
.avatar { width: 34px; height: 34px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 600; color: #fff; }
.avatar.e { background: #5c3d2e; }
.avatar.m { background: #b85c3a; }
/* ── Filter bar ── */
.filter-bar { background: var(--bg-alt); border-bottom: 1px solid var(--border); padding: 10px 28px; display: flex; align-items: center; gap: 12px; flex-shrink: 0; flex-wrap: wrap; }
.filter-bar select, .filter-bar input { padding: 7px 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 13px; font-family: 'Lora', serif; color: var(--text); background: var(--bg-input); outline: none; }
.filter-bar select:focus, .filter-bar input:focus { border-color: var(--accent); }
.filter-bar label { font-size: 12px; color: var(--text-hint); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; }
.filter-group { display: flex; align-items: center; gap: 6px; }
.btn-clear-filters { padding: 6px 14px; background: transparent; border: 1px solid var(--border); border-radius: 8px; font-size: 12px; font-family: 'Lora', serif; color: var(--text-muted); cursor: pointer; }
.btn-clear-filters:hover { background: var(--border); color: var(--text); }
.card.filtered-out { display: none; }
/* ── Frise container ── */
.frise-wrapper { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
/* Timeline area */
.timeline-area { flex: 1; overflow-x: auto; overflow-y: auto; padding: 20px 24px 12px; position: relative; }
/* Acte markers row */
.acte-markers { display: flex; gap: 0; min-width: min-content; margin-bottom: 0; position: relative; }
.acte-section { min-width: 200px; flex-shrink: 0; position: relative; }
.acte-header { text-align: center; padding-bottom: 12px; }
.acte-label { font-family: 'Playfair Display', serif; font-size: 18px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; }
.acte-section[data-acte="Avant l'histoire"] .acte-label { color: var(--blue-grey); }
.acte-section[data-acte="Acte 1"] .acte-label { color: var(--brown); }
.acte-section[data-acte="Acte 2"] .acte-label { color: var(--terra); }
.acte-section[data-acte="Acte 3"] .acte-label { color: var(--olive); }
/* Timeline line */
.timeline-line { height: 3px; background: var(--border); display: flex; min-width: min-content; border-radius: 2px; }
.timeline-segment { flex-shrink: 0; height: 3px; }
.acte-section[data-acte="Avant l'histoire"] .timeline-segment { background: var(--blue-grey); }
.acte-section[data-acte="Acte 1"] .timeline-segment { background: var(--brown); }
.acte-section[data-acte="Acte 2"] .timeline-segment { background: var(--terra); }
.acte-section[data-acte="Acte 3"] .timeline-segment { background: var(--olive); }
/* Cards row under each acte */
.cards-row { display: flex; gap: 14px; padding: 16px 10px 10px; min-height: 140px; align-items: flex-start; }
/* Cards */
.card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; padding: 18px 20px; cursor: grab; border-top: 5px solid var(--border); width: 260px; min-width: 260px; flex-shrink: 0; box-shadow: 0 1px 4px var(--shadow); transition: box-shadow 0.15s; }
.card:hover { box-shadow: 0 4px 14px var(--shadow-hover); }
.card.sortable-ghost { opacity: 0.3; }
.card.sortable-chosen { box-shadow: 0 6px 20px var(--shadow-hover); }
.card-title { font-family: 'Playfair Display', serif; font-size: 18px; font-weight: 600; margin-bottom: 8px; color: var(--text); }
.card-summary { font-size: 15px; color: var(--text-muted); line-height: 1.5; margin-bottom: 12px; display: -webkit-box; -webkit-line-clamp: 8; -webkit-box-orient: vertical; overflow: hidden; }
.card-meta { display: flex; gap: 6px; flex-wrap: wrap; }
.card-badge { font-size: 13px; padding: 4px 10px; border-radius: 8px; font-weight: 500; }
/* Acte colors on card top border */
.card[data-acte="Avant l'histoire"] { border-top-color: var(--blue-grey); }
.card[data-acte="Acte 1"] { border-top-color: var(--brown); }
.card[data-acte="Acte 2"] { border-top-color: var(--terra); }
.card[data-acte="Acte 3"] { border-top-color: var(--olive); }
.card[data-acte="En attente"] { border-top-color: var(--border); }
/* Status badge colors */
.badge-germe { background: var(--border); color: var(--text-muted); }
.badge-etudier { background: #f5f0e4; color: #a07850; }
.badge-posee { background: #eef3e8; color: #5a7a3a; }
.badge-abandonnee { background: var(--border); color: var(--text-hint); text-decoration: line-through; }
.badge-conflit { background: #f5e4e4; color: #8b3030; }
.dark .badge-etudier { background: #3a3020; color: #c49860; }
.dark .badge-posee { background: #2a3520; color: #8aaa6a; }
.dark .badge-conflit { background: #3a2020; color: #c06060; }
.badge-perso { background: var(--bg); color: var(--text-muted); border: 1px solid var(--border); }
/* ── Waiting zone (bottom) ── */
.waiting-zone { border-top: 2px dashed var(--border); padding: 14px 24px; background: var(--bg-alt); flex-shrink: 0; }
.waiting-header { font-size: 13px; color: var(--text-hint); text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 10px; font-weight: 600; }
.waiting-cards { display: flex; gap: 12px; overflow-x: auto; min-height: 100px; padding: 4px 0; align-items: flex-start; }
.waiting-cards .card { border-style: dashed; border-top: 4px dashed var(--border); }
/* ── Modal ── */
.modal-overlay { display: none; position: fixed; inset: 0; background: var(--overlay); z-index: 100; align-items: center; justify-content: center; }
.modal-overlay.open { display: flex; }
.modal { background: var(--bg-card); border-radius: 16px; width: 580px; max-width: 92vw; max-height: 88vh; overflow-y: auto; box-shadow: 0 12px 40px var(--shadow-hover); }
.modal-header { padding: 22px 26px 0; display: flex; justify-content: space-between; align-items: start; }
.modal-close { background: none; border: none; font-size: 22px; color: var(--text-hint); cursor: pointer; padding: 4px 8px; }
.modal-close:hover { color: var(--text); }
.modal-body { padding: 14px 26px 26px; }
.modal-body .field { margin-bottom: 16px; }
.modal-body .field label { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: 0.6px; color: var(--text-hint); font-weight: 600; margin-bottom: 5px; }
.modal-body .field input,
.modal-body .field textarea,
.modal-body .field select { width: 100%; padding: 9px 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 14px; font-family: 'Lora', serif; color: var(--text); outline: none; background: var(--bg-input); }
.modal-body .field input:focus,
.modal-body .field textarea:focus,
.modal-body .field select:focus { border-color: var(--accent); }
.modal-body .field textarea { min-height: 70px; resize: vertical; line-height: 1.6; }
.modal-body .field textarea#f-resume { min-height: 140px; }
.modal-body .field-title input { font-family: 'Playfair Display', serif; font-size: 20px; font-weight: 600; border: none; background: transparent; padding: 0; }
.modal-body .field-title input:focus { border-bottom: 2px solid var(--accent); }
.modal-body .field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.multi-select-container { display: flex; flex-wrap: wrap; gap: 5px; padding: 8px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-input); min-height: 38px; }
.multi-select-container .ms-tag { font-size: 12px; padding: 3px 9px; border-radius: 6px; background: var(--border); color: var(--text-muted); cursor: pointer; }
.multi-select-container .ms-tag.selected { background: var(--accent); color: var(--bg); }
.modal-actions { display: flex; gap: 10px; justify-content: space-between; margin-top: 8px; }
.btn-save { padding: 9px 22px; background: var(--accent); color: var(--bg); border: none; border-radius: 8px; font-size: 14px; font-weight: 600; font-family: 'Lora', serif; cursor: pointer; }
.btn-save:hover { background: var(--accent-hover); }
.btn-delete { padding: 9px 14px; background: transparent; color: #8b3030; border: 1px solid #e8c4c0; border-radius: 8px; font-size: 13px; font-family: 'Lora', serif; cursor: pointer; }
.btn-delete:hover { background: #fdf0ef; }
.dark .btn-delete { border-color: #5a3030; }
.dark .btn-delete:hover { background: #3a2020; }
.loading { text-align: center; padding: 60px; color: var(--text-hint); font-style: italic; }
/* ── Vision page ── */
.view { display: none; }
.view.active { display: flex; flex-direction: column; flex: 1; overflow: hidden; }
.vision-content { flex: 1; overflow-y: auto; padding: 28px 36px; }
.vision-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; }
.v-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; padding: 24px; box-shadow: 0 1px 4px var(--shadow); }
.v-card.full { grid-column: 1 / -1; }
.v-card h3 { font-family: 'Playfair Display', serif; font-size: 14px; font-weight: 600; color: var(--text-hint); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 16px; }
.hero-banner { grid-column: 1 / -1; border-radius: 12px; height: 180px; background: linear-gradient(135deg, #d4a574 0%, #c4956a 30%, #a07850 60%, #7a5c40 100%); position: relative; overflow: hidden; display: flex; align-items: flex-end; }
.dark .hero-banner { background: linear-gradient(135deg, #5a4030 0%, #4a3528 30%, #3a2a20 60%, #2a1e18 100%); }
.hero-banner::after { content: ''; position: absolute; inset: 0; background: linear-gradient(transparent 40%, rgba(44,24,16,0.4)); }
.hero-overlay { position: relative; z-index: 1; padding: 20px 28px; width: 100%; }
.hero-overlay .three-words { font-family: 'Playfair Display', serif; font-size: 32px; font-weight: 700; color: #fff; letter-spacing: 2px; }
.hero-overlay .hero-sub { font-size: 14px; color: rgba(255,255,255,0.75); margin-top: 6px; font-style: italic; }
.vision-phrase { font-family: 'Playfair Display', serif; font-size: 22px; font-weight: 400; font-style: italic; line-height: 1.7; color: var(--text); }
.vision-detail { font-size: 15px; color: var(--text-muted); line-height: 1.8; margin-top: 12px; }
.tag-group { display: flex; flex-wrap: wrap; gap: 8px; }
.tag { padding: 8px 16px; border-radius: 20px; font-size: 14px; font-weight: 500; }
.tag.ambiance { background: #f5ede4; color: #8b5e3c; border: 1px solid #ddc9b4; }
.tag.epoque { background: #eef3e8; color: #5a7a3a; border: 1px solid #c8d8b4; }
.tag.rythme { background: #f5e8e4; color: #a0523d; border: 1px solid #ddc0b4; }
.tag.genre { background: #e8ecf3; color: #4a5a7a; border: 1px solid #b4c0d8; }
.tag.theme { background: #f0eaf5; color: #6a4a7a; border: 1px solid #d0c0e0; }
.dark .tag.ambiance { background: #3a2e20; color: #c49060; border-color: #5a4530; }
.dark .tag.epoque { background: #2a3520; color: #8aaa6a; border-color: #3a4a30; }
.dark .tag.rythme { background: #3a2520; color: #c07050; border-color: #5a3530; }
.dark .tag.genre { background: #20283a; color: #7a8aaa; border-color: #303a50; }
.dark .tag.theme { background: #2a2035; color: #a080b0; border-color: #403050; }
.two-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.list-block h4 { font-family: 'Playfair Display', serif; font-size: 15px; font-weight: 600; margin-bottom: 12px; }
.list-block h4.yes { color: #5a7a3a; }
.list-block h4.no { color: #a0523d; }
.dark .list-block h4.yes { color: #8aaa6a; }
.dark .list-block h4.no { color: #c07050; }
.list-item { padding: 8px 0; font-size: 15px; color: var(--text); display: flex; align-items: flex-start; gap: 8px; }
.list-item .bullet { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; margin-top: 8px; }
.list-item.yes .bullet { background: #5a7a3a; }
.list-item.no .bullet { background: #a0523d; }
.dark .list-item.yes .bullet { background: #8aaa6a; }
.dark .list-item.no .bullet { background: #c07050; }
.inspi-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px; }
.inspi-card { background: var(--bg); border-radius: 10px; padding: 16px; text-align: center; border: 1px solid var(--border); }
.inspi-icon { font-size: 28px; margin-bottom: 8px; }
.inspi-title { font-size: 14px; font-weight: 500; color: var(--brown); }
.inspi-type { font-size: 13px; color: var(--text-hint); margin-top: 4px; }
.vision-quote { font-family: 'Playfair Display', serif; font-size: 17px; font-style: italic; color: var(--text-muted); line-height: 1.7; border-left: 3px solid var(--accent); padding-left: 16px; margin: 12px 0; }
/* ── Editable Vision ── */
[contenteditable] { outline: none; border-radius: 6px; transition: background 0.15s, box-shadow 0.15s; cursor: text; }
[contenteditable]:hover { background: rgba(138,112,96,0.06); }
[contenteditable]:focus { background: rgba(138,112,96,0.08); box-shadow: 0 0 0 2px var(--accent); }
.dark [contenteditable]:hover { background: rgba(196,168,130,0.08); }
.dark [contenteditable]:focus { background: rgba(196,168,130,0.1); }
[contenteditable].placeholder { color: var(--text-light); font-style: italic; }
.vision-saving { position: fixed; bottom: 20px; right: 20px; background: var(--accent); color: var(--bg); padding: 8px 18px; border-radius: 8px; font-size: 13px; font-family: 'Lora', serif; opacity: 0; transition: opacity 0.3s; z-index: 200; pointer-events: none; }
.vision-saving.show { opacity: 1; }
.tag-remove { display: none; margin-left: 4px; cursor: pointer; font-size: 11px; opacity: 0.5; }
.tag:hover .tag-remove { display: inline; }
.tag-remove:hover { opacity: 1; }
.tag-add { padding: 8px 16px; border-radius: 20px; font-size: 14px; border: 1px dashed var(--border); background: transparent; color: var(--text-hint); cursor: pointer; font-family: 'Lora', serif; }
.tag-add:hover { background: var(--bg-alt); color: var(--text); }
.list-item-text { flex: 1; }
.list-item-remove { display: none; cursor: pointer; color: var(--text-hint); font-size: 14px; margin-left: 8px; }
.list-item:hover .list-item-remove { display: inline; }
.list-item-remove:hover { color: #8b3030; }
.list-add { padding: 6px 0; font-size: 14px; color: var(--text-hint); cursor: pointer; display: flex; align-items: center; gap: 6px; }
.list-add:hover { color: var(--accent); }
.inspi-card-remove { position: absolute; top: 4px; right: 4px; display: none; background: rgba(139,48,48,0.8); color: #fff; border: none; border-radius: 50%; width: 20px; height: 20px; font-size: 12px; cursor: pointer; align-items: center; justify-content: center; }
.inspi-card:hover .inspi-card-remove { display: flex; }
.inspi-card { position: relative; }
/* Responsive */
@media (max-width: 768px) {
.sidebar { display: none; }
.modal { width: 100%; max-width: 100%; border-radius: 12px 12px 0 0; }
.card { width: 150px; min-width: 150px; }
.vision-grid { grid-template-columns: 1fr; }
.two-cols { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="app">
<div class="sidebar">
<div class="sidebar-logo">Tableau de bord</div>
<div class="sidebar-project">Projet RdB</div>
<div class="nav-item active" data-view="vision" onclick="switchView('vision')"><span class="nav-icon">&#9733;</span> Vision</div>
<div class="nav-item" data-view="frise" onclick="switchView('frise')"><span class="nav-icon">&#9203;</span> Frise</div>
<div class="sidebar-footer">Etienne & Myriam</div>
</div>
<div class="main">
<div class="topbar">
<div class="topbar-title">Vision</div>
<div class="topbar-right">
<button class="btn-new" onclick="openNew()" style="display:none;">+ Nouvelle idee</button>
<button class="dark-toggle" onclick="toggleDark()" title="Mode sombre"></button>
<div class="avatar e">E</div>
<div class="avatar m">M</div>
</div>
</div>
<!-- ══ VUE FRISE ══ -->
<div class="view" id="view-frise">
<div class="filter-bar" id="filter-bar">
<div class="filter-group">
<label>Personnage</label>
<select id="filter-perso" onchange="applyFilters()"><option value="">Tous</option></select>
</div>
<div class="filter-group">
<label>Statut</label>
<select id="filter-statut" onchange="applyFilters()"><option value="">Tous</option></select>
</div>
<div class="filter-group">
<label>Type</label>
<select id="filter-type" onchange="applyFilters()"><option value="">Tous</option></select>
</div>
<div class="filter-group">
<label>Recherche</label>
<input type="text" id="filter-search" placeholder="Mot-cle..." oninput="applyFilters()">
</div>
<button class="btn-clear-filters" onclick="clearFilters()">Effacer</button>
</div>
<div class="frise-wrapper">
<div class="timeline-area" id="timeline-area">
<div class="loading" id="loading">Chargement...</div>
</div>
<div class="waiting-zone">
<div class="waiting-header">En attente — glisser sur la frise</div>
<div class="waiting-cards" id="waiting-cards"></div>
</div>
</div>
</div>
<!-- ══ VUE VISION ══ -->
<div class="view active" id="view-vision">
<div class="vision-content">
<div class="vision-grid">
<div class="hero-banner">
<div class="hero-overlay">
<div class="three-words" id="v-trois-mots" contenteditable="true"></div>
<div class="hero-sub" id="v-hero-sub" contenteditable="true"></div>
</div>
</div>
<div class="v-card full">
<h3>L'histoire en une phrase</h3>
<div class="vision-phrase" id="v-phrase" contenteditable="true"></div>
<div class="vision-detail" id="v-phrase-detail" contenteditable="true"></div>
</div>
<div class="v-card full">
<h3>Synopsis</h3>
<div class="vision-detail" id="v-synopsis" contenteditable="true" style="min-height:80px;"></div>
</div>
<div class="v-card">
<h3>Ambiance, époque, rythme</h3>
<div class="tag-group" id="v-tags"></div>
</div>
<div class="v-card">
<h3>Ce qu'on veut / ne veut pas</h3>
<div class="two-cols">
<div class="list-block">
<h4 class="yes">On veut</h4>
<div id="v-on-veut"></div>
</div>
<div class="list-block">
<h4 class="no">On ne veut pas</h4>
<div id="v-on-ne-veut-pas"></div>
</div>
</div>
</div>
<div class="v-card">
<h3>Inspirations et références</h3>
<div class="inspi-grid" id="v-inspirations"></div>
</div>
</div>
</div>
</div>
<div class="vision-saving" id="vision-saving">Enregistré</div>
</div>
</div>
<!-- Modal -->
<div class="modal-overlay" id="modal">
<div class="modal">
<div class="modal-header">
<span style="font-family:'Playfair Display',serif;font-size:12px;text-transform:uppercase;letter-spacing:1px;color:var(--text-hint);" id="modal-label">Nouvelle idee</span>
<button class="modal-close" onclick="closeModal()">&times;</button>
</div>
<div class="modal-body">
<div class="field field-title">
<input type="text" id="f-title" placeholder="Titre de l'idee...">
</div>
<div class="field-row">
<div class="field">
<label>Type</label>
<select id="f-type"><option value=""></option></select>
</div>
<div class="field">
<label>Statut</label>
<select id="f-statut"><option value=""></option></select>
</div>
</div>
<div class="field-row">
<div class="field">
<label>Acte</label>
<select id="f-acte"><option value=""></option></select>
</div>
<div class="field">
<label>Personnages</label>
<div class="multi-select-container" id="f-perso"></div>
</div>
</div>
<div class="field">
<label>Resume</label>
<textarea id="f-resume" placeholder="Description, resume de la scene ou de l'idee..."></textarea>
</div>
<div class="field">
<label>Interet narratif</label>
<textarea id="f-interet" placeholder="Pourquoi cette idee est importante pour l'histoire..."></textarea>
</div>
<div class="field">
<label>Implications</label>
<textarea id="f-implications" placeholder="Consequences dans l'histoire..."></textarea>
</div>
<div class="field">
<label>Origine</label>
<textarea id="f-origine" placeholder="D'ou vient cette idee..."></textarea>
</div>
<div class="field">
<label>Lien redaction</label>
<textarea id="f-lien" placeholder="Notes pour la redaction..."></textarea>
</div>
<div class="modal-actions">
<button class="btn-delete" id="btn-delete" onclick="deleteCard()" style="display:none;">Supprimer</button>
<button class="btn-save" onclick="saveCard()">Enregistrer</button>
</div>
</div>
</div>
</div>
<script>
const ACTES_FRISE = ["Avant l'histoire", "Acte 1", "Acte 2", "Acte 3"];
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));
const topTitle = document.querySelector('.topbar-title');
const btnNew = document.querySelector('.btn-new');
if (name === 'vision') {
topTitle.textContent = 'Vision';
if (btnNew) btnNew.style.display = 'none';
} else {
topTitle.textContent = 'Frise';
if (btnNew) btnNew.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 = '<option value="">Tous</option>';
[...persoSet].sort().forEach(p => {
const opt = document.createElement('option');
opt.value = p; opt.textContent = p;
persoSel.appendChild(opt);
});
persoSel.value = curPerso;
// Statut and Type from column options
['Statut', 'Type'].forEach(name => {
const sel = document.getElementById(`filter-${name.toLowerCase()}`);
const cur = sel.value;
sel.innerHTML = '<option value="">Tous</option>';
if (columns[name]) {
columns[name].options.forEach(o => {
const opt = document.createElement('option');
opt.value = o; opt.textContent = o;
sel.appendChild(opt);
});
}
sel.value = cur;
});
}
function applyFilters() {
const perso = document.getElementById('filter-perso').value;
const statut = document.getElementById('filter-statut').value;
const type = document.getElementById('filter-type').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 (statut && r.Statut !== statut) visible = false;
if (type && r.Type !== type) 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-statut').value = '';
document.getElementById('filter-type').value = '';
document.getElementById('filter-search').value = '';
applyFilters();
}
const statusClass = s => {
if (!s) return '';
const map = { 'Germe': 'badge-germe', 'À étudier': 'badge-etudier', 'Posée': 'badge-posee', 'Abandonnée': 'badge-abandonnee', 'En conflit': 'badge-conflit' };
return map[s] || 'badge-germe';
};
function makeCard(r) {
const card = document.createElement('div');
card.className = 'card';
card.dataset.id = r.Id;
card.dataset.type = r.Type || '';
card.dataset.acte = r.Acte || 'En attente';
card.onclick = () => openEdit(r.Id);
const persos = r.Personnage ? r.Personnage.split(',').map(p =>
`<span class="card-badge badge-perso">${esc(p.trim())}</span>`
).join('') : '';
card.innerHTML = `
<div class="card-title">${esc(r.Title || '')}</div>
${r['Résumé'] ? `<div class="card-summary">${esc(r['Résumé'])}</div>` : ''}
<div class="card-meta">
${r.Statut ? `<span class="card-badge ${statusClass(r.Statut)}">${esc(r.Statut)}</span>` : ''}
${r.Type ? `<span class="card-badge badge-perso">${esc(r.Type)}</span>` : ''}
${persos}
</div>
`;
return card;
}
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;';
ACTES_FRISE.forEach(acte => {
const acteRecords = records
.filter(r => (r.Acte || 'En attente') === acte)
.sort((a, b) => (a.Ordre || 0) - (b.Ordre || 0));
const section = document.createElement('div');
section.className = 'acte-section';
section.dataset.acte = acte;
// Width adapts to content
const minW = Math.max(280, acteRecords.length * 274 + 20);
section.style.width = minW + 'px';
section.innerHTML = `
<div class="acte-header"><span class="acte-label">${esc(acte)}</span></div>
<div class="timeline-segment" style="width:100%;height:3px;border-radius:2px;"></div>
`;
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) => (a.Ordre || 0) - (b.Ordre || 0));
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() {
['Type', 'Statut', 'Acte'].forEach(name => {
const sel = document.getElementById(`f-${name.toLowerCase()}`);
if (!sel || !columns[name]) return;
sel.innerHTML = '<option value="">—</option>';
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);
});
}
}
function openNew() {
editingId = null;
document.getElementById('modal-label').textContent = 'Nouvelle idee';
document.getElementById('f-title').value = '';
document.getElementById('f-type').value = '';
document.getElementById('f-statut').value = 'Germe';
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('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 || '';
document.getElementById('f-type').value = r.Type || '';
document.getElementById('f-statut').value = r.Statut || '';
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('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,
Type: document.getElementById('f-type').value || null,
Statut: document.getElementById('f-statut').value || 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();
}
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();
renderFrise();
applyFilters();
}
function esc(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
document.getElementById('modal').addEventListener('click', e => {
if (e.target === document.getElementById('modal')) closeModal();
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape') closeModal();
});
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;
await fetch(`/api/vision/${row.Id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ Contenu: contenu }),
});
row.Contenu = contenu;
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 ──
const TAG_TYPES = ['ambiance', 'epoque', 'rythme', 'genre', 'theme'];
function renderTags() {
const container = document.getElementById('v-tags');
const row = visionData['tags'];
let tags = [];
try { tags = JSON.parse(row?.Contenu || '[]'); } catch { tags = []; }
container.innerHTML = '';
tags.forEach((t, i) => {
const span = document.createElement('span');
span.className = `tag ${t.type || 'ambiance'}`;
span.innerHTML = `${esc(t.label)}<span class="tag-remove" onclick="removeTag(${i})">&times;</span>`;
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;
const type = prompt('Type (ambiance, epoque, rythme, genre, theme) :', 'genre') || 'genre';
tags.push({ label, type });
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.splice(index, 1);
saveVisionSection('tags', JSON.stringify(tags));
renderTags();
}
// ── Lists (on-veut / on-ne-veut-pas) ──
function renderList(section, containerId, cssClass) {
const container = document.getElementById(containerId);
const row = visionData[section];
let items = [];
try { items = JSON.parse(row?.Contenu || '[]'); } catch { items = []; }
container.innerHTML = '';
items.forEach((text, i) => {
const div = document.createElement('div');
div.className = `list-item ${cssClass}`;
div.innerHTML = `<span class="bullet"></span><span class="list-item-text" contenteditable="true">${esc(text)}</span><span class="list-item-remove" onclick="removeListItem('${section}','${containerId}','${cssClass}',${i})">&times;</span>`;
const textEl = div.querySelector('.list-item-text');
textEl.addEventListener('blur', () => {
const newText = textEl.textContent.trim();
if (newText && newText !== items[i]) {
items[i] = newText;
saveVisionSection(section, JSON.stringify(items));
}
});
textEl.addEventListener('keydown', e => {
if (e.key === 'Enter') { e.preventDefault(); textEl.blur(); }
});
container.appendChild(div);
});
const addEl = document.createElement('div');
addEl.className = 'list-add';
addEl.innerHTML = '<span>+</span> Ajouter';
addEl.onclick = () => {
const text = prompt('Nouvel élément :');
if (!text) return;
items.push(text);
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);
}
// ── Inspirations ──
const INSPI_ICONS = { 'Livre': '\u{1F4D6}', 'Film': '\u{1F3AC}', 'Peinture': '\u{1F3A8}', 'Musique': '\u{1F3B5}', 'Série': '\u{1F4FA}', 'Jeu': '\u{1F3AE}', 'Autre': '\u{2728}' };
function renderInspirations() {
const container = document.getElementById('v-inspirations');
const row = visionData['inspirations'];
let items = [];
try { items = JSON.parse(row?.Contenu || '[]'); } catch { items = []; }
container.innerHTML = '';
items.forEach((item, i) => {
const card = document.createElement('div');
card.className = 'inspi-card';
card.innerHTML = `
<button class="inspi-card-remove" onclick="event.stopPropagation();removeInspiration(${i})">&times;</button>
<div class="inspi-icon">${item.icon || INSPI_ICONS[item.type] || '\u{2728}'}</div>
<div class="inspi-title" contenteditable="true">${esc(item.title)}</div>
<div class="inspi-type">${esc(item.type)}</div>
`;
const titleEl = card.querySelector('.inspi-title');
titleEl.addEventListener('blur', () => {
const newTitle = titleEl.textContent.trim();
if (newTitle && newTitle !== items[i].title) {
items[i].title = newTitle;
saveVisionSection('inspirations', JSON.stringify(items));
}
});
titleEl.addEventListener('keydown', e => {
if (e.key === 'Enter') { e.preventDefault(); titleEl.blur(); }
});
container.appendChild(card);
});
const addCard = document.createElement('div');
addCard.className = 'inspi-card';
addCard.style.cssText = 'border:1px dashed var(--border); background:transparent; display:flex; flex-direction:column; align-items:center; justify-content:center; cursor:pointer;';
addCard.innerHTML = '<div style="font-size:24px; color:var(--text-hint);">+</div><div class="inspi-type">Ajouter</div>';
addCard.onclick = () => {
const title = prompt('Titre de la référence :');
if (!title) return;
const type = prompt('Type (Livre, Film, Peinture, Musique, Série, Jeu, Autre) :', 'Livre') || 'Livre';
items.push({ title, type, icon: INSPI_ICONS[type] || '\u{2728}' });
saveVisionSection('inspirations', JSON.stringify(items));
renderInspirations();
};
container.appendChild(addCard);
}
function removeInspiration(index) {
const row = visionData['inspirations'];
let items = [];
try { items = JSON.parse(row?.Contenu || '[]'); } catch { return; }
items.splice(index, 1);
saveVisionSection('inspirations', JSON.stringify(items));
renderInspirations();
}
// ── 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);
}
// Render structured sections
renderTags();
renderList('on-veut', 'v-on-veut', 'yes');
renderList('on-ne-veut-pas', 'v-on-ne-veut-pas', 'no');
renderInspirations();
} catch (e) {
console.error('Vision load error:', e);
}
}
loadVision();
</script>
</body>
</html>