Simplify moodboard: free images + editable captions

- Remove category/label system, replace with free text captions
- Click image area to upload, caption editable below
- Images uploaded as files to server (same as hero, avoids NocoDB limit)
- Wider cards (160px min), cleaner layout

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Etienne Delvarre
2026-05-29 12:55:53 +02:00
parent ea396c7bde
commit 1f0087c73b
2 changed files with 75 additions and 30 deletions
+56 -30
View File
@@ -289,14 +289,15 @@ window.dispatchEvent(new Event('tiptap-loaded'));
.list-item-expand { font-size: 12px; color: var(--text-hint); cursor: pointer; margin-left: 8px; white-space: nowrap; } .list-item-expand { font-size: 12px; color: var(--text-hint); cursor: pointer; margin-left: 8px; white-space: nowrap; }
.list-item-expand:hover { color: var(--accent); } .list-item-expand:hover { color: var(--accent); }
.mood-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 12px; } .mood-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 16px; }
.mood-card { position: relative; border-radius: 10px; overflow: hidden; aspect-ratio: 1; background: var(--bg-alt); border: 1px solid var(--border); cursor: pointer; display: flex; align-items: flex-end; background-size: cover; background-position: center; } .mood-card { position: relative; border-radius: 10px; overflow: hidden; background: var(--bg-alt); border: 1px solid var(--border); }
.mood-card.has-image { border-color: transparent; } .mood-card-img { width: 100%; aspect-ratio: 1; background-size: cover; background-position: center; cursor: pointer; background-color: var(--bg-alt); display: flex; align-items: center; justify-content: center; }
.mood-label { width: 100%; padding: 6px 10px; font-size: 12px; font-weight: 600; color: #fff; background: rgba(44,24,16,0.55); backdrop-filter: blur(4px); text-align: center; } .mood-card-img.empty { border: 1px dashed var(--border); border-radius: 10px 10px 0 0; }
.mood-card:not(.has-image) .mood-label { color: var(--text-muted); background: transparent; } .mood-card-img.empty::after { content: '+'; font-size: 28px; color: var(--text-hint); }
.mood-card-caption { padding: 8px 10px; font-size: 13px; color: var(--text); min-height: 32px; }
.mood-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; z-index: 2; } .mood-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; z-index: 2; }
.mood-card:hover .mood-card-remove { display: flex; } .mood-card:hover .mood-card-remove { display: flex; }
.mood-add { border: 1px dashed var(--border); background: transparent; display: flex; flex-direction: column; align-items: center; justify-content: center; cursor: pointer; aspect-ratio: 1; border-radius: 10px; } .mood-add { border: 1px dashed var(--border); background: transparent; display: flex; flex-direction: column; align-items: center; justify-content: center; cursor: pointer; min-height: 120px; border-radius: 10px; }
.mood-add:hover { background: var(--bg-alt); } .mood-add:hover { background: var(--bg-alt); }
.mood-add-icon { font-size: 24px; color: var(--text-hint); } .mood-add-icon { font-size: 24px; color: var(--text-hint); }
@@ -1208,7 +1209,7 @@ function removeListItem(section, containerId, cssClass, index) {
renderList(section, containerId, cssClass); renderList(section, containerId, cssClass);
} }
// ── Moodboard ── // ── Moodboard (images + free text captions) ──
function renderMoodboard() { function renderMoodboard() {
const container = document.getElementById('v-moodboard'); const container = document.getElementById('v-moodboard');
const row = visionData['moodboard']; const row = visionData['moodboard'];
@@ -1218,18 +1219,34 @@ function renderMoodboard() {
container.innerHTML = ''; container.innerHTML = '';
items.forEach((item, i) => { items.forEach((item, i) => {
const card = document.createElement('div'); const card = document.createElement('div');
card.className = 'mood-card' + (item.image ? ' has-image' : ''); card.className = 'mood-card';
if (item.image) card.style.backgroundImage = `url(${item.image})`;
card.innerHTML = ` const imgDiv = document.createElement('div');
<button class="mood-card-remove" onclick="event.stopPropagation();removeMoodItem(${i})">&times;</button> imgDiv.className = 'mood-card-img' + (item.image ? '' : ' empty');
<div class="mood-label">${esc(item.label)}</div> if (item.image) imgDiv.style.backgroundImage = `url(${item.image})`;
`; imgDiv.onclick = () => { moodUploadTarget = i; document.getElementById('mood-file-input').click(); };
card.onclick = (e) => {
if (e.target.closest('.mood-card-remove')) return; const caption = document.createElement('div');
moodUploadTarget = i; caption.className = 'mood-card-caption';
document.getElementById('mood-file-input').click(); caption.contentEditable = 'true';
}; caption.textContent = item.caption || '';
caption.setAttribute('placeholder', 'Légende...');
caption.addEventListener('blur', () => {
const v = caption.textContent.trim();
if (v !== (items[i].caption || '')) {
items[i].caption = v;
saveVisionSection('moodboard', JSON.stringify(items));
}
});
const removeBtn = document.createElement('button');
removeBtn.className = 'mood-card-remove';
removeBtn.innerHTML = '&times;';
removeBtn.onclick = (e) => { e.stopPropagation(); removeMoodItem(i); };
card.appendChild(removeBtn);
card.appendChild(imgDiv);
card.appendChild(caption);
container.appendChild(card); container.appendChild(card);
}); });
@@ -1237,9 +1254,7 @@ function renderMoodboard() {
addCard.className = 'mood-add'; addCard.className = 'mood-add';
addCard.innerHTML = '<div class="mood-add-icon">+</div>'; addCard.innerHTML = '<div class="mood-add-icon">+</div>';
addCard.onclick = () => { addCard.onclick = () => {
const label = prompt('Catégorie (ex : Ambiance, Architecture, Costumes...) :'); items.push({ caption: '', image: '' });
if (!label) return;
items.push({ label, image: '' });
saveVisionSection('moodboard', JSON.stringify(items)); saveVisionSection('moodboard', JSON.stringify(items));
renderMoodboard(); renderMoodboard();
}; };
@@ -1255,7 +1270,7 @@ document.addEventListener('DOMContentLoaded', () => {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => { reader.onload = (e) => {
const img = new Image(); const img = new Image();
img.onload = () => { img.onload = async () => {
const maxW = 400; const maxW = 400;
let w = img.width, h = img.height; let w = img.width, h = img.height;
if (w > maxW) { h = Math.round(h * maxW / w); w = maxW; } if (w > maxW) { h = Math.round(h * maxW / w); w = maxW; }
@@ -1263,14 +1278,25 @@ document.addEventListener('DOMContentLoaded', () => {
canvas.width = w; canvas.height = h; canvas.width = w; canvas.height = h;
canvas.getContext('2d').drawImage(img, 0, 0, w, h); canvas.getContext('2d').drawImage(img, 0, 0, w, h);
const dataUrl = canvas.toDataURL('image/jpeg', 0.8); const dataUrl = canvas.toDataURL('image/jpeg', 0.8);
const row = visionData['moodboard'];
let items = []; try {
try { items = JSON.parse(row?.Contenu || '[]'); } catch { items = []; } const res = await fetch('/api/mood-upload', {
if (items[moodUploadTarget]) { method: 'POST',
items[moodUploadTarget].image = dataUrl; headers: { 'Content-Type': 'application/json' },
saveVisionSection('moodboard', JSON.stringify(items)); body: JSON.stringify({ dataUrl, index: moodUploadTarget }),
renderMoodboard(); });
} const data = await res.json();
if (data.path) {
const row = visionData['moodboard'];
let items = [];
try { items = JSON.parse(row?.Contenu || '[]'); } catch { items = []; }
if (items[moodUploadTarget]) {
items[moodUploadTarget].image = data.path;
saveVisionSection('moodboard', JSON.stringify(items));
renderMoodboard();
}
}
} catch (err) { console.error('Mood upload error:', err); }
}; };
img.src = e.target.result; img.src = e.target.result;
}; };
+19
View File
@@ -261,6 +261,25 @@ app.post('/api/hero-upload', requireAuth, (req, res) => {
} }
}); });
// Moodboard image upload
app.post('/api/mood-upload', requireAuth, (req, res) => {
try {
const { dataUrl, index } = req.body;
if (!dataUrl || !dataUrl.startsWith('data:image/')) return res.status(400).json({ error: 'Invalid image' });
const matches = dataUrl.match(/^data:image\/(\w+);base64,(.+)$/);
if (!matches) return res.status(400).json({ error: 'Invalid format' });
const ext = matches[1] === 'jpeg' ? 'jpg' : matches[1];
const buffer = Buffer.from(matches[2], 'base64');
const uploadDir = path.join(__dirname, 'public', 'uploads');
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
const filename = `mood-${index}.${ext}`;
fs.writeFileSync(path.join(uploadDir, filename), buffer);
res.json({ path: `/uploads/${filename}?t=${Date.now()}` });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// Auth gate — redirect to login page if not authenticated // Auth gate — redirect to login page if not authenticated
app.get('/', (req, res) => { app.get('/', (req, res) => {
if (isAuth(req)) { if (isAuth(req)) {