- Timestamp:
- 06/21/2026 02:32:39 PM (3 months ago)
- Branches:
- main
- Children:
- 4885eab
- Parents:
- 929d98d
- Location:
- src/views/pages
- Files:
-
- 16 edited
-
admin-audio.ejs (modified) (23 diffs)
-
admin-circle.ejs (modified) (4 diffs)
-
admin-comments.ejs (modified) (5 diffs)
-
admin-epk.ejs (modified) (3 diffs)
-
admin-google.ejs (modified) (2 diffs)
-
admin-help.ejs (modified) (2 diffs)
-
admin-newsletter.ejs (modified) (1 diff)
-
admin-playlists.ejs (modified) (7 diffs)
-
admin-seo.ejs (modified) (4 diffs)
-
admin-settings.ejs (modified) (13 diffs)
-
admin-shows.ejs (modified) (4 diffs)
-
admin-site-edit.ejs (modified) (12 diffs)
-
admin-sites.ejs (modified) (7 diffs)
-
admin-stats.ejs (modified) (7 diffs)
-
admin-updates.ejs (modified) (5 diffs)
-
admin-users.ejs (modified) (6 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/views/pages/admin-audio.ejs
r929d98d rbbf9d1a 4 4 <header class="ax-header"> 5 5 <div> 6 <h1> Audio tracks</h1>7 <p class="ax-tagline"> Site-level MP3s. Gebruik <code>[[track:<id>]]</code> in een post om een play-knop in te voegen.</p>6 <h1><%= t('aaud.title') %></h1> 7 <p class="ax-tagline"><%= t('aaud.tagline_pre') %> <code>[[track:<id>]]</code> <%= t('aaud.tagline_post') %></p> 8 8 </div> 9 9 </header> … … 14 14 <%# ── UPLOAD ──────────────────────────────────────────────── %> 15 15 <section class="ax-card"> 16 <div class="ax-card-title"> Upload</div>16 <div class="ax-card-title"><%= t('aaud.upload') %></div> 17 17 18 18 <%# Bulk drag-drop zone. Multiple files at once, transcoded sequentially. … … 25 25 <div class="ax-row ax-row-2"> 26 26 <label class="ax-field"> 27 <span> Artiest <small>(toegepast op alle bestanden)</small></span>28 <input type="text" id="batch-artist" placeholder=" Optioneel">27 <span><%= t('aaud.artist') %> <small><%= t('aaud.applied_all') %></small></span> 28 <input type="text" id="batch-artist" placeholder="<%= t('aaud.optional') %>"> 29 29 </label> 30 30 <label class="ax-field"> 31 <span> Album <small>(toegepast op alle bestanden)</small></span>32 <input type="text" id="batch-album" placeholder=" Optioneel">31 <span><%= t('aaud.album') %> <small><%= t('aaud.applied_all') %></small></span> 32 <input type="text" id="batch-album" placeholder="<%= t('aaud.optional') %>"> 33 33 </label> 34 34 </div> 35 35 <label class="ax-field ax-file"> 36 <span> Cover <small>(optioneel, toegepast op alle bestanden — jpg/png/webp/gif, max 5 MB)</small></span>36 <span><%= t('aaud.cover') %> <small><%= t('aaud.cover_hint') %></small></span> 37 37 <span class="ax-file-control"> 38 <span class="ax-btn ax-btn-secondary ax-file-btn">🖼 Kies cover</span>39 <span class="ax-file-name" data-empty> Geen bestand gekozen</span>38 <span class="ax-btn ax-btn-secondary ax-file-btn">🖼 <%= t('aaud.choose_cover') %></span> 39 <span class="ax-file-name" data-empty><%= t('aaud.no_file') %></span> 40 40 <input type="file" id="batch-cover" accept="image/jpeg,image/png,image/webp,image/gif"> 41 41 </span> … … 45 45 <label class="ax-dropzone" id="audio-dropzone" tabindex="0"> 46 46 <span class="ax-dropzone-icon">🎵</span> 47 <strong class="ax-dropzone-title"> Sleep audio hierheen</strong>48 <span class="ax-dropzone-sub"> of klik om bestanden te kiezen<br><small>mp3, m4a, ogg, opus, flac · max <%= maxBytesMb %> MB · wav max <%= maxWavMb %> MB</small></span>47 <strong class="ax-dropzone-title"><%= t('aaud.drag_here') %></strong> 48 <span class="ax-dropzone-sub"><%= t('aaud.or_click') %><br><small>mp3, m4a, ogg, opus, flac · max <%= maxBytesMb %> MB · wav max <%= maxWavMb %> MB</small></span> 49 49 <input type="file" id="audio-files" accept="audio/*" multiple> 50 50 </label> … … 54 54 55 55 <div class="ax-form-actions" id="upload-actions" hidden> 56 <button type="button" class="ax-btn ax-btn-primary" id="start-upload-btn">⬆ Start upload</button>57 <button type="button" class="ax-btn" id="clear-queue-btn"> Wis lijst</button>56 <button type="button" class="ax-btn ax-btn-primary" id="start-upload-btn">⬆ <%= t('aaud.start_upload') %></button> 57 <button type="button" class="ax-btn" id="clear-queue-btn"><%= t('aaud.clear_list') %></button> 58 58 </div> 59 59 </div> … … 63 63 <section class="ax-card"> 64 64 <div class="ax-card-title" style="display:flex;align-items:center;gap:.5rem"> 65 Tracks<span class="ax-count"><%= tracks.length %></span>65 <%= t('aaud.tracks') %> <span class="ax-count"><%= tracks.length %></span> 66 66 <button type="button" class="ax-btn ax-btn-secondary" id="add-link-track-btn" 67 style="margin-left:auto;font-size:.85rem" title=" Een track zonder audiobestand — alleen titel + open-in links">68 + Track zonder audio67 style="margin-left:auto;font-size:.85rem" title="<%= t('aaud.add_link_track_title') %>"> 68 + <%= t('aaud.add_link_track') %> 69 69 </button> 70 70 </div> … … 73 73 <div class="ax-empty"> 74 74 <div class="ax-empty-icon">♫</div> 75 <p> Nog geen tracks. Upload er een hierboven.</p>75 <p><%= t('aaud.no_tracks') %></p> 76 76 </div> 77 77 <% } else { %> 78 <% 79 // Pre-fetch loop labels — `t` is shadowed by the loop var below. 80 var _aaudUntitled = t('aaud.untitled'); 81 var _aaudPlay = t('aaud.play'); 82 var _aaudEdit = t('aaud.edit'); 83 var _aaudDlOn = t('aaud.dl_on'); 84 var _aaudDlOff = t('aaud.dl_off'); 85 var _aaudDelete = t('aaud.delete'); 86 var _aaudCopy = t('aaud.copy_click'); 87 var _aaudDelConfirm = t('aaud.delete_confirm'); 88 %> 78 89 <ul class="ax-list"> 79 90 <% tracks.forEach(function(t) { %> … … 86 97 87 98 <div class="ax-track-meta"> 88 <div class="ax-track-title" data-cell="title"><%= t.title || '(zonder titel)'%></div>99 <div class="ax-track-title" data-cell="title"><%= t.title || _aaudUntitled %></div> 89 100 <div class="ax-track-sub"> 90 101 <span data-cell="artist"><%= t.artist || '—' %></span> … … 92 103 <span class="ax-track-sep">·</span><span><%= t.size ? Math.round(t.size/1024) + ' KB' : '—' %></span> 93 104 </div> 94 <code class="ax-embed" data-copy="[[track:<%= t.id %>]]" title=" Klik om te kopiëren">[[track:<%= t.id %>]]</code>105 <code class="ax-embed" data-copy="[[track:<%= t.id %>]]" title="<%= _aaudCopy %>">[[track:<%= t.id %>]]</code> 95 106 </div> 96 107 … … 100 111 data-stream-url="<%= t.stream_url %>" 101 112 data-track-id="<%= t.id %>" 102 aria-label=" Afspelen" title="Afspelen">113 aria-label="<%= _aaudPlay %>" title="<%= _aaudPlay %>"> 103 114 <span class="ax-track-play-icon" aria-hidden="true">▶</span> 104 115 </button> 105 116 <% } %> 106 <button type="button" class="ax-icon-btn" data-track-edit data-id="<%= t.id %>" aria-label=" Bewerken" title="Bewerken">✎</button>117 <button type="button" class="ax-icon-btn" data-track-edit data-id="<%= t.id %>" aria-label="<%= _aaudEdit %>" title="<%= _aaudEdit %>">✎</button> 107 118 <% if (typeof premiumUnlocked === 'undefined' || premiumUnlocked) { %> 108 119 <button type="button" class="ax-icon-btn" data-track-dl data-id="<%= t.id %>" data-on="<%= t.downloadable ? '1' : '0' %>" 109 aria-label=" Download-voor-email" title="<%= t.downloadable ? 'Download-voor-email staat AAN — klik om uit te zetten' : 'Download-voor-email staat uit — klik om aan te zetten'%>"120 aria-label="<%= t.downloadable ? _aaudDlOn : _aaudDlOff %>" title="<%= t.downloadable ? _aaudDlOn : _aaudDlOff %>" 110 121 style="<%= t.downloadable ? 'color:var(--accent,#6b8f71)' : 'opacity:.5' %>">⬇</button> 111 122 <% } %> 112 <form action="/admin/audio/<%= t.id %>/delete" method="post" onsubmit="return confirm(' Track verwijderen?')" class="ax-track-delete">113 <button type="submit" class="ax-icon-btn ax-icon-btn-danger" aria-label=" Verwijderen" title="Verwijderen">🗑</button>123 <form action="/admin/audio/<%= t.id %>/delete" method="post" onsubmit="return confirm('<%= _aaudDelConfirm %>')" class="ax-track-delete"> 124 <button type="submit" class="ax-icon-btn ax-icon-btn-danger" aria-label="<%= _aaudDelete %>" title="<%= _aaudDelete %>">🗑</button> 114 125 </form> 115 126 </div> … … 122 133 <% if ((typeof premiumUnlocked === 'undefined' || premiumUnlocked) && typeof embedUrl !== 'undefined') { %> 123 134 <section class="ax-section" style="margin-top:1.5rem"> 124 <h2 style="margin:0 0 .4rem">🔊 Embedbare speler</h2>125 <p style="opacity:.75;font-size:.9rem;margin:0 0 .6rem"> Plak deze code op je eigen website/blog om je muziek met deze speler in te sluiten:</p>135 <h2 style="margin:0 0 .4rem">🔊 <%= t('aaud.embed_player') %></h2> 136 <p style="opacity:.75;font-size:.9rem;margin:0 0 .6rem"><%= t('aaud.embed_hint') %></p> 126 137 <textarea readonly rows="3" onclick="this.select()" style="width:100%;font-family:monospace;font-size:12px;padding:10px;border-radius:8px;border:1px solid var(--rule);background:var(--paper-2);color:inherit;"><iframe src="<%= embedUrl %>" width="100%" height="420" frameborder="0" loading="lazy" style="border-radius:12px"></iframe></textarea> 127 <p style="margin:.6rem 0 0"><a class="btn" href="<%= embedUrl %>" target="_blank">▶ Speler-voorbeeld openen</a></p>138 <p style="margin:.6rem 0 0"><a class="btn" href="<%= embedUrl %>" target="_blank">▶ <%= t('aaud.preview_player') %></a></p> 128 139 </section> 129 140 <% } %> … … 573 584 nameEl.removeAttribute('data-empty'); 574 585 } else { 575 nameEl.textContent = ' Geen bestand gekozen';586 nameEl.textContent = '<%= t('aaud.no_file') %>'; 576 587 nameEl.setAttribute('data-empty', ''); 577 588 } … … 592 603 if (icon) icon.textContent = playing ? '⏸' : '▶'; 593 604 btn.classList.toggle('is-playing', playing); 594 btn.setAttribute('aria-label', playing ? ' Pauzeren' : 'Afspelen');605 btn.setAttribute('aria-label', playing ? '<%= t('aaud.pause') %>' : '<%= t('aaud.play') %>'); 595 606 } 596 607 … … 695 706 function setItemStatus(item, status, label) { 696 707 const labels = { 697 queued: ' Wachten',698 uploading: ' Uploaden…',699 transcoding: ' Converteren…',700 done: '✓ Klaar',701 error: '✗ Fout',708 queued: '<%= t('aaud.st_queued') %>', 709 uploading: '<%= t('aaud.st_uploading') %>', 710 transcoding: '<%= t('aaud.st_transcoding') %>', 711 done: '✓ <%= t('aaud.st_done') %>', 712 error: '✗ <%= t('aaud.st_error') %>', 702 713 }; 703 714 item.status = status; … … 726 737 li.innerHTML = 727 738 '<div class="ax-queue-name"></div>' + 728 '<span class="ax-queue-status ax-queue-status--queued"> Wachten</span>';739 '<span class="ax-queue-status ax-queue-status--queued"><%= t('aaud.st_queued') %></span>'; 729 740 // Use textContent to avoid HTML-injection if a filename contains markup. 730 741 li.querySelector('.ax-queue-name').textContent = file.name; … … 813 824 } catch (err) { 814 825 console.error('upload failed for', item.file.name, err); 815 setItemStatus(item, 'error', '✗ ' + (err.message || ' Mislukt'));826 setItemStatus(item, 'error', '✗ ' + (err.message || '<%= t('aaud.failed') %>')); 816 827 } 817 828 } … … 864 875 let data; 865 876 try { data = await res.json(); } 866 catch (_) { throw new Error(' Onverwacht serverantwoord(' + res.status + ')'); }877 catch (_) { throw new Error('<%= t('aaud.err_unexpected') %> (' + res.status + ')'); } 867 878 868 879 if (!res.ok || !data.ok) { … … 870 881 } 871 882 872 setItemStatus(item, 'done', '✓ ' + (data.title || ' Klaar'));883 setItemStatus(item, 'done', '✓ ' + (data.title || '<%= t('aaud.st_done') %>')); 873 884 } catch (err) { 874 885 clearTimeout(transcodeHint); … … 885 896 el.classList.add('is-copied'); 886 897 const original = el.textContent; 887 el.textContent = '✓ gekopieerd';898 el.textContent = '✓ <%= t('aaud.copied') %>'; 888 899 setTimeout(() => { 889 900 el.classList.remove('is-copied'); … … 903 914 method: 'POST', credentials: 'same-origin', 904 915 headers: { 'Content-Type': 'application/json' }, 905 body: JSON.stringify({ title: ' Nieuwe track' }),916 body: JSON.stringify({ title: '<%= t('aaud.new_track') %>' }), 906 917 }); 907 918 const j = await r.json(); 908 if (!r.ok || !j.ok) throw new Error(j.error || ' Aanmaken mislukt');919 if (!r.ok || !j.ok) throw new Error(j.error || '<%= t('aaud.create_failed') %>'); 909 920 if (typeof window.openTrackEditor !== 'function') { location.reload(); return; } 910 921 window.openTrackEditor({ id: j.id, onSaved: () => location.reload() }); 911 922 } catch (err) { 912 alert(' Track aanmaken mislukt: ' + err.message);923 alert('<%= t('aaud.create_failed') %>: ' + err.message); 913 924 } finally { 914 925 addLinkBtn.disabled = false; … … 923 934 btn.addEventListener('click', () => { 924 935 if (typeof window.openTrackEditor !== 'function') { 925 alert(' Track editor niet geladen');936 alert('<%= t('aaud.editor_not_loaded') %>'); 926 937 return; 927 938 } … … 936 947 const artistEl = row.querySelector('[data-cell="artist"]'); 937 948 const albumEl = row.querySelector('[data-cell="album"]'); 938 if (titleEl) titleEl.textContent = track.title || ' (zonder titel)';949 if (titleEl) titleEl.textContent = track.title || '<%= t('aaud.untitled') %>'; 939 950 if (artistEl) artistEl.textContent = track.artist || '—'; 940 951 if (albumEl) { … … 984 995 btn.style.opacity = want ? '1' : '.5'; 985 996 btn.title = want 986 ? ' Download-voor-email staat AAN — klik om uit te zetten'987 : ' Download-voor-email staat uit — klik om aan te zetten';997 ? '<%= t('aaud.dl_on') %>' 998 : '<%= t('aaud.dl_off') %>'; 988 999 } catch (e) { 989 alert(' Kon niet wijzigen: ' + (e.message || e));1000 alert('<%= t('aaud.change_failed') %>: ' + (e.message || e)); 990 1001 } finally { 991 1002 btn.disabled = false; -
src/views/pages/admin-circle.ejs
r929d98d rbbf9d1a 1 1 <div class="container admin-page"> 2 <h1> Cirkel</h1>3 <p><a href="/admin/settings" class="btn">← Instellingen</a></p>2 <h1><%= t('acir.title') %></h1> 3 <p><a href="/admin/settings" class="btn">← <%= t('acir.back_settings') %></a></p> 4 4 5 5 <% if (success) { %><div class="alert alert-success"><%= success %></div><% } %> … … 8 8 <% if (tenancy !== 'circle') { %> 9 9 <section class="set-card"> 10 <p class="set-help"> De modus staat niet op <strong>Cirkels</strong>. Zet 'm aan bij11 <a href="/admin/settings"> Instellingen</a> om je cirkel-feed te tonen op<code>/cirkel</code>.12 Je kunt hieronder al wel bronnen klaarzetten.</p>10 <p class="set-help"><%= t('acir.mode_off_1') %> <strong><%= t('acir.circles') %></strong>. <%= t('acir.mode_off_2') %> 11 <a href="/admin/settings"><%= t('acir.settings') %></a> <%= t('acir.mode_off_3') %> <code>/cirkel</code>. 12 <%= t('acir.mode_off_4') %></p> 13 13 </section> 14 14 <% } %> 15 15 16 16 <section class="set-card" style="margin-top:1rem"> 17 <h2>Mijn zichtbaarheid</h2> 18 <p class="set-help">Doe je mee aan cirkels? Dit maakt je <strong>al-publieke</strong> posts 19 ophaalbaar voor andere Klonkt-sites via een ondertekende feed 20 (<code>/.klonkt/outbox.json</code>). Het is een deelname-keuze, geen privacy-slot: 21 je posts blijven sowieso openbaar op je site, ook als dit uit staat. Wil je iets 22 afschermen, maak die post dan niet-publiek.</p> 17 <h2><%= t('acir.visibility_title') %></h2> 18 <p class="set-help"><%= t('acir.visibility_help_1') %> <strong><%= t('acir.all_public') %></strong> <%= t('acir.visibility_help_2') %> 19 (<code>/.klonkt/outbox.json</code>). <%= t('acir.visibility_help_3') %></p> 23 20 <form method="post" action="/admin/circle/allow" class="set-form"> 24 21 <label class="set-opt" style="align-items:center"> 25 22 <input type="checkbox" name="allow_circle" value="on" <%= allowCircle ? 'checked' : '' %>> 26 <span><strong> Toon mijn site in cirkels van anderen</strong></span>23 <span><strong><%= t('acir.show_in_circles') %></strong></span> 27 24 </label> 28 <button type="submit" class="btn btn-primary"> Opslaan</button>25 <button type="submit" class="btn btn-primary"><%= t('acir.save') %></button> 29 26 </form> 30 27 </section> 31 28 32 29 <section class="set-card" style="margin-top:1rem"> 33 <h2>Klonkt-site toevoegen</h2> 34 <p class="set-help">Plak de basis-URL van een andere Klonkt-site. Asymmetrisch: jij toont 35 hen, los van of zij jou tonen.</p> 30 <h2><%= t('acir.add_title') %></h2> 31 <p class="set-help"><%= t('acir.add_help') %></p> 36 32 <form method="post" action="/admin/circle/add" class="set-form"> 37 33 <label class="set-field"> 38 <span> URL</span>34 <span><%= t('acir.url') %></span> 39 35 <input type="url" name="remote_url" placeholder="https://artiest.klonkt.com" required> 40 36 </label> 41 37 <label class="set-field"> 42 <span> Label <small style="font-weight:400">— optioneel</small></span>43 <input type="text" name="label" maxlength="80" placeholder=" bijv. Joost Klein">38 <span><%= t('acir.label') %> <small style="font-weight:400">— <%= t('acir.optional') %></small></span> 39 <input type="text" name="label" maxlength="80" placeholder="<%= t('acir.label_ph') %>"> 44 40 </label> 45 <button type="submit" class="btn btn-primary"> Toevoegen</button>41 <button type="submit" class="btn btn-primary"><%= t('acir.add') %></button> 46 42 </form> 47 43 </section> 48 44 49 45 <section class="set-card" style="margin-top:1rem"> 50 <h2> In mijn cirkel (<%= links.length %>)</h2>46 <h2><%= t('acir.in_circle', { n: links.length }) %></h2> 51 47 <% if (!links.length) { %> 52 <p class="set-help"> Nog geen bronnen. Voeg er hierboven een toe.</p>48 <p class="set-help"><%= t('acir.no_sources') %></p> 53 49 <% } else { %> 54 50 <form method="post" action="/admin/circle/sync-all" style="margin:0 0 .9rem"> 55 <button type="submit" class="btn">↻ Alles nu synchroniseren</button>51 <button type="submit" class="btn">↻ <%= t('acir.sync_all') %></button> 56 52 </form> 53 <% 54 var _cActive = t('acir.st_active'); 55 var _cPosts = t('acir.posts'); 56 var _cLast = t('acir.last'); 57 var _cMismatch = t('acir.st_mismatch'); 58 var _cMismatchReason = t('acir.mismatch_reason'); 59 var _cError = t('acir.st_error'); 60 var _cPaused = t('acir.st_paused'); 61 var _cRefresh = t('acir.refresh'); 62 var _cRemove = t('acir.remove'); 63 var _cRemoveConfirm = t('acir.remove_confirm'); 64 %> 57 65 <ul class="circ-list"> 58 66 <% links.forEach(function(l){ %> … … 63 71 <div class="circ-status"> 64 72 <% if (l.status === 'active') { %> 65 <span class="circ-badge ok">✓ actief</span>66 <span class="circ-meta"><%= counts[l.id] || 0 %> posts<% if (l.last_synced) { %> · laatst: <%= l.last_synced %><% } %></span>73 <span class="circ-badge ok">✓ <%= _cActive %></span> 74 <span class="circ-meta"><%= counts[l.id] || 0 %> <%= _cPosts %><% if (l.last_synced) { %> · <%= _cLast %>: <%= l.last_synced %><% } %></span> 67 75 <% } else if (l.status === 'outdated') { %> 68 <span class="circ-badge err">↻ versie-mismatch</span>69 <div class="circ-reason"><%= l.last_error || 'Deze site draait een andere Klonkt-protocolversie — bijwerken nodig om te federeren.'%></div>76 <span class="circ-badge err">↻ <%= _cMismatch %></span> 77 <div class="circ-reason"><%= l.last_error || _cMismatchReason %></div> 70 78 <% } else if (l.status === 'error') { %> 71 <span class="circ-badge err">⚠ fout</span>79 <span class="circ-badge err">⚠ <%= _cError %></span> 72 80 <% if (l.last_error) { %><div class="circ-reason"><%= l.last_error %></div><% } %> 73 81 <% } else if (l.status === 'paused') { %> 74 <span class="circ-badge">⏸ gepauzeerd</span>82 <span class="circ-badge">⏸ <%= _cPaused %></span> 75 83 <% } else { %> 76 84 <%# Onbekende status: toon 'm rauw + eventuele reden i.p.v. stil "gepauzeerd". %> … … 82 90 <div class="circ-actions"> 83 91 <form method="post" action="/admin/circle/<%= l.id %>/sync" style="display:inline"> 84 <button type="submit" class="btn"> Verversen</button>92 <button type="submit" class="btn"><%= _cRefresh %></button> 85 93 </form> 86 94 <form method="post" action="/admin/circle/<%= l.id %>/remove" style="display:inline" 87 onsubmit="return confirm(' Verwijderen uit je cirkel?')">88 <button type="submit" class="btn btn-danger"> Verwijder</button>95 onsubmit="return confirm('<%= _cRemoveConfirm %>')"> 96 <button type="submit" class="btn btn-danger"><%= _cRemove %></button> 89 97 </form> 90 98 </div> -
src/views/pages/admin-comments.ejs
r929d98d rbbf9d1a 1 1 <div class="container admin-comments-page"> 2 <p><a href="/admin" class="btn">← Beheer</a></p>3 <h1> Comment moderation</h1>2 <p><a href="/admin" class="btn">← <%= t('acom.back') %></a></p> 3 <h1><%= t('acom.title') %></h1> 4 4 5 5 <p class="muted"> 6 Mode for this site:<strong><%= moderationMode %></strong>6 <%= t('acom.mode_for_site') %> <strong><%= moderationMode %></strong> 7 7 <% if (moderationMode === 'trust') { %> 8 — comments are auto-approved. Switch to <em>moderate</em> in9 <a href="/admin/sites/<%= site.slug %>/edit"> site settings</a> to queue them.8 — <%= t('acom.mode_trust_a') %> <em><%= t('acom.mode_moderate_word') %></em> 9 <a href="/admin/sites/<%= site.slug %>/edit"><%= t('acom.site_settings') %></a> <%= t('acom.mode_trust_b') %> 10 10 <% } else { %> 11 — new comments require approval before showing up on posts.11 — <%= t('acom.mode_moderate_hint') %> 12 12 <% } %> 13 13 </p> … … 16 16 <% if (error) { %><div class="audio-flash audio-flash--err"><%= error %></div><% } %> 17 17 18 <h2> Pending (<%= pending.length %>)</h2>18 <h2><%= t('acom.pending', { n: pending.length }) %></h2> 19 19 <% if (!pending.length) { %> 20 <p class="muted"> Nothing waiting.</p>20 <p class="muted"><%= t('acom.nothing_waiting') %></p> 21 21 <% } else { %> 22 <% 23 var _c_reply = t('acom.reply'); 24 var _c_on = t('acom.on'); 25 var _c_approve = t('acom.approve'); 26 var _c_reject = t('acom.reject'); 27 %> 22 28 <ol class="moderation-list"> 23 29 <% pending.forEach(function(c) { %> … … 25 31 <div class="moderation-meta"> 26 32 <strong><%= c.author_username %></strong> 27 <% if (c.parent_comment_id) { %><span class="muted">( reply)</span><% } %>28 on<a href="<%= siteUrlBase %>/<%= c.post_slug %>"><%= c.post_title || c.post_slug %></a>33 <% if (c.parent_comment_id) { %><span class="muted">(<%= _c_reply %>)</span><% } %> 34 <%= _c_on %> <a href="<%= siteUrlBase %>/<%= c.post_slug %>"><%= c.post_title || c.post_slug %></a> 29 35 <span class="muted">· <%= formatDateTime(c.created_at) %></span> 30 36 </div> … … 32 38 <div class="moderation-actions"> 33 39 <form method="post" action="<%= siteUrlBase %>/admin/comments/<%= c.id %>/approve" style="display:inline"> 34 <button type="submit" class="btn btn-primary"> Approve</button>40 <button type="submit" class="btn btn-primary"><%= _c_approve %></button> 35 41 </form> 36 42 <form method="post" action="<%= siteUrlBase %>/admin/comments/<%= c.id %>/reject" style="display:inline"> 37 <button type="submit" class="btn btn-danger"> Reject</button>43 <button type="submit" class="btn btn-danger"><%= _c_reject %></button> 38 44 </form> 39 45 </div> … … 43 49 <% } %> 44 50 45 <h2> Recent decisions</h2>51 <h2><%= t('acom.recent') %></h2> 46 52 <% if (!recent.length) { %> 47 <p class="muted"> Nothing yet.</p>53 <p class="muted"><%= t('acom.nothing_yet') %></p> 48 54 <% } else { %> 49 55 <ul class="decision-list"> -
src/views/pages/admin-epk.ejs
r929d98d rbbf9d1a 1 1 <div class="container admin-page"> 2 <h1> Perskit bewerken</h1>3 <p><a href="/admin" class="btn">← Beheer</a> <a href="/pers" class="btn" target="_blank" rel="noopener">📰 Bekijk perskit</a></p>2 <h1><%= t('aepk.title') %></h1> 3 <p><a href="/admin" class="btn">← <%= t('aepk.back_admin') %></a> <a href="/pers" class="btn" target="_blank" rel="noopener">📰 <%= t('aepk.view_epk') %></a></p> 4 4 5 5 <% if (success) { %><div class="alert alert-success"><%= success %></div><% } %> 6 6 7 7 <section class="set-card"> 8 <h2> Tekst</h2>8 <h2><%= t('aepk.text_heading') %></h2> 9 9 <p class="set-help"> 10 De perskit (<code>/pers</code>) toont je sitenaam + foto, deze bio en contact, 11 plus automatisch je meest beluisterde nummers en recente posts. Laat de bio 12 leeg om de site-tagline te gebruiken; laat contact leeg om niets te tonen 13 (je login-mail wordt nooit automatisch getoond). 10 <%= t('aepk.text_help') %> 14 11 </p> 15 12 16 13 <form method="post" action="/admin/epk" class="set-form"> 17 14 <label class="set-field"> 18 <span> Pers-bio</span>15 <span><%= t('aepk.bio_label') %></span> 19 16 <textarea name="epk_bio" rows="5" maxlength="1000" 20 placeholder=" Korte beschrijving van jou/het project voor pers & boekers."><%= epkBio %></textarea>17 placeholder="<%= t('aepk.bio_ph') %>"><%= epkBio %></textarea> 21 18 </label> 22 19 <label class="set-field"> 23 <span> Pers-contact</span>20 <span><%= t('aepk.contact_label') %></span> 24 21 <input type="text" name="epk_contact" maxlength="300" 25 placeholder=" bv. pers@jouwdomein.nl of een boekingslink"22 placeholder="<%= t('aepk.contact_ph') %>" 26 23 value="<%= epkContact %>"> 27 24 </label> 28 25 29 26 <div class="set-field"> 30 <span> Nummers op de perskit <small>(kies er max <%= maxEpkTracks %>; laat leeg voor automatisch de top <%= maxEpkTracks %> meest beluisterd)</small></span>27 <span><%= t('aepk.tracks_label') %> <small><%= t('aepk.tracks_hint', { n: maxEpkTracks }) %></small></span> 31 28 <% if (!allTracks.length) { %> 32 <p class="set-help" style="margin:0"> Nog geen nummers — voeg eerst audio toe in Beheer → Audio.</p>29 <p class="set-help" style="margin:0"><%= t('aepk.no_tracks') %></p> 33 30 <% } else { %> 31 <% var _untitled = t('aepk.untitled'); %> 34 32 <div class="epk-track-pick" data-max="<%= maxEpkTracks %>"> 35 33 <% allTracks.forEach(function(t){ %> … … 37 35 <input type="checkbox" name="epk_tracks" value="<%= t.id %>" <%= chosenTracks.indexOf(t.id) >= 0 ? 'checked' : '' %>> 38 36 <span class="epk-box" aria-hidden="true">✓</span> 39 <span class="epk-pick-title"><%= t.title || '(zonder titel)'%></span>37 <span class="epk-pick-title"><%= t.title || _untitled %></span> 40 38 <% if (t.artist) { %><span class="epk-pick-artist"><%= t.artist %></span><% } %> 41 39 </label> … … 45 43 </div> 46 44 47 <button type="submit" class="btn btn-primary"> Opslaan</button>45 <button type="submit" class="btn btn-primary"><%= t('aepk.save') %></button> 48 46 </form> 49 47 </section> -
src/views/pages/admin-google.ejs
r929d98d rbbf9d1a 1 1 <div class="container admin-page"> 2 <h1> Google-login</h1>3 <p><a href="/admin/settings" class="btn">← Instellingen</a></p>2 <h1><%= t('agoog.title') %></h1> 3 <p><a href="/admin/settings" class="btn">← <%= t('agoog.back') %></a></p> 4 4 5 5 <% if (success) { %><div class="alert alert-success"><%= success %></div><% } %> … … 7 7 8 8 <section class="set-card"> 9 <h2> Google-login (bezoekers)</h2>10 <p class="set-help"> Laat bezoekers met Google inloggen om te reageren — met je <strong>eigen</strong>11 Google Cloud-client. Maak er een aan op9 <h2><%= t('agoog.card_title') %></h2> 10 <p class="set-help"><%= t('agoog.card_help_pre') %> <strong><%= t('agoog.card_help_own') %></strong> 11 <%= t('agoog.card_help_mid') %> 12 12 <a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener">console.cloud.google.com</a> 13 (OAuth client-ID, type "Web application").14 <% if (google && google.configured) { %><strong> Status: ingesteld ✓</strong><% } else { %>Nog niet ingesteld.<% } %></p>13 <%= t('agoog.card_help_post') %> 14 <% if (google && google.configured) { %><strong><%= t('agoog.status_set') %></strong><% } else { %><%= t('agoog.status_unset') %><% } %></p> 15 15 16 16 <details class="set-google-guide"> 17 <summary> Hoe stel ik dit in? (stap voor stap)</summary>17 <summary><%= t('agoog.guide_summary') %></summary> 18 18 <ol class="set-google-steps"> 19 <li> Ga naar de <a href="https://console.cloud.google.com/" target="_blank" rel="noopener">Google Cloud Console</a> en maak (of kies) een project.</li>20 <li><strong>APIs & Services → OAuth consent screen</strong>: User type <strong>External</strong>. Voeg de scopes <code>email</code>, <code>profile</code> en <code>openid</code> toe. (In testmodus: voeg jezelf toe als testgebruiker.)</li>21 <li><strong>APIs & Services → Credentials → Create credentials → OAuth client ID</strong>, type<strong>Web application</strong>.</li>22 <li> Bij <strong>Authorized redirect URIs</strong>: plak de redirect-URI hieronder (moet <strong>exact</strong> matchen).</li>23 <li> Kopieer de <strong>Client ID</strong> + <strong>Client Secret</strong> die je krijgt naar de velden hieronder en klik Opslaan.</li>19 <li><%= t('agoog.step1_pre') %> <a href="https://console.cloud.google.com/" target="_blank" rel="noopener">Google Cloud Console</a> <%= t('agoog.step1_post') %></li> 20 <li><strong>APIs & Services → OAuth consent screen</strong>: <%= t('agoog.step2_usertype') %> <strong>External</strong>. <%= t('agoog.step2_scopes_pre') %> <code>email</code>, <code>profile</code> <%= t('agoog.step2_scopes_and') %> <code>openid</code> <%= t('agoog.step2_scopes_post') %> <%= t('agoog.step2_testmode') %></li> 21 <li><strong>APIs & Services → Credentials → Create credentials → OAuth client ID</strong>, <%= t('agoog.step3_type') %> <strong>Web application</strong>.</li> 22 <li><%= t('agoog.step4_pre') %> <strong>Authorized redirect URIs</strong><%= t('agoog.step4_post') %></li> 23 <li><%= t('agoog.step5_pre') %> <strong>Client ID</strong> + <strong>Client Secret</strong> <%= t('agoog.step5_post') %></li> 24 24 </ol> 25 <p class="set-help" style="margin:.5rem 0 0"><strong> Let op:</strong> je hoeft <strong>geen API's aan te zetten</strong> — "Google Cloud APIs", People API e.d. zijn <em>niet</em> nodig. Inloggen werkt via OpenID Connect (alleen het toestemmingsscherm + de OAuth-client).</p>25 <p class="set-help" style="margin:.5rem 0 0"><strong><%= t('agoog.note_label') %></strong> <%= t('agoog.note_pre') %> <strong><%= t('agoog.note_noapi') %></strong> <%= t('agoog.note_mid') %> <em><%= t('agoog.note_not') %></em> <%= t('agoog.note_post') %></p> 26 26 </details> 27 27 28 28 <% if (google && google.redirectUri) { %> 29 29 <div class="set-redirect"> 30 <div class="set-redirect-label"> Redirect-URI — kopieer deze en plak 'm in Google</div>30 <div class="set-redirect-label"><%= t('agoog.redirect_label') %></div> 31 31 <code class="set-redirect-url" style="user-select:all"><%= google.redirectUri %></code> 32 32 <ol class="set-redirect-steps"> 33 <li> Ga naar<a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener">Google Cloud Console → APIs & Services → Credentials</a>.</li>34 <li> Open jouw <strong>OAuth client-ID</strong> (type "Web application").</li>35 <li> Onder <strong>Authorized redirect URIs</strong> → klik <strong>+ ADD URI</strong> en plak de regel hierboven.</li>36 <li> Klik <strong>Save</strong> en wacht ~1 minuut.</li>33 <li><%= t('agoog.rstep1_pre') %> <a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener">Google Cloud Console → APIs & Services → Credentials</a>.</li> 34 <li><%= t('agoog.rstep2') %></li> 35 <li><%= t('agoog.rstep3_pre') %> <strong>Authorized redirect URIs</strong> → <%= t('agoog.rstep3_mid') %> <strong>+ ADD URI</strong> <%= t('agoog.rstep3_post') %></li> 36 <li><%= t('agoog.rstep4_pre') %> <strong>Save</strong> <%= t('agoog.rstep4_post') %></li> 37 37 </ol> 38 <p class="set-redirect-warn"> Moet <strong>exact</strong> kloppen: <code>https://</code> (geen <code>http</code>), juiste domein, en <strong>geen</strong> schuine streep aan het eind. Anders krijg je <em>"Error 400: redirect_uri_mismatch"</em>.</p>38 <p class="set-redirect-warn"><%= t('agoog.warn_pre') %> <strong><%= t('agoog.warn_exact') %></strong> <%= t('agoog.warn_mid1') %> <code>https://</code> (<%= t('agoog.warn_nohttp') %> <code>http</code>)<%= t('agoog.warn_mid2') %> <strong><%= t('agoog.warn_noslash') %></strong> <%= t('agoog.warn_mid3') %> <em><%= t('agoog.warn_error') %></em>.</p> 39 39 </div> 40 40 <% } else { %> 41 <p class="set-help" style="color:var(--accent)"> Stel eerst <code>PUBLIC_BASE_URL</code> in — daar wordt de redirect-URI van afgeleid.</p>41 <p class="set-help" style="color:var(--accent)"><%= t('agoog.set_baseurl_pre') %> <code>PUBLIC_BASE_URL</code> <%= t('agoog.set_baseurl_post') %></p> 42 42 <% } %> 43 43 44 44 <form method="post" action="/admin/settings/google" class="set-form"> 45 45 <label class="set-field"> 46 <span> Client ID</span>46 <span><%= t('agoog.client_id') %></span> 47 47 <input type="text" name="google_client_id" value="<%= google ? google.clientId : '' %>" placeholder="123-abc.apps.googleusercontent.com" autocomplete="off"> 48 48 </label> 49 49 <label class="set-field"> 50 <span> Client Secret <% if (google && google.secretSet) { %><small style="font-weight:400">— ingesteld; leeg laten = ongewijzigd</small><% } %></span>51 <input type="password" name="google_client_secret" placeholder="<%= (google && google.secretSet) ? '•••••••• (ingesteld)': 'GOCSPX-…' %>" autocomplete="off">50 <span><%= t('agoog.client_secret') %> <% if (google && google.secretSet) { %><small style="font-weight:400">— <%= t('agoog.client_secret_set') %></small><% } %></span> 51 <input type="password" name="google_client_secret" placeholder="<%= (google && google.secretSet) ? t('agoog.secret_ph_set') : 'GOCSPX-…' %>" autocomplete="off"> 52 52 </label> 53 53 <div class="set-actions"> 54 <button type="submit" class="btn btn-primary"> Opslaan</button>54 <button type="submit" class="btn btn-primary"><%= t('agoog.save') %></button> 55 55 <% if (google && (google.clientId || google.secretSet)) { %> 56 <button type="submit" name="clear" value="1" class="btn"> Loskoppelen</button>56 <button type="submit" name="clear" value="1" class="btn"><%= t('agoog.disconnect') %></button> 57 57 <% } %> 58 58 </div> 59 59 </form> 60 60 <% if (typeof premiumEnabled !== 'undefined' && premiumEnabled && !isPremium) { %> 61 <p class="set-help" style="margin-top:.7rem; color:var(--accent)"> Premium-laag staat aan zonder Patreon — de Google-knop blijft verborgen tot premium ontgrendeld is.</p>61 <p class="set-help" style="margin-top:.7rem; color:var(--accent)"><%= t('agoog.premium_locked') %></p> 62 62 <% } %> 63 63 </section> -
src/views/pages/admin-help.ejs
r929d98d rbbf9d1a 1 1 <div class="container admin-page hb"> 2 <p><a href="/admin" class="btn">← Beheer</a></p>3 <h1> Handleiding</h1>4 <p class="hb-intro"> Uitleg van alle functies. Typ hieronder om te zoeken op een onderwerp of instructie.</p>2 <p><a href="/admin" class="btn">← <%= t('ahelp.back') %></a></p> 3 <h1><%= t('ahelp.title') %></h1> 4 <p class="hb-intro"><%= t('ahelp.intro') %></p> 5 5 6 6 <div class="hb-search"> 7 <input type="search" id="hb-q" placeholder=" Zoek… (bv. 'agenda', 'foto', 'cirkel', 'downloads')" autocomplete="off" aria-label="Zoek in de handleiding">7 <input type="search" id="hb-q" placeholder="<%= t('ahelp.search_placeholder') %>" autocomplete="off" aria-label="<%= t('ahelp.search_aria') %>"> 8 8 </div> 9 9 <p class="hb-count" id="hb-count" aria-live="polite"></p> … … 12 12 13 13 <section class="hb-item"> 14 <h2>✍️ Nieuwe post schrijven</h2>15 <p> Beheer → <strong>Nieuwe post</strong>. Geef een titel en schrijf je inhoud. Onderaan kies je de status: <em>concept</em> (niet zichtbaar) of <em>gepubliceerd</em>. Concepten staan bovenaan in je Beheer-overzicht zodat je ze terugvindt.</p>14 <h2>✍️ <%= t('ahelp.s_newpost_h') %></h2> 15 <p><%- t('ahelp.s_newpost_b') %></p> 16 16 </section> 17 17 18 18 <section class="hb-item"> 19 <h2>📝 Samenvatting & Cirkel Preview</h2>20 <p> Het veld <strong>Samenvatting & Cirkel Preview</strong> (de excerpt) is de korte previewtekst onder een post in lijsten, én de samenvatting die andere sites tonen als ze je post via een <strong>Cirkel</strong> overnemen. Laat je 'm leeg, dan wordt automatisch het begin van de post gebruikt.</p>19 <h2>📝 <%= t('ahelp.s_excerpt_h') %></h2> 20 <p><%- t('ahelp.s_excerpt_b') %></p> 21 21 </section> 22 22 23 23 <section class="hb-item"> 24 <h2>📌 Post pinnen / volgorde</h2>25 <p> In de post-editor kun je een post <strong>pinnen</strong> met een rang (1 = bovenaan). Gepinde posts staan vooraan in de tijdlijn/grid, op volgorde van hun rang. Rang leeg of 0 = niet gepind.</p>24 <h2>📌 <%= t('ahelp.s_pin_h') %></h2> 25 <p><%- t('ahelp.s_pin_b') %></p> 26 26 </section> 27 27 28 28 <section class="hb-item"> 29 <h2>🗓️ Publiceren plannen & alleen voor fans <small>(premium)</small></h2>30 <p> In de editor kun je een <strong>publicatiedatum</strong> in de toekomst zetten — de post verschijnt dan automatisch op dat moment. Met <strong>Alleen voor fans</strong> zien niet-ingelogde bezoekers alleen een teaser + login-uitnodiging; ingelogde fans zien alles.</p>29 <h2>🗓️ <%= t('ahelp.s_schedule_h') %> <small>(<%= t('ahelp.premium') %>)</small></h2> 30 <p><%- t('ahelp.s_schedule_b') %></p> 31 31 </section> 32 32 33 33 <section class="hb-item"> 34 <h2>🖼️ Afbeeldingen in posts</h2>35 <p> Afbeeldingen in de tekst en de cover-afbeelding worden altijd <strong>volledig</strong> getoond op de volledige breedte (niet bijgesneden), met hun natuurlijke hoogte.</p>34 <h2>🖼️ <%= t('ahelp.s_images_h') %></h2> 35 <p><%- t('ahelp.s_images_b') %></p> 36 36 </section> 37 37 38 38 <section class="hb-item"> 39 <h2>🎵 Audio & nummers toevoegen</h2>40 <p> Beheer → <strong>Audio</strong>. Upload een bestand of voeg een <em>link-only</em> nummer toe (zonder upload, alleen "open in"-links). Per nummer vul je titel, artiest, cover, en optioneel album/positie in. In een post toon je een nummer met de shortcode <code>[[track:id]]</code>, een album met <code>[[album:Naam]]</code>, een playlist met <code>[[playlist:id]]</code>.</p>39 <h2>🎵 <%= t('ahelp.s_audio_h') %></h2> 40 <p><%- t('ahelp.s_audio_b') %></p> 41 41 </section> 42 42 43 43 <section class="hb-item"> 44 <h2>© Credit, licentie & "open in"</h2>45 <p> Per nummer kun je een <strong>eigenaar/credit</strong> (met © -knop) en een <strong>licentie</strong> instellen — die worden ook in de mp3-metadata geschreven. Met de <strong>open-in</strong>-velden (Spotify / YouTube / SoundCloud) verschijnen knoppen om het nummer op die platforms te openen.</p>44 <h2>© <%= t('ahelp.s_credit_h') %></h2> 45 <p><%- t('ahelp.s_credit_b') %></p> 46 46 </section> 47 47 48 48 <section class="hb-item"> 49 <h2>⬇ Downloads <small>(premium)</small></h2>50 <p> Markeer een nummer als <strong>downloadbaar</strong> in Beheer → Audio (⬇-knop). Bezoekers vinden ze op de <strong>/downloads</strong>-pagina en laten hun e-mail achter om het bestand te krijgen (komt op je mailinglijst). Wil je downloads prominent in de feed? Maak een gewone post met slug <code>downloads</code> en pin 'm.</p>49 <h2>⬇ <%= t('ahelp.s_downloads_h') %> <small>(<%= t('ahelp.premium') %>)</small></h2> 50 <p><%- t('ahelp.s_downloads_b') %></p> 51 51 </section> 52 52 53 53 <section class="hb-item"> 54 <h2>📃 Albums & playlists</h2>55 <p> Geef nummers hetzelfde <strong>album</strong> + een <strong>positie</strong> om een album te vormen. Playlists maak je in Beheer → <strong>Playlists</strong>. Beide toon je in een post met <code>[[album:Naam]]</code> of <code>[[playlist:id]]</code>.</p>54 <h2>📃 <%= t('ahelp.s_albums_h') %></h2> 55 <p><%- t('ahelp.s_albums_b') %></p> 56 56 </section> 57 57 58 58 <section class="hb-item"> 59 <h2>📅 Agenda / evenementen <small>(premium)</small></h2>60 <p> Beheer → <strong>Agenda</strong>. Zet bovenaan <strong>"Agenda tonen op de site"</strong> aan — dan verschijnt de Agenda-knop in de balk en is de agendapagina bereikbaar. Voeg evenementen toe (datum, plaats, locatie, tickets). Bezoekers kunnen zich (los van de nieuwsbrief) aanmelden voor een seintje bij een nieuw evenement.</p>59 <h2>📅 <%= t('ahelp.s_agenda_h') %> <small>(<%= t('ahelp.premium') %>)</small></h2> 60 <p><%- t('ahelp.s_agenda_b') %></p> 61 61 </section> 62 62 63 63 <section class="hb-item"> 64 <h2>📰 Perskit <small>(premium)</small></h2>65 <p> Een deelbare perspagina op <strong>/pers</strong>. Bewerk 'm via de <strong>✎ Bewerken</strong>-knop op die pagina zelf (alleen jij ziet die). Stel een korte pers-bio + contact in, en kies <strong>tot 5 nummers</strong> die getoond worden (of laat leeg = automatisch de top-5 meest beluisterd).</p>64 <h2>📰 <%= t('ahelp.s_presskit_h') %> <small>(<%= t('ahelp.premium') %>)</small></h2> 65 <p><%- t('ahelp.s_presskit_b') %></p> 66 66 </section> 67 67 68 68 <section class="hb-item"> 69 <h2>🔗 Cirkels (federatie)</h2>70 <p> In Beheer → Instellingen zet je je site op <strong>Cirkel</strong>-modus. Daarna voeg je in Beheer → <strong>Cirkel</strong> andere Klonkt-sites toe; hun publieke posts verschijnen dan in je <strong>/cirkel</strong>-feed. Wat anderen van jóu zien is de titel + de "Samenvatting & Cirkel Preview". Hubs doen niet mee aan cirkels (alleen solo-sites).</p>69 <h2>🔗 <%= t('ahelp.s_circles_h') %></h2> 70 <p><%- t('ahelp.s_circles_b') %></p> 71 71 </section> 72 72 73 73 <section class="hb-item"> 74 <h2>📊 Statistieken <small>(premium)</small></h2>75 <p> Beheer → <strong>Statistieken</strong>. Cookievrij gemeten. <strong>Bezoeker-dagen</strong> = unieke bezoekers per dag, opgeteld (géén aantal personen). <strong>Weergaven</strong> = home/feed- en post-loads. Beheerder-bezoeken en bots tellen niet mee. Goed voor trends; absolute aantallen met een korrel zout.</p>74 <h2>📊 <%= t('ahelp.s_stats_h') %> <small>(<%= t('ahelp.premium') %>)</small></h2> 75 <p><%- t('ahelp.s_stats_b') %></p> 76 76 </section> 77 77 78 78 <section class="hb-item"> 79 <h2>✉️ Nieuwsbrief <small>(premium)</small></h2>80 <p> Beheer → <strong>Nieuwsbrief</strong>: stel een bericht op en stuur het naar je bevestigde abonnees. Bezoekers melden zich aan via de footer of <strong>/nieuwsbrief</strong>. Versturen vereist dat e-mail (SMTP) is ingesteld.</p>79 <h2>✉️ <%= t('ahelp.s_newsletter_h') %> <small>(<%= t('ahelp.premium') %>)</small></h2> 80 <p><%- t('ahelp.s_newsletter_b') %></p> 81 81 </section> 82 82 83 83 <section class="hb-item"> 84 <h2>🔗 Link-in-bio <small>(premium)</small></h2>85 <p> Een Linktree-achtige pagina op <strong>/links</strong> met je profiel-links. De kliks per link zie je terug in Statistieken.</p>84 <h2>🔗 <%= t('ahelp.s_linkbio_h') %> <small>(<%= t('ahelp.premium') %>)</small></h2> 85 <p><%- t('ahelp.s_linkbio_b') %></p> 86 86 </section> 87 87 88 88 <section class="hb-item"> 89 <h2>▶️ Embedbare speler <small>(premium)</small></h2>90 <p> Beheer → Audio toont een kopieerbare <code><iframe></code>-code (<strong>/embed</strong>) waarmee je je speler op een andere website kunt insluiten.</p>89 <h2>▶️ <%= t('ahelp.s_embed_h') %> <small>(<%= t('ahelp.premium') %>)</small></h2> 90 <p><%- t('ahelp.s_embed_b') %></p> 91 91 </section> 92 92 93 93 <section class="hb-item"> 94 <h2>🎨 Uiterlijk (thema, foto, accent)</h2>95 <p> Beheer → <strong>Uiterlijk</strong>: stel je sitenaam, tagline, profielfoto, accentkleur en kleurpalet in, en de standaard feed-weergave (Tijdlijn of Grid).</p>94 <h2>🎨 <%= t('ahelp.s_appearance_h') %></h2> 95 <p><%- t('ahelp.s_appearance_b') %></p> 96 96 </section> 97 97 98 98 <section class="hb-item"> 99 <h2>👥 Solo- / Hub-modus</h2>100 <p> Beheer → <strong>Instellingen</strong>: <em>Solo</em> = één site (jij). <em>Hub</em> = een bedrijfs-/labelsite met meerdere makers, elk een eigen Klonkt onder <code>/user/<naam></code>. <em>Cirkel</em> = solo + federatie met andere sites.</p>99 <h2>👥 <%= t('ahelp.s_tenancy_h') %></h2> 100 <p><%- t('ahelp.s_tenancy_b') %></p> 101 101 </section> 102 102 103 103 <section class="hb-item"> 104 <h2>⭐ Premium / Patreon</h2>105 <p> Premium-functies (Hub, Statistieken, Agenda, Downloads, Perskit, Nieuwsbrief, Link-in-bio, Embed, fan-login) ontgrendel je in Beheer → Instellingen door Patreon te koppelen ($10 lifetime). Updates en de kernapp blijven altijd gratis.</p>104 <h2>⭐ <%= t('ahelp.s_premium_h') %></h2> 105 <p><%- t('ahelp.s_premium_b') %></p> 106 106 </section> 107 107 108 108 <section class="hb-item"> 109 <h2>🔑 Google-login (bezoekers)</h2>110 <p> Beheer → Instellingen → <strong>Google</strong>. Optioneel: laat bezoekers/fans met Google inloggen om te reageren. Jij beheert met je wachtwoord — Google geeft nooit beheerrechten. Vul je eigen Google-client-gegevens in (per site).</p>109 <h2>🔑 <%= t('ahelp.s_google_h') %></h2> 110 <p><%- t('ahelp.s_google_b') %></p> 111 111 </section> 112 112 113 113 <section class="hb-item"> 114 <h2>🔄 Updates</h2>115 <p> Beheer → <strong>Updates</strong> (alleen god) toont de huidige versie en of er een nieuwere is. Met "Nu bijwerken" haal je de laatste versie binnen.</p>114 <h2>🔄 <%= t('ahelp.s_updates_h') %></h2> 115 <p><%- t('ahelp.s_updates_b') %></p> 116 116 </section> 117 117 118 118 </div> 119 119 120 <p class="hb-empty" id="hb-empty" hidden> Geen onderwerpen gevonden voor je zoekopdracht.</p>120 <p class="hb-empty" id="hb-empty" hidden><%= t('ahelp.empty') %></p> 121 121 </div> 122 122 -
src/views/pages/admin-newsletter.ejs
r929d98d rbbf9d1a 7 7 %> 8 8 <section class="anl"> 9 <h1> Nieuwsbrief</h1>9 <h1><%= t('anews.title') %></h1> 10 10 11 11 <% if (msg) { %><div class="anl-msg anl-<%= msgKind %>"><%= msg %></div><% } %> 12 12 13 13 <div class="anl-stats"> 14 <div class="anl-stat"><span class="anl-n"><%= c.confirmed %></span><span class="anl-l"> bevestigd</span></div>15 <div class="anl-stat"><span class="anl-n"><%= c.pending %></span><span class="anl-l"> in afwachting</span></div>16 <div class="anl-stat"><span class="anl-n"><%= c.unsub %></span><span class="anl-l"> uitgeschreven</span></div>14 <div class="anl-stat"><span class="anl-n"><%= c.confirmed %></span><span class="anl-l"><%= t('anews.confirmed') %></span></div> 15 <div class="anl-stat"><span class="anl-n"><%= c.pending %></span><span class="anl-l"><%= t('anews.pending') %></span></div> 16 <div class="anl-stat"><span class="anl-n"><%= c.unsub %></span><span class="anl-l"><%= t('anews.unsub') %></span></div> 17 17 </div> 18 18 19 19 <% if (!smtp) { %> 20 <div class="anl-msg anl-warn"> ⚠ SMTP is nog niet ingesteld. Aanmeldingen worden wél verzameld, maar versturen kan pas als je SMTP-gegevens invult (<code>SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS / SMTP_FROM</code> in<code>.env</code>).</div>20 <div class="anl-msg anl-warn"><%= t('anews.smtp_warn_1') %> (<code>SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS / SMTP_FROM</code> <%= t('anews.smtp_warn_2') %> <code>.env</code>).</div> 21 21 <% } %> 22 22 23 <p class="anl-share"> Aanmeldlink om te delen:<a href="<%= nlSubscribeUrl %>"><%= nlSubscribeUrl %></a></p>23 <p class="anl-share"><%= t('anews.share') %> <a href="<%= nlSubscribeUrl %>"><%= nlSubscribeUrl %></a></p> 24 24 25 <% var _sendConfirm = t('anews.send_confirm', { n: c.confirmed }); %> 25 26 <form method="POST" action="<%= siteUrlBase %>/admin/newsletter/send" class="anl-form"> 26 <label> Onderwerp27 <input type="text" name="subject" required maxlength="200" value="<%= (typeof nlSubject!=='undefined')?nlSubject:'' %>" placeholder=" Nieuwe single uit!">27 <label><%= t('anews.subject') %> 28 <input type="text" name="subject" required maxlength="200" value="<%= (typeof nlSubject!=='undefined')?nlSubject:'' %>" placeholder="<%= t('anews.subject_ph') %>"> 28 29 </label> 29 <label> Bericht30 <textarea name="body" rows="10" required placeholder=" Schrijf je update…"><%= (typeof nlBody!=='undefined')?nlBody:'' %></textarea>30 <label><%= t('anews.body') %> 31 <textarea name="body" rows="10" required placeholder="<%= t('anews.body_ph') %>"><%= (typeof nlBody!=='undefined')?nlBody:'' %></textarea> 31 32 </label> 32 <button type="submit" class="anl-send" <%= smtp ? '' : 'disabled' %> onclick="return confirm(' Nieuwsbrief versturen naar <%= c.confirmed %> bevestigde abonnee(s)?')">Versturen naar <%= c.confirmed %> abonnee(s)</button>33 <button type="submit" class="anl-send" <%= smtp ? '' : 'disabled' %> onclick="return confirm('<%= _sendConfirm %>')"><%= t('anews.send_btn', { n: c.confirmed }) %></button> 33 34 </form> 34 35 35 36 <% if (hist.length) { %> 36 <h2 class="anl-h2">Verstuurd</h2> 37 <h2 class="anl-h2"><%= t('anews.sent_heading') %></h2> 38 <% var _recipients = t('anews.recipients'); %> 37 39 <ul class="anl-hist"> 38 40 <% hist.forEach(function(h){ %> 39 <li><span class="anl-hist-subj"><%= h.subject %></span><span class="anl-hist-meta"><%= h.recipient_count %> ontvanger(s)· <%= formatDateTime ? formatDateTime(h.sent_at) : h.sent_at %></span></li>41 <li><span class="anl-hist-subj"><%= h.subject %></span><span class="anl-hist-meta"><%= h.recipient_count %> <%= _recipients %> · <%= formatDateTime ? formatDateTime(h.sent_at) : h.sent_at %></span></li> 40 42 <% }); %> 41 43 </ul> -
src/views/pages/admin-playlists.ejs
r929d98d rbbf9d1a 10 10 <header class="ax-header"> 11 11 <div> 12 <h1> Playlists</h1>12 <h1><%= t('apl.title') %></h1> 13 13 <p class="ax-tagline"> 14 Canonieke playlists. Bewerk een playlist hier en de wijzigingen werken 15 door in álle posts die hem gebruiken via <code>[[playlist:<id>]]</code>. 14 <%= t('apl.tagline_pre') %> <code>[[playlist:<id>]]</code><%= t('apl.tagline_post') %> 16 15 </p> 17 16 </div> … … 21 20 <div class="ax-actions-bar"> 22 21 <button type="button" class="ax-btn ax-btn-primary" id="pl-new-btn"> 23 + Nieuwe playlist22 + <%= t('apl.new_playlist') %> 24 23 </button> 25 24 </div> … … 29 28 <div class="ax-empty"> 30 29 <div class="ax-empty-icon">📃</div> 31 <p> Nog geen playlists.</p>32 <p class="ax-empty-sub"> Maak er een via de knop hierboven, of via de 📃 knop in de post-editor.</p>30 <p><%= t('apl.none') %></p> 31 <p class="ax-empty-sub"><%= t('apl.none_sub') %></p> 33 32 </div> 34 33 </div> 35 34 <% } else { %> 35 <% 36 // Pre-fetch loop labels (defensive — keep `t` calls out of the loop body). 37 var _aplPlaylist = t('apl.pill_playlist'); 38 var _aplAlbum = t('apl.pill_album'); 39 var _aplTrack = t('apl.track'); 40 var _aplTracks = t('apl.tracks'); 41 var _aplCopy = t('apl.copy_click'); 42 var _aplEdit = t('apl.edit'); 43 var _aplDelete = t('apl.delete'); 44 %> 36 45 <ul class="ax-list"> 37 46 <% playlists.forEach(function(p) { %> … … 44 53 <div class="ax-track-title"> 45 54 <%= p.title %> 46 <span class="ax-pill"><%= p.kind === 'playlist' ? '📃 playlist' : '💿 album'%></span>55 <span class="ax-pill"><%= p.kind === 'playlist' ? '📃 ' + _aplPlaylist : '💿 ' + _aplAlbum %></span> 47 56 </div> 48 57 <div class="ax-track-sub"> 49 <span><%= p.track_count %> <%= p.track_count === 1 ? 'track' : 'tracks'%></span>58 <span><%= p.track_count %> <%= p.track_count === 1 ? _aplTrack : _aplTracks %></span> 50 59 <% if (p.artist) { %><span class="ax-track-sep">·</span><span><%= p.artist %></span><% } %> 51 60 <% if (p.year) { %><span class="ax-track-sep">·</span><span><%= p.year %></span><% } %> 52 61 </div> 53 <code class="ax-embed" data-copy="[[playlist:<%= p.id %>]]" title=" Klik om te kopiëren">[[playlist:<%= p.id %>]]</code>62 <code class="ax-embed" data-copy="[[playlist:<%= p.id %>]]" title="<%= _aplCopy %>">[[playlist:<%= p.id %>]]</code> 54 63 </div> 55 64 56 65 <div class="ax-track-actions"> 57 <button type="button" class="ax-icon-btn" data-pl-edit data-id="<%= p.id %>" aria-label=" Bewerken" title="Bewerken">✎</button>58 <button type="button" class="ax-icon-btn ax-icon-btn-danger" data-pl-delete data-id="<%= p.id %>" data-title="<%= p.title %>" aria-label=" Verwijderen" title="Verwijderen">🗑</button>66 <button type="button" class="ax-icon-btn" data-pl-edit data-id="<%= p.id %>" aria-label="<%= _aplEdit %>" title="<%= _aplEdit %>">✎</button> 67 <button type="button" class="ax-icon-btn ax-icon-btn-danger" data-pl-delete data-id="<%= p.id %>" data-title="<%= p.title %>" aria-label="<%= _aplDelete %>" title="<%= _aplDelete %>">🗑</button> 59 68 </div> 60 69 </li> … … 229 238 el.classList.add('is-copied'); 230 239 const original = el.textContent; 231 el.textContent = '✓ gekopieerd';240 el.textContent = '✓ <%= t('apl.copied') %>'; 232 241 setTimeout(() => { el.classList.remove('is-copied'); el.textContent = original; }, 1200); 233 242 } catch (_) { /* fall back to selection */ } … … 247 256 const id = btn.dataset.id; 248 257 const title = btn.dataset.title || id; 249 if (!confirm( `Playlist "${title}" verwijderen? Posts die deze playlist embedden tonen vanaf nu een placeholder.`)) return;258 if (!confirm('<%= t('apl.delete_confirm') %>'.replace('{title}', title))) return; 250 259 try { 251 260 const r = await fetch(`/admin/playlists/api/${encodeURIComponent(id)}/delete`, { … … 256 265 const j = await r.json(); 257 266 if (j.ok) location.reload(); 258 else alert(' Verwijderen mislukt: ' + (j.error || ''));267 else alert('<%= t('apl.delete_failed') %>: ' + (j.error || '')); 259 268 } catch (err) { 260 alert(' Verwijderen mislukt: ' + err.message);269 alert('<%= t('apl.delete_failed') %>: ' + err.message); 261 270 } 262 271 }); -
src/views/pages/admin-seo.ejs
r929d98d rbbf9d1a 1 1 <div class="container admin-site-edit-page"> 2 <p><a href="/admin" class="btn">← Beheer</a></p>3 <h1> SEO & vindbaarheid</h1>4 <p class="admin-tagline"> Geavanceerde SEO van <strong><%= site.title %></strong> — hoe je site in zoekmachines en bij het delen op social media verschijnt.</p>2 <p><a href="/admin" class="btn">← <%= t('aseo.back') %></a></p> 3 <h1><%= t('aseo.title') %></h1> 4 <p class="admin-tagline"><%= t('aseo.tagline_pre') %> <strong><%= site.title %></strong> <%= t('aseo.tagline_post') %></p> 5 5 6 6 <% if (typeof success !== 'undefined' && success) { %><div class="audio-flash audio-flash--ok"><%= success %></div><% } %> … … 10 10 11 11 <fieldset> 12 <legend> Indexeren</legend>12 <legend><%= t('aseo.index_legend') %></legend> 13 13 <label class="cb"> 14 14 <input type="checkbox" name="robots_index" value="1" <%= site.robots_index ? 'checked' : '' %>> 15 <span> Zoekmachines mogen deze site indexeren <small class="form-hint-inline">— uit = <code>noindex</code> + sitemap.xml verborgen</small></span>15 <span><%= t('aseo.index_label') %> <small class="form-hint-inline">— <%= t('aseo.index_hint_pre') %> <code>noindex</code> <%= t('aseo.index_hint_post') %></small></span> 16 16 </label> 17 17 </fieldset> 18 18 19 19 <fieldset> 20 <legend> Titel & omschrijving</legend>20 <legend><%= t('aseo.title_legend') %></legend> 21 21 <label> 22 <span> Titel-sjabloon <small class="form-hint-inline">— gebruik <code>{title}</code> en<code>{site}</code></small></span>22 <span><%= t('aseo.title_template') %> <small class="form-hint-inline">— <%= t('aseo.title_template_hint_pre') %> <code>{title}</code> <%= t('aseo.title_template_hint_and') %> <code>{site}</code></small></span> 23 23 <input type="text" name="title_template" value="<%= site.title_template || '{title} — {site}' %>" maxlength="200" placeholder="{title} — {site}"> 24 24 </label> 25 25 <label> 26 <span> Standaard-omschrijving <small class="form-hint-inline">— meta description / og:description als een pagina er geen heeft</small></span>27 <textarea name="default_description" rows="2" maxlength="500" placeholder=" Korte omschrijving van je site (max ~160 tekens werkt het best)"><%= site.default_description || '' %></textarea>26 <span><%= t('aseo.default_desc') %> <small class="form-hint-inline">— <%= t('aseo.default_desc_hint') %></small></span> 27 <textarea name="default_description" rows="2" maxlength="500" placeholder="<%= t('aseo.default_desc_ph') %>"><%= site.default_description || '' %></textarea> 28 28 </label> 29 29 <label> 30 <span> Canonieke basis-URL <small class="form-hint-inline">— de productie-HTTPS-URL, voorkomt dubbele-content-straf</small></span>30 <span><%= t('aseo.canonical') %> <small class="form-hint-inline">— <%= t('aseo.canonical_hint') %></small></span> 31 31 <input type="text" name="canonical" value="<%= site.canonical || '' %>" placeholder="https://jouw-domein.com" maxlength="200"> 32 32 </label> 33 33 <label> 34 <span> Auteur <small class="form-hint-inline">— meta author-tag</small></span>35 <input type="text" name="author" value="<%= site.author || '' %>" maxlength="120" placeholder=" Jouw naam">34 <span><%= t('aseo.author') %> <small class="form-hint-inline">— <%= t('aseo.author_hint') %></small></span> 35 <input type="text" name="author" value="<%= site.author || '' %>" maxlength="120" placeholder="<%= t('aseo.author_ph') %>"> 36 36 </label> 37 37 </fieldset> 38 38 39 39 <fieldset> 40 <legend> Delen op social media</legend>40 <legend><%= t('aseo.social_legend') %></legend> 41 41 <label> 42 <span> Standaard deel-afbeelding (URL) <small class="form-hint-inline">— og:image / Twitter-card; ~1200×630px</small></span>42 <span><%= t('aseo.og_image') %> <small class="form-hint-inline">— <%= t('aseo.og_image_hint') %></small></span> 43 43 <input type="text" name="og_image_default" value="<%= site.og_image_default || '' %>" placeholder="https://jouw-domein.com/share.jpg" maxlength="500"> 44 44 </label> 45 45 <label> 46 <span> Taal-locale <small class="form-hint-inline">— og:locale, bv. <code>nl_NL</code> of<code>en_US</code></small></span>46 <span><%= t('aseo.og_locale') %> <small class="form-hint-inline">— <%= t('aseo.og_locale_hint_pre') %> <code>nl_NL</code> <%= t('aseo.og_locale_hint_or') %> <code>en_US</code></small></span> 47 47 <input type="text" name="og_locale" value="<%= site.og_locale || '' %>" placeholder="nl_NL" maxlength="32"> 48 48 </label> 49 49 <label> 50 <span> Twitter / X-handle <small class="form-hint-inline">— met @</small></span>50 <span><%= t('aseo.twitter') %> <small class="form-hint-inline">— <%= t('aseo.twitter_hint') %></small></span> 51 51 <input type="text" name="twitter" value="<%= site.twitter || '' %>" placeholder="@jouwnaam" maxlength="64"> 52 52 </label> 53 53 <label> 54 <span> Facebook App-ID <small class="form-hint-inline">— fb:app_id (optioneel)</small></span>54 <span><%= t('aseo.fb_app') %> <small class="form-hint-inline">— <%= t('aseo.fb_app_hint') %></small></span> 55 55 <input type="text" name="facebook_app_id" value="<%= site.facebook_app_id || '' %>" maxlength="64"> 56 56 </label> … … 58 58 59 59 <fieldset> 60 <legend> Uitgever (JSON-LD / rich results)</legend>60 <legend><%= t('aseo.publisher_legend') %></legend> 61 61 <label> 62 <span> Type</span>62 <span><%= t('aseo.type') %></span> 63 63 <select name="schema_type"> 64 <option value="Person" <%= site.schema_type === 'Organization' ? '' : 'selected' %>> Persoon</option>65 <option value="Organization" <%= site.schema_type === 'Organization' ? 'selected' : '' %>> Organisatie / bedrijf</option>64 <option value="Person" <%= site.schema_type === 'Organization' ? '' : 'selected' %>><%= t('aseo.type_person') %></option> 65 <option value="Organization" <%= site.schema_type === 'Organization' ? 'selected' : '' %>><%= t('aseo.type_org') %></option> 66 66 </select> 67 67 </label> 68 68 <label> 69 <span> Naam <small class="form-hint-inline">— valt terug op de site-titel</small></span>69 <span><%= t('aseo.publisher_name') %> <small class="form-hint-inline">— <%= t('aseo.publisher_name_hint') %></small></span> 70 70 <input type="text" name="publisher_name" value="<%= site.publisher_name || '' %>" maxlength="200"> 71 71 </label> 72 72 <label> 73 <span> URL</span>73 <span><%= t('aseo.publisher_url') %></span> 74 74 <input type="text" name="publisher_url" value="<%= site.publisher_url || '' %>" placeholder="https://jouw-domein.com" maxlength="200"> 75 75 </label> 76 76 <label> 77 <span> Logo (URL)</span>77 <span><%= t('aseo.publisher_logo') %></span> 78 78 <input type="text" name="publisher_logo" value="<%= site.publisher_logo || '' %>" placeholder="https://jouw-domein.com/logo.png" maxlength="500"> 79 79 </label> … … 81 81 82 82 <fieldset> 83 <legend> Zoekmachine-verificatie</legend>83 <legend><%= t('aseo.verify_legend') %></legend> 84 84 <label> 85 <span> Google site-verificatie</span>85 <span><%= t('aseo.verify_google') %></span> 86 86 <input type="text" name="google_verification" value="<%= site.google_verification || '' %>" maxlength="200"> 87 87 </label> 88 88 <label> 89 <span> Bing <small class="form-hint-inline">— msvalidate.01</small></span>89 <span><%= t('aseo.verify_bing') %> <small class="form-hint-inline">— <%= t('aseo.verify_bing_hint') %></small></span> 90 90 <input type="text" name="bing_verification" value="<%= site.bing_verification || '' %>" maxlength="200"> 91 91 </label> 92 92 <label> 93 <span> Pinterest <small class="form-hint-inline">— p:domain_verify</small></span>93 <span><%= t('aseo.verify_pinterest') %> <small class="form-hint-inline">— <%= t('aseo.verify_pinterest_hint') %></small></span> 94 94 <input type="text" name="pinterest_verification" value="<%= site.pinterest_verification || '' %>" maxlength="200"> 95 95 </label> 96 96 <label> 97 <span> Yandex</span>97 <span><%= t('aseo.verify_yandex') %></span> 98 98 <input type="text" name="yandex_verification" value="<%= site.yandex_verification || '' %>" maxlength="200"> 99 99 </label> 100 100 </fieldset> 101 101 102 <button type="submit" class="btn btn-primary"> SEO opslaan</button>102 <button type="submit" class="btn btn-primary"><%= t('aseo.save') %></button> 103 103 </form> 104 104 </div> -
src/views/pages/admin-settings.ejs
r929d98d rbbf9d1a 1 1 <div class="container admin-page"> 2 <h1> Instellingen</h1>3 <p><a href="/admin" class="btn">← Beheer</a></p>2 <h1><%= t('aset.title') %></h1> 3 <p><a href="/admin" class="btn">← <%= t('aset.back_admin') %></a></p> 4 4 5 5 <% if (success) { %><div class="alert alert-success"><%= success %></div><% } %> … … 7 7 8 8 <section class="set-card"> 9 <h2> Modus</h2>9 <h2><%= t('aset.mode') %></h2> 10 10 <p class="set-help"> 11 Bepaalt hoe deze installatie werkt. Wisselen is veilig: er wordt niets verwijderd — 12 Solo verbergt alleen de multi-onderdelen en toont je primaire site. 11 <%= t('aset.mode_help') %> 13 12 </p> 14 13 … … 17 16 <input type="radio" name="tenancy" value="solo" <%= tenancy === 'solo' ? 'checked' : '' %>> 18 17 <span> 19 <strong> Solo</strong> — één site (de jouwe).20 <small> Geen gebruikers-directory, geen site-wissel. Bezoekers kunnen wel met Google inloggen om te reageren.</small>18 <strong><%= t('aset.solo') %></strong> — <%= t('aset.solo_title') %> 19 <small><%= t('aset.solo_desc') %></small> 21 20 </span> 22 21 </label> … … 25 24 <input type="radio" name="tenancy" value="hub" <%= tenancy === 'hub' ? 'checked' : '' %> <%= _hubLocked ? 'disabled' : '' %>> 26 25 <span> 27 <strong> Hub</strong> — bedrijfssite met<code>/user/</code>.28 <% if (_hubLocked) { %><span style="display:inline-block;font-size:.7rem;font-weight:700;color:var(--accent);border:1px solid var(--accent);border-radius:999px;padding:.05rem .45rem;margin-left:.3rem;vertical-align:1px;">🔒 premium</span><% } %>29 <small> Jij (admin) wijst gebruikers een eigen Klonkt Hub toe. Gesloten cirkel: geen open registratie.<% if (_hubLocked) { %> <em>Vereist premium — koppel Patreon in deze pagina.</em><% } %></small>26 <strong><%= t('aset.hub') %></strong> — <%= t('aset.hub_title') %> <code>/user/</code>. 27 <% if (_hubLocked) { %><span style="display:inline-block;font-size:.7rem;font-weight:700;color:var(--accent);border:1px solid var(--accent);border-radius:999px;padding:.05rem .45rem;margin-left:.3rem;vertical-align:1px;">🔒 <%= t('aset.premium_badge') %></span><% } %> 28 <small><%= t('aset.hub_desc') %><% if (_hubLocked) { %> <em><%= t('aset.hub_locked_note') %></em><% } %></small> 30 29 </span> 31 30 </label> … … 33 32 <input type="radio" name="tenancy" value="circle" <%= tenancy === 'circle' ? 'checked' : '' %>> 34 33 <span> 35 <strong> Cirkels</strong> — solo + federatie.36 <small> Eén eigen site die de publieke posts van andere Klonkt-sites toont. Asymmetrisch: jij bepaalt wie in jouw cirkel zit.</small>37 </span> 38 </label> 39 <button type="submit" class="btn btn-primary"> Opslaan</button>34 <strong><%= t('aset.circle') %></strong> — <%= t('aset.circle_title') %> 35 <small><%= t('aset.circle_desc') %></small> 36 </span> 37 </label> 38 <button type="submit" class="btn btn-primary"><%= t('aset.save') %></button> 40 39 </form> 41 40 </section> … … 45 44 <% if (tenancy === 'hub') { %> 46 45 <section class="set-card set-card--mode" style="margin-top:1rem"> 47 <h2> Hub-hoofdpagina</h2>46 <h2><%= t('aset.hub_home') %></h2> 48 47 <p class="set-help"> 49 De openbare hub-pagina (<code>/</code>) is generiek — van geen enkele gebruiker. 50 Hier stel je de naam en intro in. Jouw eigen Klonkt Hub blijft los bestaan onder <code>/user/</code>. 48 <%= t('aset.hub_home_help_1') %> (<code>/</code>) <%= t('aset.hub_home_help_2') %> <code>/user/</code>. 51 49 </p> 52 50 <form method="post" action="/admin/settings" class="set-form" enctype="multipart/form-data"> 53 51 <label class="set-field"> 54 <span> Naam</span>55 <input type="text" name="hub_title" maxlength="80" value="<%= hubTitle %>" placeholder=" bijv. Studio Noord">56 </label> 57 <label class="set-field"> 58 <span> Tagline</span>59 <input type="text" name="hub_tagline" maxlength="120" value="<%= hubTagline %>" placeholder=" bijv. Onafhankelijk muzieklabel">60 </label> 61 <label class="set-field"> 62 <span> Intro</span>63 <textarea name="hub_intro" maxlength="400" rows="3" placeholder=" Korte introtekst onder de titel."><%= hubIntro %></textarea>64 </label> 65 <label class="set-field"> 66 <span> Hero-afbeelding (URL) <small style="font-weight:400;color:var(--ink-muted)">— optioneel; achtergrond van de hero</small></span>52 <span><%= t('aset.name') %></span> 53 <input type="text" name="hub_title" maxlength="80" value="<%= hubTitle %>" placeholder="<%= t('aset.name_ph') %>"> 54 </label> 55 <label class="set-field"> 56 <span><%= t('aset.tagline') %></span> 57 <input type="text" name="hub_tagline" maxlength="120" value="<%= hubTagline %>" placeholder="<%= t('aset.tagline_ph') %>"> 58 </label> 59 <label class="set-field"> 60 <span><%= t('aset.intro') %></span> 61 <textarea name="hub_intro" maxlength="400" rows="3" placeholder="<%= t('aset.intro_ph') %>"><%= hubIntro %></textarea> 62 </label> 63 <label class="set-field"> 64 <span><%= t('aset.hero_image') %> <small style="font-weight:400;color:var(--ink-muted)">— <%= t('aset.hero_image_hint') %></small></span> 67 65 <input type="text" name="hub_hero_image" maxlength="300" value="<%= hubHeroImage %>" placeholder="/media/... of https://..."> 68 66 </label> 69 67 <label class="set-field"> 70 <span> …of upload een afbeelding <small style="font-weight:400;color:var(--ink-muted)">— jpg/png/webp/gif, max 5 MB; vervangt de URL hierboven</small></span>68 <span><%= t('aset.hero_upload') %> <small style="font-weight:400;color:var(--ink-muted)">— <%= t('aset.hero_upload_hint') %></small></span> 71 69 <input type="file" name="hub_hero_file" accept="image/png,image/jpeg,image/webp,image/gif"> 72 70 </label> 73 71 <label class="set-field"> 74 <span> Donkere overlay <small style="font-weight:400;color:var(--ink-muted)">— maakt de hero donkerder zodat de tekst leesbaar blijft</small></span>72 <span><%= t('aset.hero_overlay') %> <small style="font-weight:400;color:var(--ink-muted)">— <%= t('aset.hero_overlay_hint') %></small></span> 75 73 <span style="display:flex;align-items:center;gap:0.75rem"> 76 74 <input type="range" name="hub_hero_overlay" min="0" max="100" step="5" value="<%= hubHeroOverlay %>" style="flex:1" … … 81 79 <% if (hubHeroImage) { %> 82 80 <div class="set-field"> 83 <span style="font-weight:400;color:var(--ink-muted);font-size:0.9rem"> Voorbeeld (met overlay):</span>81 <span style="font-weight:400;color:var(--ink-muted);font-size:0.9rem"><%= t('aset.preview_overlay') %></span> 84 82 <div style="position:relative;width:100%;aspect-ratio:16/7;margin-top:0.4rem;border-radius:8px;overflow:hidden;border:1px solid var(--rule);background:var(--paper-2)"> 85 <img src="<%= hubHeroImage %>" alt=" Hero-voorbeeld" style="display:block;width:100%;height:100%;object-fit:cover">83 <img src="<%= hubHeroImage %>" alt="<%= t('aset.hero_preview_alt') %>" style="display:block;width:100%;height:100%;object-fit:cover"> 86 84 <div id="hero-ov-preview" style="position:absolute;inset:0;background:#000;opacity:<%= (hubHeroOverlay/100).toFixed(2) %>;pointer-events:none"></div> 87 85 </div> 88 86 </div> 89 87 <% } %> 90 <button type="submit" class="btn btn-primary"> Opslaan</button>88 <button type="submit" class="btn btn-primary"><%= t('aset.save') %></button> 91 89 </form> 92 90 </section> … … 95 93 <% if (tenancy === 'circle') { %> 96 94 <section class="set-card set-card--mode" style="margin-top:1rem"> 97 <h2>🔗 Je cirkel</h2>95 <h2>🔗 <%= t('aset.your_circle') %></h2> 98 96 <p class="set-help"> 99 Beheer welke andere Klonkt-sites je in je cirkel toont, en of jouw site in 100 cirkels van anderen mag verschijnen. Asymmetrisch: jij bepaalt wie je volgt. 97 <%= t('aset.your_circle_help') %> 101 98 </p> 102 99 <div class="set-actions"> 103 <a href="/admin/circle" class="btn btn-primary"> Beheer je cirkel→</a>100 <a href="/admin/circle" class="btn btn-primary"><%= t('aset.manage_circle') %> →</a> 104 101 </div> 105 102 </section> … … 110 107 <% if (premium && premium.enabled) { %> 111 108 <section class="set-card" style="margin-top:1rem"> 112 <h2>Premium (Patreon)</h2> 113 <p class="set-help">Ontgrendel de premium-modules (Hub-modus, Statistieken, Fan-login, 114 Prutter-DMs) met je <strong>$10-lifetime</strong> Patreon-steun. De app en alle updates 115 blijven gratis.</p> 109 <h2><%= t('aset.premium') %></h2> 110 <p class="set-help"><%= t('aset.premium_help_1') %> <strong><%= t('aset.premium_lifetime') %></strong> <%= t('aset.premium_help_2') %></p> 116 111 <% if (premium.premium) { %> 117 <p class="set-help"><strong>✓ Premium actief.</strong>118 <% if (premium.supportCents) { %> Lifetime-steun:$<%= (premium.supportCents/100).toFixed(2) %>.<% } %></p>119 <a href="/admin/patreon/disconnect" class="btn"> Patreon ontkoppelen</a>112 <p class="set-help"><strong>✓ <%= t('aset.premium_active') %></strong> 113 <% if (premium.supportCents) { %><%= t('aset.lifetime_support') %> $<%= (premium.supportCents/100).toFixed(2) %>.<% } %></p> 114 <a href="/admin/patreon/disconnect" class="btn"><%= t('aset.patreon_disconnect') %></a> 120 115 <% } else if (premium.connected) { %> 121 <p class="set-help"> Patreon gekoppeld, maar nog geen $10 lifetime<% if (premium.supportCents) { %>122 ( nu $<%= (premium.supportCents/100).toFixed(2) %>)<% } %>. Steun de campagne en koppel opnieuw.</p>123 <a href="/admin/patreon/connect" class="btn btn-primary"> Opnieuw koppelen</a>116 <p class="set-help"><%= t('aset.patreon_no_lifetime') %><% if (premium.supportCents) { %> 117 (<%= t('aset.now') %> $<%= (premium.supportCents/100).toFixed(2) %>)<% } %>. <%= t('aset.patreon_support_again') %></p> 118 <a href="/admin/patreon/connect" class="btn btn-primary"><%= t('aset.patreon_reconnect') %></a> 124 119 <% } else { %> 125 <p class="set-help"> Nog niet gekoppeld.</p>126 <a href="/admin/patreon/connect" class="btn btn-primary"> Koppel Patreon</a>120 <p class="set-help"><%= t('aset.patreon_not_connected') %></p> 121 <a href="/admin/patreon/connect" class="btn btn-primary"><%= t('aset.patreon_connect') %></a> 127 122 <% } %> 128 123 </section> … … 131 126 <%# Google-login staat op een eigen Beheer-pagina (compacte link hier). %> 132 127 <section class="set-card" style="margin-top:1rem"> 133 <h2> Google-login (bezoekers)</h2>134 <p class="set-help"> Laat bezoekers met Google inloggen om te reageren — met je eigen Google Cloud-client.135 <% if (google && google.configured) { %><strong> Status: ingesteld ✓</strong><% } else { %>Nog niet ingesteld.<% } %></p>136 <div class="set-actions"><a href="/admin/settings/google" class="btn btn-primary"> Google-login instellen→</a></div>128 <h2><%= t('aset.google_login') %></h2> 129 <p class="set-help"><%= t('aset.google_login_help') %> 130 <% if (google && google.configured) { %><strong><%= t('aset.status_set') %> ✓</strong><% } else { %><%= t('aset.not_set_yet') %><% } %></p> 131 <div class="set-actions"><a href="/admin/settings/google" class="btn btn-primary"><%= t('aset.google_login_setup') %> →</a></div> 137 132 </section> 138 133 … … 140 135 <% if (typeof premiumUnlocked === 'undefined' || premiumUnlocked) { %> 141 136 <section class="set-card" style="margin-top:1rem"> 142 <h2> Nieuwsbrief</h2>143 <p class="set-help"> Toon een <strong>aanmeldveld in de footer</strong> van je site zodat bezoekers zich op elke pagina kunnen inschrijven. (De volledige aanmeldpagina blijft op<code><%= (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '' %>/nieuwsbrief</code>.)</p>137 <h2><%= t('aset.newsletter') %></h2> 138 <p class="set-help"><%= t('aset.newsletter_help_1') %> <strong><%= t('aset.newsletter_footer_field') %></strong> <%= t('aset.newsletter_help_2') %> <code><%= (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '' %>/nieuwsbrief</code>.)</p> 144 139 <form method="post" action="/admin/settings/footer" class="set-form"> 145 140 <label class="set-toggle" style="display:flex;align-items:center;gap:.6rem;cursor:pointer"> 146 141 <input type="checkbox" name="footer_newsletter" value="1" <%= footerNewsletter ? 'checked' : '' %>> 147 <span> Aanmeldveld in de footer tonen</span>148 </label> 149 <div class="set-actions"><button type="submit" class="btn btn-primary"> Opslaan</button></div>142 <span><%= t('aset.newsletter_show_footer') %></span> 143 </label> 144 <div class="set-actions"><button type="submit" class="btn btn-primary"><%= t('aset.save') %></button></div> 150 145 </form> 151 146 </section> … … 154 149 <%# E-mail / SMTP — nodig voor nieuwsbrief versturen, notify-mails en wachtwoord-reset. %> 155 150 <section class="set-card" style="margin-top:1rem"> 156 <h2> E-mail (SMTP)</h2>157 <p class="set-help"> Nodig om de <strong>nieuwsbrief te versturen</strong>, <strong>show-notify</strong>-mails te sturen en wachtwoord-reset per mail te laten werken. Vul de gegevens van je mailprovider in (bv. je hostingmail, Gmail-app-wachtwoord, Brevo, Mailgun…).158 <% if (smtp && smtp.configured) { %><br><strong> Status: ingesteld ✓</strong><% if (smtp.fromEnv) { %> <small>(via .env)</small><% } %><% } else { %><br>Nog niet ingesteld — versturen werkt nog niet.<% } %></p>151 <h2><%= t('aset.email_smtp') %></h2> 152 <p class="set-help"><%= t('aset.smtp_help_1') %> <strong><%= t('aset.smtp_help_newsletter') %></strong><%= t('aset.smtp_help_2') %> <strong><%= t('aset.smtp_help_notify') %></strong><%= t('aset.smtp_help_3') %> 153 <% if (smtp && smtp.configured) { %><br><strong><%= t('aset.status_set') %> ✓</strong><% if (smtp.fromEnv) { %> <small>(<%= t('aset.via_env') %>)</small><% } %><% } else { %><br><%= t('aset.smtp_not_set') %><% } %></p> 159 154 160 155 <form method="post" action="/admin/settings/smtp" class="set-form"> 161 <label><span> SMTP-host</span>156 <label><span><%= t('aset.smtp_host') %></span> 162 157 <input type="text" name="smtp_host" value="<%= smtp ? smtp.host : '' %>" placeholder="smtp.jouwprovider.nl" autocomplete="off"> 163 158 </label> 164 <label><span> Poort <small style="font-weight:400">— 587 (STARTTLS) of 465 (SSL)</small></span>159 <label><span><%= t('aset.smtp_port') %> <small style="font-weight:400">— <%= t('aset.smtp_port_hint') %></small></span> 165 160 <input type="text" name="smtp_port" value="<%= (smtp && smtp.port) ? smtp.port : '' %>" placeholder="587" autocomplete="off" inputmode="numeric"> 166 161 </label> 167 <label><span> Gebruikersnaam</span>162 <label><span><%= t('aset.smtp_user') %></span> 168 163 <input type="text" name="smtp_user" value="<%= smtp ? smtp.user : '' %>" placeholder="jij@jouwprovider.nl" autocomplete="off"> 169 164 </label> 170 <label><span> Wachtwoord <% if (smtp && smtp.passSet) { %><small style="font-weight:400">— ingesteld; leeg laten = ongewijzigd</small><% } %></span>171 <input type="password" name="smtp_pass" placeholder="<%= (smtp && smtp.passSet) ? '•••••••• (ingesteld)' : 'app-wachtwoord'%>" autocomplete="off">172 </label> 173 <label><span> Afzender <small style="font-weight:400">— optioneel; standaard = gebruikersnaam</small></span>174 <input type="text" name="smtp_from" value="<%= smtp ? smtp.from : '' %>" placeholder=" Jouw Naam <jij@jouwprovider.nl>" autocomplete="off">165 <label><span><%= t('aset.smtp_pass') %> <% if (smtp && smtp.passSet) { %><small style="font-weight:400">— <%= t('aset.smtp_pass_set_hint') %></small><% } %></span> 166 <input type="password" name="smtp_pass" placeholder="<%= (smtp && smtp.passSet) ? t('aset.smtp_pass_ph_set') : t('aset.smtp_pass_ph') %>" autocomplete="off"> 167 </label> 168 <label><span><%= t('aset.smtp_from') %> <small style="font-weight:400">— <%= t('aset.smtp_from_hint') %></small></span> 169 <input type="text" name="smtp_from" value="<%= smtp ? smtp.from : '' %>" placeholder="<%= t('aset.smtp_from_ph') %>" autocomplete="off"> 175 170 </label> 176 171 <div class="set-actions"> 177 <button type="submit" class="btn btn-primary"> SMTP opslaan</button>172 <button type="submit" class="btn btn-primary"><%= t('aset.smtp_save') %></button> 178 173 <% if (smtp && (smtp.host || smtp.passSet)) { %> 179 <button type="submit" name="clear" value="1" class="btn" formnovalidate> Wissen</button>174 <button type="submit" name="clear" value="1" class="btn" formnovalidate><%= t('aset.clear') %></button> 180 175 <% } %> 181 176 </div> … … 184 179 <% if (smtp && smtp.configured) { %> 185 180 <form method="post" action="/admin/settings/smtp/test" class="set-form" style="margin-top:.8rem"> 186 <label><span> Testmail sturen naar</span>181 <label><span><%= t('aset.test_mail_to') %></span> 187 182 <input type="email" name="to" placeholder="jij@email.nl" autocomplete="off"> 188 183 </label> 189 <div class="set-actions"><button type="submit" class="btn">✉ Testmail sturen</button></div>184 <div class="set-actions"><button type="submit" class="btn">✉ <%= t('aset.send_test_mail') %></button></div> 190 185 </form> 191 186 <% } %> -
src/views/pages/admin-shows.ejs
r929d98d rbbf9d1a 6 6 %> 7 7 <section class="ash"> 8 <p><a href="/admin" class="btn">← Beheer</a></p>9 <h1> Agenda</h1>8 <p><a href="/admin" class="btn">← <%= t('ashow.back_admin') %></a></p> 9 <h1><%= t('ashow.title') %></h1> 10 10 11 11 <% if (msg) { %><div class="ash-msg ash-<%= (typeof msgKind!=='undefined')?msgKind:'' %>"><%= msg %></div><% } %> … … 15 15 <label style="display:flex;align-items:center;gap:10px;cursor:pointer;font-weight:600"> 16 16 <input type="checkbox" name="enabled" value="1" <%= agOn ? 'checked' : '' %> onchange="this.form.submit()" style="width:18px;height:18px;accent-color:var(--accent)"> 17 <span> Agenda tonen op de site <small style="font-weight:400;opacity:.7">(Agenda-knop in de balk + de agendapagina)</small></span>17 <span><%= t('ashow.show_toggle') %> <small style="font-weight:400;opacity:.7"><%= t('ashow.show_toggle_hint') %></small></span> 18 18 </label> 19 <noscript><button type="submit" class="btn"> Opslaan</button></noscript>19 <noscript><button type="submit" class="btn"><%= t('ashow.save') %></button></noscript> 20 20 </form> 21 <% if (!agOn) { %><div class="ash-msg"> De agenda staat nu <strong>uit</strong> — bezoekers zien geen Agenda-knop en de agendapagina is niet bereikbaar. Zet 'm aan om je evenementen te tonen.</div><% } %>21 <% if (!agOn) { %><div class="ash-msg"><%= t('ashow.off_notice_1') %> <strong><%= t('ashow.off') %></strong> <%= t('ashow.off_notice_2') %></div><% } %> 22 22 23 <p class="ash-note"><strong><%= nc %></strong> abonnee(s) voor evenement-aankondigingen.24 <% if (!smtp) { %><br> ⚠ SMTP niet ingesteld — evenementen worden opgeslagen, maar notify-mails kunnen pas verstuurd worden zodra je SMTP invult.<% } %></p>23 <p class="ash-note"><strong><%= nc %></strong> <%= t('ashow.subscribers') %> 24 <% if (!smtp) { %><br><%= t('ashow.smtp_warn') %><% } %></p> 25 25 26 26 <form method="POST" action="<%= siteUrlBase %>/admin/shows" class="ash-form"> 27 27 <div class="ash-grid"> 28 <label> Datum<input type="date" name="date" required></label>29 <label> Tijd (optioneel)<input type="time" name="time"></label>30 <label> Plaats<input type="text" name="city" required placeholder="Amsterdam"></label>31 <label> Land (optioneel)<input type="text" name="country" placeholder="NL"></label>32 <label class="ash-wide"> Locatie/zaal (optioneel)<input type="text" name="venue" placeholder="Paradiso"></label>33 <label class="ash-wide"> Ticket-URL (optioneel)<input type="url" name="ticket_url" placeholder="https://..."></label>34 <label class="ash-wide"> Notitie (optioneel)<input type="text" name="notes" placeholder="Support: ..."></label>28 <label><%= t('ashow.f_date') %><input type="date" name="date" required></label> 29 <label><%= t('ashow.f_time') %><input type="time" name="time"></label> 30 <label><%= t('ashow.f_city') %><input type="text" name="city" required placeholder="Amsterdam"></label> 31 <label><%= t('ashow.f_country') %><input type="text" name="country" placeholder="NL"></label> 32 <label class="ash-wide"><%= t('ashow.f_venue') %><input type="text" name="venue" placeholder="Paradiso"></label> 33 <label class="ash-wide"><%= t('ashow.f_ticket') %><input type="url" name="ticket_url" placeholder="https://..."></label> 34 <label class="ash-wide"><%= t('ashow.f_notes') %><input type="text" name="notes" placeholder="<%= t('ashow.f_notes_ph') %>"></label> 35 35 </div> 36 <label class="ash-check"><input type="checkbox" name="notify" value="1" <%= smtp ? '' : 'disabled' %>> Abonnees per e-mail op de hoogte brengen<%= smtp ? '' : ' (SMTP vereist)'%></label>37 <button type="submit" class="ash-add"> + Evenement toevoegen</button>36 <label class="ash-check"><input type="checkbox" name="notify" value="1" <%= smtp ? '' : 'disabled' %>> <%= t('ashow.notify_label') %><%= smtp ? '' : ' ' + t('ashow.smtp_required') %></label> 37 <button type="submit" class="ash-add"><%= t('ashow.add_event') %></button> 38 38 </form> 39 39 40 40 <% if (ss.length) { %> 41 <% var _delConfirm = t('ashow.del_confirm'); %> 41 42 <ul class="ash-list"> 42 43 <% ss.forEach(function(s){ %> … … 44 45 <span class="ash-when"><%= s.date %><% if (s.time) { %> <%= s.time %><% } %></span> 45 46 <span class="ash-where"><%= s.city %><% if (s.venue) { %> — <%= s.venue %><% } %></span> 46 <form method="POST" action="<%= siteUrlBase %>/admin/shows/<%= s.id %>/delete" onsubmit="return confirm(' Evenement verwijderen??')" style="margin:0">47 <form method="POST" action="<%= siteUrlBase %>/admin/shows/<%= s.id %>/delete" onsubmit="return confirm('<%= _delConfirm %>')" style="margin:0"> 47 48 <button type="submit" class="ash-del">🗑</button> 48 49 </form> … … 51 52 </ul> 52 53 <% } else { %> 53 <p class="ash-empty"> Nog geen evenementen.</p>54 <p class="ash-empty"><%= t('ashow.empty') %></p> 54 55 <% } %> 55 56 </section> -
src/views/pages/admin-site-edit.ejs
r929d98d rbbf9d1a 1 1 <div class="container admin-site-edit-page"> 2 <p><a href="/admin" class="btn">← Beheer</a></p>3 <h1><%= isNew ? 'Nieuwe site' : 'Uiterlijk: ' + site.title %></h1>2 <p><a href="/admin" class="btn">← <%= t('asite.back_admin') %></a></p> 3 <h1><%= isNew ? t('asite.title_new') : t('asite.title_edit') + ': ' + site.title %></h1> 4 4 5 5 <% if (typeof success !== 'undefined' && success) { %><div class="audio-flash audio-flash--ok"><%= success %></div><% } %> … … 9 9 10 10 <fieldset> 11 <legend> Identiteit</legend>12 <label> 13 <span> Slug (URL) <% if (!isNew) { %><em>(vast)</em><% } %></span>11 <legend><%= t('asite.identity') %></legend> 12 <label> 13 <span><%= t('asite.slug') %> <% if (!isNew) { %><em><%= t('asite.slug_fixed') %></em><% } %></span> 14 14 <span class="slug-url"> 15 15 <span class="slug-url-host" id="slug-host" 16 16 data-prefix="<%= (typeof tenancy !== 'undefined' && tenancy === 'hub') ? '/user/' : '/' %>">website.com/</span><input 17 17 type="text" name="slug" id="slug-input" class="slug-url-input" value="<%= site.slug %>" required <% if (!isNew) { %>readonly<% } %> 18 pattern="[a-z0-9_-]{2,40}" placeholder=" jouwslug"18 pattern="[a-z0-9_-]{2,40}" placeholder="<%= t('asite.slug_placeholder') %>" 19 19 autocapitalize="none" autocorrect="off" spellcheck="false"> 20 20 </span> 21 21 </label> 22 22 <label> 23 <span> Titel <small class="form-hint-inline">— getoond in de kop en als weergavenaam</small></span>23 <span><%= t('asite.field_title') %> <small class="form-hint-inline"><%= t('asite.field_title_hint') %></small></span> 24 24 <input type="text" name="title" value="<%= site.title || '' %>" required maxlength="200"> 25 25 </label> … … 28 28 <% if (user && user.role === 'god') { %> 29 29 <label> 30 <span>Eigenaar <small class="form-hint-inline">— wie deze Klonkt zelf mag beheren</small></span> 30 <span><%= t('asite.owner') %> <small class="form-hint-inline"><%= t('asite.owner_hint') %></small></span> 31 <% var _godSuffix = t('asite.owner_god_suffix'); %> 31 32 <select name="owner_id"> 32 33 <% (typeof users !== 'undefined' ? users : []).forEach(function(u){ %> 33 <option value="<%= u.id %>" <%= (site.owner_id === u.id) ? 'selected' : '' %>><%= u.username %><%= u.role === 'god' ? ' (god)': '' %></option>34 <option value="<%= u.id %>" <%= (site.owner_id === u.id) ? 'selected' : '' %>><%= u.username %><%= u.role === 'god' ? _godSuffix : '' %></option> 34 35 <% }); %> 35 36 </select> … … 37 38 <% } %> 38 39 <label> 39 <span> Tagline <small class="form-hint-inline">— korte oneliner</small></span>40 <span><%= t('asite.tagline') %> <small class="form-hint-inline"><%= t('asite.tagline_hint') %></small></span> 40 41 <input type="text" name="tagline" value="<%= site.tagline || '' %>" maxlength="200"> 41 42 </label> 42 43 <label> 43 <span> Bio / omschrijving <small class="form-hint-inline">— getoond in de profielkop en gebruikt voor SEO</small></span>44 <span><%= t('asite.bio') %> <small class="form-hint-inline"><%= t('asite.bio_hint') %></small></span> 44 45 <textarea name="description" rows="3" maxlength="500"><%= site.description || '' %></textarea> 45 46 </label> 46 47 <label> 47 <span> Profielfoto</span>48 <span><%= t('asite.profile_photo') %></span> 48 49 <div class="photo-picker" id="photo-picker"> 49 50 <div class="photo-thumb" id="photo-thumb" <%= site.profile_photo ? '' : 'data-empty' %>> … … 54 55 </div> 55 56 <div class="photo-controls"> 56 <input type="text" name="profile_photo" id="photo-url" value="<%= site.profile_photo || '' %>" placeholder=" /media/avatars/foo.jpg of https://…">57 <input type="text" name="profile_photo" id="photo-url" value="<%= site.profile_photo || '' %>" placeholder="<%= t('asite.photo_url_placeholder') %>"> 57 58 <div class="photo-actions"> 58 <button type="button" class="btn" id="photo-upload-trigger"> 📷 Uploaden</button>59 <button type="button" class="btn" id="photo-clear" <%= site.profile_photo ? '' : 'hidden' %>> Verwijder</button>59 <button type="button" class="btn" id="photo-upload-trigger"><%= t('asite.photo_upload') %></button> 60 <button type="button" class="btn" id="photo-clear" <%= site.profile_photo ? '' : 'hidden' %>><%= t('asite.photo_clear') %></button> 60 61 <small id="photo-status" class="form-hint photo-status"></small> 61 62 </div> … … 65 66 </label> 66 67 <label> 67 <span> Taal (ISO-code)</span>68 <span><%= t('asite.language') %></span> 68 69 <input type="text" name="language" value="<%= site.language || 'nl' %>" maxlength="8"> 69 70 </label> 70 71 <label class="cb"> 71 72 <input type="checkbox" name="profile_enabled" value="1" <%= site.profile_enabled ? 'checked' : '' %>> 72 <span> Profielkop tonen onder de navigatie</span>73 <span><%= t('asite.profile_enabled') %></span> 73 74 </label> 74 75 </fieldset> 75 76 76 77 <fieldset> 77 <legend> Vormgeving</legend>78 <legend><%= t('asite.appearance') %></legend> 78 79 79 80 <%# ── Curated accent picker ─────────────────────────────────────── … … 82 83 still what's stored in the DB — we just constrain WHICH hex. %> 83 84 <fieldset class="accent-grid"> 84 <legend> Accentkleur</legend>85 <legend><%= t('asite.accent_color') %></legend> 85 86 <% const _curAccent = (site.accent || '#c2410c').toLowerCase(); %> 86 87 <% accents.forEach(function(a) { %> … … 99 100 visitors with a localStorage override, still win over this default. %> 100 101 <fieldset class="theme-default"> 101 <legend> Standaardthema voor nieuwe bezoekers</legend>102 <legend><%= t('asite.theme_default') %></legend> 102 103 <% const td = site.theme_override || ''; %> 103 104 <label class="cb"> 104 105 <input type="radio" name="theme_override" value="" <%= td === '' ? 'checked' : '' %>> 105 <span> Auto (volg device-voorkeur)</span>106 <span><%= t('asite.theme_auto') %></span> 106 107 </label> 107 108 <label class="cb"> 108 109 <input type="radio" name="theme_override" value="light" <%= td === 'light' ? 'checked' : '' %>> 109 <span> Licht</span>110 <span><%= t('asite.theme_light') %></span> 110 111 </label> 111 112 <label class="cb"> 112 113 <input type="radio" name="theme_override" value="dark" <%= td === 'dark' ? 'checked' : '' %>> 113 <span> Donker</span>114 <span><%= t('asite.theme_dark') %></span> 114 115 </label> 115 116 </fieldset> 116 117 117 118 <fieldset class="palette-grid"> 118 <legend> Palet</legend>119 <legend><%= t('asite.palette') %></legend> 119 120 <% palettes.forEach(function(p) { %> 120 121 <label class="palette-swatch"> … … 137 138 138 139 <fieldset> 139 <legend> Gedrag</legend>140 <legend><%= t('asite.behavior') %></legend> 140 141 <label class="cb"> 141 142 <input type="checkbox" name="is_public" value="1" <%= site.is_public ? 'checked' : '' %>> 142 <span> Publieke site (uitvinken voor een besloten kring)</span>143 <span><%= t('asite.is_public') %></span> 143 144 </label> 144 145 <label class="cb"> 145 146 <input type="checkbox" name="robots_index" value="1" <%= site.robots_index ? 'checked' : '' %>> 146 <span> Zoekmachines mogen indexeren (sitemap.xml is verborgen indien uit)</span>147 <span><%= t('asite.robots_index') %></span> 147 148 </label> 148 149 <label class="cb"> 149 150 <input type="checkbox" name="require_login_to_comment" value="1" <%= site.require_login_to_comment ? 'checked' : '' %>> 150 <span> Inloggen vereist om te reageren</span>151 </label> 152 <label> 153 <span> Reactie-moderatie</span>151 <span><%= t('asite.require_login_comment') %></span> 152 </label> 153 <label> 154 <span><%= t('asite.comment_moderation') %></span> 154 155 <select name="comments_moderation_mode"> 155 <option value="trust" <%= (site.comments_moderation_mode || 'trust') === 'trust' ? 'selected' : '' %>> Vertrouwen (automatisch goedkeuren)</option>156 <option value="moderate" <%= site.comments_moderation_mode === 'moderate' ? 'selected' : '' %>> Modereren (eerst in de wachtrij)</option>156 <option value="trust" <%= (site.comments_moderation_mode || 'trust') === 'trust' ? 'selected' : '' %>><%= t('asite.moderation_trust') %></option> 157 <option value="moderate" <%= site.comments_moderation_mode === 'moderate' ? 'selected' : '' %>><%= t('asite.moderation_moderate') %></option> 157 158 </select> 158 159 </label> 159 160 <label class="cb"> 160 161 <input type="checkbox" name="enable_audio_player" value="1" <%= site.enable_audio_player ? 'checked' : '' %>> 161 <span> Audiospeler + embeds inschakelen</span>162 <span><%= t('asite.enable_audio') %></span> 162 163 </label> 163 164 <%# Prutter is een Hub-only premium-functie — toggle alleen tonen in Hub-modus. %> … … 165 166 <label class="cb"> 166 167 <input type="checkbox" name="enable_prutter" value="1" <%= site.enable_prutter ? 'checked' : '' %>> 167 <span> Prutter (DM’s tussen leden) inschakelen <small class="form-hint-inline">— Hub-functie, premium</small></span>168 <span><%= t('asite.enable_prutter') %> <small class="form-hint-inline"><%= t('asite.enable_prutter_hint') %></small></span> 168 169 </label> 169 170 <% } %> … … 171 172 172 173 <fieldset class="profile-links-fieldset"> 173 <legend> Social / streaming-links</legend>174 <p class="form-hint"> Getoond als merk-iconen op de profielkop. Voeg er zoveel toe als je wilt.</p>174 <legend><%= t('asite.links') %></legend> 175 <p class="form-hint"><%= t('asite.links_hint') %></p> 175 176 176 177 <div id="profile-links-rows"> … … 200 201 </template> 201 202 202 <button type="button" id="profile-link-add" class="btn"> + Link toevoegen</button>203 <button type="button" id="profile-link-add" class="btn"><%= t('asite.link_add') %></button> 203 204 </fieldset> 204 205 205 206 <fieldset> 206 <legend> Feed-weergave</legend>207 <label> 208 <span> Standaardweergave voor de homepage</span>207 <legend><%= t('asite.feed_view') %></legend> 208 <label> 209 <span><%= t('asite.feed_default') %></span> 209 210 <select name="feed_view_default"> 210 <option value="timeline" <%= (site.feed_view_default || 'timeline') === 'timeline' ? 'selected' : '' %>> Tijdlijn (chronologische lijst)</option>211 <option value="grid" <%= site.feed_view_default === 'grid' ? 'selected' : '' %>> Grid (kaarten)</option>211 <option value="timeline" <%= (site.feed_view_default || 'timeline') === 'timeline' ? 'selected' : '' %>><%= t('asite.feed_timeline') %></option> 212 <option value="grid" <%= site.feed_view_default === 'grid' ? 'selected' : '' %>><%= t('asite.feed_grid') %></option> 212 213 </select> 213 214 </label> 214 215 <label class="cb"> 215 216 <input type="checkbox" name="feed_view_switch" value="1" <%= site.feed_view_switch ? 'checked' : '' %>> 216 <span> Tijdlijn ↔ grid-schakelaar boven de feed tonen</span>217 <span><%= t('asite.feed_switch') %></span> 217 218 </label> 218 219 <label class="cb"> 219 220 <input type="checkbox" name="show_search" value="1" <%= site.show_search ? 'checked' : '' %>> 220 <span> Zoekknop in de navigatie tonen</span>221 <span><%= t('asite.show_search') %></span> 221 222 </label> 222 223 <label class="cb"> 223 224 <input type="checkbox" name="show_archive_link" value="1" <%= site.show_archive_link ? 'checked' : '' %>> 224 <span> Archief-link in de navigatie tonen</span>225 <span><%= t('asite.show_archive') %></span> 225 226 </label> 226 227 </fieldset> 227 228 228 229 <fieldset class="seo-pointer"> 229 <legend>SEO & social</legend> 230 <p>Titel-sjabloon, canonical, deel-afbeelding, verificatie-metas en meer staan 231 nu op een eigen pagina: <a href="/admin/seo">🔎 SEO & vindbaarheid</a>.</p> 230 <legend><%= t('asite.seo') %></legend> 231 <p><%= t('asite.seo_pointer') %> <a href="/admin/seo"><%= t('asite.seo_link') %></a>.</p> 232 232 </fieldset> 233 233 234 234 <fieldset> 235 <legend> Eigen CSS & HTML <em>(optioneel)</em></legend>236 <label> 237 <span> Eigen CSS (geïnjecteerd als <style> in <head>)</span>235 <legend><%= t('asite.custom_legend') %> <em><%= t('asite.optional') %></em></legend> 236 <label> 237 <span><%= t('asite.custom_css') %></span> 238 238 <textarea name="custom_css" rows="3" placeholder=".foo { color: red; }"><%= site.custom_css || '' %></textarea> 239 239 </label> 240 240 <label> 241 <span> Eigen <head>-HTML (analytics, extra meta’s)</span>241 <span><%= t('asite.custom_head') %></span> 242 242 <textarea name="custom_head_html" rows="3" placeholder="<meta name="...">"><%= site.custom_head_html || '' %></textarea> 243 243 </label> 244 244 <label> 245 <span> Eigen footer-HTML</span>245 <span><%= t('asite.custom_foot') %></span> 246 246 <textarea name="custom_foot_html" rows="3"><%= site.custom_foot_html || '' %></textarea> 247 247 </label> 248 248 </fieldset> 249 249 250 <button type="submit" class="btn btn-primary"><%= isNew ? 'Site aanmaken' : 'Wijzigingen opslaan'%></button>250 <button type="submit" class="btn btn-primary"><%= isNew ? t('asite.submit_create') : t('asite.submit_save') %></button> 251 251 </form> 252 252 </div> -
src/views/pages/admin-sites.ejs
r929d98d rbbf9d1a 1 1 <div class="container ax-page"> 2 2 3 <p><a href="/admin" class="btn">← Beheer</a></p>3 <p><a href="/admin" class="btn">← <%= t('asit2.back') %></a></p> 4 4 <header class="ax-header"> 5 5 <div> 6 <h1> Sites</h1>7 <p class="ax-tagline"> Beheer alle sites op deze installatie.</p>6 <h1><%= t('asit2.title') %></h1> 7 <p class="ax-tagline"><%= t('asit2.tagline') %></p> 8 8 </div> 9 9 </header> … … 13 13 14 14 <div class="ax-actions-bar"> 15 <a href="/admin/sites/new" class="ax-btn ax-btn-primary">+ Nieuwe site</a>15 <a href="/admin/sites/new" class="ax-btn ax-btn-primary">+ <%= t('asit2.new_site') %></a> 16 16 </div> 17 17 … … 20 20 <div class="ax-empty"> 21 21 <div class="ax-empty-icon">🌐</div> 22 <p> Nog geen sites.</p>23 <p class="ax-empty-sub"> Maak er een via de knop hierboven.</p>22 <p><%= t('asit2.empty') %></p> 23 <p class="ax-empty-sub"><%= t('asit2.empty_sub') %></p> 24 24 </div> 25 25 </div> 26 26 <% } else { %> 27 <% 28 var _s_primary = t('asit2.pill_primary'); 29 var _s_primary_t = t('asit2.pill_primary_title'); 30 var _s_public = t('asit2.pill_public'); 31 var _s_public_t = t('asit2.pill_public_title'); 32 var _s_private = t('asit2.pill_private'); 33 var _s_private_t = t('asit2.pill_private_title'); 34 var _s_noindex = t('asit2.pill_noindex'); 35 var _s_noindex_t = t('asit2.pill_noindex_title'); 36 var _s_by = t('asit2.by'); 37 var _s_t_posts = t('asit2.t_posts'); 38 var _s_l_posts = t('asit2.l_posts'); 39 var _s_t_created = t('asit2.t_created'); 40 var _s_l_created = t('asit2.l_created'); 41 var _s_makeprim = t('asit2.make_primary'); 42 var _s_makeprim_t = t('asit2.make_primary_title'); 43 var _s_makeprim_c = t('asit2.make_primary_confirm'); 44 var _s_edit = t('asit2.edit'); 45 var _s_del = t('asit2.delete'); 46 var _s_del_c = t('asit2.delete_confirm'); 47 %> 27 48 <ul class="ax-list"> 28 49 <% sites.forEach(function(s) { %> … … 37 58 <a href="/user/<%= s.slug %>"><%= s.title %></a> 38 59 <% if (s.is_primary) { %> 39 <span class="ax-pill ax-pill-primary" title=" Hoofd-/labelsite van deze installatie">★ primair</span>60 <span class="ax-pill ax-pill-primary" title="<%= _s_primary_t %>">★ <%= _s_primary %></span> 40 61 <% } %> 41 62 <% if (s.is_public) { %> 42 <span class="ax-pill ax-pill-ok" title=" Publiek zichtbaar">✓ public</span>63 <span class="ax-pill ax-pill-ok" title="<%= _s_public_t %>">✓ <%= _s_public %></span> 43 64 <% } else { %> 44 <span class="ax-pill" title=" Niet publiek">🔒 private</span>65 <span class="ax-pill" title="<%= _s_private_t %>">🔒 <%= _s_private %></span> 45 66 <% } %> 46 67 <% if (!s.robots_index) { %> 47 <span class="ax-pill" title=" Niet geïndexeerd door zoekmachines">🚫🤖 noindex</span>68 <span class="ax-pill" title="<%= _s_noindex_t %>">🚫🤖 <%= _s_noindex %></span> 48 69 <% } %> 49 70 </div> … … 52 73 <% if (s.owner_username) { %> 53 74 <span class="ax-track-sep">·</span> 54 <span> door<%= s.owner_username %></span>75 <span><%= _s_by %> <%= s.owner_username %></span> 55 76 <% } %> 56 77 </div> … … 59 80 60 81 <div class="ax-site-stats"> 61 <span class="ax-stat" title=" Aantal posts">82 <span class="ax-stat" title="<%= _s_t_posts %>"> 62 83 <span class="ax-stat-num"><%= s.post_count %></span> 63 <span class="ax-stat-label"> posts</span>84 <span class="ax-stat-label"><%= _s_l_posts %></span> 64 85 </span> 65 <span class="ax-stat" title=" Aangemaakt op">86 <span class="ax-stat" title="<%= _s_t_created %>"> 66 87 <span class="ax-stat-num ax-stat-date"><%= formatDate(s.created_at) %></span> 67 <span class="ax-stat-label"> created</span>88 <span class="ax-stat-label"><%= _s_l_created %></span> 68 89 </span> 69 90 </div> … … 72 93 <% if (!s.is_primary && (typeof canMutate === 'undefined' || canMutate)) { %> 73 94 <form method="post" action="/admin/sites/<%= s.slug %>/make-primary" style="margin:0;display:inline" 74 onsubmit="return confirm(' Deze site instellen als de primaire/hoofd-site?')">75 <button type="submit" class="ax-icon-btn" aria-label=" Maak primair" title="Maak primaire/hoofd-site">★</button>95 onsubmit="return confirm('<%= _s_makeprim_c %>')"> 96 <button type="submit" class="ax-icon-btn" aria-label="<%= _s_makeprim %>" title="<%= _s_makeprim_t %>">★</button> 76 97 </form> 77 98 <% } %> 78 <a href="/admin/sites/<%= s.slug %>/edit" class="ax-icon-btn" aria-label=" Bewerken" title="Bewerken">✎</a>99 <a href="/admin/sites/<%= s.slug %>/edit" class="ax-icon-btn" aria-label="<%= _s_edit %>" title="<%= _s_edit %>">✎</a> 79 100 <form method="post" action="/admin/sites/<%= s.slug %>/delete" class="ax-delete-form" 80 onsubmit="return confirm(' Site verwijderen? Lukt alleen als er geen posts zijn.')">81 <button type="submit" class="ax-icon-btn ax-icon-btn-danger" aria-label=" Verwijderen" title="Verwijderen">🗑</button>101 onsubmit="return confirm('<%= _s_del_c %>')"> 102 <button type="submit" class="ax-icon-btn ax-icon-btn-danger" aria-label="<%= _s_del %>" title="<%= _s_del %>">🗑</button> 82 103 </form> 83 104 </div> -
src/views/pages/admin-stats.ejs
r929d98d rbbf9d1a 1 1 <div class="container stats-page"> 2 2 <p><a href="/admin" class="btn">← Beheer</a></p> 3 <h1>Statistieken</h1> 4 <p class="stats-note">Cookievrij gemeten — geen tracking-cookies, geen toestemmingsbanner. 5 Bezoekers worden per dag geteld via een dagelijks roterende, anonieme hash (IP/browser worden 6 niet bewaard). Je eigen beheerder-bezoeken en bekende bots/crawlers tellen niet mee.</p> 3 <h1><%= t('astat.title') %></h1> 4 <p class="stats-note"><%= t('astat.intro') %></p> 7 5 8 6 <% var s = stats; var _max = Math.max(1, ...s.series.map(function(d){ return d.pageviews; })); %> 9 7 10 8 <div class="stats-cards"> 11 <div class="stat-card"><div class="stat-num"><%= s.totals.visitors %></div><div class="stat-label"> Bezoeker-dagen (<%= s.days %>d)</div></div>12 <div class="stat-card"><div class="stat-num"><%= s.totals.pageviews %></div><div class="stat-label"> Weergaven (<%= s.days %>d)</div></div>13 <div class="stat-card"><div class="stat-num"><%= s.totals.plays %></div><div class="stat-label"> Plays (totaal)</div></div>14 <div class="stat-card"><div class="stat-num"><%= s.totals.postViews %></div><div class="stat-label"> Post-weergaven (totaal)</div></div>9 <div class="stat-card"><div class="stat-num"><%= s.totals.visitors %></div><div class="stat-label"><%= t('astat.visitor_days', { n: s.days }) %></div></div> 10 <div class="stat-card"><div class="stat-num"><%= s.totals.pageviews %></div><div class="stat-label"><%= t('astat.pageviews_days', { n: s.days }) %></div></div> 11 <div class="stat-card"><div class="stat-num"><%= s.totals.plays %></div><div class="stat-label"><%= t('astat.plays_total') %></div></div> 12 <div class="stat-card"><div class="stat-num"><%= s.totals.postViews %></div><div class="stat-label"><%= t('astat.postviews_total') %></div></div> 15 13 </div> 16 14 17 15 <% if (typeof s.allTime !== 'undefined') { %> 18 <p class="stats-note"> All-time: <strong><%= s.allTime.pageviews %></strong> weergaven · <strong><%= s.allTime.visitorDays %></strong> bezoeker-dagen.</p>16 <p class="stats-note"><%= t('astat.alltime_pre') %> <strong><%= s.allTime.pageviews %></strong> <%= t('astat.alltime_mid') %> · <strong><%= s.allTime.visitorDays %></strong> <%= t('astat.alltime_post') %></p> 19 17 <% } %> 20 18 21 19 <details class="stats-help"> 22 <summary> Wat betekenen deze cijfers?</summary>20 <summary><%= t('astat.help_summary') %></summary> 23 21 <ul> 24 <li><strong> Bezoeker-dagen</strong> — het aantal unieke bezoekers <em>per dag, bij elkaar opgeteld</em>. Eén persoon die 5 dagen langskomt = 5 bezoeker-dagen. Cookieloos kan er niet over dagen heen geteld worden, dus dit is géén aantal unieke personen — het echte aantal mensen ligt (vaak fors) lager.</li>25 <li><strong> Weergaven</strong> — hoe vaak de home/feed of een post geladen is (ook bij klikken binnen de site). Andere pagina's (agenda, downloads, links) tellen hier niet in mee.</li>26 <li><strong> Plays</strong> — totaal aantal keren dat een track is gestart.</li>27 <li><strong> Post-weergaven</strong> — totaal over alle posts samen.</li>22 <li><strong><%= t('astat.help_vd_term') %></strong> — <%= t('astat.help_vd_a') %> <em><%= t('astat.help_vd_em') %></em><%= t('astat.help_vd_b') %></li> 23 <li><strong><%= t('astat.help_pv_term') %></strong> — <%= t('astat.help_pv') %></li> 24 <li><strong><%= t('astat.help_plays_term') %></strong> — <%= t('astat.help_plays') %></li> 25 <li><strong><%= t('astat.help_postviews_term') %></strong> — <%= t('astat.help_postviews') %></li> 28 26 </ul> 29 <p> Beheerder-bezoeken en bekende bots/crawlers worden overgeslagen. Het ruwe IP wordt nooit bewaard. Goed voor trends; neem absolute aantallen met een korrel zout.</p>27 <p><%= t('astat.help_footer') %></p> 30 28 </details> 31 29 32 30 <div class="stats-period"> 33 Periode:31 <%= t('astat.period') %> 34 32 <% [7,14,30,90].forEach(function(n){ %> 35 33 <a href="/admin/stats?days=<%= n %>" class="<%= s.days === n ? 'is-on' : '' %>"><%= n %>d</a> … … 39 37 <section class="stats-chart-wrap"> 40 38 <div class="stats-chart-head"> 41 <h2> Laatste <%= s.days %> dagen</h2>39 <h2><%= t('astat.last_n_days', { n: s.days }) %></h2> 42 40 <div class="stats-legend"> 43 <span class="lg lg-vis"> Bezoeker-dagen</span>44 <span class="lg lg-pv"> Weergaven</span>41 <span class="lg lg-vis"><%= t('astat.lg_visitor_days') %></span> 42 <span class="lg lg-pv"><%= t('astat.lg_pageviews') %></span> 45 43 </div> 46 44 </div> … … 51 49 var visPct = Math.round(d.visitors / _max * 100); 52 50 %> 53 <div class="sc-col" title="<%= d.day %> — <%= d.pageviews %> weergaven, <%= d.visitors %> bezoeker-dagen">51 <div class="sc-col" title="<%= t('astat.bar_title', { day: d.day, pv: d.pageviews, vd: d.visitors }) %>"> 54 52 <div class="sc-bar"> 55 53 <span class="sc-pv" style="height: <%= pvPct %>%"></span> … … 64 62 <div class="stats-tops"> 65 63 <section class="stats-top"> 66 <h2> Populairste posts</h2>64 <h2><%= t('astat.top_posts') %></h2> 67 65 <% if (!s.topPosts.length || s.topPosts[0].views === 0) { %> 68 <p class="stats-empty"> Nog geen weergaven.</p>66 <p class="stats-empty"><%= t('astat.no_views') %></p> 69 67 <% } else { %> 70 68 <ol> … … 76 74 </section> 77 75 <section class="stats-top"> 78 <h2> Meest geluisterd</h2>76 <h2><%= t('astat.most_played') %></h2> 79 77 <% if (!s.topTracks.length || s.topTracks[0].plays === 0) { %> 80 <p class="stats-empty"> Nog geen plays.</p>78 <p class="stats-empty"><%= t('astat.no_plays') %></p> 81 79 <% } else { %> 82 80 <ol> … … 91 89 <% if (typeof s.referrers !== 'undefined' && s.referrers.length) { %> 92 90 <section class="stats-top" style="margin-top:1.2rem"> 93 <h2>🌐 Bronnen (waar bezoekers vandaan komen)</h2>91 <h2>🌐 <%= t('astat.sources') %></h2> 94 92 <ol> 95 93 <% s.referrers.forEach(function(r){ %> … … 102 100 <% if (typeof linkClicks !== 'undefined' && linkClicks.length) { %> 103 101 <section class="stats-top" style="margin-top:1.2rem"> 104 <h2>🔗 Link-in-bio klikken</h2>102 <h2>🔗 <%= t('astat.linkbio_clicks') %></h2> 105 103 <ol> 106 104 <% linkClicks.forEach(function(l){ %> -
src/views/pages/admin-updates.ejs
r929d98d rbbf9d1a 1 1 <div class="container admin-page"> 2 <h1> Updates</h1>3 <p><a href="/admin" class="btn">← Beheer</a></p>2 <h1><%= t('aupd.title') %></h1> 3 <p><a href="/admin" class="btn">← <%= t('aupd.back_admin') %></a></p> 4 4 5 5 <% if (success) { %><div class="alert alert-success"><%= success %></div><% } %> … … 7 7 8 8 <section class="set-card"> 9 <h2> Versie van deze Klonkt</h2>9 <h2><%= t('aupd.version_heading') %></h2> 10 10 11 11 <div class="upd-row"> 12 <span class="upd-label"> App-versie</span>12 <span class="upd-label"><%= t('aupd.app_version') %></span> 13 13 <span class="upd-val"><%= appVersion || '—' %></span> 14 14 </div> 15 15 <div class="upd-row"> 16 <span class="upd-label"> Huidig</span>16 <span class="upd-label"><%= t('aupd.current') %></span> 17 17 <span class="upd-val"> 18 18 <% if (currentSha) { %><code><%= currentSha %></code> <span class="upd-desc"><%= currentDesc %></span> 19 <% } else { %><em> onbekend (nog niet via de update-knop bijgewerkt)</em><% } %>19 <% } else { %><em><%= t('aupd.current_unknown') %></em><% } %> 20 20 </span> 21 21 </div> 22 22 <div class="upd-row"> 23 <span class="upd-label"> Nieuwste</span>23 <span class="upd-label"><%= t('aupd.latest') %></span> 24 24 <span class="upd-val"> 25 25 <% if (latestSha) { %><code><%= latestSha %></code> <span class="upd-desc"><%= latestDesc %></span> 26 <% } else { %><em> kon de nieuwste versie niet ophalen</em><% } %>26 <% } else { %><em><%= t('aupd.latest_failed') %></em><% } %> 27 27 </span> 28 28 </div> … … 30 30 <div class="upd-status"> 31 31 <% if (!canCheck) { %> 32 <span class="upd-badge warn">⚠ Geen update-bron bereikbaar</span>32 <span class="upd-badge warn">⚠ <%= t('aupd.no_source') %></span> 33 33 <% } else if (upToDate) { %> 34 <span class="upd-badge ok">✓ Up-to-date</span>34 <span class="upd-badge ok">✓ <%= t('aupd.uptodate') %></span> 35 35 <% } else { %> 36 <span class="upd-badge new">↻ Update beschikbaar<% if (behind) { %> (<%= behind %> commit<%= behind === '1' ? '' : 's' %> achter)<% } %></span>36 <span class="upd-badge new">↻ <%= t('aupd.update_available') %><% if (behind) { %> (<%= behind === '1' ? t('aupd.behind_one', { n: behind }) : t('aupd.behind_many', { n: behind }) %>)<% } %></span> 37 37 <% } %> 38 38 </div> … … 40 40 <% if (canCheck) { %> 41 41 <form method="post" action="/admin/updates/run" class="set-form" 42 onsubmit="return confirm(' De site wordt naar de nieuwste versie gebracht en herstart kort. Doorgaan?');">42 onsubmit="return confirm('<%= t('aupd.run_confirm') %>');"> 43 43 <button type="submit" class="btn btn-primary"> 44 <%= upToDate ? '↻ Opnieuw uitrollen' : '⬆ Nu bijwerken'%>44 <%= upToDate ? '↻ ' + t('aupd.redeploy') : '⬆ ' + t('aupd.update_now') %> 45 45 </button> 46 46 </form> … … 48 48 49 49 <p class="set-help" style="margin-top:1rem"> 50 Bijwerken haalt de nieuwste code op en herstart deze site kort (~10s). Doe dit 51 rustig — er gaat niets verloren (je posts, instellingen en cirkel blijven staan). 50 <%= t('aupd.help') %> 52 51 </p> 53 52 </section> -
src/views/pages/admin-users.ejs
r929d98d rbbf9d1a 1 1 <div class="container ax-page"> 2 2 3 <p><a href="/admin" class="btn">← Beheer</a></p>3 <p><a href="/admin" class="btn">← <%= t('ausr.back') %></a></p> 4 4 <header class="ax-header"> 5 5 <div> 6 <h1> Users</h1>7 <p class="ax-tagline"> Beheer gebruikers, rollen, en verwijderingen. Rol <strong>kijker</strong> = alles bekijken (incl. Beheer), niets wijzigen — handig voor demo's.</p>6 <h1><%= t('ausr.title') %></h1> 7 <p class="ax-tagline"><%= t('ausr.tagline_a') %> <strong><%= t('ausr.role_kijker') %></strong> <%= t('ausr.tagline_b') %></p> 8 8 </div> 9 9 </header> … … 16 16 <div class="ax-empty"> 17 17 <div class="ax-empty-icon">👤</div> 18 <p> Geen gebruikers.</p>18 <p><%= t('ausr.empty') %></p> 19 19 </div> 20 20 </div> 21 21 <% } else { %> 22 <% 23 var _u_self = t('ausr.you'); 24 var _u_t_sites = t('ausr.t_sites'); 25 var _u_t_posts = t('ausr.t_posts'); 26 var _u_t_joined = t('ausr.t_joined'); 27 var _u_l_sites = t('ausr.l_sites'); 28 var _u_l_posts = t('ausr.l_posts'); 29 var _u_l_joined = t('ausr.l_joined'); 30 var _u_newklonkt = t('ausr.new_klonkt'); 31 var _u_rol = t('ausr.role_label'); 32 var _u_r_kijker = t('ausr.role_kijker'); 33 var _u_r_member = t('ausr.role_member'); 34 var _u_r_admin = t('ausr.role_admin'); 35 var _u_r_god = t('ausr.role_god'); 36 var _u_del = t('ausr.delete'); 37 %> 22 38 <ul class="ax-list"> 23 39 <% users.forEach(function(u) { %> … … 33 49 <div class="ax-user-name"> 34 50 <a href="/users/<%= encodeURIComponent(u.username) %>"><%= u.username %></a> 35 <% if (u.id === user.id) { %><span class="ax-pill"> jij</span><% } %>51 <% if (u.id === user.id) { %><span class="ax-pill"><%= _u_self %></span><% } %> 36 52 </div> 37 53 <div class="ax-user-email"><%= u.email %></div> … … 40 56 41 57 <div class="ax-user-stats"> 42 <span class="ax-stat" title=" Sites">58 <span class="ax-stat" title="<%= _u_t_sites %>"> 43 59 <span class="ax-stat-num"><%= u.site_count %></span> 44 <span class="ax-stat-label"> sites</span>60 <span class="ax-stat-label"><%= _u_l_sites %></span> 45 61 </span> 46 <span class="ax-stat" title=" Posts">62 <span class="ax-stat" title="<%= _u_t_posts %>"> 47 63 <span class="ax-stat-num"><%= u.post_count %></span> 48 <span class="ax-stat-label"> posts</span>64 <span class="ax-stat-label"><%= _u_l_posts %></span> 49 65 </span> 50 <span class="ax-stat" title=" Geregistreerd op">66 <span class="ax-stat" title="<%= _u_t_joined %>"> 51 67 <span class="ax-stat-num ax-stat-date"><%= formatDate(u.created_at) %></span> 52 <span class="ax-stat-label"> joined</span>68 <span class="ax-stat-label"><%= _u_l_joined %></span> 53 69 </span> 54 70 </div> … … 59 75 <% if (typeof tenancy !== 'undefined' && tenancy === 'hub' && canMutate) { %> 60 76 <a href="/admin/sites/new?owner=<%= u.id %>" class="ax-icon-btn" 61 aria-label=" Nieuwe Klonkt voor <%= u.username %>" title="Nieuwe Klonkt voor deze gebruiker">+🌐</a>77 aria-label="<%= t('ausr.new_klonkt_for', { name: u.username }) %>" title="<%= _u_newklonkt %>">+🌐</a> 62 78 <% } %> 63 79 <form method="post" action="/admin/users/<%= u.id %>/role" class="ax-role-form"> 64 80 <label class="ax-role"> 65 <span> Rol</span>81 <span><%= _u_rol %></span> 66 82 <select name="role" onchange="this.form.submit()" <% if (u.id === user.id || !canMutate) { %>disabled<% } %>> 67 <option value="kijker" <%= u.role === 'kijker' ? 'selected' : '' %>> kijker</option>68 <option value="member" <%= u.role === 'member' ? 'selected' : '' %>> member</option>69 <option value="admin" <%= u.role === 'admin' ? 'selected' : '' %>> admin</option>70 <option value="god" <%= u.role === 'god' ? 'selected' : '' %>> god</option>83 <option value="kijker" <%= u.role === 'kijker' ? 'selected' : '' %>><%= _u_r_kijker %></option> 84 <option value="member" <%= u.role === 'member' ? 'selected' : '' %>><%= _u_r_member %></option> 85 <option value="admin" <%= u.role === 'admin' ? 'selected' : '' %>><%= _u_r_admin %></option> 86 <option value="god" <%= u.role === 'god' ? 'selected' : '' %>><%= _u_r_god %></option> 71 87 </select> 72 88 </label> … … 75 91 <% if (u.id !== user.id && canMutate) { %> 76 92 <% var _warn = (u.post_count + u.site_count > 0) 77 ? ( 'Dit verwijdert ook hun site + ' + u.post_count + ' post(s).')93 ? (t('ausr.del_warn', { n: u.post_count }) + ' ') 78 94 : ''; %> 79 95 <form method="post" action="/admin/users/<%= u.id %>/delete" class="ax-delete-form" 80 onsubmit="return confirm(' Gebruiker <%= u.username %> verwijderen? <%= _warn %>Dit kan niet ongedaan worden.')">81 <button type="submit" class="ax-icon-btn ax-icon-btn-danger" aria-label=" Verwijderen" title="Verwijderen">🗑</button>96 onsubmit="return confirm('<%= t('ausr.del_confirm', { name: u.username }) %> <%= _warn %><%= t('ausr.del_undo') %>')"> 97 <button type="submit" class="ax-icon-btn ax-icon-btn-danger" aria-label="<%= _u_del %>" title="<%= _u_del %>">🗑</button> 82 98 </form> 83 99 <% } else { %>
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)