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

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

Beheer: audio en playlists ook op de gedeelde ax-romp

Mijn regressie van de vorige commit: ik haalde de opmaak van de tabbalk
uit media-tabs.ejs en zette hem in de partial, maar audio en playlists
nemen die partial niet op. Hun tabbalk stond er dus wel in de markup en
had geen enkele regel CSS -- vijf kale links, en de pagina zat er niet
meer onder.

Beide pagina's nemen de partial nu op, boven hun eigen style-blok zodat
wat daar blijft staan later komt en dus wint.

En daarmee kon het ontdubbelen alsnog, maar nu met een echte ontleding:
scripts/admin-css.mjs leest de tekens een voor een en kent commentaar,
tekenreeksen en geneste blokken. Dat was nodig -- mijn regex van
vanochtend telde @media-varianten als losse regels en commentaar als
selector, en zat er voor playlists 7 regels naast. Twee fouten zaten ook
in de eerste versie van dit script: /* Mobiel */ voor een @media maakte
het blok onherkenbaar, en geneste regels droegen posities uit hun
binnentekst -- daarmee had het weghalen de verkeerde bytes geknipt.

Weg: 27 regels uit admin-audio, 34 uit admin-playlists. Wat afwijkt
blijft staan en wordt gemeld (audio heeft 10 eigen varianten, playlists
2).

scripts/admin-css-gelijk.mjs is het bewijs: hij rendert elke pagina nu
en zoals hij was, rekent per (media-context, selector) uit wat er
uiteindelijk geldt, en legt de twee naast elkaar. Uitkomst over alle
vijf: geen regel verdwenen, geen regel veranderd, en op audio en
playlists 21 resp. 22 regels erbij -- de tabbalk voorop.

Dat audio en playlists nu ook .ax-grid en .ax-tile meekrijgen die ze niet
gebruiken, is de prijs van een gedeelde partial. Een paar honderd bytes.

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

  • Property mode set to 100644
File size: 4.6 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<%# De gedeelde ax-*-primitieven. MOET boven het eigen style-blok van deze
8 pagina staan: wat hieronder blijft staan wijkt bewust af en hoort dus
9 later te komen, anders wint de partial ervan. %>
10<%- include('../partials/admin-styles') %>
11<div class="container ax-page">
12
13 <p><a href="/admin" class="btn">&larr; <%= t('nav.admin') %></a></p>
14 <%- include('../partials/media-tabs', { active: 'playlists', audioOn: true }) %>
15 <header class="ax-header">
16 <div>
17 <h1><%= t('apl.title') %></h1>
18 <p class="ax-tagline">
19 <%= t('apl.tagline_pre') %> <code>[[playlist:&lt;id&gt;]]</code><%= t('apl.tagline_post') %>
20 </p>
21 </div>
22 </header>
23
24 <%# Sticky-on-mobile create button — easier reach than a header button. %>
25 <div class="ax-actions-bar">
26 <button type="button" class="ax-btn ax-btn-primary" id="pl-new-btn">
27 + <%= t('apl.new_playlist') %>
28 </button>
29 </div>
30
31 <% if (!playlists.length) { %>
32 <div class="ax-card">
33 <div class="ax-empty">
34 <div class="ax-empty-icon">📃</div>
35 <p><%= t('apl.none') %></p>
36 <p class="ax-empty-sub"><%= t('apl.none_sub') %></p>
37 </div>
38 </div>
39 <% } else { %>
40 <%
41 // Pre-fetch loop labels (defensive — keep `t` calls out of the loop body).
42 var _aplPlaylist = t('apl.pill_playlist');
43 var _aplAlbum = t('apl.pill_album');
44 var _aplTrack = t('apl.track');
45 var _aplTracks = t('apl.tracks');
46 var _aplCopy = t('apl.copy_click');
47 var _aplEdit = t('apl.edit');
48 var _aplDelete = t('apl.delete');
49 %>
50 <ul class="ax-list">
51 <% playlists.forEach(function(p) { %>
52 <li class="ax-track" data-id="<%= p.id %>">
53 <div class="ax-track-cover" <%- p.cover ? `style="background-image:url('${p.cover}'); background-size:cover; background-position:center"` : '' %>>
54 <% if (!p.cover) { %><span><%= p.kind === 'playlist' ? '📃' : '💿' %></span><% } %>
55 </div>
56
57 <div class="ax-track-meta">
58 <div class="ax-track-title">
59 <%= p.title %>
60 <span class="ax-pill"><%= p.kind === 'playlist' ? '📃 ' + _aplPlaylist : '💿 ' + _aplAlbum %></span>
61 </div>
62 <div class="ax-track-sub">
63 <span><%= p.track_count %> <%= p.track_count === 1 ? _aplTrack : _aplTracks %></span>
64 <% if (p.artist) { %><span class="ax-track-sep">·</span><span><%= p.artist %></span><% } %>
65 <% if (p.year) { %><span class="ax-track-sep">·</span><span><%= p.year %></span><% } %>
66 </div>
67 <code class="ax-embed" data-copy="[[playlist:<%= p.id %>]]" title="<%= _aplCopy %>">[[playlist:<%= p.id %>]]</code>
68 </div>
69
70 <div class="ax-track-actions">
71 <button type="button" class="ax-icon-btn" data-pl-edit data-id="<%= p.id %>" aria-label="<%= _aplEdit %>" title="<%= _aplEdit %>">✎</button>
72 <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>
73 </div>
74 </li>
75 <% }); %>
76 </ul>
77 <% } %>
78
79</div>
80
81<%# Editor modal lives in its own partial so the post-editor can include it too %>
82<%- include('../partials/playlist-editor', { csrfToken: _csrf }) %>
83
84<style>
85/* Re-use the admin-audio (.ax-*) primitives. Define them here too so this
86 page is self-contained — both pages get refreshed independently. */
87
88.ax-page { max-width: 880px; margin: 1.5rem auto 4rem; padding: 0 1rem; }
89.ax-btn-primary:hover { opacity: 0.92; }
90.ax-track-cover {
91 width: 64px; height: 64px; border-radius: 8px;
92 background: var(--paper-2);
93 display: flex; align-items: center; justify-content: center;
94 font-family: var(--font-display, serif);
95 font-size: 1.5rem; color: var(--accent);
96 flex-shrink: 0;
97}
98
99/* ─── Mobile ──────────────────────────────────────────────────── */
100@media (max-width: 480px) {
101}
102</style>
103
104<%# Het script staat in assets/js/mod/admin-playlists.js; de gegevens via partials/page-data.ejs (shaer-bqr). %>
105<%- 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.