Changeset 63edb0d in Klonkt
- Timestamp:
- 06/16/2026 01:08:28 AM (3 months ago)
- Branches:
- main
- Children:
- 0b7ebbf
- Parents:
- af5407f
- Location:
- src
- Files:
-
- 4 edited
-
routes/admin-settings.js (modified) (4 diffs)
-
routes/hub.js (modified) (1 diff)
-
views/pages/admin-settings.ejs (modified) (2 diffs)
-
views/pages/hub-home.ejs (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/routes/admin-settings.js
raf5407f r63edb0d 2 2 * Admin: globale instellingen. 3 3 * - tenancy-modus (Solo/Hub) 4 * - hub-branding (naam/tagline/intro van de generieke hub-hoofdpagina)4 * - hub-branding (naam/tagline/intro/hero van de generieke hub-hoofdpagina) 5 5 * 6 6 * GET /admin/settings -> toon huidige instellingen 7 * POST /admin/settings -> sla op (god-only) 7 * POST /admin/settings -> sla op (god-only). Accepteert nu ook een geuploade 8 * hero-afbeelding (multipart); een upload wint van het 9 * URL-tekstveld. Zonder upload blijft het URL-veld leidend. 8 10 * 9 11 * De hub-pagina is generiek (van geen enkele user); deze branding leeft in … … 12 14 13 15 import express from 'express'; 16 import path from 'path'; 17 import fs from 'fs'; 18 import { fileURLToPath } from 'url'; 19 import multer from 'multer'; 20 import { v4 as uuid } from 'uuid'; 14 21 import { renderPage } from '../middleware/render.js'; 15 22 import { requireGod } from '../middleware/auth.js'; … … 17 24 18 25 const router = express.Router(); 26 27 // Hero dark-overlay: percentage 0-100 (0 = geen overlay, 100 = volledig zwart). 28 // Default 45 = de oude hardgecodeerde waarde, zodat bestaande hubs niet wijzigen. 29 function clampOverlay(raw) { 30 const v = parseInt(raw, 10); 31 return Number.isFinite(v) ? Math.max(0, Math.min(100, v)) : 45; 32 } 33 34 const __dirname = path.dirname(fileURLToPath(import.meta.url)); 35 // Hero-uploads landen in storage/media/hero → bereikbaar als /media/hero/<file> 36 // (de /media static handler serveert storage/media). Zelfde model als avatars. 37 const HERO_DIR = path.resolve( 38 process.env.HERO_PATH || path.join(__dirname, '..', '..', 'storage', 'media', 'hero') 39 ); 40 fs.mkdirSync(HERO_DIR, { recursive: true }); 41 42 // Alleen raster-formaten voor de upload. SVG mag bewust NIET via upload (raw 43 // SVG kan script bevatten → opgeslagen-XSS bij direct openen); een SVG-hero kan 44 // nog steeds via het URL-veld (zoals de meegeleverde demo-placeholder). 45 const ALLOWED_HERO_EXT = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif']); 46 const MAX_HERO_BYTES = 5 * 1024 * 1024; 47 48 const heroUpload = multer({ 49 storage: multer.diskStorage({ 50 destination: (req, file, cb) => cb(null, HERO_DIR), 51 filename: (req, file, cb) => { 52 const ext = path.extname(file.originalname).toLowerCase(); 53 cb(null, `${uuid()}${ext}`); 54 }, 55 }), 56 limits: { fileSize: MAX_HERO_BYTES }, 57 fileFilter: (req, file, cb) => { 58 const ext = path.extname(file.originalname).toLowerCase(); 59 if (!ALLOWED_HERO_EXT.has(ext)) { 60 return cb(new Error('Hero-afbeelding moet jpg/png/webp/gif zijn')); 61 } 62 cb(null, true); 63 }, 64 }); 19 65 20 66 router.get('/', requireGod, (req, res) => { … … 27 73 hubIntro: getSetting('hub_intro') || '', 28 74 hubHeroImage: getSetting('hub_hero_image') || '', 75 hubHeroOverlay: clampOverlay(getSetting('hub_hero_overlay')), 29 76 success: req.query.success || null, 77 error: req.query.error || null, 30 78 }); 31 79 }); 32 80 33 81 router.post('/', requireGod, (req, res) => { 34 if (typeof req.body.tenancy !== 'undefined') { 35 setTenancy(req.body.tenancy === 'hub' ? 'hub' : 'solo'); 36 } 37 if (typeof req.body.hub_title !== 'undefined') { 38 setSetting('hub_title', (req.body.hub_title || '').toString().slice(0, 80).trim()); 39 } 40 if (typeof req.body.hub_tagline !== 'undefined') { 41 setSetting('hub_tagline', (req.body.hub_tagline || '').toString().slice(0, 120).trim()); 42 } 43 if (typeof req.body.hub_intro !== 'undefined') { 44 setSetting('hub_intro', (req.body.hub_intro || '').toString().slice(0, 400).trim()); 45 } 46 if (typeof req.body.hub_hero_image !== 'undefined') { 47 setSetting('hub_hero_image', (req.body.hub_hero_image || '').toString().slice(0, 300).trim()); 48 } 49 res.redirect('/admin/settings?success=' + encodeURIComponent('Opgeslagen')); 82 // multer.single verwerkt multipart (hub-branding form). Bij een gewone 83 // urlencoded POST (tenancy-form) doet multer niets en blijft req.body intact. 84 heroUpload.single('hub_hero_file')(req, res, (err) => { 85 if (err) { 86 return res.redirect('/admin/settings?error=' + encodeURIComponent(err.message)); 87 } 88 89 if (typeof req.body.tenancy !== 'undefined') { 90 setTenancy(req.body.tenancy === 'hub' ? 'hub' : 'solo'); 91 } 92 if (typeof req.body.hub_title !== 'undefined') { 93 setSetting('hub_title', (req.body.hub_title || '').toString().slice(0, 80).trim()); 94 } 95 if (typeof req.body.hub_tagline !== 'undefined') { 96 setSetting('hub_tagline', (req.body.hub_tagline || '').toString().slice(0, 120).trim()); 97 } 98 if (typeof req.body.hub_intro !== 'undefined') { 99 setSetting('hub_intro', (req.body.hub_intro || '').toString().slice(0, 400).trim()); 100 } 101 102 // Hero: een geüploade afbeelding wint; anders het URL-tekstveld. 103 if (req.file) { 104 const newUrl = `/media/hero/${req.file.filename}`; 105 // Ruim een vorige geüploade hero op (alleen als die uit onze hero-map kwam). 106 const old = getSetting('hub_hero_image') || ''; 107 if (old.startsWith('/media/hero/')) { 108 try { fs.unlinkSync(path.join(HERO_DIR, path.basename(old))); } catch {} 109 } 110 setSetting('hub_hero_image', newUrl); 111 } else if (typeof req.body.hub_hero_image !== 'undefined') { 112 setSetting('hub_hero_image', (req.body.hub_hero_image || '').toString().slice(0, 300).trim()); 113 } 114 115 if (typeof req.body.hub_hero_overlay !== 'undefined') { 116 setSetting('hub_hero_overlay', String(clampOverlay(req.body.hub_hero_overlay))); 117 } 118 119 res.redirect('/admin/settings?success=' + encodeURIComponent('Opgeslagen')); 120 }); 50 121 }); 51 122 -
src/routes/hub.js
raf5407f r63edb0d 71 71 intro: getSetting('hub_intro') || '', 72 72 heroImage: getSetting('hub_hero_image') || '', 73 heroOverlay: (() => { const v = parseInt(getSetting('hub_hero_overlay'), 10); return Number.isFinite(v) ? Math.max(0, Math.min(100, v)) : 45; })(), 73 74 }; 74 75 -
src/views/pages/admin-settings.ejs
raf5407f r63edb0d 38 38 Hier stel je de naam en intro in. Jouw eigen Klonkt Hub blijft los bestaan onder <code>/user/</code>. 39 39 </p> 40 <form method="post" action="/admin/settings" class="set-form" >40 <form method="post" action="/admin/settings" class="set-form" enctype="multipart/form-data"> 41 41 <label class="set-field"> 42 42 <span>Naam</span> … … 55 55 <input type="text" name="hub_hero_image" maxlength="300" value="<%= hubHeroImage %>" placeholder="/media/... of https://..."> 56 56 </label> 57 <label class="set-field"> 58 <span>…of upload een afbeelding <small style="font-weight:400;color:var(--ink-muted)">— jpg/png/webp/gif, max 5 MB; vervangt de URL hierboven</small></span> 59 <input type="file" name="hub_hero_file" accept="image/png,image/jpeg,image/webp,image/gif"> 60 </label> 61 <label class="set-field"> 62 <span>Donkere overlay <small style="font-weight:400;color:var(--ink-muted)">— maakt de hero donkerder zodat de tekst leesbaar blijft</small></span> 63 <span style="display:flex;align-items:center;gap:0.75rem"> 64 <input type="range" name="hub_hero_overlay" min="0" max="100" step="5" value="<%= hubHeroOverlay %>" style="flex:1" 65 oninput="this.parentNode.querySelector('.ov-val').textContent=this.value+'%';var o=document.getElementById('hero-ov-preview');if(o)o.style.opacity=this.value/100"> 66 <span class="ov-val" style="min-width:3.5ch;text-align:right;font-variant-numeric:tabular-nums"><%= hubHeroOverlay %>%</span> 67 </span> 68 </label> 69 <% if (hubHeroImage) { %> 70 <div class="set-field"> 71 <span style="font-weight:400;color:var(--ink-muted);font-size:0.9rem">Voorbeeld (met overlay):</span> 72 <div style="position:relative;display:inline-block;margin-top:0.4rem;max-width:100%"> 73 <img src="<%= hubHeroImage %>" alt="Hero-voorbeeld" style="display:block;max-width:100%;max-height:160px;border-radius:8px;border:1px solid var(--rule)"> 74 <div id="hero-ov-preview" style="position:absolute;inset:0;background:#000;opacity:<%= (hubHeroOverlay/100).toFixed(2) %>;border-radius:8px;pointer-events:none"></div> 75 </div> 76 </div> 77 <% } %> 57 78 <button type="submit" class="btn btn-primary">Opslaan</button> 58 79 </form> -
src/views/pages/hub-home.ejs
raf5407f r63edb0d 17 17 18 18 <%# ── HERO (label) — de pagina begint hier, geen header erboven ────── %> 19 <header class="hub-hero<%= hub.heroImage ? ' has-img' : '' %>"<% if (hub.heroImage) { %> style="--hero-img:url('<%= hub.heroImage %>') "<% } %>>19 <header class="hub-hero<%= hub.heroImage ? ' has-img' : '' %>"<% if (hub.heroImage) { %> style="--hero-img:url('<%= hub.heroImage %>'); --hero-ov:<%= ((hub.heroOverlay == null ? 45 : hub.heroOverlay)/100).toFixed(2) %>"<% } %>> 20 20 <div class="hub-hero-inner container"> 21 21 <h1 class="hub-hero-title"><%= hub.title %></h1> … … 128 128 .hub-hero.has-img { 129 129 background: 130 linear-gradient( 180deg, rgba(0,0,0,.45) 0%, rgba(0,0,0,.55) 100%),130 linear-gradient(rgba(0,0,0,var(--hero-ov,.45)), rgba(0,0,0,var(--hero-ov,.45))), 131 131 var(--hero-img) center / cover no-repeat; 132 132 border-bottom: 0;
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)