Files
tdb-rdb/public/index.html
T
Etienne Delvarre 89d68e0c84 Vision : retirer max-width pour occuper toute la largeur
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-28 18:29:55 +02:00

894 lines
42 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-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; }
/* 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="frise" onclick="switchView('frise')"><span class="nav-icon">&#9203;</span> Frise</div>
<div class="nav-item" data-view="vision" onclick="switchView('vision')"><span class="nav-icon">&#9733;</span> Vision</div>
<div class="sidebar-footer">Etienne & Myriam</div>
</div>
<div class="main">
<div class="topbar">
<div class="topbar-title">Frise</div>
<div class="topbar-right">
<button class="btn-new" onclick="openNew()">+ 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 active" 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" id="view-vision">
<div class="vision-content">
<div class="vision-grid">
<div class="hero-banner">
<div class="hero-overlay">
<div class="three-words">Gracieux &middot; Ingenieux &middot; Chaleureux</div>
<div class="hero-sub">Fin XVIIIe / debut XIXe &mdash; univers indefini assume</div>
</div>
</div>
<div class="v-card full">
<h3>Ce que raconte vraiment ce livre</h3>
<div class="vision-phrase">L'histoire exterieure &mdash; empecher l'opportuniste &mdash; est la tension narrative. Mais ce qui se passe vraiment, c'est Tristan qui decouvre qui il est, Emeline qui choisit qui elle veut etre, et Leopold qui apprend ce que son pere n'a pas pu lui enseigner.</div>
<div class="vision-detail">Le lecteur suit l'opportuniste par tension. Il reste pour les personnages.</div>
</div>
<div class="v-card full">
<h3>Synopsis</h3>
<div class="vision-detail">
Apres la faillite de sa startup, Tristan revient dans la ville ou son pere a travaille toute sa vie &mdash; une cite autrefois prospere, desormais sous le joug de Darius, un opportuniste qui a saisi le chaos d'une catastrophe pour instaurer son regime. Tristan decouvre un monde ou les biens communs sont privatises, les artistes bannis, et les habitants convaincus que toute action est vouee a l'echec.
<br><br>
Guide par Gedeon, un vieil exile portant ses propres fautes, Tristan apprend a vivre avec peu et a tirer des enseignements de la nature. Quand il frappe a la porte d'Emeline &mdash; une jeune femme au bord du desespoir &mdash; leur rencontre devient l'etincelle qui relance tout. Ensemble, avec un groupe grandissant d'exiles, ils construisent une societe parallele fondee sur la creation de valeur, pas sur le vol.
<br><br>
Mais Tristan porte le poids d'un pere calomnie, d'une identite a reconquerir et d'un choix impossible entre la mission collective et sa quete personnelle. Et dans l'ombre, Leopold &mdash; le fils de l'Empereur disparu &mdash; joue un double jeu dont personne ne soupconnera l'issue.
</div>
</div>
<div class="v-card">
<h3>Ambiance et themes</h3>
<div class="tag-group">
<span class="tag ambiance">Conte / legende</span>
<span class="tag genre">Aventure</span>
<span class="tag genre">Drame</span>
<span class="tag rythme">Lent puis rapide</span>
<span class="tag epoque">Fin XVIIIe</span>
<span class="tag theme">Entrepreneuriat</span>
<span class="tag theme">Creation de valeur</span>
<span class="tag theme">Resistance</span>
<span class="tag theme">Leadership</span>
<span class="tag theme">Magnanimite</span>
<span class="tag theme">Ingeniosity / low-tech</span>
<span class="tag theme">Transcendance</span>
<span class="tag ambiance">Non-violence</span>
</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 class="list-item yes"><span class="bullet"></span> Des personnages profonds avec des arcs complets</div>
<div class="list-item yes"><span class="bullet"></span> Des retournements credibles et prepares</div>
<div class="list-item yes"><span class="bullet"></span> Un univers immersif, chaleureux et lumineux</div>
<div class="list-item yes"><span class="bullet"></span> La creation de valeur, pas le vol</div>
<div class="list-item yes"><span class="bullet"></span> Un heros qui gagne par l'intelligence et la bonte</div>
<div class="list-item yes"><span class="bullet"></span> La complementarite Tristan/Emeline comme moteur</div>
</div>
<div class="list-block">
<h4 class="no">On ne veut pas</h4>
<div class="list-item no"><span class="bullet"></span> Un ton Young Adult</div>
<div class="list-item no"><span class="bullet"></span> Des coincidences faciles</div>
<div class="list-item no"><span class="bullet"></span> De la violence gratuite</div>
<div class="list-item no"><span class="bullet"></span> Un antagoniste caricatural</div>
<div class="list-item no"><span class="bullet"></span> Un recit moralisateur</div>
</div>
</div>
</div>
<div class="v-card full">
<h3>Inspirations et references</h3>
<div class="inspi-grid">
<div class="inspi-card">
<div class="inspi-icon">&#128214;</div>
<div class="inspi-title">Robin des Bois</div>
<div class="inspi-type">Structure de base</div>
</div>
<div class="inspi-card">
<div class="inspi-icon">&#127916;</div>
<div class="inspi-title">Le Comte de Monte-Cristo</div>
<div class="inspi-type">Revelation / vengeance</div>
</div>
<div class="inspi-card">
<div class="inspi-icon">&#127916;</div>
<div class="inspi-title">Les Evades</div>
<div class="inspi-type">Espoir et liberte</div>
</div>
<div class="inspi-card">
<div class="inspi-icon">&#128214;</div>
<div class="inspi-title">L'Homme le plus riche de Babylone</div>
<div class="inspi-type">Economie vertueuse</div>
</div>
<div class="inspi-card">
<div class="inspi-icon">&#127916;</div>
<div class="inspi-title">Stardust</div>
<div class="inspi-type">Conte lumineux</div>
</div>
<div class="inspi-card">
<div class="inspi-icon">&#127916;</div>
<div class="inspi-title">Danse avec les Loups</div>
<div class="inspi-type">Romance asymetrique</div>
</div>
<div class="inspi-card">
<div class="inspi-icon">&#127916;</div>
<div class="inspi-title">L'Auberge du 6e Bonheur</div>
<div class="inspi-type">Resistance / foi</div>
</div>
<div class="inspi-card">
<div class="inspi-icon">&#127925;</div>
<div class="inspi-title">Tri Yann</div>
<div class="inspi-type">Memoire d'une cite</div>
</div>
<div class="inspi-card">
<div class="inspi-icon">&#127916;</div>
<div class="inspi-title">Le Seigneur des Anneaux</div>
<div class="inspi-type">Univers visuel</div>
</div>
<div class="inspi-card">
<div class="inspi-icon">&#127916;</div>
<div class="inspi-title">Benjamin Gates</div>
<div class="inspi-type">Gala / fenetre d'opportunite</div>
</div>
<div class="inspi-card">
<div class="inspi-icon">&#127916;</div>
<div class="inspi-title">A tout jamais</div>
<div class="inspi-type">Clemence du vainqueur</div>
</div>
<div class="inspi-card">
<div class="inspi-icon">&#127916;</div>
<div class="inspi-title">Nord et Sud</div>
<div class="inspi-type">Tension sociale / romance</div>
</div>
</div>
</div>
<div class="v-card full">
<h3>Citation en filigrane</h3>
<div class="vision-quote">&laquo; On t'appellera celui qui repare les breches &raquo;</div>
<div class="vision-detail">Citation biblique en prologue d'un chapitre, quand Tristan initie son action vers la ville. Dimension prophetique sans alourdissement.</div>
</div>
</div>
</div>
</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();
</script>
</body>
</html>