Changeset 749605a in Klonkt for src/views/pages


Ignore:
Timestamp:
08/16/2026 03:06:18 PM (4 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
a5caa73
Parents:
a5515b4 (diff), 9026de5 (diff)
Note: this is a merge changeset, the changes displayed below correspond to the merge itself.
Use the (diff) links above to see all the changes relative to each parent.
Message:

Bibliotheek en album-federatie terug op main (shaer-0nh, shaer-756s)

Achttien commits, in drie brokken.

DE BIBLIOTHEEK. Een Library-skelet met een eigen volgerssoort: wie de
bibliotheek volgt krijgt de muziek en met opzet niet de gewone posts, en
die staan daarom in een eigen tabel -- een vlag die iemand vergeet te
filteren bezorgt stilletjes de Krant aan mensen die er niet om vroegen.
Daarna wat er nodig bleek om werkelijk gelezen te worden: audience
(zonder dat gaf hun create() een KeyError en dus een 500 op onze URL),
de ongeordende collectievorm, en paginering op elke collectie die first
belooft -- die laatste vond meteen dat featured hetzelfde gat had.

HET VOCABULAIRE. fw:track in de kleine vorm die Emissary ook stuurt,
artist_credit met de site-actor als Artist (mogelijk geworden door de
MusicBrainz-koppeling), en het album als ingesloten fw:Album met
released, musicbrainzId en een eigen artist_credit. Daarvoor kwamen er
twee kolommen op playlists, alleen bij kind='album' en afgedwongen in
PlaylistService -- niet in het scherm, want de API ligt open.

HET BEHEER. De vijf mediapagina's stonden op drie verschillende
ontwerpen; nu op een gedeelde ax-romp, met de primitieven op een plek in
plaats van in acht kopieen. De playlist-modal ging alsnog door i18n: die
teksten stonden hard in het script, wat niet opviel omdat het toevallig
de brontaal was.

WAT ER NIET IN ZIT: bitrate en size top-level als fw:. Dat is bewust
(shaer-3f8a). Hun eigen outbox zet ze op de Link, net als wij, en het
wordt een gesprek in plaats van een aanpassing -- briefing in
~/bewaard/funkwhale-briefing-en.md.

Gereedschap dat meekomt: scripts/funkwhale-gat.mjs meet het gat naar hun
ingest, admin-css.mjs en admin-css-gelijk.mjs ontleden en vergelijken de
beheer-CSS, render-admin-views.mjs rendert de views gevuld en leeg. Die
laatste drie omdat de testsuite geen enkele view aanraakt.

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

Location:
src/views/pages
Files:
1 added
4 edited

Legend:

Unmodified
Added
Removed
  • src/views/pages/admin-audio.ejs

    ra5515b4 r749605a  
     1<%# De gedeelde ax-*-primitieven. MOET boven het eigen style-blok van deze
     2    pagina staan: wat hieronder blijft staan wijkt bewust af en hoort dus
     3    later te komen, anders wint de partial ervan. %>
     4<%- include('../partials/admin-styles') %>
    15<div class="container ax-page">
    26
    3   <p><a href="/admin" class="btn">&larr; <%= t('nav.admin') %></a></p>
     7  <%- include('../partials/admin-back') %>
    48  <%- include('../partials/media-tabs', { active: 'audio', audioOn: true }) %>
    59  <header class="ax-header">
     
    150154  padding: 0 1rem;
    151155}
    152 .ax-header {
    153   display: flex; align-items: flex-start; gap: 0.75rem;
    154   margin-bottom: 1.5rem;
    155 }
    156 .ax-header h1 {
    157   font-family: var(--font-display, serif);
    158   font-size: 1.75rem;
    159   margin: 0 0 0.25rem;
    160 }
    161 .ax-tagline {
    162   color: var(--ink-muted, var(--ink-soft));
    163   margin: 0;
    164   font-size: 0.9rem;
    165   line-height: 1.5;
    166 }
    167 .ax-tagline code {
    168   background: var(--paper-2);
    169   padding: 0.1em 0.4em;
    170   border-radius: 4px;
    171   font-size: 0.9em;
    172 }
    173 .ax-back {
    174   display: inline-flex; align-items: center; justify-content: center;
    175   width: 40px; height: 40px;
    176   border: 1px solid var(--rule);
    177   border-radius: 8px;
    178   background: var(--paper);
    179   color: var(--ink);
    180   text-decoration: none;
    181   font-size: 1.1rem;
    182   flex-shrink: 0;
    183   transition: border-color 120ms;
    184 }
    185 .ax-back:hover { border-color: var(--accent); }
    186 
    187 /* ─── Flash messages ───────────────────────────────────────────── */
    188 .ax-flash {
    189   padding: 0.75rem 1rem;
    190   border-radius: 8px;
    191   margin-bottom: 1rem;
    192   font-size: 0.9rem;
    193 }
    194 .ax-flash-ok  { background: rgba(40,160,90,.15); color: #2a9d5e; border: 1px solid rgba(40,160,90,.3); }
    195 .ax-flash-err { background: rgba(200,60,60,.15); color: #c33;    border: 1px solid rgba(200,60,60,.3); }
    196 
    197 /* ─── Card sections ────────────────────────────────────────────── */
    198 .ax-card {
    199   background: var(--paper);
    200   border: 1px solid var(--rule);
    201   border-radius: 12px;
    202   padding: 1.25rem;
    203   margin-bottom: 1rem;
    204 }
    205156.ax-card-title {
    206157  font-family: var(--font-display, serif);
     
    303254.ax-form-actions[hidden] { display: none; }
    304255
    305 /* ─── Buttons ──────────────────────────────────────────────────── */
    306 .ax-btn {
    307   display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem;
    308   padding: 0.55rem 1rem;
    309   border: 1px solid var(--rule);
    310   background: var(--paper-2);
    311   color: var(--ink);
    312   font-family: var(--font-ui, system-ui), sans-serif;
    313   font-size: 0.9rem; font-weight: 500;
    314   text-decoration: none;
    315   border-radius: 6px;
    316   cursor: pointer;
    317   min-height: 40px;
    318   transition: background 120ms, border-color 120ms;
    319   -webkit-tap-highlight-color: transparent;
    320 }
    321 .ax-btn:hover { border-color: var(--accent); }
    322 .ax-btn-secondary { background: var(--paper-2); }
    323 .ax-btn-primary {
    324   background: var(--accent); color: white;
    325   border-color: var(--accent);
    326 }
    327 .ax-btn-primary:hover { opacity: 0.92; border-color: var(--accent); }
    328 
    329 /* Tiny icon-only button for row actions */
    330 .ax-icon-btn {
    331   display: inline-flex; align-items: center; justify-content: center;
    332   width: 40px; height: 40px;
    333   border: 1px solid var(--rule);
    334   background: var(--paper-2);
    335   color: var(--ink);
    336   border-radius: 8px;
    337   cursor: pointer;
    338   font-size: 1rem;
    339   transition: background 120ms, border-color 120ms, color 120ms;
    340 }
    341 .ax-icon-btn:hover { border-color: var(--accent); }
    342 .ax-icon-btn-danger:hover { color: #dc2626; border-color: #dc2626; }
    343 
    344256.ax-track-play.is-playing {
    345257  border-color: var(--accent);
     
    348260}
    349261.ax-track-play-icon { font-size: 0.85rem; line-height: 1; }
    350 
    351 /* ─── Empty state ──────────────────────────────────────────────── */
    352 .ax-empty {
    353   text-align: center;
    354   padding: 2rem 1rem;
    355   color: var(--ink-muted, var(--ink-soft));
    356 }
    357 .ax-empty-icon {
    358   font-size: 2.5rem;
    359   font-family: var(--font-display, serif);
    360   color: var(--ink-soft);
    361   opacity: 0.4;
    362   margin-bottom: 0.5rem;
    363 }
    364 .ax-empty p { margin: 0; }
    365 
    366 /* ─── Track list (card per row) ────────────────────────────────── */
    367 .ax-list {
    368   list-style: none; padding: 0; margin: 0;
    369   display: flex; flex-direction: column;
    370   gap: 0.6rem;
    371 }
    372 .ax-track {
    373   display: grid;
    374   grid-template-columns: 56px 1fr auto;
    375   gap: 0.85rem;
    376   align-items: center;
    377   padding: 0.75rem;
    378   background: var(--paper-2);
    379   border: 1px solid var(--rule);
    380   border-radius: 10px;
    381   transition: border-color 120ms;
    382 }
    383 .ax-track:hover { border-color: var(--accent); }
    384 .ax-track-cover {
    385   width: 56px; height: 56px;
    386   border-radius: 6px;
    387   object-fit: cover;
    388   background: var(--paper);
    389   display: flex; align-items: center; justify-content: center;
    390   font-family: var(--font-display, serif);
    391   font-size: 1.5rem;
    392   color: var(--accent);
    393   flex-shrink: 0;
    394 }
    395262.ax-track-cover-empty { /* span variant */ }
    396 .ax-track-meta {
    397   min-width: 0;  /* allow text-overflow on long titles */
    398   display: flex; flex-direction: column; gap: 0.2rem;
    399 }
    400 .ax-track-title {
    401   font-weight: 600;
    402   font-size: 0.95rem;
    403   color: var(--ink);
    404   overflow: hidden;
    405   text-overflow: ellipsis;
    406   white-space: nowrap;
    407 }
    408 .ax-track-sub {
    409   font-size: 0.8rem;
    410   color: var(--ink-muted, var(--ink-soft));
    411   display: flex; flex-wrap: wrap; gap: 0.3rem; align-items: center;
    412 }
    413 .ax-track-sep { opacity: 0.5; }
    414 .ax-embed {
    415   display: inline-block;
    416   background: var(--paper);
    417   padding: 0.15rem 0.5rem;
    418   border-radius: 4px;
    419   font-family: var(--font-mono, ui-monospace, monospace);
    420   font-size: 0.75rem;
    421   color: var(--ink-soft);
    422   cursor: pointer;
    423   user-select: all;
    424   margin-top: 0.15rem;
    425   word-break: break-all;
    426   align-self: flex-start;
    427   transition: background 120ms;
    428 }
    429 .ax-embed:hover { background: var(--rule); }
    430 .ax-embed.is-copied { background: rgba(40,160,90,0.2); color: #2a9d5e; }
    431 
    432 .ax-track-actions {
    433   display: flex; gap: 0.4rem;
    434   flex-shrink: 0;
    435 }
    436263.ax-track-delete { display: inline; }
    437264
    438265/* ─── Mobile tweaks ───────────────────────────────────────────── */
    439266@media (max-width: 480px) {
    440   .ax-track {
    441     grid-template-columns: 48px 1fr;
    442     grid-template-areas: "cover meta" "actions actions";
    443     gap: 0.6rem;
    444   }
    445   .ax-track-cover { grid-area: cover; width: 48px; height: 48px; }
    446   .ax-track-meta  { grid-area: meta; }
    447   .ax-track-actions {
    448     grid-area: actions;
    449     justify-content: flex-end;
    450     border-top: 1px solid var(--rule);
    451     padding-top: 0.5rem;
    452     margin-top: 0.25rem;
    453   }
    454267}
    455268
  • src/views/pages/admin-media.ejs

    ra5515b4 r749605a  
    1 <%# Beheer → Media: image library + cleanup (the Audio half links out to /admin/audio). %>
    2 <div class="admin-wrap" style="max-width:980px;margin:0 auto;padding:1.5rem 1rem;">
    3   <a href="/admin" class="btn" style="margin-bottom:1rem;display:inline-block;">← <%= t('nav.admin') %></a>
    4   <h1 style="font-family:var(--display,serif);margin:.2rem 0 1rem;"><%= t('admin.t_media') %></h1>
     1<%# Beheer -> Media: de afbeeldingenbibliotheek met opruiming.
     2    De audio-helft zit onder /admin/audio, als eigen tab.
    53
     4    Deze pagina stond volledig op inline styles en viel daarmee buiten het
     5    ax-*-systeem waar audio en playlists al op zaten. Nu op dezelfde romp:
     6    zelfde breedte, zelfde kop, tabs op dezelfde plek. %>
     7<%- include('../partials/admin-styles') %>
     8<div class="container ax-page">
     9
     10  <%- include('../partials/admin-back') %>
    611  <%- include('../partials/media-tabs', { active: 'images', audioOn: audioOn }) %>
     12  <header class="ax-header">
     13    <div>
     14      <h1><%= t('admin.t_media') %></h1>
     15      <p class="ax-tagline">
     16        <%= items.length %> <%= t('admin.media_count') %><% if (unusedCount) { %> &middot; <strong><%= unusedCount %></strong> <%= t('admin.media_unused') %><% } %>
     17      </p>
     18    </div>
     19  </header>
    720
    8   <% if (success) { %><p style="background:rgba(60,160,90,.15);border:1px solid rgba(60,160,90,.4);padding:.5rem .8rem;border-radius:8px;"><%= success %></p><% } %>
     21  <% if (success) { %><div class="ax-flash ax-flash-ok"><%= success %></div><% } %>
    922
    10   <div style="display:flex;justify-content:space-between;align-items:center;gap:1rem;flex-wrap:wrap;margin-bottom:1rem;">
    11     <p style="margin:0;opacity:.75;">
    12       <%= items.length %> <%= t('admin.media_count') %><% if (unusedCount) { %> · <strong><%= unusedCount %></strong> <%= t('admin.media_unused') %><% } %>
    13     </p>
    14     <% if (unusedCount) { %>
    15     <button type="button" id="media-cleanup" class="btn" style="background:#b4452e;color:#fff;border-color:#b4452e;">🗑 <%= t('admin.media_cleanup') %> (<%= unusedCount %>)</button>
    16     <% } %>
     23  <% if (unusedCount) { %>
     24  <div class="ax-actions-bar">
     25    <button type="button" id="media-cleanup" class="ax-btn ax-btn-danger">&#128465; <%= t('admin.media_cleanup') %> (<%= unusedCount %>)</button>
    1726  </div>
     27  <% } %>
    1828
    1929  <% if (!items.length) { %>
    20     <p style="opacity:.7;"><%= t('admin.media_empty') %></p>
     30    <div class="ax-empty">
     31      <div class="ax-empty-icon">&#128247;</div>
     32      <p><%= t('admin.media_empty') %></p>
     33    </div>
    2134  <% } else { %>
    22   <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:1rem;">
     35  <div class="ax-grid">
    2336    <% items.forEach(function(it){ %>
    24       <div class="media-card" data-file="<%= it.file %>" style="border:1px solid var(--rule,rgba(128,128,128,.25));border-radius:10px;overflow:hidden;background:var(--paper-2,rgba(128,128,128,.06));">
    25         <div style="position:relative;aspect-ratio:1;background:#0a0e1a;">
    26           <img src="<%= thumb(it.url, 320) %>" alt="" loading="lazy" decoding="async" style="width:100%;height:100%;object-fit:cover;display:block;">
    27           <% if (it.hasVideo) { %><span title="animated cover" style="position:absolute;top:6px;right:6px;background:rgba(0,0,0,.6);border-radius:6px;padding:0 6px;font-size:13px;">🎬</span><% } %>
    28           <% if (!it.usedCount) { %><span style="position:absolute;top:6px;left:6px;background:#b4452e;color:#fff;border-radius:6px;padding:1px 6px;font-size:11px;"><%= t('admin.media_unused') %></span><% } %>
     37      <div class="ax-tile media-card" data-file="<%= it.file %>">
     38        <div class="ax-tile-media" style="aspect-ratio:1;">
     39          <img src="<%= thumb(it.url, 320) %>" alt="" loading="lazy" decoding="async" style="object-fit:cover;height:100%;">
     40          <% if (it.hasVideo) { %><span class="ax-tile-badge ax-tile-badge-r" title="animated cover">&#127916;</span><% } %>
     41          <% if (!it.usedCount) { %><span class="ax-tile-badge"><%= t('admin.media_unused') %></span><% } %>
    2942        </div>
    30         <div style="padding:.5rem .6rem;font-size:13px;">
    31           <div style="opacity:.7;"><%= it.kb %> KB · <%= it.usedCount %>×</div>
    32           <div style="display:flex;gap:.4rem;margin-top:.45rem;">
    33             <button type="button" class="btn" style="padding:3px 8px;font-size:12px;" data-copy="<%= it.url %>"><%= t('admin.media_copy') %></button>
    34             <button type="button" class="btn" style="padding:3px 8px;font-size:12px;background:transparent;border-color:#b4452e;color:#b4452e;" data-del="<%= it.file %>">🗑</button>
     43        <div class="ax-tile-body">
     44          <div class="ax-tile-meta"><%= it.kb %> KB &middot; <%= it.usedCount %>&times;</div>
     45          <div class="ax-tile-actions">
     46            <button type="button" class="ax-btn ax-btn-sm" data-copy="<%= it.url %>"><%= t('admin.media_copy') %></button>
     47            <button type="button" class="ax-btn ax-btn-sm ax-btn-danger" data-del="<%= it.file %>">&#128465;</button>
    3548          </div>
    3649        </div>
  • src/views/pages/admin-playlists.ejs

    ra5515b4 r749605a  
    55const _csrf = (typeof csrfToken !== 'undefined' && csrfToken) || '';
    66%>
     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') %>
    711<div class="container ax-page">
    812
    9   <p><a href="/admin" class="btn">&larr; <%= t('nav.admin') %></a></p>
     13  <%- include('../partials/admin-back') %>
    1014  <%- include('../partials/media-tabs', { active: 'playlists', audioOn: true }) %>
    1115  <header class="ax-header">
     
    8387
    8488.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; }
    20489
    20590/* ─── Mobile ──────────────────────────────────────────────────── */
    20691@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   }
    22192}
    22293</style>
  • src/views/pages/admin-videos.ejs

    ra5515b4 r749605a  
    1 <%# Beheer → Media → Video's (Robins opdracht, 30-7): the videos C2S uploads
    2     brought in (Shaer's composer, the help buoy), with usage and cleanup.
    3     Same shape as the image page; the preview is the video itself with
    4     preload="metadata", so the browser shows the first frame as its
    5     thumbnail until real poster generation lands (bead: ffmpeg ontbreekt). %>
    6 <div class="admin-wrap" style="max-width:980px;margin:0 auto;padding:1.5rem 1rem;">
    7   <a href="/admin" class="btn" style="margin-bottom:1rem;display:inline-block;">← <%= t('nav.admin') %></a>
    8   <h1 style="font-family:var(--display,serif);margin:.2rem 0 1rem;"><%= t('admin.t_media') %></h1>
     1<%# Beheer -> Media -> Video's (Robins opdracht, 30-7): de video's die via C2S
     2    binnenkwamen (Shaers composer, de reddingsboei), met gebruik en opruiming.
    93
     4    De voorvertoning is de video zelf met preload="metadata", zodat de browser
     5    het eerste beeld als miniatuur toont tot echte poster-generatie landt
     6    (bead: ffmpeg ontbreekt).
     7
     8    Zelfde romp als de andere mediapagina's; stond hiervoor op inline styles. %>
     9<%- include('../partials/admin-styles') %>
     10<div class="container ax-page">
     11
     12  <%- include('../partials/admin-back') %>
    1013  <%- include('../partials/media-tabs', { active: 'videos', audioOn: audioOn }) %>
     14  <header class="ax-header">
     15    <div>
     16      <h1><%= t('admin.t_media') %></h1>
     17      <p class="ax-tagline"><%= items.length %> <%= t('admin.videos_count') %></p>
     18    </div>
     19  </header>
    1120
    12   <% if (success) { %><p style="background:rgba(60,160,90,.15);border:1px solid rgba(60,160,90,.4);padding:.5rem .8rem;border-radius:8px;"><%= success %></p><% } %>
    13 
    14   <p style="margin:0 0 1rem;opacity:.75;"><%= items.length %> <%= t('admin.videos_count') %></p>
     21  <% if (success) { %><div class="ax-flash ax-flash-ok"><%= success %></div><% } %>
    1522
    1623  <% if (!items.length) { %>
    17     <p style="opacity:.7;"><%= t('admin.videos_empty') %></p>
     24    <div class="ax-empty">
     25      <div class="ax-empty-icon">&#127916;</div>
     26      <p><%= t('admin.videos_empty') %></p>
     27    </div>
    1828  <% } else { %>
    19   <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:1rem;">
     29  <div class="ax-grid" style="--ax-tile:220px;">
    2030    <% items.forEach(function(it){ %>
    21       <div class="media-card" data-file="<%= it.file %>" style="border:1px solid var(--rule,rgba(128,128,128,.25));border-radius:10px;overflow:hidden;background:var(--paper-2,rgba(128,128,128,.06));">
    22         <div style="position:relative;aspect-ratio:16/10;background:#000;">
    23           <video src="<%= it.url %>" preload="metadata" controls playsinline style="width:100%;height:100%;object-fit:contain;display:block;"></video>
    24           <% if (!it.usedCount) { %><span style="position:absolute;top:6px;left:6px;background:#b4452e;color:#fff;border-radius:6px;padding:1px 6px;font-size:11px;z-index:1;"><%= t('admin.media_unused') %></span><% } %>
     31      <div class="ax-tile media-card" data-file="<%= it.file %>">
     32        <div class="ax-tile-media" style="aspect-ratio:16/10;background:#000;">
     33          <video src="<%= it.url %>" preload="metadata" controls playsinline style="height:100%;object-fit:contain;"></video>
     34          <% if (!it.usedCount) { %><span class="ax-tile-badge"><%= t('admin.media_unused') %></span><% } %>
    2535        </div>
    26         <div style="padding:.5rem .6rem;font-size:13px;">
    27           <div style="opacity:.7;word-break:break-all;"><%= it.file %></div>
    28           <div style="opacity:.7;"><%= it.kb %> KB · <%= it.usedCount %>×</div>
    29           <div style="display:flex;gap:.4rem;margin-top:.45rem;">
    30             <button type="button" class="btn" style="padding:3px 8px;font-size:12px;" data-copy="<%= it.url %>"><%= t('admin.media_copy') %></button>
     36        <div class="ax-tile-body">
     37          <div class="ax-tile-meta" style="word-break:break-all;"><%= it.file %></div>
     38          <div class="ax-tile-meta"><%= it.kb %> KB &middot; <%= it.usedCount %>&times;</div>
     39          <div class="ax-tile-actions">
     40            <button type="button" class="ax-btn ax-btn-sm" data-copy="<%= it.url %>"><%= t('admin.media_copy') %></button>
    3141            <% if (!it.usedCount) { %>
    32             <button type="button" class="btn" style="padding:3px 8px;font-size:12px;background:transparent;border-color:#b4452e;color:#b4452e;" data-del="<%= it.file %>">🗑</button>
     42            <button type="button" class="ax-btn ax-btn-sm ax-btn-danger" data-del="<%= it.file %>">&#128465;</button>
    3343            <% } %>
    3444          </div>
Note: See TracChangeset for help on using the changeset viewer.