source: Klonkt/src/views/pages/admin-playlists.ejs@ 156baa3

main
Last change on this file since 156baa3 was 156baa3, checked in by roboburr <roboburr@…>, 5 weeks ago

De chrome en vier schermen met servergegevens (shaer-bqr, stap 3e)

topnav en profile-sheet naar mod/chrome.js; admin-media, admin-videos, paid-gate,
paid-passkey en admin-playlists naar eigen modules.

TWEE MANIEREN om servergegevens bij een module te krijgen, en de keuze is niet
willekeurig:

OP HET ELEMENT waar de tekst bij EEN ding hoort. De zoekteksten staan nu op

#search-overlay, de twee themalabels op het label zelf. Dat is
ook robuuster: bij een htmx-navigatie wordt de chrome
vervangen, en dan komt de tekst gewoon mee.

VIA pageData() waar het om paginabrede gegevens gaat (csrf, slug, een blob,

een setje meldingen).

De chrome kon niet via pageData: die leeft langer dan een pagina.

DRIE FOUTEN ONDERWEG, alle drie gevangen voordat er iets stuk ging:

  1. Ik zocht de eerste </script> in het HELE bestand in plaats van die na het openende tag. paid-gate en paid-passkey hebben een <script src> erboven, dus het "blok" werd leeg en er werd een lege module geschreven. Teruggezet uit git en de zoekfunctie kreeg een offset plus een controle dat het blok niet leeg is.
  2. Een zoekterm sloot te vroeg af: de tekst van delete_failed loopt door in dezelfde JS-string (": " + err.message), dus het patroon met de sluitquote matchte niet.
  3. Een eerdere poging verving nul regels en schreef toch. Nu telt de helper de vervangingen en weigert als het er niet exact zoveel zijn als opgegeven.

Die controle op "geen enkele module bevat nog EJS" draait mee, en er is er nu een
bij die een verdacht lege module afvangt -- precies wat fout 1 opleverde.

Templates compileren, suite 565/565.

  • Property mode set to 100644
File size: 8.9 KB
Line 
1<%
2// Admin: Playlists management
3// Shows all playlists for current site, with new/edit/delete actions.
4// The editor itself is the same modal that's used from the post-editor.
5const _csrf = (typeof csrfToken !== 'undefined' && csrfToken) || '';
6%>
7<div class="container ax-page">
8
9 <p><a href="/admin" class="btn">&larr; <%= t('nav.admin') %></a></p>
10 <%- include('../partials/media-tabs', { active: 'playlists', audioOn: true }) %>
11 <header class="ax-header">
12 <div>
13 <h1><%= t('apl.title') %></h1>
14 <p class="ax-tagline">
15 <%= t('apl.tagline_pre') %> <code>[[playlist:&lt;id&gt;]]</code><%= t('apl.tagline_post') %>
16 </p>
17 </div>
18 </header>
19
20 <%# Sticky-on-mobile create button — easier reach than a header button. %>
21 <div class="ax-actions-bar">
22 <button type="button" class="ax-btn ax-btn-primary" id="pl-new-btn">
23 + <%= t('apl.new_playlist') %>
24 </button>
25 </div>
26
27 <% if (!playlists.length) { %>
28 <div class="ax-card">
29 <div class="ax-empty">
30 <div class="ax-empty-icon">📃</div>
31 <p><%= t('apl.none') %></p>
32 <p class="ax-empty-sub"><%= t('apl.none_sub') %></p>
33 </div>
34 </div>
35 <% } else { %>
36 <%
37 // Pre-fetch loop labels (defensive — keep `t` calls out of the loop body).
38 var _aplPlaylist = t('apl.pill_playlist');
39 var _aplAlbum = t('apl.pill_album');
40 var _aplTrack = t('apl.track');
41 var _aplTracks = t('apl.tracks');
42 var _aplCopy = t('apl.copy_click');
43 var _aplEdit = t('apl.edit');
44 var _aplDelete = t('apl.delete');
45 %>
46 <ul class="ax-list">
47 <% playlists.forEach(function(p) { %>
48 <li class="ax-track" data-id="<%= p.id %>">
49 <div class="ax-track-cover" <%- p.cover ? `style="background-image:url('${p.cover}'); background-size:cover; background-position:center"` : '' %>>
50 <% if (!p.cover) { %><span><%= p.kind === 'playlist' ? '📃' : '💿' %></span><% } %>
51 </div>
52
53 <div class="ax-track-meta">
54 <div class="ax-track-title">
55 <%= p.title %>
56 <span class="ax-pill"><%= p.kind === 'playlist' ? '📃 ' + _aplPlaylist : '💿 ' + _aplAlbum %></span>
57 </div>
58 <div class="ax-track-sub">
59 <span><%= p.track_count %> <%= p.track_count === 1 ? _aplTrack : _aplTracks %></span>
60 <% if (p.artist) { %><span class="ax-track-sep">·</span><span><%= p.artist %></span><% } %>
61 <% if (p.year) { %><span class="ax-track-sep">·</span><span><%= p.year %></span><% } %>
62 </div>
63 <code class="ax-embed" data-copy="[[playlist:<%= p.id %>]]" title="<%= _aplCopy %>">[[playlist:<%= p.id %>]]</code>
64 </div>
65
66 <div class="ax-track-actions">
67 <button type="button" class="ax-icon-btn" data-pl-edit data-id="<%= p.id %>" aria-label="<%= _aplEdit %>" title="<%= _aplEdit %>">✎</button>
68 <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>
69 </div>
70 </li>
71 <% }); %>
72 </ul>
73 <% } %>
74
75</div>
76
77<%# Editor modal lives in its own partial so the post-editor can include it too %>
78<%- include('../partials/playlist-editor', { csrfToken: _csrf }) %>
79
80<style>
81/* Re-use the admin-audio (.ax-*) primitives. Define them here too so this
82 page is self-contained — both pages get refreshed independently. */
83
84.ax-page { max-width: 880px; margin: 1.5rem auto 4rem; padding: 0 1rem; }
85.ax-header { display: flex; align-items: flex-start; gap: 0.75rem; margin-bottom: 1.5rem; }
86.ax-header h1 { font-family: var(--font-display, serif); font-size: 1.75rem; margin: 0 0 0.25rem; }
87.ax-tagline { color: var(--ink-muted, var(--ink-soft)); margin: 0; font-size: 0.9rem; line-height: 1.5; }
88.ax-tagline code { background: var(--paper-2); padding: 0.1em 0.4em; border-radius: 4px; font-size: 0.9em; }
89.ax-back {
90 display: inline-flex; align-items: center; justify-content: center;
91 width: 40px; height: 40px;
92 border: 1px solid var(--rule); border-radius: 8px;
93 background: var(--paper); color: var(--ink); text-decoration: none;
94 font-size: 1.1rem; flex-shrink: 0; transition: border-color 120ms;
95}
96.ax-back:hover { border-color: var(--accent); }
97
98.ax-actions-bar {
99 display: flex;
100 margin-bottom: 1rem;
101}
102
103.ax-card {
104 background: var(--paper);
105 border: 1px solid var(--rule);
106 border-radius: 12px;
107 padding: 1.25rem;
108 margin-bottom: 1rem;
109}
110
111/* ─── Buttons (same primitives as admin-audio) ─────────────────── */
112.ax-btn {
113 display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem;
114 padding: 0.6rem 1.1rem;
115 border: 1px solid var(--rule); background: var(--paper-2);
116 color: var(--ink);
117 font-family: var(--font-ui, system-ui), sans-serif;
118 font-size: 0.95rem; font-weight: 500;
119 text-decoration: none; border-radius: 8px; cursor: pointer;
120 min-height: 44px;
121 transition: background 120ms, border-color 120ms;
122 -webkit-tap-highlight-color: transparent;
123}
124.ax-btn:hover { border-color: var(--accent); }
125.ax-btn-primary { background: var(--accent); color: white; border-color: var(--accent); }
126.ax-btn-primary:hover { opacity: 0.92; }
127
128.ax-icon-btn {
129 display: inline-flex; align-items: center; justify-content: center;
130 width: 40px; height: 40px;
131 border: 1px solid var(--rule); border-radius: 8px;
132 background: var(--paper-2); color: var(--ink);
133 cursor: pointer; font-size: 1rem;
134 transition: background 120ms, border-color 120ms, color 120ms;
135}
136.ax-icon-btn:hover { border-color: var(--accent); }
137.ax-icon-btn-danger:hover { color: #dc2626; border-color: #dc2626; }
138
139/* ─── Empty state ──────────────────────────────────────────────── */
140.ax-empty { text-align: center; padding: 2rem 1rem; color: var(--ink-muted, var(--ink-soft)); }
141.ax-empty-icon { font-size: 2.5rem; margin-bottom: 0.5rem; opacity: 0.5; }
142.ax-empty p { margin: 0; }
143.ax-empty-sub { font-size: 0.85rem; margin-top: 0.5rem !important; }
144
145/* ─── List items (same as audio tracks) ───────────────────────── */
146.ax-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.6rem; }
147.ax-track {
148 display: grid;
149 grid-template-columns: 64px 1fr auto;
150 gap: 0.85rem; align-items: center;
151 padding: 0.85rem;
152 background: var(--paper);
153 border: 1px solid var(--rule);
154 border-radius: 12px;
155 transition: border-color 120ms;
156}
157.ax-track:hover { border-color: var(--accent); }
158.ax-track-cover {
159 width: 64px; height: 64px; border-radius: 8px;
160 background: var(--paper-2);
161 display: flex; align-items: center; justify-content: center;
162 font-family: var(--font-display, serif);
163 font-size: 1.5rem; color: var(--accent);
164 flex-shrink: 0;
165}
166.ax-track-meta { min-width: 0; display: flex; flex-direction: column; gap: 0.25rem; }
167.ax-track-title {
168 font-family: var(--font-display, serif);
169 font-weight: 600; font-size: 1.05rem; color: var(--ink);
170 display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap;
171}
172.ax-pill {
173 font-family: var(--font-ui);
174 font-size: 0.7rem; font-weight: 500;
175 color: var(--ink-soft);
176 background: var(--paper-2);
177 padding: 0.15em 0.55em; border-radius: 999px;
178 letter-spacing: 0;
179}
180.ax-track-sub {
181 font-size: 0.85rem;
182 color: var(--ink-muted, var(--ink-soft));
183 display: flex; flex-wrap: wrap; gap: 0.3rem; align-items: center;
184}
185.ax-track-sep { opacity: 0.5; }
186.ax-embed {
187 display: inline-block;
188 background: var(--paper-2);
189 padding: 0.15rem 0.5rem;
190 border-radius: 4px;
191 font-family: var(--font-mono, ui-monospace, monospace);
192 font-size: 0.75rem;
193 color: var(--ink-soft);
194 cursor: pointer; user-select: all;
195 margin-top: 0.15rem;
196 word-break: break-all;
197 align-self: flex-start;
198 transition: background 120ms;
199}
200.ax-embed:hover { background: var(--rule); }
201.ax-embed.is-copied { background: rgba(40,160,90,0.2); color: #2a9d5e; }
202
203.ax-track-actions { display: flex; gap: 0.4rem; flex-shrink: 0; }
204
205/* ─── Mobile ──────────────────────────────────────────────────── */
206@media (max-width: 480px) {
207 .ax-track {
208 grid-template-columns: 56px 1fr;
209 grid-template-areas: "cover meta" "actions actions";
210 gap: 0.6rem;
211 }
212 .ax-track-cover { grid-area: cover; width: 56px; height: 56px; }
213 .ax-track-meta { grid-area: meta; }
214 .ax-track-actions {
215 grid-area: actions;
216 justify-content: flex-end;
217 border-top: 1px solid var(--rule);
218 padding-top: 0.5rem;
219 margin-top: 0.25rem;
220 }
221}
222</style>
223
224<%# Het script staat in assets/js/mod/admin-playlists.js; de gegevens via partials/page-data.ejs (shaer-bqr). %>
225<%- include('../partials/page-data', { pageData: { csrf: _csrf, copied: t('apl.copied'), delConfirm: t('apl.delete_confirm'), delFailed: t('apl.delete_failed') } }) %>
Note: See TracBrowser for help on using the repository browser.