Files
KantSites/templates/admin/media.html.twig
T
kantarciandClaude Opus 5 14031a5880 Benutzerverwaltung in der Oberfläche, Mediathek als eigene Seite
Konten gab es bisher nur über die Konsole. Unter "Benutzer" legt ein
Administrator jetzt Konten an, ändert Rollen, setzt Passwörter und löscht
Konten; unter "Mein Konto" ändert jeder sein eigenes Passwort, aber nur mit
dem aktuellen in der Hand.

Eine Rolle je Konto: Administrator, Veröffentlicher, Redakteur. Ein Passwort
oder eine Rolle zu ändern meldet jede offene Sitzung dieses Kontos ab - dafür
war nichts zu bauen, CmsUser::isEqualTo vergleicht Hash und Rollen mit der
Datenbank. Genau eine Sitzung überlebt: die, die ihr eigenes Passwort geändert
hat. "Jemanden sofort aussperren" ist damit dasselbe wie "Passwort setzen".

Es bleibt immer ein Administrator übrig, nicht über eine Zählung, sondern weil
niemand sein eigenes Konto löschen oder herabstufen kann und jede Änderung die
Administratorrolle voraussetzt. Die zusätzliche Prüfung auf einen zweiten
Administrator war damit unerreichbar und ist wieder heraus.

Mediathek (Umut: "das sollte direkt laden in pagination"): /admin/media zeigt
die Bilder jetzt selbst, 24 je Seite, neueste zuerst, mit Blättern und einem
Schalter für archivierte. Hochladen und Bildangaben laufen über gewöhnliche
Formulare ohne JavaScript; der Dialog bleibt der Weg, beim Bearbeiten ein Bild
auszuwählen. Die Bild-ID steht in jeder Karte, weil Beitragsbild und
Teilen-Vorschau genau diese ID erwarten.

Die Migrationsliste der Testfixtures stand dreimal da und steht jetzt einmal
in tests/TestSchema.php.

217 PHP-Tests / 845 Assertions unter PHP 8.2.12 und 8.4.22. axe deckt jetzt
elf Bildschirme ab statt drei - alle Verwaltungsseiten, nicht nur Anmeldung,
Seitenliste und Editor -, null Verstöße. Nicht geprüft: kein Screenreader;
Blättern und Upload sind mit zwei Bildern geprüft, nicht mit einer großen
Bibliothek.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 18:14:50 +02:00

83 lines
5.1 KiB
Twig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends 'admin/shell.html.twig' %}
{% set nav = 'media' %}
{% block title %}Mediathek{% endblock %}
{% block heading %}Mediathek{% endblock %}
{% block lead %}Alle Bilder, neueste zuerst. Beim Hochladen wird jedes Bild in WebP umgewandelt und in mehreren Breiten abgelegt; die Seite liefert dann die passende aus.{% endblock %}
{% block content %}
{% if is_granted('ROLE_EDITOR') %}
<form action="{{ path('admin_media_upload') }}" method="post" enctype="multipart/form-data" class="card card-body">
<input type="hidden" name="_csrf_token" value="{{ csrf_token('media-form') }}">
<input type="hidden" name="seite" value="{{ library.page }}">
<input type="hidden" name="archiv" value="{{ archived ? '1' : '' }}">
<label for="files">Bilder hochladen</label>
<input id="files" name="files[]" type="file" accept="image/jpeg,image/png,image/webp" multiple required>
<p class="field-help">JPEG, PNG oder WebP, höchstens 8 MB je Bild. Mehrere Dateien sind möglich; das Hosting kann ein kleineres Limit setzen.</p>
<button class="primary" type="submit">Hochladen</button>
</form>
{% endif %}
<div class="section-head">
<h2>{{ archived ? 'Alle Bilder einschließlich Archiv' : 'Bilder' }} <span class="count">{{ library.total }}</span></h2>
<span class="muted">
{% if archived %}<a href="{{ path('admin_media') }}">Archivierte ausblenden</a>{% else %}<a href="{{ path('admin_media', {archiv: 1}) }}">Archivierte anzeigen</a>{% endif %}
{% if library.pages > 1 %} · Seite {{ library.page }} von {{ library.pages }}{% endif %}
</span>
</div>
{% if library.items is empty %}
<div class="empty">
<h3>{{ archived ? 'Keine Bilder vorhanden.' : 'Keine Bilder in der Mediathek.' }}</h3>
<p>{{ is_granted('ROLE_EDITOR') ? 'Lade oben dein erstes Bild hoch.' : 'Ein Redakteur kann Bilder hochladen.' }}</p>
</div>
{% else %}
<ul class="library-grid">
{% for item in library.items %}
<li class="library-tile">
{# The smallest variant is the thumbnail; the largest opens in a new tab. #}
<a href="{{ (item.variants|last).src }}" target="_blank" rel="noopener" aria-label="{{ item.title }} in Originalgröße öffnen">
<img src="{{ (item.variants|first).src }}" alt="" loading="lazy" width="{{ (item.variants|first).width }}" height="{{ (item.variants|first).height }}">
</a>
<div class="library-body">
<strong>{{ item.title }}</strong>
<span class="muted">{{ (item.variants|last).width }} × {{ (item.variants|last).height }} px{% if item.archived %} · archiviert{% endif %}</span>
<span class="muted">{{ item.alt ? 'Alternativtext gesetzt' : 'Kein Alternativtext' }}</span>
<code>{{ item.id }}</code>
</div>
{% if is_granted('ROLE_EDITOR') %}
<details class="sheet">
<summary aria-label="Angaben zu {{ item.title }}">Angaben</summary>
<div class="sheet-body">
<form action="{{ path('admin_media_save', {id: item.id}) }}" method="post">
<input type="hidden" name="_csrf_token" value="{{ csrf_token('media-form') }}">
<input type="hidden" name="version" value="{{ item.version }}">
<input type="hidden" name="seite" value="{{ library.page }}">
<input type="hidden" name="archiv" value="{{ archived ? '1' : '' }}">
<label for="title-{{ item.id }}">Bildtitel</label>
<input id="title-{{ item.id }}" name="title" maxlength="160" required value="{{ item.title }}">
<label for="alt-{{ item.id }}">Alternativtext</label>
<textarea id="alt-{{ item.id }}" name="alt" maxlength="300" rows="2">{{ item.alt }}</textarea>
<p class="field-help">Beschreibt, was zu sehen ist. Das ist, was Menschen mit Screenreader vom Bild haben. Bei rein dekorativen Bildern leer lassen.</p>
<label class="check-label" for="archived-{{ item.id }}"><input id="archived-{{ item.id }}" name="archived" type="checkbox" value="1"{{ item.archived ? ' checked' : '' }}> Archiviert</label>
<p class="field-help">Archivierte Bilder erscheinen nicht mehr in der Auswahl, bleiben aber ausgeliefert – eine Seite, die das Bild verwendet, verliert es nicht.</p>
<button class="primary" type="submit">Angaben speichern</button>
</form>
</div>
</details>
{% endif %}
</li>
{% endfor %}
</ul>
{% if library.pages > 1 %}
<nav class="pager" aria-label="Seiten der Mediathek">
{% if library.page > 1 %}<a class="secondary" href="{{ path('admin_media', {seite: library.page - 1, archiv: archived ? 1 : null}) }}" rel="prev"><span aria-hidden="true">←</span> Neuere</a>{% endif %}
<span class="muted">Seite {{ library.page }} von {{ library.pages }} · {{ library.total }} Bilder</span>
{% if library.page < library.pages %}<a class="secondary" href="{{ path('admin_media', {seite: library.page + 1, archiv: archived ? 1 : null}) }}" rel="next">Ältere <span aria-hidden="true">→</span></a>{% endif %}
</nav>
{% endif %}
{% endif %}
<p class="footnote">Kant CMS · Arbeitsstand</p>
{% endblock %}