Hero banner — plus haut (280px) + image de fond uploadable

- Hauteur 280px (était 180px)
- Clic sur la bannière ouvre un sélecteur de fichier
- Image redimensionnée (max 1600px) et stockée en base64 dans NocoDB
- Indicateur "Cliquer pour changer l'image" au survol
- Texte reste éditable (clic ne déclenche pas l'upload)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Etienne Delvarre
2026-05-29 09:17:36 +02:00
parent 0574b9ab1e
commit faa8da6820
+50 -6
View File
@@ -201,10 +201,13 @@
.v-card.full { grid-column: 1 / -1; } .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; } .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; } .hero-banner { grid-column: 1 / -1; border-radius: 12px; height: 280px; background: linear-gradient(135deg, #d4a574 0%, #c4956a 30%, #a07850 60%, #7a5c40 100%); position: relative; overflow: hidden; display: flex; align-items: flex-end; cursor: pointer; background-size: cover; background-position: center; }
.dark .hero-banner { background: linear-gradient(135deg, #5a4030 0%, #4a3528 30%, #3a2a20 60%, #2a1e18 100%); } .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-banner.has-image { background-color: #1a1614; }
.hero-overlay { position: relative; z-index: 1; padding: 20px 28px; width: 100%; } .hero-banner::after { content: ''; position: absolute; inset: 0; background: linear-gradient(transparent 30%, rgba(44,24,16,0.55)); }
.hero-overlay { position: relative; z-index: 1; padding: 24px 32px; width: 100%; }
.hero-upload-hint { position: absolute; top: 12px; right: 16px; z-index: 2; font-size: 12px; color: rgba(255,255,255,0.5); background: rgba(44,24,16,0.4); padding: 4px 12px; border-radius: 6px; opacity: 0; transition: opacity 0.2s; pointer-events: none; }
.hero-banner:hover .hero-upload-hint { opacity: 1; }
.hero-overlay .three-words { font-family: 'Playfair Display', serif; font-size: 32px; font-weight: 700; color: #fff; letter-spacing: 2px; } .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; } .hero-overlay .hero-sub { font-size: 14px; color: rgba(255,255,255,0.75); margin-top: 6px; font-style: italic; }
@@ -338,12 +341,14 @@
<div class="vision-content"> <div class="vision-content">
<div class="vision-grid"> <div class="vision-grid">
<div class="hero-banner"> <div class="hero-banner" id="hero-banner" onclick="document.getElementById('hero-file-input').click()">
<span class="hero-upload-hint">Cliquer pour changer l'image</span>
<div class="hero-overlay"> <div class="hero-overlay">
<div class="three-words" id="v-trois-mots" contenteditable="true"></div> <div class="three-words" id="v-trois-mots" contenteditable="true" onclick="event.stopPropagation()"></div>
<div class="hero-sub" id="v-hero-sub" contenteditable="true"></div> <div class="hero-sub" id="v-hero-sub" contenteditable="true" onclick="event.stopPropagation()"></div>
</div> </div>
</div> </div>
<input type="file" id="hero-file-input" accept="image/*" style="display:none;" onchange="uploadHeroImage(this)">
<div class="v-card full"> <div class="v-card full">
<h3>L'histoire en une phrase</h3> <h3>L'histoire en une phrase</h3>
@@ -1056,6 +1061,42 @@ function removeInspiration(index) {
renderInspirations(); renderInspirations();
} }
// ── Hero image ──
function applyHeroImage(dataUrl) {
const banner = document.getElementById('hero-banner');
if (dataUrl) {
banner.style.backgroundImage = `url(${dataUrl})`;
banner.classList.add('has-image');
} else {
banner.style.backgroundImage = '';
banner.classList.remove('has-image');
}
}
function uploadHeroImage(input) {
const file = input.files[0];
if (!file) return;
// Resize to max 1600px wide to keep base64 reasonable
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
const maxW = 1600;
let w = img.width, h = img.height;
if (w > maxW) { h = Math.round(h * maxW / w); w = maxW; }
const canvas = document.createElement('canvas');
canvas.width = w; canvas.height = h;
canvas.getContext('2d').drawImage(img, 0, 0, w, h);
const dataUrl = canvas.toDataURL('image/jpeg', 0.85);
applyHeroImage(dataUrl);
saveVisionSection('hero-image', dataUrl);
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
input.value = '';
}
// ── Load Vision data ── // ── Load Vision data ──
async function loadVision() { async function loadVision() {
try { try {
@@ -1077,6 +1118,9 @@ async function loadVision() {
setupEditable(elId, section); setupEditable(elId, section);
} }
// Hero image
applyHeroImage(visionData['hero-image']?.Contenu || '');
// Render structured sections // Render structured sections
renderTags(); renderTags();
renderList('on-veut', 'v-on-veut', 'yes'); renderList('on-veut', 'v-on-veut', 'yes');