diff --git a/public/index.html b/public/index.html index 023c254..f816164 100644 --- a/public/index.html +++ b/public/index.html @@ -245,6 +245,33 @@ .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; } @@ -313,44 +340,25 @@
-
Les trois mots qui définissent votre univers...
-
Image d'ambiance — cliquer pour remplacer
+
+

L'histoire en une phrase

-
Cliquez pour écrire la phrase qui résume votre histoire...
-
Modifiable à tout moment par les deux auteurs
+
+

Synopsis

-
Développez ici le résumé de votre histoire en quelques paragraphes — les grandes lignes, les enjeux, l'arc principal...
-
Peut évoluer au fil de l'écriture
+

Ambiance, époque, rythme

-
- Ambiance sombre - Époque fictive - Rythme lent puis rapide - Aventure - Drame - Suspense -
-
- -
-

Moodboard

-
-
Ambiance nuit
-
Architecture
-
Forêt
-
Costumes
-
+
-
+
@@ -358,47 +366,24 @@

On veut

-
Des personnages profonds
-
Des retournements crédibles
-
Un univers immersif
+

On ne veut pas

-
Un ton Young Adult
-
Des coïncidences faciles
-
De la violence gratuite
+

Inspirations et références

-
-
-
📖
-
Roman A
-
Livre
-
-
-
🎬
-
Film B
-
Film
-
-
-
🎨
-
Tableau C
-
Peinture
-
-
-
+
-
Ajouter
-
-
+
+
Enregistré
@@ -836,6 +821,273 @@ document.addEventListener('keydown', e => { }); 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)}×`; + 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 = `${esc(text)}×`; + 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 = '+ 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 = ` + +
${item.icon || INSPI_ICONS[item.type] || '\u{2728}'}
+
${esc(item.title)}
+
${esc(item.type)}
+ `; + 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 = '
+
Ajouter
'; + 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(); diff --git a/server.js b/server.js index d51ba2d..5c61808 100644 --- a/server.js +++ b/server.js @@ -14,6 +14,7 @@ const NOCODB_URL = process.env.NOCODB_URL || 'https://nocodb.hub.delvarre.net'; const NOCODB_TOKEN = process.env.NOCODB_TOKEN || ''; const NOCODB_BASE_ID = process.env.NOCODB_BASE_ID || ''; const NOCODB_TABLE_ID = process.env.NOCODB_TABLE_ID || ''; +const NOCODB_VISION_TABLE_ID = process.env.NOCODB_VISION_TABLE_ID || ''; // Auth function makeToken(ts) { @@ -142,6 +143,36 @@ app.post('/api/reorder', requireAuth, async (req, res) => { } }); +// ── Vision API ── + +// List all vision sections +app.get('/api/vision', requireAuth, async (req, res) => { + try { + const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_VISION_TABLE_ID}?limit=50&sort=Ordre`; + const r = await fetch(url, { headers: { 'xc-token': NOCODB_TOKEN } }); + const data = await r.json(); + res.json(data); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// Update a vision section +app.patch('/api/vision/:id', requireAuth, async (req, res) => { + try { + const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_VISION_TABLE_ID}/${req.params.id}`; + const r = await fetch(url, { + method: 'PATCH', + headers: { 'xc-token': NOCODB_TOKEN, 'Content-Type': 'application/json' }, + body: JSON.stringify(req.body), + }); + const data = await r.json(); + res.status(r.status).json(data); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + // Auth gate — redirect to login page if not authenticated app.get('/', (req, res) => { if (isAuth(req)) {