source: Klonkt/src/views/partials/playlist-editor.ejs@ b0ccd0d

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

Playlist-modal: de teksten door i18n

Ze stonden hard in mod/playlist-editor.js -- 34 stuks, van "Titel *" tot
"Upload mislukt". Dat viel niet op omdat het toevallig de brontaal was:
een Engelse of Duitse beheerder kreeg een Nederlandse modal midden in een
verder vertaalde pagina, en daar valt niets van om.

EIGEN GEGEVENSBLOK, niet partials/page-data.ejs. pageData() pakt met
querySelector EEN blok, en deze modal hangt onder pagina's die er zelf al
een hebben (admin-playlists, post-edit). Een tweede blok zou daar
stilzwijgend genegeerd worden -- en dan was de modal alsnog Nederlands
zonder dat er een fout verscheen.

De terugval in T() is de SLEUTELNAAM en niet de Nederlandse tekst:
ontbreekt er iets, dan zie je DAT er iets ontbreekt in plaats van een
scherm dat er half vertaald uitziet en waarvan niemand merkt welke helft.

De test sluit de hele keten -- script vraagt T('x'), partial geeft 'x'
door, i18n kent 'ple.x' in alle drie de talen. Drie schakels die elk los
kunnen kloppen terwijl het geheel stuk is, en de vergeten TAAL is de
schakel die niemand ziet die in het Nederlands werkt. Hij vond meteen een
echte misser: saveBtn.textContent = 'Opslaan...' was ik overgeslagen.

Zijn eerste versie sloeg ook alarm op een COMMENTAAR dat die woorden
noemde. Dat is nu weggefilterd: een test die op uitleg valt leert je hem
negeren.

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

  • Property mode set to 100644
File size: 11.0 KB
Line 
1<%
2// Playlist editor modal — included once per page (in admin-playlists, and in
3// post-edit when present). Exposes `window.openPlaylistEditor({ mode, id?, onSaved? })`.
4//
5// mode: 'create' or 'edit'
6// id : (edit only) playlist id to load
7// onSaved: callback receiving { id, playlist } — used by post-editor
8// to insert [[playlist:id]] at the cursor.
9//
10// The modal itself is appended on first call; subsequent calls reuse the
11// same DOM. CSS is included once. JS lives below.
12
13const _csrf = (typeof csrfToken !== 'undefined' && csrfToken) || '';
14%>
15
16<style>
17/* ─── Editor modal ─────────────────────────────────────────────── */
18.pl-modal-backdrop {
19 position: fixed; inset: 0; z-index: 9000;
20 background: color-mix(in srgb, #000 50%, transparent);
21 display: flex; align-items: center; justify-content: center;
22 padding: 1rem;
23 -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
24}
25.pl-modal {
26 background: var(--paper);
27 border: 1px solid var(--rule); border-radius: 16px;
28 width: 100%; max-width: 960px;
29 max-height: 92vh;
30 display: flex; flex-direction: column;
31 box-shadow: 0 24px 60px rgba(0, 0, 0, .35);
32 overflow: hidden;
33}
34.pl-modal-header {
35 padding: 1rem 1.25rem;
36 border-bottom: 1px solid var(--rule);
37 display: flex; align-items: center; justify-content: space-between;
38}
39.pl-modal-header h3 { margin: 0; font-size: 1.15rem; }
40.pl-modal-close {
41 background: transparent; border: 0;
42 font-size: 1.5rem; line-height: 1; cursor: pointer;
43 width: 36px; height: 36px; border-radius: 8px;
44 display: inline-flex; align-items: center; justify-content: center;
45 color: var(--ink-soft);
46}
47.pl-modal-close:hover { background: var(--paper-2); color: var(--ink); }
48.pl-modal-body {
49 flex: 1 1 auto; min-height: 0;
50 overflow: hidden;
51 padding: 1.25rem;
52 /* Become a flex container so .pl-editor-cols (grid) can flex into the
53 full available height. Without this, .pl-editor-cols had no bounded
54 parent height and inner overflow:auto never fired. */
55 display: flex; flex-direction: column;
56}
57.pl-modal-footer {
58 padding: .75rem 1.25rem;
59 border-top: 1px solid var(--rule);
60 display: flex; justify-content: flex-end; gap: .5rem;
61}
62
63.pl-editor-cols {
64 flex: 1 1 auto; min-height: 0;
65 display: grid;
66 grid-template-columns: 1fr 1fr;
67 gap: 1.5rem;
68 overflow: hidden;
69}
70@media (max-width: 720px) {
71 /* On mobile, abandon the inner-scroll-container model entirely. Two
72 scrolling boxes side-by-side don't fit, and stacked scroll boxes inside
73 a fixed modal create a scroll-trap (touch scroll easily lands on the
74 wrong layer). Instead: let the modal-body scroll the whole content,
75 and let lists flow at their natural height. */
76 .pl-editor-cols {
77 grid-template-columns: 1fr;
78 gap: 1rem;
79 height: auto;
80 overflow: visible;
81 }
82 .pl-modal-body { overflow: auto; -webkit-overflow-scrolling: touch; }
83 .pl-editor-left, .pl-editor-right { overflow: visible; min-height: 0; }
84 .pl-selected-list, .pl-available-list { overflow: visible; flex: 0 0 auto; }
85}
86
87.pl-editor-left,
88.pl-editor-right {
89 display: flex; flex-direction: column;
90 min-height: 0;
91 overflow: hidden;
92}
93
94/* Meta grid (title/artist/year/kind/cover) */
95.pl-meta-grid {
96 display: grid; gap: .5rem .75rem;
97 grid-template-columns: 1fr 1fr;
98 margin-bottom: 1rem;
99}
100.pl-field { display: flex; flex-direction: column; gap: .25rem; }
101/* `display: flex` wint van het hidden-attribuut, dus zonder deze regel doet
102 [hidden] op een .pl-field NIETS -- het veld blijft gewoon staan. De
103 uitgavevelden schakelen daarop (album versus afspeellijst). */
104.pl-field[hidden] { display: none; }
105.pl-field-full { grid-column: 1 / -1; }
106.pl-field > span {
107 font-size: .8rem; color: var(--ink-soft); font-weight: 500;
108}
109.pl-field input,
110.pl-field select {
111 width: 100%;
112 padding: .5rem .75rem;
113 border: 1px solid var(--rule); border-radius: 8px;
114 background: var(--paper); color: var(--ink);
115 font-family: var(--font-ui); font-size: .95rem;
116}
117.pl-field input:focus,
118.pl-field select:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
119
120.pl-cover-row {
121 display: flex; align-items: center; gap: .5rem;
122}
123.pl-cover-thumb {
124 width: 48px; height: 48px; border-radius: 8px;
125 background: var(--paper-2);
126 border: 1px solid var(--rule);
127 display: flex; align-items: center; justify-content: center;
128 cursor: pointer; padding: 0; overflow: hidden;
129}
130.pl-cover-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
131.pl-cover-empty { font-size: 1.25rem; color: var(--ink-muted); }
132
133/* File upload row + status — only renders in edit mode */
134.pl-cover-upload-row {
135 display: flex; align-items: center; gap: .5rem;
136 margin-top: .35rem; flex-wrap: wrap;
137}
138.pl-btn-small {
139 background: var(--paper-2); color: var(--ink);
140 border: 1px solid var(--rule); border-radius: 999px;
141 padding: .35rem .75rem; font-size: .8rem;
142 cursor: pointer; transition: border-color 120ms;
143 -webkit-tap-highlight-color: transparent;
144}
145.pl-btn-small:hover { border-color: var(--accent); }
146.pl-cover-status { font-size: .8rem; color: var(--ink-muted); }
147.pl-cover-status.is-error { color: #dc2626; }
148.pl-cover-status.is-ok { color: #2a9d5e; }
149.pl-cover-note { font-size: .75rem; color: var(--ink-muted); margin-top: .25rem; display: block; }
150
151/* Section titles */
152.pl-section-title {
153 display: flex; align-items: baseline; gap: .5rem;
154 font-size: .8rem; font-weight: 600;
155 text-transform: uppercase; letter-spacing: .05em;
156 color: var(--ink-soft);
157 padding: .5rem 0;
158 margin-bottom: .5rem;
159 border-bottom: 1px solid var(--rule);
160 flex-shrink: 0;
161}
162.pl-section-title small {
163 font-size: .8em; font-weight: 400; text-transform: none; letter-spacing: 0;
164 color: var(--ink-muted);
165}
166.pl-track-count {
167 background: var(--accent); color: #fff;
168 padding: .1em .55em; border-radius: 999px; font-size: .75rem;
169 font-weight: 600; margin-left: .25rem;
170}
171
172/* Selected + available lists */
173.pl-selected-list,
174.pl-available-list {
175 flex: 1; overflow-y: auto; min-height: 0;
176 padding-right: .25rem;
177}
178.pl-empty {
179 color: var(--ink-muted); font-style: italic;
180 padding: 1.5rem; text-align: center; font-size: .9rem;
181}
182
183/* Search input on right pane */
184.pl-search-input {
185 width: 100%;
186 padding: .5rem .75rem;
187 border: 1px solid var(--rule); border-radius: 8px;
188 background: var(--paper); color: var(--ink);
189 font-family: var(--font-ui); font-size: .9rem;
190 margin-bottom: .5rem;
191 flex-shrink: 0;
192}
193
194/* Selected row (sortable) */
195.pl-row {
196 display: flex; align-items: center; gap: .5rem;
197 padding: .5rem;
198 border: 1px solid transparent;
199 border-radius: 8px;
200 margin-bottom: .25rem;
201 background: var(--paper-2);
202 user-select: none;
203}
204.pl-row.is-dragging { opacity: 0.5; }
205.pl-row.drop-above { box-shadow: 0 -2px 0 var(--accent); }
206.pl-row.drop-below { box-shadow: 0 2px 0 var(--accent); }
207.pl-row-handle {
208 cursor: grab; padding: .25rem .5rem;
209 color: var(--ink-muted);
210 touch-action: none;
211}
212.pl-row-handle:active { cursor: grabbing; }
213.pl-row-num {
214 width: 22px; text-align: right;
215 font-family: var(--font-mono, monospace);
216 font-size: .8rem; color: var(--ink-muted);
217}
218.pl-row-cover {
219 width: 32px; height: 32px; border-radius: 4px;
220 flex-shrink: 0;
221 background: var(--paper); border: 1px solid var(--rule);
222 display: inline-flex; align-items: center; justify-content: center;
223 overflow: hidden;
224}
225.pl-row-cover img { width: 100%; height: 100%; object-fit: cover; }
226.pl-row-cover-empty { color: var(--ink-muted); font-size: .8rem; }
227.pl-row-info {
228 flex: 1; min-width: 0;
229 display: flex; flex-direction: column;
230 font-size: .85rem;
231}
232.pl-row-title {
233 font-weight: 500;
234 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
235}
236.pl-row-artist {
237 font-size: .8em; color: var(--ink-soft);
238 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
239}
240.pl-row-x {
241 background: transparent; border: 0;
242 width: 28px; height: 28px;
243 border-radius: 6px;
244 cursor: pointer;
245 color: var(--ink-muted);
246 font-size: 1.1rem;
247}
248.pl-row-x:hover { background: var(--paper); color: #dc2626; }
249
250/* Available row (clickable) */
251.pl-avail-row {
252 display: flex; align-items: center; gap: .5rem;
253 padding: .5rem; border-radius: 8px;
254 cursor: pointer; margin-bottom: .25rem;
255 transition: background 120ms ease;
256}
257.pl-avail-row:hover { background: var(--paper-2); }
258.pl-avail-row.is-added { opacity: .55; }
259.pl-avail-row.is-unplayable { opacity: .4; cursor: not-allowed; }
260.pl-avail-action {
261 width: 22px; height: 22px;
262 border-radius: 50%; display: flex; align-items: center; justify-content: center;
263 font-size: .9rem; font-weight: 600;
264 background: var(--paper-2); color: var(--ink-soft);
265 flex-shrink: 0;
266}
267.pl-avail-row.is-added .pl-avail-action {
268 background: var(--accent); color: #fff;
269}
270
271/* Cover-picker grid (sub-modal) */
272.pl-cover-picker-grid {
273 display: grid;
274 grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
275 gap: .5rem; max-height: 60vh; overflow-y: auto;
276}
277.pl-cover-picker-grid button {
278 border: 1px solid var(--rule); border-radius: 8px;
279 background: var(--paper-2); padding: 0; cursor: pointer;
280 aspect-ratio: 1; overflow: hidden;
281}
282.pl-cover-picker-grid button:hover { border-color: var(--accent); }
283.pl-cover-picker-grid img { width: 100%; height: 100%; object-fit: cover; display: block; }
284</style>
285
286<%# Het script staat in assets/js/mod/playlist-editor.js.
287
288 EIGEN GEGEVENSBLOK, niet dat van partials/page-data.ejs. pageData() pakt met
289 querySelector EEN blok, en deze modal wordt ingesloten op pagina's die er al
290 zelf een hebben (admin-playlists, post-edit) -- een tweede blok zou daar
291 stilzwijgend genegeerd worden en dan stond de hele modal weer in het
292 Nederlands. Eigen kenmerk, eigen lezer.
293
294 De teksten stonden hier tot 16-8 hard in het script. Dat viel niet op omdat
295 het toevallig de brontaal was: een Engelse of Duitse beheerder kreeg een
296 Nederlandse modal midden in een vertaalde pagina. %>
297<script type="application/json" data-playlist-editor-i18n><%- JSON.stringify({
298 titel: t('ple.titel'), artiest: t('ple.artiest'), jaar: t('ple.jaar'), type: t('ple.type'),
299 k_album: t('ple.k_album'), k_playlist: t('ple.k_playlist'),
300 uitgave: t('ple.uitgave'), mb_release: t('ple.mb_release'),
301 cover: t('ple.cover'), cover_kies: t('ple.cover_kies'), cover_url: t('ple.cover_url'),
302 tracks_in: t('ple.tracks_in'), sleep_hint: t('ple.sleep_hint'),
303 beschikbaar: t('ple.beschikbaar'), zoek: t('ple.zoek'), geen_res: t('ple.geen_res'),
304 leeg_sel: t('ple.leeg_sel'),
305 t_edit: t('ple.t_edit'), t_new: t('ple.t_new'), dialoog: t('ple.dialoog'),
306 sluiten: t('ple.sluiten'), annuleren: t('ple.annuleren'),
307 opslaan: t('ple.opslaan'), aanmaken: t('ple.aanmaken'),
308 bezig_opslaan: t('ple.bezig_opslaan'),
309 versleep: t('ple.versleep'), verwijder: t('ple.verwijder'),
310 geen_audio: t('ple.geen_audio'), e_geen_tracks: t('ple.e_geen_tracks'),
311 e_tracks: t('ple.e_tracks'), e_opslaan: t('ple.e_opslaan'), e_mislukt: t('ple.e_mislukt'),
312 e_alleen_afb: t('ple.e_alleen_afb'), bezig: t('ple.bezig'), e_upload: t('ple.e_upload'),
313}).replace(/</g, '\\u003c') %></script>
Note: See TracBrowser for help on using the repository browser.