diff --git a/public/index.html b/public/index.html
index 7d1a279..50dc335 100644
--- a/public/index.html
+++ b/public/index.html
@@ -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:hover { color: var(--accent); }
- .mood-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 12px; }
- .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.has-image { border-color: transparent; }
- .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:not(.has-image) .mood-label { color: var(--text-muted); background: transparent; }
+ .mood-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 16px; }
+ .mood-card { position: relative; border-radius: 10px; overflow: hidden; background: var(--bg-alt); border: 1px solid var(--border); }
+ .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-card-img.empty { border: 1px dashed var(--border); border-radius: 10px 10px 0 0; }
+ .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: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-icon { font-size: 24px; color: var(--text-hint); }
@@ -1208,7 +1209,7 @@ function removeListItem(section, containerId, cssClass, index) {
renderList(section, containerId, cssClass);
}
-// ── Moodboard ──
+// ── Moodboard (images + free text captions) ──
function renderMoodboard() {
const container = document.getElementById('v-moodboard');
const row = visionData['moodboard'];
@@ -1218,18 +1219,34 @@ function renderMoodboard() {
container.innerHTML = '';
items.forEach((item, i) => {
const card = document.createElement('div');
- card.className = 'mood-card' + (item.image ? ' has-image' : '');
- if (item.image) card.style.backgroundImage = `url(${item.image})`;
+ card.className = 'mood-card';
- card.innerHTML = `
-
-
${esc(item.label)}
- `;
- card.onclick = (e) => {
- if (e.target.closest('.mood-card-remove')) return;
- moodUploadTarget = i;
- document.getElementById('mood-file-input').click();
- };
+ const imgDiv = document.createElement('div');
+ imgDiv.className = 'mood-card-img' + (item.image ? '' : ' empty');
+ if (item.image) imgDiv.style.backgroundImage = `url(${item.image})`;
+ imgDiv.onclick = () => { moodUploadTarget = i; document.getElementById('mood-file-input').click(); };
+
+ const caption = document.createElement('div');
+ caption.className = 'mood-card-caption';
+ 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 = '×';
+ removeBtn.onclick = (e) => { e.stopPropagation(); removeMoodItem(i); };
+
+ card.appendChild(removeBtn);
+ card.appendChild(imgDiv);
+ card.appendChild(caption);
container.appendChild(card);
});
@@ -1237,9 +1254,7 @@ function renderMoodboard() {
addCard.className = 'mood-add';
addCard.innerHTML = '+
';
addCard.onclick = () => {
- const label = prompt('Catégorie (ex : Ambiance, Architecture, Costumes...) :');
- if (!label) return;
- items.push({ label, image: '' });
+ items.push({ caption: '', image: '' });
saveVisionSection('moodboard', JSON.stringify(items));
renderMoodboard();
};
@@ -1255,7 +1270,7 @@ document.addEventListener('DOMContentLoaded', () => {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
- img.onload = () => {
+ img.onload = async () => {
const maxW = 400;
let w = img.width, h = img.height;
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.getContext('2d').drawImage(img, 0, 0, w, h);
const dataUrl = canvas.toDataURL('image/jpeg', 0.8);
- const row = visionData['moodboard'];
- let items = [];
- try { items = JSON.parse(row?.Contenu || '[]'); } catch { items = []; }
- if (items[moodUploadTarget]) {
- items[moodUploadTarget].image = dataUrl;
- saveVisionSection('moodboard', JSON.stringify(items));
- renderMoodboard();
- }
+
+ try {
+ const res = await fetch('/api/mood-upload', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ dataUrl, index: moodUploadTarget }),
+ });
+ 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;
};
diff --git a/server.js b/server.js
index fa6dbe3..2d3d397 100644
--- a/server.js
+++ b/server.js
@@ -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
app.get('/', (req, res) => {
if (isAuth(req)) {