source: Klonkt/src/views/pages/paid-gate.ejs@ 9a00f28

main
Last change on this file since 9a00f28 was 9a00f28, checked in by Robin <roboburr@…>, 7 weeks ago

Fix: test feedback round, cirkels tagline + report details + full i18n

Three findings from Bart's test pass, tied together by the new i18n keys:

  1. The admin tagline said "Solo mode" even with Cirkels (federation) on. Solo-tenancy now distinguishes: apEnabled -> "Cirkels-modus: jouw site, verbonden met de fediverse", else the solo line. New key in nl/en/de.
  1. Reports only showed who reported; now they show the reason and WHICH post. getNotifications resolves the stored objects URIs of a report: our own /ap/notes/<id> become "Over de post: <title>" links under the description; an empty reason renders "Geen reden opgegeven". Other URIs (the actor itself) are skipped, the row already names the account.
  1. Notificaties and Betaalde posts were hardcoded Dutch on an English-configured Klonkt. Everything now goes through t(): the two admin pages (including their client-side status strings, passed via the JSON data block), the admin menu buttons, and the visitor-facing paid pages (gate, passkey, result). Server-side push payloads translate too, using the SITE's content language (fallback KLONKT_DEFAULT_LANG), and the test ping uses the request language. Full nl/en/de dictionaries.

Changed files:
src/services/i18n.js

  • admin.tagline_cirkels, admin.b_paid/b_push/back, notif.report_about/ report_noreason, and the push.*, apaid.*, pgate.*, ppk.*, pres.* blocks in nl/en/de

src/routes/admin.js, src/views/pages/admin.ejs

  • circlesOn (apEnabled) -> cirkels tagline; menu buttons via t()

src/services/ActivityPubService.js

  • report objects resolved to post links; push payloads via i18nT with pushLang(slug) (site language)

src/views/pages/fedi-notifications.ejs

  • report reason fallback + "Over de post" links (+ styles)

src/routes/admin-push.js, src/views/pages/admin-push.ejs

  • pageTitleKey push.t; all copy + JS status strings via t()

src/routes/admin-paid.js, src/views/pages/admin-paid.ejs

  • pageTitleKey apaid.t; all copy via t(), copy-button label via data-attr

src/routes/paid.js

  • pageTitleKey pres.t / ppk.t

src/views/pages/paid-gate.ejs, paid-passkey.ejs, paid-result.ejs

  • visitor copy + JS strings via t()

src/routes/push.js

  • test notification via resolveLang(req)

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

  • Property mode set to 100644
File size: 5.5 KB
Line 
1<div class="container pg-navwrap">
2 <%- include('../partials/post-nav', { newerPost: (typeof newerPost !== 'undefined' ? newerPost : null), olderPost: (typeof olderPost !== 'undefined' ? olderPost : null) }) %>
3</div>
4
5<article class="pg-page" id="pg-page">
6 <% if (typeof pgTitle !== 'undefined' && pgTitle) { %><h1 class="pg-title"><%= pgTitle %></h1><% } %>
7
8 <% if (typeof pgTeaser !== 'undefined' && pgTeaser) { %>
9 <div class="pg-teaser"><p><%= pgTeaser %></p></div>
10 <% } %>
11
12 <% var _hasPatron = (typeof pgPatronUrl !== 'undefined' && pgPatronUrl); %>
13 <section class="pg-card">
14 <div class="pg-lock">💶</div>
15 <h2 class="pg-h2"><%= t('pgate.h') %></h2>
16 <p class="pg-sub">
17 <% if (typeof pgCents !== 'undefined' && pgCents) { %><%= t('pgate.sub_cents', { eur: (pgCents/100).toFixed(2) }) %><% } else { %><%= t('pgate.sub') %><% } %>
18 </p>
19
20 <div class="pg-actions">
21 <% if (_hasPatron) { %>
22 <a class="pg-btn" href="<%= pgPatronUrl %>" target="_blank" rel="noopener"><%= t('pgate.join') %></a>
23 <button type="button" id="pg-unlock" class="pg-btn pg-btn-ghost"><%= t('pgate.unlock_have') %></button>
24 <% } else { %>
25 <button type="button" id="pg-unlock" class="pg-btn"><%= t('pgate.unlock') %></button>
26 <% } %>
27 </div>
28 <p id="pg-status" class="pg-status" hidden></p>
29 </section>
30</article>
31
32<script src="/assets/vendor/simplewebauthn-browser.umd.min.js"></script>
33<script>
34(function () {
35 var base = "<%= (typeof siteUrlBase !== 'undefined' && siteUrlBase ? siteUrlBase : '') %>";
36 var slug = "<%= pgSlug %>";
37 var hasPatron = <%= _hasPatron ? 'true' : 'false' %>;
38 var I = { join: "<%= t('pgate.join_short') %>", confirm: "<%= t('pgate.confirm') %>", failed: "<%= t('pgate.failed') %>", error: "<%= t('pgate.error') %>" };
39 var btn = document.getElementById('pg-unlock');
40 var status = document.getElementById('pg-status');
41 function say(msg, err) { status.hidden = false; status.textContent = msg; status.classList.toggle('is-err', !!err); }
42 function toLink() { location.href = base + '/paid/link?post=' + encodeURIComponent(slug); }
43
44 // No WebAuthn here: an assertion is impossible. With a Patreon page there is
45 // already a "Word supporter" button, so hide the (dead) unlock button rather
46 // than turn it into a second "Word supporter". Without one, this IS the button.
47 if (!window.SimpleWebAuthnBrowser || !window.PublicKeyCredential) {
48 if (hasPatron) { btn.style.display = 'none'; }
49 else { btn.textContent = I.join; btn.addEventListener('click', toLink); }
50 return;
51 }
52
53 btn.addEventListener('click', function () {
54 btn.disabled = true;
55 say(I.confirm);
56 fetch(base + '/paid/challenge?post=' + encodeURIComponent(slug))
57 .then(function (r) { if (!r.ok) throw { link: true }; return r.json(); })
58 .then(function (data) {
59 return window.SimpleWebAuthnBrowser.startAuthentication({ optionsJSON: data.options })
60 .then(function (response) {
61 return fetch(base + '/paid/unlock', {
62 method: 'POST', headers: { 'Content-Type': 'application/json' },
63 body: JSON.stringify({ response: response, blob: data.blob }),
64 });
65 });
66 })
67 .then(function (r) { return r.json().then(function (j) { return { status: r.status, j: j }; }); })
68 .then(function (res) {
69 if (res.j && res.j.ok && res.j.redirect) {
70 // Reload the real post page via the one-shot unlock capability, so it
71 // renders through its normal template (layout, styles, audio).
72 location.href = res.j.redirect;
73 } else if (res.status === 403) {
74 toLink(); // no valid passkey yet (or lapsed tier): link via Patreon
75 } else {
76 btn.disabled = false; say(I.failed, true);
77 }
78 })
79 .catch(function (e) {
80 if (e && e.link) { toLink(); return; }
81 if (e && e.name === 'NotAllowedError') { toLink(); return; } // cancelled / no passkey -> link
82 btn.disabled = false; say(I.error, true);
83 });
84 });
85})();
86</script>
87
88<style>
89 .pg-navwrap { max-width: 720px; margin: 0.5rem auto 1.5rem; padding: 0 1rem; }
90 .pg-page { max-width: 720px; margin: 0 auto 3rem; padding: 0 1rem; }
91 .pg-title { font-family: var(--font-display, serif); font-size: clamp(1.6rem, 4vw, 2.2rem); margin: 0 0 1rem; }
92 .pg-teaser { font-family: var(--font-body, serif); font-size: 1.1rem; line-height: 1.7; color: var(--ink); opacity: .95;
93 -webkit-mask-image: linear-gradient(180deg, #000 55%, transparent); mask-image: linear-gradient(180deg, #000 55%, transparent); }
94 .pg-card { margin: 1.5rem 0 0; border: 1px solid color-mix(in srgb, var(--ink, #000) 16%, transparent); border-radius: 18px; padding: 30px 26px; text-align: center; }
95 .pg-lock { font-size: 38px; margin-bottom: 6px; }
96 .pg-h2 { font-size: 22px; margin: 0 0 8px; }
97 .pg-sub { opacity: .85; line-height: 1.6; margin: 0 auto 12px; max-width: 34em; }
98 .pg-btn { display: inline-block; text-align: center; text-decoration: none; padding: 12px 24px; border: none; border-radius: 10px; font: inherit; font-weight: 600; cursor: pointer;
99 background: var(--accent, #6b8f71); color: #fff; }
100 .pg-btn:disabled { opacity: .6; cursor: default; }
101 .pg-btn:hover { filter: brightness(1.05); }
102 .pg-actions { display: flex; flex-direction: column; gap: 10px; align-items: stretch; max-width: 320px; margin: 0 auto; }
103 .pg-btn-ghost { background: transparent; color: var(--ink, #222); border: 1px solid color-mix(in srgb, var(--ink, #000) 22%, transparent); }
104 .pg-status { margin: 12px 0 0; opacity: .9; }
105 .pg-status.is-err { color: #c0392b; }
106</style>
Note: See TracBrowser for help on using the repository browser.