source: Klonkt/src/views/pages/ward.ejs@ 2086ea2

Last change on this file since 2086ea2 was 2086ea2, checked in by Robin <roboburr@…>, 3 months ago

Ward-PWA: de composer met media

Schrijven met foto, geluid of video, in de browser.

DEZELFDE POORT ALS DE APP, en dat is de kern. De composer bouwt precies
de AS2 Create die Shaer over C2S stuurt en geeft die aan
ingestOutboxActivity: die doet de compose-poort (par. 5), de
attachment-validatie en de zichtbaarheid. GEEN eigen posting-pad, want
dan zou de web-ward een sluiproute om de guardians heen zijn. Een test
bewaakt precies dat: een gated ward wordt ook via de browser
tegengehouden.

Media gaan eerst apart naar /ward/api/upload, in DEZELFDE bak als de
app (reply-media), zodat een foto van een app-ward en een browser-ward
niet van twee plekken federeert. De server maakt de rest af zoals altijd:
posterframe voor video, waveform voor audio. De post draagt daarna alleen
de url, dus de payload blijft klein.

Sessie-geauthenticeerd, niet met een bearer: /ward draait op je eigen
ingelogde Klonkt, dus de browser heeft geen token nodig zoals de app.

New file: (geen)
Changed files:
src/routes/ward.js

  • /api/upload: sessie-geauthenticeerd, zelfde filters/map als C2S
  • /api/post: bouwt de AS2 Create, door ingestOutboxActivity

src/views/pages/ward.ejs

  • schrijfknop, composer-dialoog met tekst, media en zichtbaarheid

src/assets/js/ward.js

  • upload-per-bestand, media-chips, plaatsen; opruiming van een dubbel geraakte doc-comment

src/assets/css/ward.css

  • composer, media-chips, tools

src/services/i18n.js

  • twaalf sleutels in nl, en, de

test/ward-pwa.test.js

  • de gate geldt ook in de browser; een vrij account plaatst met media; leeg wordt geweigerd; een vreemde kan niet uploaden

remarks: 903 tests groen. Opnemen van audio/video in de browser zelf
(MediaRecorder) is nog niet gebouwd; nu kies je een bestand. Bead volgt.

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

  • Property mode set to 100644
File size: 6.5 KB
Line 
1<!DOCTYPE html>
2<html lang="<%= lang %>">
3<head>
4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6 <title><%= state.strings.title %></title>
7 <link rel="manifest" href="/ward/manifest.webmanifest">
8 <link rel="icon" href="/ward/icon.svg" type="image/svg+xml">
9 <meta name="theme-color" content="#5a32e6">
10 <link rel="stylesheet" href="/ward/app.css">
11</head>
12<body>
13 <!-- De hemel ligt ACHTER alles en draait altijd, ook in kaartenstand: de
14 kaarten vallen straks weg en dan is hij er gewoon nog, precies zoals in
15 Shaer. Aria-hidden want hij is decor tot je hem aanzet. -->
16 <canvas id="sky" aria-hidden="true"></canvas>
17
18 <main id="app">
19 <div class="topbar">
20 <h1><%= state.strings.title %></h1>
21 <button id="mode" type="button" aria-pressed="false"><%= state.strings.toSky %></button>
22 </div>
23
24 <!-- De hulpknop staat BOVENAAN en niet onderaan bij de instellingen.
25 Als je hem nodig hebt, moet je niet hoeven zoeken. -->
26 <section class="help">
27 <button id="help-btn" type="button"><%= state.strings.help %></button>
28 <p class="hint" id="help-hint"></p>
29 </section>
30
31 <section id="feed">
32 <div class="feed-head">
33 <h2><%= state.strings.feed %></h2>
34 <button id="compose-open" type="button" class="primary"><%= state.strings.write %></button>
35 </div>
36 <div class="cards" id="card-list"></div>
37 <p class="empty" id="feed-empty" hidden><%= state.strings.feedEmpty %></p>
38 </section>
39
40 <section id="guardians">
41 <h2><%= state.strings.guardians %></h2>
42 <ul class="list" id="guardian-list"></ul>
43 <p class="empty" id="guardians-empty" hidden><%= state.strings.noGuardians %></p>
44 </section>
45
46 <section id="offers" hidden>
47 <h2><%= state.strings.offers %></h2>
48 <ul class="list" id="offer-list"></ul>
49 </section>
50
51 <section id="waiting" hidden>
52 <h2><%= state.strings.waiting %></h2>
53 <ul class="list" id="waiting-list"></ul>
54 </section>
55 </main>
56
57 <!-- De composer: schrijven met foto, geluid of video. De posttypes
58 dicteren hoe de widget eruitziet (Robins wens), maar de kern is een
59 tekstvak plus tot vier media; de server maakt de rest af (poster,
60 waveform) zoals bij de app. -->
61 <dialog id="compose-dialog">
62 <div class="compose-head">
63 <button id="compose-cancel" type="button" aria-label="<%= state.strings.cancel %>">✕</button>
64 <select id="compose-vis" aria-label="<%= state.strings.visibility %>">
65 <option value="friends"><%= state.strings.visFriends %></option>
66 <option value="public"><%= state.strings.visPublic %></option>
67 </select>
68 <button id="compose-send" type="button" class="primary"><%= state.strings.post %></button>
69 </div>
70 <textarea id="compose-text" rows="4" maxlength="5000" placeholder="<%= state.strings.writeHere %>"></textarea>
71 <div id="compose-media" class="compose-media"></div>
72 <div class="compose-tools">
73 <label class="tool" title="<%= state.strings.addPhoto %>">
74 📷<input type="file" accept="image/*" hidden data-kind="image">
75 </label>
76 <label class="tool" title="<%= state.strings.addAudio %>">
77 🎤<input type="file" accept="audio/*" hidden data-kind="audio">
78 </label>
79 <label class="tool" title="<%= state.strings.addVideo %>">
80 🎬<input type="file" accept="video/*" hidden data-kind="video">
81 </label>
82 <span id="compose-status" class="hint"></span>
83 </div>
84 </dialog>
85
86 <!-- De reddingsboei zweeft boven de pagina en is sleepbaar, net als in
87 Shaer. Een knop, geen plaatje: dan werkt hij ook met een toetsenbord en
88 leest een schermlezer hem voor. -->
89 <button id="buoy" type="button" aria-describedby="buoy-hint">🛟</button>
90 <p id="buoy-hint" class="sr-only"><%= state.strings.buoyHint %></p>
91
92 <!-- De zwaai-hand (§5): alleen in berichtenstand, dezelfde hoek als de
93 boei. Ze wisselen elkaar af: met de kaarten verdwijnt de boei, op de
94 hemel hangt de hand. -->
95 <button id="hand" type="button" hidden aria-describedby="hand-hint">👋</button>
96 <p id="hand-hint" class="sr-only"><%= state.strings.handHint %></p>
97
98 <!-- Het gesprek schuift OVER de hemel; die blijft eronder gewoon draaien. -->
99 <section id="thread" hidden>
100 <header>
101 <button id="thread-close" type="button" aria-label="<%= state.strings.close %>">←</button>
102 <h2 id="thread-name"></h2>
103 </header>
104 <div id="thread-list"></div>
105 <form id="thread-form">
106 <input id="thread-text" type="text" autocomplete="off" maxlength="2000"
107 placeholder="<%= state.strings.writeHere %>">
108 <button type="submit" class="primary"><%= state.strings.send %></button>
109 </form>
110 </section>
111
112 <!-- De vriendenkiezer achter de (+): aan wie wil je iets sturen? -->
113 <dialog id="friends-dialog">
114 <h2><%= state.strings.newTitle %></h2>
115 <ul id="friends-list" class="list"></ul>
116 <p class="empty" id="friends-empty" hidden><%= state.strings.noFriends %></p>
117 <button id="friends-cancel" type="button"><%= state.strings.cancel %></button>
118 </dialog>
119
120 <!-- Het zwaai-paneel: de tekst die je kiest IS het bericht. -->
121 <dialog id="wave-dialog">
122 <h2 id="wave-title"></h2>
123 <div class="wave-answers">
124 <button data-wave="1">👋 <%= state.strings.wave %></button>
125 <button data-text="<%= state.strings.waveCall %>"><%= state.strings.waveCall %></button>
126 <button data-text="<%= state.strings.waveThanks %>"><%= state.strings.waveThanks %></button>
127 </div>
128 <button id="wave-cancel" type="button"><%= state.strings.notNow %></button>
129 </dialog>
130
131 <dialog id="help-dialog">
132 <form method="dialog">
133 <h2 id="help-title"></h2>
134 <label for="help-words"><%= state.strings.helpWords %></label>
135 <textarea id="help-words" rows="3" maxlength="500"></textarea>
136 <menu>
137 <button value="cancel"><%= state.strings.cancel %></button>
138 <button id="help-send" value="send" class="primary"><%= state.strings.helpSend %></button>
139 </menu>
140 </form>
141 </dialog>
142
143 <!-- De begintoestand reist als JSON mee, zodat de pagina meteen compleet is
144 en niet eerst leeg staat te wachten op een fetch. Geen inline script:
145 CSP staat dat niet toe, dus het gaat als data door een script-tag met
146 een type dat de browser niet uitvoert. -->
147 <script type="application/json" id="ward-state"><%- JSON.stringify(state) %></script>
148 <!-- Module, zodat app.js het hemelmodel kan importeren. -->
149 <script type="module" src="/ward/app.js"></script>
150</body>
151</html>
Note: See TracBrowser for help on using the repository browser.