Changeset c26cc18 in Klonkt for src/assets
- Timestamp:
- 07/24/2026 06:04:03 PM (7 weeks ago)
- Branches:
- main
- Children:
- 780a7c6
- Parents:
- 1a2d8ac
- Location:
- src/assets
- Files:
-
- 2 edited
-
css/guardian.css (modified) (2 diffs)
-
js/guardian.js (modified) (6 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/css/guardian.css
r1a2d8ac rc26cc18 6 6 --ink: #e8ecf2; 7 7 --sub: #93a0b4; 8 --line: #2c3646; 8 9 --accent: #ff6b35; 9 10 --ok: #4caf7d; … … 15 16 color: var(--ink); 16 17 font: 16px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; 17 padding-bottom: 4 0px;18 padding-bottom: 48px; 18 19 } 19 20 .g-head { 20 21 display: flex; 21 22 align-items: center; 22 gap: 1 0px;23 padding: 1 4px 16px;23 gap: 12px; 24 padding: 16px; 24 25 background: var(--card); 26 border-bottom: 1px solid var(--line); 25 27 position: sticky; 26 28 top: 0; 29 z-index: 2; 27 30 } 28 .g-head h1 { font-size: 1.15rem; margin: 0; flex: 1; } 29 .g-buoy { font-size: 1.5rem; } 30 .g-me { color: var(--sub); font-size: .9rem; } 31 .g-head-txt { flex: 1; } 32 .g-head h1 { font-size: 1.15rem; margin: 0; } 33 .g-head .g-sub { margin: 0; color: var(--sub); font-size: .78rem; } 34 .g-buoy { font-size: 1.7rem; } 35 .g-me { color: var(--sub); font-size: .85rem; } 31 36 #site-picker { 32 37 background: var(--bg); color: var(--ink); 33 border: 1px solid #33405233; border-radius: 8px; padding: 6px 8px;38 border: 1px solid var(--line); border-radius: 8px; padding: 6px 8px; 34 39 } 35 main { max-width: 640px; margin: 0 auto; padding: 0 16px; } 36 h2 { font-size: 1rem; margin: 26px 0 10px; } 37 .g-sub { color: var(--sub); font-size: .85rem; margin: 0 0 10px; } 38 .g-empty { color: var(--sub); font-size: .9rem; } 39 .g-msg { font-size: .85rem; color: var(--ok); } 40 main { max-width: 640px; margin: 0 auto; padding: 8px 16px; } 41 42 section { padding: 18px 0; border-bottom: 1px solid var(--line); } 43 section:last-child { border-bottom: 0; } 44 .g-sec-head { display: flex; align-items: center; gap: 8px; } 45 .g-sec-head h2 { font-size: 1.02rem; margin: 0; } 46 .g-badge { 47 background: var(--accent); color: #fff; font-size: .72rem; font-weight: 700; 48 min-width: 20px; text-align: center; border-radius: 10px; padding: 1px 7px; 49 } 50 .g-sec-sub { color: var(--sub); font-size: .84rem; margin: 4px 0 12px; } 51 .g-empty { color: var(--sub); font-size: .9rem; margin: 6px 0 0; } 52 .g-msg { font-size: .85rem; color: var(--ok); margin: 8px 0 0; } 40 53 .g-msg.err { color: #ff7a7a; } 41 54 .g-list { display: flex; flex-direction: column; gap: 8px; } 42 55 43 .g-card { 44 background: var(--card); 45 border-radius: 12px; 46 padding: 12px 14px; 47 } 56 .g-card { background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px; } 48 57 .g-card.help { border-left: 3px solid var(--accent); } 49 .g-card .who { font-weight: 600; }50 .g-card .when { color: var(--sub); font-size: .78rem; }51 .g-card .body { margin-top: 6px; font-size: .92rem; overflow-wrap: anywhere; }52 .g-card .body img { max-width: 100%; border-radius: 8px; }53 58 .g-card .row { display: flex; align-items: center; gap: 10px; } 54 .g-card .row .grow { flex: 1; } 55 .g-card .pend { color: var(--sub); font-size: .8rem; } 59 .g-card .row .grow { flex: 1; min-width: 0; } 60 .g-card .who { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } 61 .g-card .when { color: var(--sub); font-size: .76rem; white-space: nowrap; } 62 .g-card .body { margin-top: 8px; font-size: .92rem; overflow-wrap: anywhere; } 63 .g-card .body img { max-width: 100%; border-radius: 8px; margin-top: 6px; } 64 .g-link { display: inline-block; margin-top: 8px; color: var(--accent); font-size: .82rem; text-decoration: none; } 56 65 57 #adopt-form { display: flex; gap: 8px; margin-bottom: 8px; } 66 .tag { font-size: .72rem; font-weight: 600; padding: 3px 9px; border-radius: 20px; white-space: nowrap; } 67 .tag.wait { background: #3a2f1a; color: #e8b04b; } 68 .tag.ok { background: #17301f; color: var(--ok); } 69 70 #adopt-form { display: flex; gap: 8px; } 58 71 #adopt-form input { 59 flex: 1; 60 background: var(-- card); color: var(--ink);61 border: 1px solid #33405255; border-radius: 10px; padding: 10px 12px;72 flex: 1; min-width: 0; 73 background: var(--bg); color: var(--ink); 74 border: 1px solid var(--line); border-radius: 10px; padding: 11px 12px; 62 75 } 63 76 button { 64 77 background: var(--accent); color: #fff; border: 0; 65 border-radius: 10px; padding: 1 0px 16px; font-weight: 600; cursor: pointer;78 border-radius: 10px; padding: 11px 18px; font-weight: 600; cursor: pointer; 66 79 } 67 button.quiet { background: #33405255; color: var(--ink); font-weight: 500; } 80 button:disabled { opacity: .5; cursor: default; } 81 button.quiet { background: transparent; color: var(--sub); border: 1px solid var(--line); font-weight: 500; } 82 button.quiet.is-on { color: var(--ok); border-color: var(--ok); } 83 button.small { padding: 6px 12px; font-size: .82rem; } -
src/assets/js/guardian.js
r1a2d8ac rc26cc18 1 /* Guardian PWA client (FEP-633c): renders the dashboard state, adopts wards,2 and manages the guardian push channel (web-push slice reused: alert types3 'help' + 'guardian'). No framework, no inline scripts (CSP). */1 /* Guardian PWA client (FEP-633c): renders the dashboard, adopts wards, and 2 manages the guardian push channel. No framework, no inline scripts (CSP). 3 All user-facing text comes from state.strings (server i18n). */ 4 4 (function () { 5 5 'use strict'; 6 var state = JSON.parse(document.getElementById('guardian-state').textContent || '{}'); 6 var S = JSON.parse(document.getElementById('guardian-state').textContent || '{}'); 7 var T = S.strings || {}; 7 8 8 9 function el(tag, cls, text) { … … 17 18 catch (e) { return uri; } 18 19 } 19 function when(s) { 20 return String(s || '').slice(0, 16).replace('T', ' '); 21 } 20 function when(s) { return String(s || '').slice(0, 16).replace('T', ' '); } 21 function show(id, on) { document.getElementById(id).hidden = !on; } 22 22 23 // ── Message centre: help requests──────────────────────────────────────23 // ── 1. Help requests ─────────────────────────────────────────────────── 24 24 function renderHelp() { 25 25 var list = document.getElementById('help-list'); 26 26 list.textContent = ''; 27 (state.help || []).forEach(function (h) { 27 var help = S.help || []; 28 help.forEach(function (h) { 28 29 var card = el('div', 'g-card help'); 29 30 var row = el('div', 'row'); 30 var who = el('span', 'who', h.actor_name || handleOf(h.actor_uri, h.actor_handle)); 31 var at = el('span', 'when', when(h.published || h.created_at)); 32 row.appendChild(who); row.appendChild(at); 31 row.appendChild(el('span', 'who grow', h.actor_name || handleOf(h.actor_uri, h.actor_handle))); 32 row.appendChild(el('span', 'when', when(h.published || h.created_at))); 33 33 card.appendChild(row); 34 34 var body = el('div', 'body'); 35 body.innerHTML = h.content || ''; // sanitized server-side on ingest35 body.innerHTML = h.content || ''; // sanitized server-side on ingest 36 36 card.appendChild(body); 37 37 if (h.note_url) { 38 var link = el('a', 'when','open');39 link.href = h.note_url; link.target = '_blank'; link.rel = 'noopener';40 card.appendChild( link);38 var a = el('a', 'g-link', T.open || 'open'); 39 a.href = h.note_url; a.target = '_blank'; a.rel = 'noopener'; 40 card.appendChild(a); 41 41 } 42 42 list.appendChild(card); 43 43 }); 44 document.getElementById('help-empty').hidden = (state.help || []).length > 0; 44 var badge = document.getElementById('help-count'); 45 badge.textContent = help.length; badge.hidden = help.length === 0; 46 show('help-empty', help.length === 0); 45 47 } 46 48 47 // ── Wards + pending offers ───────────────────────────────────────────── 48 function wardCard(w, pending) { 49 var card = el('div', 'g-card'); 50 var row = el('div', 'row'); 51 var who = el('span', 'who grow', handleOf(w.other_uri, w.other_handle)); 52 row.appendChild(who); 53 if (pending) row.appendChild(el('span', 'pend', state.strings.pending)); 54 var btn = el('button', 'quiet', pending ? state.strings.retract : state.strings.release); 55 btn.addEventListener('click', function () { 56 fetch('/guardian/wards/remove', { 57 method: 'POST', headers: { 'Content-Type': 'application/json' }, 58 body: JSON.stringify({ uri: w.other_uri, site: state.site }), 59 }).then(refresh); 49 // ── 3. Pending offers I sent ─────────────────────────────────────────── 50 function renderPending() { 51 var list = document.getElementById('pending-list'); 52 list.textContent = ''; 53 var pend = S.pendingOffers || []; 54 pend.forEach(function (w) { 55 var card = el('div', 'g-card'); 56 var row = el('div', 'row'); 57 row.appendChild(el('span', 'who grow', handleOf(w.other_uri, w.other_handle))); 58 row.appendChild(el('span', 'tag wait', T.pending)); 59 var btn = el('button', 'quiet small', T.retract); 60 btn.addEventListener('click', function () { remove(w.other_uri, btn); }); 61 row.appendChild(btn); 62 card.appendChild(row); 63 list.appendChild(card); 60 64 }); 61 row.appendChild(btn); 62 card.appendChild(row); 63 return card; 64 } 65 function renderWards() { 66 var wl = document.getElementById('wards-list'); 67 var ol = document.getElementById('offers-list'); 68 wl.textContent = ''; ol.textContent = ''; 69 (state.wards || []).forEach(function (w) { wl.appendChild(wardCard(w, false)); }); 70 (state.pendingOffers || []).forEach(function (w) { ol.appendChild(wardCard(w, true)); }); 71 document.getElementById('wards-empty').hidden = 72 (state.wards || []).length + (state.pendingOffers || []).length > 0; 65 show('pending-section', pend.length > 0); 73 66 } 74 67 75 function refresh() { 76 fetch('/guardian/api/state?site=' + encodeURIComponent(state.site)) 77 .then(function (r) { return r.json(); }) 78 .then(function (s) { if (s && !s.error) { state = s; renderHelp(); renderWards(); } }); 68 // ── 4. Accepted wards ────────────────────────────────────────────────── 69 function renderWards() { 70 var list = document.getElementById('wards-list'); 71 list.textContent = ''; 72 var wards = S.wards || []; 73 wards.forEach(function (w) { 74 var card = el('div', 'g-card'); 75 var row = el('div', 'row'); 76 row.appendChild(el('span', 'who grow', handleOf(w.other_uri, w.other_handle))); 77 row.appendChild(el('span', 'tag ok', T.active)); 78 var btn = el('button', 'quiet small', T.release); 79 btn.addEventListener('click', function () { remove(w.other_uri, btn); }); 80 row.appendChild(btn); 81 card.appendChild(row); 82 list.appendChild(card); 83 }); 84 show('wards-empty', wards.length === 0); 79 85 } 80 86 81 // ── Adopt ────────────────────────────────────────────────────────────── 82 document.getElementById('adopt-form').addEventListener('submit', function (ev) { 87 function remove(uri, btn) { 88 btn.disabled = true; 89 fetch('/guardian/wards/remove', { 90 method: 'POST', headers: { 'Content-Type': 'application/json' }, 91 body: JSON.stringify({ uri: uri, site: S.site }), 92 }).then(refresh); 93 } 94 95 function renderAll() { renderHelp(); renderPending(); renderWards(); } 96 97 function refresh() { 98 return fetch('/guardian/api/state?site=' + encodeURIComponent(S.site)) 99 .then(function (r) { return r.json(); }) 100 .then(function (s) { if (s && !s.error) { S = s; T = s.strings || T; renderAll(); } }); 101 } 102 103 // ── 2. Adopt ─────────────────────────────────────────────────────────── 104 var form = document.getElementById('adopt-form'); 105 var input = document.getElementById('adopt-handle'); 106 var adoptBtn = document.getElementById('adopt-btn'); 107 var msg = document.getElementById('adopt-msg'); 108 function setMsg(text, isErr) { msg.hidden = false; msg.className = 'g-msg' + (isErr ? ' err' : ''); msg.textContent = text; } 109 110 form.addEventListener('submit', function (ev) { 83 111 ev.preventDefault(); 84 var input = document.getElementById('adopt-handle');85 var msg = document.getElementById('adopt-msg');86 112 var handle = input.value.trim(); 87 113 if (!handle) return; 88 msg.hidden = false; msg.classList.remove('err'); msg.textContent = '…'; 114 adoptBtn.disabled = true; 115 setMsg(T.sending || '…', false); 89 116 fetch('/guardian/adopt', { 90 117 method: 'POST', headers: { 'Content-Type': 'application/json' }, 91 body: JSON.stringify({ handle: handle, site: state.site }),118 body: JSON.stringify({ handle: handle, site: S.site }), 92 119 }).then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); }) 93 120 .then(function (res) { 94 if (res.ok) { msg.textContent = state.strings.sent; input.value = ''; refresh(); } 95 else { msg.classList.add('err'); msg.textContent = state.strings.failed + ': ' + (res.j.error || '?'); } 121 adoptBtn.disabled = false; 122 if (res.ok) { 123 input.value = ''; 124 // Always refresh: the offer is recorded even if delivery is still 125 // in flight. Show it under "Verzonden aanvragen". 126 setMsg(res.j.delivered === false ? T.sent_retry : T.sent, false); 127 refresh(); 128 } else { 129 setMsg((res.j.error === 'not_found' ? T.not_found : T.failed) , true); 130 } 96 131 }) 97 .catch(function () { msg.classList.add('err'); msg.textContent = state.strings.network; });132 .catch(function () { adoptBtn.disabled = false; setMsg(T.network, true); }); 98 133 }); 99 134 … … 104 139 }); 105 140 106 // ── Push: the guardian channel (help + guardian alerts)────────────────141 // ── 5. Push ──────────────────────────────────────────────────────────── 107 142 var toggle = document.getElementById('push-toggle'); 108 143 var pmsg = document.getElementById('push-msg'); … … 115 150 toggle.textContent = sub ? toggle.dataset.onLabel : toggle.dataset.offLabel; 116 151 toggle.dataset.subscribed = sub ? '1' : ''; 152 toggle.classList.toggle('is-on', !!sub); 117 153 }); 118 154 } … … 145 181 body: JSON.stringify({ 146 182 subscription: sub.toJSON(), 147 // The guardian channel: calls for help + adoption traffic.148 183 alerts: { help: 1, guardian: 1, dm: 1, follow: 0, reply: 0, like: 0, boost: 0 }, 149 184 uaLabel: 'Guardian PWA', … … 151 186 }); 152 187 }).then(pushState).catch(function (e) { 153 pmsg.hidden = false; pmsg.class List.add('err');154 pmsg.textContent = 'Push niet beschikbaar: ' + e.message;188 pmsg.hidden = false; pmsg.className = 'g-msg err'; 189 pmsg.textContent = (T.push_unavailable || 'Push unavailable') + ': ' + e.message; 155 190 }); 156 191 }); 157 192 }); 158 193 159 renderHelp(); renderWards(); pushState(); 160 // Live-ish: poll the state every 45s while the PWA is open. 161 setInterval(refresh, 45000); 194 renderAll(); pushState(); 195 setInterval(refresh, 45000); // live-ish while open 162 196 })();
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)