From 5f60ba2829c87e1534ddd39cb9cc16e25bae1b74 Mon Sep 17 00:00:00 2001 From: Etienne Delvarre Date: Fri, 29 May 2026 10:52:21 +0200 Subject: [PATCH] Hero image: drag to reposition + adjustable height - Click and drag the hero image vertically to adjust framing - Height slider appears on hover below the banner - Position (Y%) and height saved to NocoDB as JSON - Backwards compatible with legacy base64-only format - Upload button moved to top-right controls overlay Co-Authored-By: Claude Opus 4.6 --- public/index.html | 110 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 97 insertions(+), 13 deletions(-) diff --git a/public/index.html b/public/index.html index 70bef7b..d672e3d 100644 --- a/public/index.html +++ b/public/index.html @@ -201,13 +201,22 @@ .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; } - .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; } + .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; background-size: cover; background-position: center; } .dark .hero-banner { background: linear-gradient(135deg, #5a4030 0%, #4a3528 30%, #3a2a20 60%, #2a1e18 100%); } - .hero-banner.has-image { background-color: #1a1614; } - .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-banner.has-image { background-color: #1a1614; cursor: grab; } + .hero-banner.has-image.dragging { cursor: grabbing; } + .hero-banner::after { content: ''; position: absolute; inset: 0; background: linear-gradient(transparent 30%, rgba(44,24,16,0.55)); pointer-events: none; } + .hero-overlay { position: relative; z-index: 1; padding: 24px 32px; width: 100%; pointer-events: none; } + .hero-overlay [contenteditable] { pointer-events: auto; } + .hero-controls { position: absolute; top: 10px; right: 12px; z-index: 3; display: flex; gap: 8px; align-items: center; opacity: 0; transition: opacity 0.2s; } + .hero-banner:hover .hero-controls { opacity: 1; } + .hero-btn { font-size: 12px; color: rgba(255,255,255,0.8); background: rgba(44,24,16,0.5); padding: 4px 12px; border-radius: 6px; border: none; cursor: pointer; backdrop-filter: blur(4px); } + .hero-btn:hover { background: rgba(44,24,16,0.7); color: #fff; } + .hero-height-control { position: absolute; bottom: -28px; left: 50%; transform: translateX(-50%); z-index: 3; display: flex; align-items: center; gap: 8px; opacity: 0; transition: opacity 0.2s; background: var(--bg-card); border: 1px solid var(--border); border-radius: 6px; padding: 4px 12px; } + .hero-banner:hover + .hero-height-control, .hero-height-control:hover { opacity: 1; } + .hero-height-control label { font-size: 11px; color: var(--text-muted); white-space: nowrap; } + .hero-height-control input[type="range"] { width: 120px; accent-color: var(--accent); } + .hero-height-control + input[type="file"] { margin-bottom: 8px; } .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; } @@ -341,13 +350,19 @@
-
- Cliquer pour changer l'image +
+
+ +
+
+ + +
@@ -1061,7 +1076,9 @@ function removeInspiration(index) { renderInspirations(); } -// ── Hero image ── +// ── Hero image with drag-to-reposition ── +let heroMeta = { posY: 50, height: 280 }; // posY in %, height in px + function applyHeroImage(dataUrl) { const banner = document.getElementById('hero-banner'); if (dataUrl) { @@ -1073,10 +1090,74 @@ function applyHeroImage(dataUrl) { } } +function applyHeroMeta() { + const banner = document.getElementById('hero-banner'); + banner.style.backgroundPosition = `center ${heroMeta.posY}%`; + banner.style.height = heroMeta.height + 'px'; + document.getElementById('hero-height-slider').value = heroMeta.height; +} + +function setHeroHeight(val) { + heroMeta.height = parseInt(val); + document.getElementById('hero-banner').style.height = val + 'px'; +} + +function saveHeroMeta() { + const imageData = visionData['hero-image']?.Contenu || ''; + if (!imageData) return; + const payload = JSON.stringify({ image: imageData, posY: heroMeta.posY, height: heroMeta.height }); + saveVisionSection('hero-image', payload); +} + +function loadHeroImageData(raw) { + if (!raw) return; + // Try JSON format (new) or plain base64 (legacy) + try { + const parsed = JSON.parse(raw); + heroMeta.posY = parsed.posY ?? 50; + heroMeta.height = parsed.height ?? 280; + applyHeroImage(parsed.image || ''); + } catch { + // Legacy: raw is the base64 image directly + applyHeroImage(raw); + } + applyHeroMeta(); +} + +// Drag to reposition +(function() { + const banner = document.getElementById('hero-banner'); + let dragging = false, startY = 0, startPos = 0; + + banner.addEventListener('mousedown', (e) => { + if (!banner.classList.contains('has-image')) return; + if (e.target.closest('[contenteditable]') || e.target.closest('.hero-controls') || e.target.closest('button')) return; + dragging = true; + startY = e.clientY; + startPos = heroMeta.posY; + banner.classList.add('dragging'); + e.preventDefault(); + }); + + window.addEventListener('mousemove', (e) => { + if (!dragging) return; + const delta = e.clientY - startY; + const sensitivity = 0.15; + heroMeta.posY = Math.max(0, Math.min(100, startPos + delta * sensitivity)); + banner.style.backgroundPosition = `center ${heroMeta.posY}%`; + }); + + window.addEventListener('mouseup', () => { + if (!dragging) return; + dragging = false; + banner.classList.remove('dragging'); + saveHeroMeta(); + }); +})(); + 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(); @@ -1089,7 +1170,10 @@ function uploadHeroImage(input) { canvas.getContext('2d').drawImage(img, 0, 0, w, h); const dataUrl = canvas.toDataURL('image/jpeg', 0.85); applyHeroImage(dataUrl); - saveVisionSection('hero-image', dataUrl); + heroMeta.posY = 50; // Reset position on new image + applyHeroMeta(); + const payload = JSON.stringify({ image: dataUrl, posY: heroMeta.posY, height: heroMeta.height }); + saveVisionSection('hero-image', payload); }; img.src = e.target.result; }; @@ -1118,8 +1202,8 @@ async function loadVision() { setupEditable(elId, section); } - // Hero image - applyHeroImage(visionData['hero-image']?.Contenu || ''); + // Hero image (JSON with position/height or legacy base64) + loadHeroImageData(visionData['hero-image']?.Contenu || ''); // Render structured sections renderTags();