Changeset 535f955 in Klonkt
- Timestamp:
- 06/19/2026 09:49:35 AM (3 months ago)
- Branches:
- main
- Children:
- 247988e
- Parents:
- 8cdb377
- Files:
-
- 2 added
- 6 edited
-
CHANGELOG.md (modified) (1 diff)
-
src/config/database.js (modified) (1 diff)
-
src/routes/posts.js (modified) (5 diffs)
-
src/views/pages/favorites.ejs (added)
-
src/views/pages/post.ejs (modified) (2 diffs)
-
src/views/partials/like-button.ejs (added)
-
src/views/partials/profile-sheet.ejs (modified) (1 diff)
-
src/views/partials/topnav.ejs (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
CHANGELOG.md
r8cdb377 r535f955 13 13 14 14 ## [Unreleased] 15 16 ### Toegevoegd 17 - Posts liken / favorieten: ingelogde gebruikers kunnen een post liken met een ā„-knop 18 (toont het aantal likes). Je gelikete posts staan op de nieuwe **Favorieten**-pagina 19 (`/favorieten`, link in het accountmenu). Niet-ingelogd ā de knop leidt naar de login. 15 20 16 21 ### Gewijzigd -
src/config/database.js
r8cdb377 r535f955 256 256 ); 257 257 `); 258 259 // Likes / favorieten: een ingelogde gebruiker kan een post liken. De set van 260 // posts die een gebruiker likte = z'n favorieten (/favorieten-pagina). EƩn rij 261 // per (post, user); uniek zodat liken idempotent is. 262 db.exec(` 263 CREATE TABLE IF NOT EXISTS post_likes ( 264 post_id TEXT NOT NULL, 265 user_id TEXT NOT NULL, 266 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 267 PRIMARY KEY (post_id, user_id) 268 ); 269 CREATE INDEX IF NOT EXISTS idx_post_likes_user ON post_likes(user_id, created_at); 270 CREATE INDEX IF NOT EXISTS idx_post_likes_post ON post_likes(post_id); 271 `); 258 272 } 259 273 -
src/routes/posts.js
r8cdb377 r535f955 5 5 import { fileURLToPath } from 'url'; 6 6 import multer from 'multer'; 7 import ejs from 'ejs'; 7 8 import db from '../config/database.js'; 8 9 import { requireAuth } from '../middleware/auth.js'; … … 63 64 'archive', 'search', 'account', 'sites', 'comments', 64 65 'posts', 'media', 'audio', 'prutter', 'forum', 65 'tag', 'type', 'user', 'users', 'artiesten', 'leden', 'f eed.xml', 'atom.xml', 'sitemap.xml',66 'tag', 'type', 'user', 'users', 'artiesten', 'leden', 'favorieten', 'feed.xml', 'atom.xml', 'sitemap.xml', 66 67 'manifest.webmanifest', 'sw.js', 'favicon.ico', 'favicon.svg', 'assets', 67 68 ]); … … 392 393 bodyClass: 'on-archive', 393 394 }); 395 }); 396 397 // Pad naar de like-knop-partial (voor de htmx-toggle re-render). 398 const LIKE_PARTIAL = path.join(__dirname, '..', 'views', 'partials', 'like-button.ejs'); 399 400 // ==================== LIKE / FAVORIET ==================== 401 // Een ingelogde gebruiker (geen kijker ā de globale guard blokkeert non-GET voor 402 // kijkers) togglet een like op een gepubliceerde post. Geeft de her-gerenderde 403 // knop terug (htmx outerHTML-swap). 404 router.post('/posts/:id/like', requireAuth, (req, res) => { 405 const userId = req.session.user.id; 406 const post = db.prepare('SELECT id, status FROM posts WHERE id = ?').get(req.params.id); 407 if (!post) return res.status(404).send('Post niet gevonden'); 408 if (post.status !== 'published') return res.status(403).send('Niet beschikbaar'); 409 410 const exists = db.prepare('SELECT 1 FROM post_likes WHERE post_id = ? AND user_id = ?').get(post.id, userId); 411 if (exists) { 412 db.prepare('DELETE FROM post_likes WHERE post_id = ? AND user_id = ?').run(post.id, userId); 413 } else { 414 db.prepare('INSERT OR IGNORE INTO post_likes (post_id, user_id) VALUES (?, ?)').run(post.id, userId); 415 } 416 const likeCount = db.prepare('SELECT COUNT(*) AS c FROM post_likes WHERE post_id = ?').get(post.id).c; 417 418 const html = ejs.render(fs.readFileSync(LIKE_PARTIAL, 'utf8'), { 419 post: { id: post.id }, likedByMe: !exists, likeCount, loggedIn: true, loginNext: '/', 420 }); 421 res.send(html); 422 }); 423 424 // Favorieten = de posts die de ingelogde gebruiker likete. Solo: binnen de 425 // huidige site. Hub: over alle sites (met juiste /user/<slug>-links). 426 router.get('/favorieten', requireAuth, (req, res) => { 427 const userId = req.session.user.id; 428 const isHub = res.locals.tenancy === 'hub'; 429 const site = res.locals.site; 430 const rows = isHub 431 ? db.prepare(` 432 SELECT p.id, p.slug, p.title, p.excerpt, p.cover_image_url, p.published_at, 433 p.tags, p.type, p.pinned, p.status, s.slug AS site_slug 434 FROM post_likes pl JOIN posts p ON p.id = pl.post_id JOIN sites s ON s.id = p.site_id 435 WHERE pl.user_id = ? AND p.status = 'published' 436 ORDER BY pl.created_at DESC 437 `).all(userId) 438 : db.prepare(` 439 SELECT p.id, p.slug, p.title, p.excerpt, p.cover_image_url, p.published_at, 440 p.tags, p.type, p.pinned, p.status 441 FROM post_likes pl JOIN posts p ON p.id = pl.post_id 442 WHERE pl.user_id = ? AND p.site_id = ? AND p.status = 'published' 443 ORDER BY pl.created_at DESC 444 `).all(userId, site ? site.id : ''); 445 const posts = rows.map((p) => ({ ...p, _urlBase: (isHub && p.site_slug) ? `/user/${p.site_slug}` : '' })); 446 renderPage(req, res, 'pages/favorites', { posts, pageTitle: 'Favorieten', bodyClass: 'on-favorites' }); 394 447 }); 395 448 … … 631 684 relatedPosts = relatedPosts.map(({ _overlap, tags, ...rest }) => ({ ...rest, _urlBase: urlBaseFor(rest) })); 632 685 686 // Likes / favorieten: aantal + of de ingelogde gebruiker deze post likete. 687 const likeCount = db.prepare('SELECT COUNT(*) AS c FROM post_likes WHERE post_id = ?').get(post.id).c; 688 const likedByMe = !!(req.session?.user && 689 db.prepare('SELECT 1 FROM post_likes WHERE post_id = ? AND user_id = ?').get(post.id, req.session.user.id)); 690 633 691 renderPage(req, res, 'pages/post', { 634 692 post, … … 638 696 comments: topLevel, 639 697 totalComments, 698 likeCount, 699 likedByMe, 640 700 pageTitle: post.title + ' - ' + site.title, 641 701 socialDescr: post.excerpt || '', -
src/views/pages/post.ejs
r8cdb377 r535f955 31 31 <% } %> 32 32 </div> 33 <% if (post.status === 'published') { %> 34 <div class="post-like-row"> 35 <%- include('../partials/like-button', { post: post, likedByMe: (typeof likedByMe !== 'undefined' && likedByMe), likeCount: (typeof likeCount !== 'undefined' ? likeCount : 0), loggedIn: !!user, loginNext: _base + '/' + post.slug }) %> 36 </div> 37 <% } %> 33 38 </header> 34 39 … … 250 255 .post-content { font-family: var(--font-body, serif); font-size: 1.1rem; line-height: 1.7; color: var(--ink); } 251 256 .post-content p { margin: 1.2em 0; } 257 .post-like-row { margin-top: 1rem; } 258 .like-btn { 259 display: inline-flex; align-items: center; gap: 0.45rem; 260 padding: 0.4rem 0.85rem; border: 1px solid var(--rule); border-radius: 999px; 261 background: var(--paper-2); color: var(--ink); font: inherit; font-size: 0.95rem; 262 cursor: pointer; text-decoration: none; transition: border-color 120ms, color 120ms, background 120ms; 263 } 264 .like-btn:hover { border-color: var(--accent); } 265 .like-btn .like-heart { font-size: 1.1rem; line-height: 1; } 266 .like-btn.is-liked { border-color: var(--accent); color: var(--accent); } 267 .like-btn.is-liked .like-heart { color: var(--accent); } 268 .like-btn .like-count { font-variant-numeric: tabular-nums; } 252 269 .post-content h2 { font-family: var(--font-display, serif); font-size: 1.6rem; margin: 2rem 0 1rem; } 253 270 .post-content h3 { font-family: var(--font-display, serif); font-size: 1.3rem; margin: 1.5rem 0 0.75rem; } -
src/views/partials/profile-sheet.ejs
r8cdb377 r535f955 53 53 <svg class="psi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg> 54 54 <span class="psi-label">Account</span> 55 <svg class="psi-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg> 56 </a> 57 </li> 58 <li role="none"> 59 <a href="/favorieten" class="profile-sheet-item" role="menuitem" data-close-sheet 60 hx-get="/favorieten?partial=1" hx-target="#pcms-main" hx-swap="innerHTML" 61 hx-push-url="/favorieten" hx-indicator="#pcms-loading"> 62 <svg class="psi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg> 63 <span class="psi-label">Favorieten</span> 55 64 <svg class="psi-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg> 56 65 </a> -
src/views/partials/topnav.ejs
r8cdb377 r535f955 119 119 <svg class="udi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg> 120 120 <span>Account</span> 121 </a> 122 <a href="/favorieten" role="menuitem" class="udi" 123 hx-get="/favorieten?partial=1" hx-target="#pcms-main" hx-swap="innerHTML" 124 hx-push-url="/favorieten" hx-indicator="#pcms-loading"> 125 <svg class="udi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg> 126 <span>Favorieten</span> 121 127 </a> 122 128 <% if (typeof canSeeBeheer !== 'undefined' && canSeeBeheer) { %>
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)