source: Klonkt/src/routes/circle.js@ e9229f2

main
Last change on this file since e9229f2 was 8ea3d0d, checked in by roboburr <roboburr@…>, 3 months ago

Defaults: new sites grid + moderate; Circle page graphical header

(A) New-site defaults aligned with Robin's preference: feed_view_default
'timeline'->'grid' and comments_moderation_mode 'trust'->'moderate'. Set in
column defaults (fresh installs), siteEditableFields + createSite INSERT,
and ensurePrimarySite. Other defaults were already correct (public/index/
audio/switcher/search/archive on, login-to-comment off).
(B) /circle header redesigned: centred, graphical "connected circles"
motif in accent colour + chips with the avatars/names of the synced circle
sites.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@…>

  • Property mode set to 100644
File size: 3.5 KB
Line 
1/**
2 * Cirkel-feed + lokale lees-pagina.
3 * GET /cirkel -> overzicht (zelfde timeline/grid-view als de home)
4 * GET /cirkel/:id -> losse remote-post in eigen chrome (blijf op je site)
5 * Alleen actief als tenancy === 'circle' (anders next() -> postsRoutes/404).
6 * Zie docs/cirkels-v1-spec.md §5c.
7 */
8
9import express from 'express';
10import { renderPage } from '../middleware/render.js';
11import db from '../config/database.js';
12import { getTenancy } from '../services/SettingsService.js';
13
14const router = express.Router();
15
16function safeUrl(u) {
17 return typeof u === 'string' && /^https?:\/\//i.test(u) ? u : null;
18}
19function safeJson(s) {
20 try { return s ? JSON.parse(s) : []; } catch { return []; }
21}
22function mediaImage(media_json) {
23 const media = safeJson(media_json).map((m) => ({ ...m, url: safeUrl(m.url) })).filter((m) => m.url);
24 return media.find((m) => m.type === 'image') || null;
25}
26
27// ── Overzicht ────────────────────────────────────────────────
28router.get('/cirkel', (req, res, next) => {
29 if (getTenancy() !== 'circle') return next();
30
31 const rows = db.prepare(`
32 SELECT p.id, p.published, p.title, p.summary, p.media_json,
33 a.name AS actor_name
34 FROM remote_posts p
35 JOIN remote_actors a ON a.id = p.actor_id
36 ORDER BY COALESCE(p.published, p.fetched_at) DESC
37 LIMIT 100
38 `).all();
39
40 const posts = rows.map((r) => {
41 const image = mediaImage(r.media_json);
42 return {
43 id: r.id,
44 // Lokale lees-pagina -> de kaart blijft op de eigen site (post-card linkt
45 // lokaal + htmx, GEEN external_url).
46 slug: 'cirkel/' + encodeURIComponent(r.id),
47 title: r.title || '(zonder titel)',
48 excerpt: r.summary || '',
49 cover_image_url: image ? image.url : null,
50 published_at: r.published,
51 created_at: r.published,
52 type: 'post',
53 tags: '',
54 pinned: 0,
55 status: 'published',
56 source_name: r.actor_name || 'Onbekend',
57 };
58 });
59
60 // Sites in de cirkel (alleen succesvol gesyncte = proto-compatibel) — voor de
61 // grafische header met avatars.
62 const sites = db.prepare('SELECT name, url, avatar FROM remote_actors ORDER BY name')
63 .all()
64 .map((s) => ({
65 name: s.name || 'Onbekend',
66 url: safeUrl(s.url),
67 avatar: safeUrl(s.avatar),
68 }));
69
70 renderPage(req, res, 'pages/circle-feed', { pageTitle: 'Cirkel', bodyClass: 'on-cirkel', posts, sites });
71});
72
73// ── Losse remote-post (lokaal lezen) ─────────────────────────
74router.get('/cirkel/:id', (req, res, next) => {
75 if (getTenancy() !== 'circle') return next();
76
77 const row = db.prepare(`
78 SELECT p.id, p.published, p.title, p.summary, p.url, p.media_json,
79 a.name AS actor_name, a.url AS actor_url, a.avatar AS actor_avatar
80 FROM remote_posts p
81 JOIN remote_actors a ON a.id = p.actor_id
82 WHERE p.id = ?
83 `).get(req.params.id);
84
85 if (!row) return next();
86
87 const image = mediaImage(row.media_json);
88 const post = {
89 title: row.title || '(zonder titel)',
90 body: row.summary || '', // platte tekst (gesanitized bij ingest)
91 published: row.published,
92 image: image ? image.url : null,
93 sourceName: row.actor_name || 'Onbekend',
94 sourceUrl: safeUrl(row.actor_url),
95 sourceAvatar: safeUrl(row.actor_avatar),
96 originalUrl: safeUrl(row.url),
97 };
98
99 renderPage(req, res, 'pages/circle-post', {
100 pageTitle: post.title,
101 bodyClass: 'on-cirkel-post',
102 post,
103 });
104});
105
106export default router;
Note: See TracBrowser for help on using the repository browser.