Changeset a5a6249 in Klonkt for src/routes/ward.js


Ignore:
Timestamp:
08/12/2026 07:52:13 PM (3 months ago)
Author:
Robin <roboburr@…>
Children:
084b046
Parents:
303bfc4
Message:

De Ward PWA: de feed en de reddingsboei

Plak 2. Nu valt er iets te doen op /ward in plaats van alleen te kijken.

DE FEED. Dezelfde bron als de Krant (getTimeline) en dezelfde partial
voor de body (renderNoteBody), zodat een post er overal hetzelfde uitziet:
opmaak, media, quote-kaart, embed. Alleen kale content doorgeven zou een
lege regel opleveren waar een foto hoort, precies de fout die het
guardian-dashboard ooit had.

DE BOEI, en die is hier BETER dan in de app. Shaer maakt een
schermafdruk van de kaart, met al het gedoe van buiten beeld renderen.
Op de server weten we wélke note het is, dus de hulpvraag draagt de note
zelf mee. Een guardian ziet daardoor de echte post en niet een plaatje
dat er ooit op leek.

Slepen met pointer-events, want dat is het enige dat op een telefoon en
met een muis hetzelfde doet. touch-action: none op de boei houdt de
pagina stil tijdens het slepen; zonder dat vecht het met scrollen en
verlies je hem halverwege. De kaart eronder licht op, want mikken met een
vinger zonder terugkoppeling is gokken. Een tik zonder slepen vraagt hulp
zonder dat er een post bij hoort, en de grote knop bovenaan doet
hetzelfde.

Changed files:
src/routes/ward.js

  • /api/feed met afgemaakte body per kaart
  • /api/help: direct bericht aan ALLE guardians, met de note erbij; 409 als er geen guardians zijn, want dan is er niets te vragen

src/views/pages/ward.ejs

  • feed-sectie, zwevende boei, hulpdialoog

src/assets/js/ward.js

  • kaarten tekenen, slepen, mikken, versturen

src/assets/css/ward.css

  • kaarten, boei, dialoog, sr-only

src/services/i18n.js

  • acht nieuwe sleutels in nl, en, de

test/ward-pwa.test.js

  • de feed levert een gerenderde body; de boei weigert zonder guardians; een vreemde kan hem niet afvuren

remarks: 889 tests groen. Slepen is alleen in code getest, niet met een
echte vinger op een echt scherm; dat is aan Robin op dev.

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/routes/ward.js

    r303bfc4 ra5a6249  
    2525import { carriesGuardians } from '../services/guardianship/context.js';
    2626import { t as i18nT, resolveLang } from '../services/i18n.js';
    27 import { injectCspNonce } from '../middleware/render.js';
     27import { injectCspNonce, renderNoteBody, formatDateTime } from '../middleware/render.js';
    2828
    2929const router = express.Router();
    … …  
    9191      waiting: i18nT(L, 'ward.waiting'),
    9292      help: i18nT(L, 'ward.help'),
     93      feed: i18nT(L, 'ward.feed'),
     94      feedEmpty: i18nT(L, 'ward.feed_empty'),
     95      buoyHint: i18nT(L, 'ward.buoy_hint'),
     96      helpWords: i18nT(L, 'ward.help_words'),
     97      helpSend: i18nT(L, 'ward.help_send'),
     98      helpSent: i18nT(L, 'ward.help_sent'),
     99      cancel: i18nT(L, 'ward.cancel'),
    93100    },
    94101  };
    … …  
    144151  }));
    145152  res.json({ checks: out });
     153});
     154
     155/**
     156 * De feed: wat er binnenkomt van de mensen die je volgt.
     157 *
     158 * Zelfde bron en dezelfde partial als de Krant, Berichten en het
     159 * guardian-dashboard (renderNoteBody), zodat een post er overal hetzelfde
     160 * uitziet: opmaak, media, quote-kaart en embed. Kale content sturen zou hier
     161 * een lege regel opleveren waar een foto hoort.
     162 */
     163router.get('/api/feed', requireAuth, (req, res) => {
     164  const site = siteForUser(req);
     165  if (!site) return res.status(404).json({ error: 'no_site' });
     166  const L = resolveLang(req);
     167  const items = AP.getTimeline(site.slug, 60, 0).map((p) => ({
     168    id: p.id,
     169    uri: p.object_uri || p.url,
     170    author: p.author_handle || p.author_name || p.author_uri,
     171    authorName: p.author_name,
     172    authorIcon: p.author_icon,
     173    url: p.url,
     174    published: p.published || p.created_at,
     175    whenText: formatDateTime(p.published || p.created_at),
     176    cw: p.cw || null,
     177    bodyHtml: renderNoteBody(p, L),
     178  }));
     179  res.json({ items });
     180});
     181
     182/**
     183 * De reddingsboei (§5.2.1): sleep hem op een post en je guardians krijgen hem.
     184 *
     185 * Shaer maakt hier een SCHERMAFDRUK van de kaart, met al het gedoe van buiten
     186 * beeld renderen. Op de server is dat onnodig: wij weten wélke note het is, dus
     187 * de hulpvraag draagt de note zelf mee. Dat is niet alleen minder werk maar ook
     188 * eerlijker: een guardian ziet de echte post en niet een plaatje dat er ooit op
     189 * leek.
     190 *
     191 * Gaat naar ALLE guardians tegelijk, als direct bericht. Geen post, nooit
     192 * boostbaar. Zonder guardians is er niets om te vragen en zegt dat het ook.
     193 */
     194router.post('/api/help', requireAuth, express.json({ limit: '4kb' }), async (req, res) => {
     195  const site = siteForUser(req);
     196  if (!site) return res.status(404).json({ error: 'no_site' });
     197  const guardians = Guardianship.listGuardians(site.slug).map((g) => g.other_uri).filter(Boolean);
     198  if (!guardians.length) return res.status(409).json({ error: 'no_guardians' });
     199
     200  const noteId = String(req.body?.note || '').trim();
     201  const words = String(req.body?.words || '').trim().slice(0, 500);
     202  // De post waar het om gaat, als die van deze tijdlijn komt. Niet verplicht:
     203  // een kind mag ook om hulp vragen zonder dat er een post bij hoort.
     204  const row = noteId ? AP.getTimeline(site.slug, 200, 0).find((p) => String(p.id) === noteId) : null;
     205  const link = row && (row.url || row.object_uri);
     206
     207  const text = [words || 'Ik heb hulp nodig.', link ? `\n\n${link}` : ''].join('');
     208  const sent = await Guardianship.deliverDirectNote(site, {
     209    recipients: guardians,
     210    text,
     211    language: site.language || 'nl',
     212    helpRequest: true,
     213  }).catch(() => null);
     214
     215  if (!sent) return res.status(502).json({ error: 'not_delivered' });
     216  res.json({ ok: true, guardians: guardians.length, note: link || null });
    146217});
    147218
Note: See TracChangeset for help on using the changeset viewer.