Changeset 8afbdd6 in Klonkt
- Timestamp:
- 06/14/2026 06:07:51 PM (3 months ago)
- Branches:
- main
- Children:
- c869272
- Parents:
- b94c08e
- Files:
-
- 3 added
- 24 edited
-
README.md (modified) (1 diff)
-
package.json (modified) (1 diff)
-
src/middleware/auth.js (modified) (3 diffs)
-
src/middleware/render.js (modified) (3 diffs)
-
src/routes/admin-sites.js (modified) (1 diff)
-
src/routes/admin-users.js (modified) (2 diffs)
-
src/routes/admin.js (modified) (2 diffs)
-
src/routes/artists.js (added)
-
src/routes/hub.js (modified) (3 diffs)
-
src/routes/posts.js (modified) (1 diff)
-
src/routes/prutter.js (modified) (3 diffs)
-
src/server.js (modified) (8 diffs)
-
src/views/pages/account.ejs (modified) (6 diffs)
-
src/views/pages/admin-settings.ejs (modified) (2 diffs)
-
src/views/pages/admin-users.ejs (modified) (4 diffs)
-
src/views/pages/admin.ejs (modified) (1 diff)
-
src/views/pages/artists-directory.ejs (added)
-
src/views/pages/home.ejs (modified) (1 diff)
-
src/views/pages/hub-home.ejs (modified) (3 diffs)
-
src/views/pages/my-site.ejs (modified) (1 diff)
-
src/views/pages/post.ejs (modified) (4 diffs)
-
src/views/pages/viewer-blocked.ejs (added)
-
src/views/partials/bottom-tab.ejs (modified) (1 diff)
-
src/views/partials/footer.ejs (modified) (1 diff)
-
src/views/partials/profile-sheet.ejs (modified) (1 diff)
-
src/views/partials/topnav.ejs (modified) (2 diffs)
-
src/views/shell.ejs (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
README.md
rb94c08e r8afbdd6 1 # PrutFolio v11 # Klonkt Hub Beta 2 2 3 3 Persoonlijk multi-site platform met editorial-feel content + sociale community. 4 4 Forked van **PrutCMS v9** (PHP, file-based) naar **Node + SQLite + htmx**. 5 5 6 > **Naam-uitleg.** "PrutCMS v9" is en blijft het file-based PHP-product. PrutFolio 7 > v1 is een nieuw product met andere DNA: het is geen *publishing tool* maar een 8 > *persoonlijk canvas* — content, profiel, sociale interactie en realtime in één. 6 > **Naam-uitleg.** "PrutCMS v9" is en blijft het file-based PHP-product. Klonkt Hub 7 > (intern pakket `prutfolio`) is een nieuw product met andere DNA: het is geen 8 > *publishing tool* maar een *persoonlijk canvas* — content, profiel, sociale 9 > interactie en realtime in één. 9 10 10 11 ## Filosofie -
package.json
rb94c08e r8afbdd6 2 2 "name": "prutfolio", 3 3 "version": "1.0.0-alpha.1", 4 "description": " PrutFolio v1— persoonlijk multi-site platform met community, realtime, en (later) E2EE DMs. Forked van PrutCMS v9 (PHP) naar Node + SQLite + htmx.",4 "description": "Klonkt Hub Beta — persoonlijk multi-site platform met community, realtime, en (later) E2EE DMs. Forked van PrutCMS v9 (PHP) naar Node + SQLite + htmx.", 5 5 "type": "module", 6 6 "main": "src/server.js", -
src/middleware/auth.js
rb94c08e r8afbdd6 37 37 } 38 38 39 // Een 'kijker' mag ALLES bekijken (incl. Beheer) maar NIETS wijzigen. De 40 // schrijf-blokkade zit in de globale guard in server.js; deze helper bepaalt 41 // alleen "is dit een alleen-lezen account?". `readonly` is de legacy-vlag die 42 // we nog meenemen zodat niet-gemigreerde demo-accounts geblokkeerd blijven. 43 export function isViewer(user) { 44 return !!user && (user.role === 'kijker' || !!user.readonly); 45 } 46 39 47 export function requireGod(req, res, next) { 40 48 if (!req.session?.user) return loginRedirect(req, res); 41 if (req.session.user.role !== 'god') { 49 const role = req.session.user.role; 50 // god beheert; een kijker mág het Beheer-paneel zien (alleen-lezen) — de 51 // globale guard 403't elke write, dus dit geeft enkel kijk-toegang. 52 if (role !== 'god' && role !== 'kijker') { 42 53 return res.status(403).send('God role required'); 43 54 } … … 52 63 if (!req.session?.user) return loginRedirect(req, res); 53 64 const u = req.session.user; 54 if (u.role === 'god' ) return next();65 if (u.role === 'god' || u.role === 'kijker') return next(); // kijker = alleen-lezen kijk-toegang 55 66 const site = res.locals.site; 56 67 if (site && site.owner_id === u.id) return next(); … … 62 73 if (!req.session?.user) return loginRedirect(req, res); 63 74 const u = req.session.user; 64 if (u.role === 'god' ) return next();75 if (u.role === 'god' || u.role === 'kijker') return next(); // kijker = alleen-lezen kijk-toegang 65 76 const site = db.prepare('SELECT owner_id FROM sites WHERE slug = ?').get(req.params.slug); 66 77 if (site && site.owner_id === u.id) return next(); -
src/middleware/render.js
rb94c08e r8afbdd6 15 15 import db from '../config/database.js'; 16 16 import PermissionsService from '../services/PermissionsService.js'; 17 import { isViewer } from './auth.js'; 17 18 import { getSetting } from '../services/SettingsService.js'; 18 19 import { PLATFORMS as PLATFORMS_CATALOG } from '../services/PlatformIcons.js'; … … 51 52 : null; 52 53 54 // Kijker-modus: alles bekijken mag, niets wijzigen. Views gebruiken canMutate 55 // om schrijf-knoppen (posten, opslaan, verwijderen) te verbergen/uit te zetten. 56 const _isViewer = isViewer(_u); 57 53 58 // Common locals 54 59 const locals = { 55 60 user: _u, 56 61 userOwnsSite, 62 isViewer: _isViewer, 63 canMutate: !_isViewer, 57 64 siteOwnerAvatar, 58 65 site: _site, … … 65 72 formatDate, 66 73 formatDateTime, 67 pageTitle: data.pageTitle || (data.site && data.site.title) || ' PrutCMS',74 pageTitle: data.pageTitle || (data.site && data.site.title) || 'Klonkt Hub Beta', 68 75 bodyClass: data.bodyClass || 'on-home', 69 76 socialDescr: data.socialDescr || '', -
src/routes/admin-sites.js
rb94c08e r8afbdd6 98 98 'auth', 'admin', 'login', 'register', 'logout', 'archive', 'search', 99 99 'account', 'sites', 'comments', 'posts', 'media', 'audio', 'prutter', 100 'forum', 'tag', 'user s', 'feed.xml', 'atom.xml', 'sitemap.xml',100 'forum', 'tag', 'user', 'users', 'artiesten', 'feed.xml', 'atom.xml', 'sitemap.xml', 101 101 'manifest.webmanifest', 'sw.js', 'favicon.ico', 'favicon.svg', 'assets', 102 102 ]); -
src/routes/admin-users.js
rb94c08e r8afbdd6 20 20 const router = express.Router(); 21 21 22 const VALID_ROLES = new Set(['member', 'admin', 'god']); 22 // 'kijker' = alleen-lezen demonstratie/audit-account: mag alles bekijken (incl. 23 // Beheer), maar de globale guard blokkeert elke wijziging. Vervangt de oude 24 // losse 'kijk-modus'-vlag (readonly), die nu door deze rol wordt afgedekt. 25 const VALID_ROLES = new Set(['kijker', 'member', 'admin', 'god']); 23 26 24 27 function godCount() { … … 66 69 } 67 70 68 db.prepare('UPDATE users SET role = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?') 71 // readonly=0: de alleen-lezen-status zit nu volledig in de 'kijker'-rol, dus 72 // bij elke rolwijziging ruimen we de legacy-vlag op (geen dubbele bron). 73 db.prepare('UPDATE users SET role = ?, readonly = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?') 69 74 .run(newRole, userId); 70 75 res.redirect('/admin/users?success=' + encodeURIComponent('Role updated')); 71 });72 73 // ==================== READ-ONLY (kijk-account) ====================74 router.post('/:id/readonly', requireGod, (req, res) => {75 const target = db.prepare('SELECT id FROM users WHERE id = ?').get(req.params.id);76 if (!target) return res.redirect('/admin/users?error=User+not+found');77 // Niet op jezelf — voorkomt dat een god zichzelf alleen-lezen maakt en vastloopt.78 if (target.id === req.session.user.id) {79 return res.redirect('/admin/users?error=' + encodeURIComponent('Kan jezelf niet op alleen-lezen zetten'));80 }81 const ro = req.body.readonly === '1' ? 1 : 0;82 db.prepare('UPDATE users SET readonly = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')83 .run(ro, req.params.id);84 res.redirect('/admin/users?success=' + encodeURIComponent(ro ? 'Kijk-modus aan' : 'Kijk-modus uit'));85 76 }); 86 77 -
src/routes/admin.js
rb94c08e r8afbdd6 16 16 const user = req.session.user; 17 17 18 // Niet-god: een artiest die een eigen site bezit krijgt een "Mijn PrutFolio"- 19 // dashboard, gescopet op z'n eigen site. Bezit 'ie geen site -> geen beheer. 20 if (user.role !== 'god') { 18 // Een kijker mag het volledige (god-)Beheer alleen-lezen inzien — net als god 19 // dus, alleen schrijven is globaal geblokkeerd. Een gewone artiest die een 20 // eigen site bezit krijgt een "Mijn Klonkt Hub"-dashboard, gescopet op z'n 21 // eigen site. Bezit 'ie geen site -> geen beheer. 22 if (user.role !== 'god' && user.role !== 'kijker') { 21 23 const mySite = db.prepare( 22 24 'SELECT * FROM sites WHERE owner_id = ? ORDER BY created_at ASC LIMIT 1' … … 29 31 }; 30 32 return renderPage(req, res, 'pages/my-site', { 31 pageTitle: 'Mijn PrutFolio',33 pageTitle: 'Mijn Klonkt Hub', 32 34 bodyClass: 'on-admin', 33 35 mySite, -
src/routes/hub.js
rb94c08e r8afbdd6 35 35 `).all(); 36 36 37 // ALLE PrutFolio's (elke user, incl. de admin z'n eigen site) voor de roster. 37 // Uitgelichte PrutFolio's voor de home-roster: meest-actief eerst (aantal 38 // gepubliceerde posts), dan nieuwste. Beperkt tot HOME_ROSTER_LIMIT zodat de 39 // home schaalt — de volledige, doorzoekbare lijst staat op /artiesten. 40 const HOME_ROSTER_LIMIT = 24; 38 41 const artists = db.prepare(` 39 42 SELECT s.slug, s.title, s.tagline, s.profile_photo, s.accent, … … 42 45 FROM sites s 43 46 LEFT JOIN users u ON u.id = s.owner_id 44 ORDER BY s.created_at ASC 45 `).all(); 47 ORDER BY post_count DESC, s.created_at DESC 48 LIMIT ? 49 `).all(HOME_ROSTER_LIMIT); 50 const totalArtists = db.prepare('SELECT COUNT(*) AS c FROM sites').get().c; 46 51 47 52 // De hub-pagina is GENERIEK (van geen enkele user) — branding komt uit globale … … 60 65 hub, 61 66 artists, 67 totalArtists, 68 rosterLimit: HOME_ROSTER_LIMIT, 62 69 posts, 63 70 }); -
src/routes/posts.js
rb94c08e r8afbdd6 62 62 'archive', 'search', 'account', 'sites', 'comments', 63 63 'posts', 'media', 'audio', 'prutter', 'forum', 64 'tag', 'type', 'user s', 'feed.xml', 'atom.xml', 'sitemap.xml',64 'tag', 'type', 'user', 'users', 'artiesten', 'feed.xml', 'atom.xml', 'sitemap.xml', 65 65 'manifest.webmanifest', 'sw.js', 'favicon.ico', 'favicon.svg', 'assets', 66 66 ]); -
src/routes/prutter.js
rb94c08e r8afbdd6 18 18 import db from '../config/database.js'; 19 19 import { renderPage } from '../middleware/render.js'; 20 import { requireAuth } from '../middleware/auth.js';20 import { requireAuth, isViewer } from '../middleware/auth.js'; 21 21 import PermissionsService from '../services/PermissionsService.js'; 22 22 … … 55 55 // ==================== START / RESUME CONVERSATION ==================== 56 56 router.get('/new', requirePrutter, (req, res) => { 57 // Een gesprek starten is een schrijf-actie (INSERT) — een kijker mag dat niet. 58 // De globale guard pakt dit niet omdat het een GET is, dus expliciet blokkeren. 59 if (isViewer(req.session.user)) { 60 res.status(403); 61 return renderPage(req, res, 'pages/viewer-blocked', { pageTitle: 'Kijker-modus', bodyClass: 'on-special' }); 62 } 57 63 const targetUsername = (req.query.to || '').toString().trim(); 58 64 if (!targetUsername) { … … 90 96 const messages = prutter.getMessages(conv.id, 200, 0).reverse(); 91 97 92 // Mark inbound messages as read 93 prutter.markAsRead(conv.id, me); 98 // Mark inbound messages as read — sla over voor kijkers (markAsRead is een 99 // UPDATE; een GET valt buiten de globale guard, dus hier expliciet skippen). 100 if (!isViewer(req.session.user)) prutter.markAsRead(conv.id, me); 94 101 95 102 renderPage(req, res, 'pages/prutter-conversation', { -
src/server.js
rb94c08e r8afbdd6 1 1 /** 2 * PrutFolio v1— server bootstrap2 * Klonkt Hub Beta — server bootstrap 3 3 * 4 4 * Persoonlijk multi-site platform forked van PrutCMS v9 (PHP, file-based). … … 21 21 22 22 import { resolveSite, loadAudioTracks, loadTheme } from './middleware/site.js'; 23 import { isViewer } from './middleware/auth.js'; 24 import { renderPage } from './middleware/render.js'; 23 25 import authRoutes from './routes/auth.js'; 24 26 import accountRoutes from './routes/account.js'; … … 39 41 import feedRoutes from './routes/feed.js'; 40 42 import hubRoutes from './routes/hub.js'; 43 import artistsRoutes from './routes/artists.js'; 41 44 import postsRoutes from './routes/posts.js'; 42 45 … … 164 167 }); 165 168 166 // Read-only/kijk-accounts: alles bekijken mag, niets wijzigen. Blokkeert elke 167 // state-wijzigende methode (de login-POST zelf zet de sessie pas, dus die valt 168 // hier nog niet onder). 169 // Kijker-accounts: alles bekijken mag (incl. Beheer), niets wijzigen. Dit is de 170 // ENIGE schrijf-blokkade — fail-closed, vóór alle route-handlers. Elke state- 171 // wijzigende methode wordt geweigerd (de login-POST zet de sessie pas ná deze 172 // guard, dus die valt er niet onder). I.p.v. rauwe 403-tekst tonen we een nette 173 // pagina (of, bij HTMX, een ingeswapte melding). 169 174 app.use((req, res, next) => { 170 if (req.session?.user?.readonly && req.method !== 'GET' && req.method !== 'HEAD' && req.method !== 'OPTIONS') { 171 return res.status(403).send('Demo-account: alleen-lezen — wijzigen is uitgeschakeld.'); 175 const mutating = req.method !== 'GET' && req.method !== 'HEAD' && req.method !== 'OPTIONS'; 176 if (mutating && isViewer(req.session?.user)) { 177 if (req.headers['hx-request'] === 'true') { 178 // htmx swapt niet op 4xx; stuur 200 + retarget zodat de melding in #pcms-main verschijnt. 179 res.setHeader('HX-Retarget', '#pcms-main'); 180 res.setHeader('HX-Reswap', 'innerHTML'); 181 res.status(200); 182 } else { 183 res.status(403); 184 } 185 return renderPage(req, res, 'pages/viewer-blocked', { 186 pageTitle: 'Kijker-modus', 187 bodyClass: 'on-special', 188 }); 172 189 } 173 190 next(); … … 192 209 // Feed/sitemap routes are mounted at root because they're at well-known paths 193 210 app.use('/', feedRoutes); 211 app.use('/artiesten', artistsRoutes); // doorzoekbare artiesten-directory (alleen hub; solo: next()) 194 212 app.use('/', hubRoutes); // hub-overview op '/' (solo: next() -> postsRoutes) 195 213 app.use('/', postsRoutes); … … 215 233 res.json({ 216 234 id: idBase, 217 name: site?.title || ' PrutFolio',218 short_name: (site?.title || ' PrutFolio').slice(0, 12),235 name: site?.title || 'Klonkt Hub Beta', 236 short_name: (site?.title || 'Klonkt').slice(0, 12), 219 237 description: site?.description || site?.tagline || '', 220 238 scope, … … 324 342 return; 325 343 } 344 // Kijker-accounts zijn alleen-lezen: weiger de WS-upgrade. De HTTP-guard 345 // dekt geen WS, dus dit is de plek om schrijven via een (toekomstige) 346 // message-handler te voorkomen. 347 if (isViewer(req.session.user)) { 348 socket.write('HTTP/1.1 403 Forbidden\r\n\r\n'); 349 socket.destroy(); 350 return; 351 } 326 352 wss.handleUpgrade(req, socket, head, (ws) => { 327 353 ws.userId = req.session.user.id; … … 350 376 server.listen(PORT, () => { 351 377 console.log(''); 352 console.log('🪶 PrutFolio v1 — alpha');378 console.log('🪶 Klonkt Hub Beta'); 353 379 console.log(` http://localhost:${PORT}`); 354 380 console.log(''); -
src/views/pages/account.ejs
rb94c08e r8afbdd6 12 12 <% if (error) { %><div class="ax-flash ax-flash-err"><%= error %></div><% } %> 13 13 14 <% if (isViewer) { %> 15 <div class="ax-viewer-note" role="note"> 16 <span class="ax-viewer-ico" aria-hidden="true">👁️</span> 17 <div> 18 <strong>Kijker-modus</strong> 19 <span>Dit is een demo-account. Je kunt alles bekijken, maar niets wijzigen — ook geen foto of bio.</span> 20 </div> 21 </div> 22 <% } %> 23 14 24 <%# ── PROFILE ────────────────────────────────────────────── %> 15 25 <section class="ax-card"> … … 17 27 18 28 <div class="ax-profile-id"> 19 <form action="/account/avatar" method="post" enctype="multipart/form-data" class="ax-avatar-form"> 20 <label class="ax-avatar-wrap" title="Klik om je foto te wijzigen"> 21 <span class="ax-profile-avatar"> 22 <% if (account.avatar_url) { %> 23 <img src="<%= account.avatar_url %>" alt=""> 24 <% } else { %> 25 <span><%= account.username.charAt(0).toUpperCase() %></span> 26 <% } %> 27 </span> 28 <span class="ax-avatar-edit" aria-hidden="true"> 29 <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg> 30 </span> 31 <input type="file" name="avatar" accept="image/jpeg,image/png,image/webp,image/gif" onchange="this.form.submit()" hidden> 32 </label> 33 </form> 29 <% if (canMutate) { %> 30 <form action="/account/avatar" method="post" enctype="multipart/form-data" class="ax-avatar-form"> 31 <label class="ax-avatar-wrap" title="Klik om je foto te wijzigen"> 32 <span class="ax-profile-avatar"> 33 <% if (account.avatar_url) { %> 34 <img src="<%= account.avatar_url %>" alt=""> 35 <% } else { %> 36 <span><%= account.username.charAt(0).toUpperCase() %></span> 37 <% } %> 38 </span> 39 <span class="ax-avatar-edit" aria-hidden="true"> 40 <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg> 41 </span> 42 <input type="file" name="avatar" accept="image/jpeg,image/png,image/webp,image/gif" onchange="this.form.submit()" hidden> 43 </label> 44 </form> 45 <% } else { %> 46 <span class="ax-profile-avatar ax-profile-avatar-static"> 47 <% if (account.avatar_url) { %> 48 <img src="<%= account.avatar_url %>" alt=""> 49 <% } else { %> 50 <span><%= account.username.charAt(0).toUpperCase() %></span> 51 <% } %> 52 </span> 53 <% } %> 34 54 <div class="ax-profile-meta"> 35 55 <div class="ax-profile-name"><%= account.username %></div> … … 38 58 <div class="ax-profile-joined">Lid sinds <%= formatDate(account.created_at) %></div> 39 59 <% } %> 40 <% if (account.avatar_url ) { %>60 <% if (account.avatar_url && canMutate) { %> 41 61 <form action="/account/avatar/remove" method="post" class="ax-avatar-remove"> 42 62 <button type="submit" class="ax-linkbtn">Foto verwijderen</button> … … 46 66 </div> 47 67 48 <form action="/account/profile" method="post" class="ax-form ax-form-bio"> 49 <label class="ax-field"> 50 <span>Bio</span> 51 <textarea name="bio" rows="3" maxlength="500" placeholder="Een korte regel over jezelf"><%= account.bio || '' %></textarea> 52 </label> 53 <div class="ax-form-actions"> 54 <button type="submit" class="ax-btn ax-btn-primary">Bio opslaan</button> 55 </div> 56 </form> 68 <% if (canMutate) { %> 69 <form action="/account/profile" method="post" class="ax-form ax-form-bio"> 70 <label class="ax-field"> 71 <span>Bio</span> 72 <textarea name="bio" rows="3" maxlength="500" placeholder="Een korte regel over jezelf"><%= account.bio || '' %></textarea> 73 </label> 74 <div class="ax-form-actions"> 75 <button type="submit" class="ax-btn ax-btn-primary">Bio opslaan</button> 76 </div> 77 </form> 78 <% } else { %> 79 <div class="ax-form ax-form-bio"> 80 <label class="ax-field"> 81 <span>Bio</span> 82 <textarea rows="3" disabled placeholder="Geen bio"><%= account.bio || '' %></textarea> 83 </label> 84 </div> 85 <% } %> 57 86 </section> 58 87 59 88 <%# ── WACHTWOORD ────────────────────────────────────────── %> 89 <%# In kijker-modus geen wachtwoord-sectie (niets te wijzigen). %> 90 <% if (canMutate) { %> 60 91 <% if (hasPassword) { %> 61 92 <section class="ax-card"> … … 87 118 </section> 88 119 <% } %> 120 <% } %> 89 121 90 122 </div> … … 107 139 .ax-flash-ok { background: rgba(40,160,90,.15); color: #2a9d5e; border: 1px solid rgba(40,160,90,.3); } 108 140 .ax-flash-err { background: rgba(200,60,60,.15); color: #c33; border: 1px solid rgba(200,60,60,.3); } 141 142 /* ─── Kijker-modus notice ──────────────────────────────────────── */ 143 .ax-viewer-note { 144 display: flex; align-items: flex-start; gap: 0.75rem; 145 padding: 0.85rem 1rem; margin-bottom: 1rem; 146 border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--rule)); 147 border-left: 3px solid var(--accent); 148 border-radius: 10px; 149 background: color-mix(in srgb, var(--accent) 8%, var(--paper)); 150 } 151 .ax-viewer-ico { font-size: 1.1rem; line-height: 1.4; } 152 .ax-viewer-note strong { display: block; font-size: 0.92rem; } 153 .ax-viewer-note span { color: var(--ink-muted, var(--ink-soft)); font-size: 0.85rem; } 154 .ax-profile-avatar-static { flex-shrink: 0; } 155 .ax-form-bio textarea:disabled { opacity: 0.7; cursor: default; } 109 156 110 157 /* ─── Cards ────────────────────────────────────────────────────── */ -
src/views/pages/admin-settings.ejs
rb94c08e r8afbdd6 24 24 <span> 25 25 <strong>Hub</strong> — bedrijfssite met <code>/user/</code>. 26 <small>Jij (admin) wijst gebruikers een eigen PrutFoliotoe. Gesloten cirkel: geen open registratie.</small>26 <small>Jij (admin) wijst gebruikers een eigen Klonkt Hub toe. Gesloten cirkel: geen open registratie.</small> 27 27 </span> 28 28 </label> … … 36 36 <p class="set-help"> 37 37 De openbare hub-pagina (<code>/</code>) is generiek — van geen enkele gebruiker. 38 Hier stel je de naam en intro in. Jouw eigen PrutFolioblijft los bestaan onder <code>/user/</code>.38 Hier stel je de naam en intro in. Jouw eigen Klonkt Hub blijft los bestaan onder <code>/user/</code>. 39 39 </p> 40 40 <form method="post" action="/admin/settings" class="set-form"> -
src/views/pages/admin-users.ejs
rb94c08e r8afbdd6 5 5 <div> 6 6 <h1>Users</h1> 7 <p class="ax-tagline">Beheer gebruikers, rollen, en verwijderingen. </p>7 <p class="ax-tagline">Beheer gebruikers, rollen, en verwijderingen. Rol <strong>kijker</strong> = alles bekijken (incl. Beheer), niets wijzigen — handig voor demo's.</p> 8 8 </div> 9 9 </header> … … 58 58 <label class="ax-role"> 59 59 <span>Rol</span> 60 <select name="role" onchange="this.form.submit()" <% if (u.id === user.id) { %>disabled<% } %>> 60 <select name="role" onchange="this.form.submit()" <% if (u.id === user.id || !canMutate) { %>disabled<% } %>> 61 <option value="kijker" <%= u.role === 'kijker' ? 'selected' : '' %>>kijker</option> 61 62 <option value="member" <%= u.role === 'member' ? 'selected' : '' %>>member</option> 62 63 <option value="admin" <%= u.role === 'admin' ? 'selected' : '' %>>admin</option> … … 66 67 </form> 67 68 68 <form method="post" action="/admin/users/<%= u.id %>/readonly" class="ax-role-form"> 69 <label class="ax-role ax-ro" title="Alleen-lezen: kan alles bekijken, niets wijzigen"> 70 <span>Kijk-modus</span> 71 <input type="checkbox" name="readonly" value="1" <%= u.readonly ? 'checked' : '' %> 72 onchange="this.form.submit()" <% if (u.id === user.id) { %>disabled<% } %>> 73 </label> 74 </form> 75 76 <% if (u.id !== user.id) { %> 69 <% if (u.id !== user.id && canMutate) { %> 77 70 <% var _warn = (u.post_count + u.site_count > 0) 78 71 ? ('Dit verwijdert ook hun site + ' + u.post_count + ' post(s). ') … … 83 76 </form> 84 77 <% } else { %> 85 <%# Lege plek zodat de kolommen (rol/kijk-modus) op elke rij uitlijnen. %>78 <%# Lege plek zodat de rol-kolom op elke rij uitlijnt. %> 86 79 <span class="ax-icon-btn-spacer" aria-hidden="true"></span> 87 80 <% } %> -
src/views/pages/admin.ejs
rb94c08e r8afbdd6 2 2 <h1>Beheer</h1> 3 3 <% if (tenancy === 'hub') { %> 4 <p class="admin-tagline">Hub-modus — bedrijfssite met gebruikers, elk hun eigen PrutFolio.</p>4 <p class="admin-tagline">Hub-modus — bedrijfssite met gebruikers, elk hun eigen Klonkt Hub.</p> 5 5 <% } else { %> 6 6 <p class="admin-tagline">Solo-modus — jouw site.</p> -
src/views/pages/home.ejs
rb94c08e r8afbdd6 25 25 <h1>Hier is het nog stil.</h1> 26 26 <p>Nog geen posts. Spannend.</p> 27 <% if (user && permissions.canCreatePost(user, site)) { %>27 <% if (user && canMutate && permissions.canCreatePost(user, site)) { %> 28 28 <p style="margin-top:2rem"> 29 29 <a class="btn" href="<%= _base %>/posts/new" -
src/views/pages/hub-home.ejs
rb94c08e r8afbdd6 33 33 <%# ── ARTIESTEN-ROSTER ─────────────────────────────────────────── %> 34 34 <% if (artists && artists.length) { %> 35 <% var _total = (typeof totalArtists !== 'undefined') ? totalArtists : artists.length; %> 36 <% var _hasMore = _total > artists.length; %> 35 37 <section class="hub-sec"> 36 <h2 class="hub-sec-title">Artiesten</h2> 38 <h2 class="hub-sec-title"> 39 Artiesten 40 <% if (_hasMore) { %><span class="hub-sec-count"><%= _total %></span><% } %> 41 </h2> 37 42 <div class="roster"> 38 43 <% artists.forEach(function(a){ %> … … 48 53 <% }); %> 49 54 </div> 55 <% if (_hasMore) { %> 56 <div class="hub-more"> 57 <a class="hub-more-link" href="/artiesten">Alle <%= _total %> artiesten <span aria-hidden="true">→</span></a> 58 </div> 59 <% } %> 50 60 </section> 51 61 <% } %> … … 131 141 } 132 142 .hub-sec-title::after { content: ''; flex: 1; height: 1px; background: var(--rule); } 143 .hub-sec-count { 144 font-family: var(--font-ui, system-ui); font-size: 0.8rem; font-weight: 700; 145 color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent); 146 padding: 0.1rem 0.55rem; border-radius: 99px; order: 1; 147 } 148 .hub-more { margin-top: 1.25rem; text-align: center; } 149 .hub-more-link { 150 display: inline-flex; align-items: center; gap: 0.4rem; 151 padding: 0.6rem 1.3rem; border: 1px solid var(--rule); border-radius: 99px; 152 background: var(--paper-2); color: var(--ink); text-decoration: none; 153 font-weight: 600; font-size: 0.92rem; transition: border-color 140ms, background 140ms; 154 } 155 .hub-more-link:hover { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 7%, var(--paper-2)); } 133 156 134 157 /* ── Roster (artiesten) ───────────────────────────────────────────── */ -
src/views/pages/my-site.ejs
rb94c08e r8afbdd6 1 1 <div class="container admin-page"> 2 2 <p><a href="/" class="btn">← Overzicht</a></p> 3 <h1>Mijn PrutFolio</h1>3 <h1>Mijn Klonkt Hub</h1> 4 4 <p class="admin-tagline"><%= mySite.title %></p> 5 5 -
src/views/pages/post.ejs
rb94c08e r8afbdd6 103 103 <div class="comment-content"><%= c.content %></div> 104 104 <div class="comment-actions"> 105 <% if (user ) { %>105 <% if (user && canMutate) { %> 106 106 <button type="button" class="comment-reply-btn" data-reply-to="<%= c.id %>">Reply</button> 107 107 <% } %> … … 114 114 115 115 <!-- Inline reply form (hidden by default) --> 116 <% if (user ) { %>116 <% if (user && canMutate) { %> 117 117 <form method="post" action="<%= _base %>/comments" class="comment-reply-form" hidden data-reply-form-for="<%= c.id %>"> 118 118 <input type="hidden" name="post_slug" value="<%= post.slug %>"> … … 163 163 164 164 <!-- Top-level comment form (only for logged-in users for now) --> 165 <% if (user ) { %>165 <% if (user && canMutate) { %> 166 166 <form method="post" action="<%= _base %>/comments" class="comment-form"> 167 167 <input type="hidden" name="post_slug" value="<%= post.slug %>"> … … 172 172 <button type="submit" class="btn btn-primary">Post comment</button> 173 173 </form> 174 <% } else { %>174 <% } else if (!user) { %> 175 175 <p class="comments-login-cta"> 176 176 <a href="/auth/login?next=<%= encodeURIComponent(_base + '/' + post.slug + '#comments') %>" class="btn">Log in to comment</a> 177 177 </p> 178 <% } else { %> 179 <p class="comments-login-cta" style="color:var(--ink-muted)">👁️ Kijker-modus — reageren is uitgeschakeld.</p> 178 180 <% } %> 179 181 </section> -
src/views/partials/bottom-tab.ejs
rb94c08e r8afbdd6 11 11 const _siteUrlBase = (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : ''; 12 12 const _path = (typeof currentPath !== 'undefined' && currentPath) ? currentPath : '/'; 13 const _canPost = !!(user && permissions && permissions.canCreatePost && permissions.canCreatePost(user, site)); 13 const _canPost = !!(user && (typeof canMutate === 'undefined' || canMutate) 14 && permissions && permissions.canCreatePost && permissions.canCreatePost(user, site)); 14 15 const _hasPrutter = !!(user && site && site.enable_prutter && (typeof tenancy !== 'undefined' && tenancy === 'hub')); 15 16 const _ownProfile = user ? ('/users/' + user.username) : null; -
src/views/partials/footer.ejs
rb94c08e r8afbdd6 2 2 <div class="container site-footer-inner"> 3 3 <div> 4 <span>© <%= new Date().getFullYear() %> <%= (site && site.title) || ' PrutFolio' %></span>4 <span>© <%= new Date().getFullYear() %> <%= (site && site.title) || 'Klonkt' %></span> 5 5 </div> 6 6 <div class="footer-meta"> 7 <span> PrutFolio v1</span>7 <span>Klonkt Hub Beta</span> 8 8 <%# Mobile-only theme toggle. On desktop the topnav has its own toggle 9 9 so we hide this one to avoid two visible toggles at once. The same -
src/views/partials/profile-sheet.ejs
rb94c08e r8afbdd6 14 14 15 15 const _isAdmin = (user.role === 'god' || user.role === 'admin'); 16 const _canPost = !!(typeof permissions !== 'undefined' 16 const _canPost = !!((typeof canMutate === 'undefined' || canMutate) 17 && typeof permissions !== 'undefined' 17 18 && permissions 18 19 && permissions.canCreatePost -
src/views/partials/topnav.ejs
rb94c08e r8afbdd6 16 16 const _hasSearch = !site || site.show_search === undefined || site.show_search; 17 17 const _hasPrutter = user && site && site.enable_prutter && (typeof tenancy !== 'undefined' && tenancy === 'hub'); 18 const _canPost = user && permissions && permissions.canCreatePost && permissions.canCreatePost(user, site); 18 const _canPost = user && (typeof canMutate === 'undefined' || canMutate) 19 && permissions && permissions.canCreatePost && permissions.canCreatePost(user, site); 19 20 %> 20 21 … … 36 37 hx-push-url="<%= _siteUrlBase %>/" hx-indicator="#pcms-loading"><%= site.title %></a> 37 38 <% } else { %> 38 <a class="site-title" href="/"> PrutFolio</a>39 <a class="site-title" href="/">Klonkt</a> 39 40 <% } %> 40 41 </div> -
src/views/shell.ejs
rb94c08e r8afbdd6 239 239 <body class="<%= bodyClass || 'on-home' %> has-bottom-tab" data-feed-view="<%= _e(safeSite.feed_view_default || 'timeline') %>" data-grid-cols="3" data-site-base="<%= _e((typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '') %>"> 240 240 241 <% if (user && user.readonly) { %> 242 <div style="position:sticky;top:0;z-index:50;background:#e8b04b;color:#1a1a1a;text-align:center;font-size:0.85rem;font-weight:600;padding:0.4rem 1rem;"> 243 👁️ Demo — alleen-lezen: je kunt alles bekijken, maar niets wijzigen. 241 <% if (typeof isViewer !== 'undefined' && isViewer) { %> 242 <div class="viewer-banner" role="status"> 243 <span class="viewer-banner-ico" aria-hidden="true">👁️</span> 244 <span class="viewer-banner-text"><strong>Kijker-modus</strong> — je kunt alles bekijken, maar niets wijzigen.</span> 244 245 </div> 246 <style> 247 .viewer-banner { 248 position: sticky; top: 0; z-index: 60; 249 display: flex; align-items: center; justify-content: center; gap: 0.5rem; 250 padding: 0.5rem 1rem; 251 background: linear-gradient(90deg, 252 color-mix(in srgb, var(--accent) 88%, #000) 0%, 253 var(--accent) 100%); 254 color: #fff; 255 font-size: 0.85rem; line-height: 1.3; 256 box-shadow: 0 1px 6px color-mix(in srgb, var(--accent) 45%, transparent); 257 } 258 .viewer-banner-ico { font-size: 1rem; } 259 .viewer-banner-text strong { font-weight: 700; } 260 </style> 245 261 <% } %> 246 262
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)