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:
+49
-23
@@ -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})">×</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 = '×';
|
||||||
|
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);
|
||||||
|
|
||||||
|
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'];
|
const row = visionData['moodboard'];
|
||||||
let items = [];
|
let items = [];
|
||||||
try { items = JSON.parse(row?.Contenu || '[]'); } catch { items = []; }
|
try { items = JSON.parse(row?.Contenu || '[]'); } catch { items = []; }
|
||||||
if (items[moodUploadTarget]) {
|
if (items[moodUploadTarget]) {
|
||||||
items[moodUploadTarget].image = dataUrl;
|
items[moodUploadTarget].image = data.path;
|
||||||
saveVisionSection('moodboard', JSON.stringify(items));
|
saveVisionSection('moodboard', JSON.stringify(items));
|
||||||
renderMoodboard();
|
renderMoodboard();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} catch (err) { console.error('Mood upload error:', err); }
|
||||||
};
|
};
|
||||||
img.src = e.target.result;
|
img.src = e.target.result;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user