Files
spiele-redaktion/plugins/neuheiten/templates/neuheiten/index.html
Flo Hartmann 742c265df8 P0: Formular-Accessibility — Fehlerboxen mit role=alert, Pflichtfeld-ARIA
- Alle fehler-Boxen: role=alert + tabindex=-1
- Login: aria-invalid/aria-describedby auf Feldern bei Fehler
- Pflichtfelder: aria-required=true ergänzt
2026-08-26 08:40:45 +00:00

148 lines
8.1 KiB
HTML

{% extends "base.html" %}
{% block titel %}{{ titel }} — Spiele-Redaktion{% endblock %}
{% block inhalt %}
<h1 class="text-2xl font-bold mb-2">{{ titel }}</h1>
<p class="text-slate-600 mb-4">
Neuheitenliste aus der <a href="https://boardgamegeek.com" class="text-emerald-700 underline">BoardGameGeek</a>-Datenbank.
Erweiterungen und Prototypen werden beim Sync automatisch gefiltert.
</p>
{% if meldung %}
<div id="meldung"
class="mb-4 px-4 py-3 rounded-lg border text-sm
{% if meldung.startswith('Sync fehlgeschlagen') %}bg-red-50 border-red-200 text-red-700{% else %}bg-emerald-50 border-emerald-200 text-emerald-800{% endif %}">
{{ meldung }}
</div>
{% endif %}
{% if fehler %}
<div class="mb-4 px-4 py-3 rounded-lg border bg-red-50 border-red-200 text-red-700 text-sm" role="alert" tabindex="-1">{{ fehler }}</div>
{% endif %}
<div class="flex flex-wrap items-end gap-3 mb-4">
{% if ist_redaktion %}
<form method="post" action="/neuheiten/sync" class="flex flex-col sm:flex-row items-stretch sm:items-end gap-2">
<div class="sm:w-72">
<label for="suchbegriff" class="block text-xs font-medium text-slate-500 mb-1">
Sofort auf BoardGameGeek suchen &amp; importieren (optional)
</label>
<input type="text" id="suchbegriff" name="suchbegriff" placeholder="Spielname oder Suchbegriff …"
class="w-full min-h-[44px] border border-slate-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500">
</div>
<button type="submit"
class="min-h-[44px] inline-flex items-center justify-center gap-1.5 bg-emerald-600 hover:bg-emerald-700 active:bg-emerald-800 text-white font-medium px-4 py-2 rounded-lg text-sm whitespace-nowrap">
{% from "icons.html" import icon_sync %}{{ icon_sync("h-4 w-4") }} Jetzt synchronisieren
</button>
</form>
{% endif %}
<p class="text-xs text-slate-400 ml-auto">
Hintergrund-Sync:
{% if sync_aktiv %}<span class="text-emerald-700 font-medium">aktiv</span>{% else %}<span class="text-slate-500">aus</span>{% endif %}
&middot; Suchbegriffe: {{ suchbegriffe }}
</p>
</div>
<div class="mb-6 bg-white rounded-xl border border-slate-200 shadow-sm p-4">
<div class="flex flex-wrap items-center gap-2 mb-3">
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500 mr-auto">Web-Quellen</h2>
{% if ist_redaktion %}
<form method="post" action="/neuheiten/quellen-sync">
<button type="submit"
class="min-h-[44px] md:min-h-0 inline-flex items-center gap-1.5 bg-sky-600 hover:bg-sky-700 active:bg-sky-800 text-white font-medium px-3 py-1.5 rounded-lg text-xs whitespace-nowrap">
{% from "icons.html" import icon_sync %}{{ icon_sync("h-3.5 w-3.5") }} Web-Quellen synchronisieren
</button>
</form>
{% endif %}
</div>
<div class="overflow-x-auto">
<table class="w-full text-xs">
<thead class="text-left text-slate-400">
<tr>
<th class="px-2 py-1.5 font-medium">Quelle</th>
<th class="px-2 py-1.5 font-medium">Status</th>
<th class="px-2 py-1.5 font-medium">Letzter Lauf</th>
<th class="px-2 py-1.5 font-medium text-right">Neu</th>
<th class="px-2 py-1.5 font-medium text-right">Aktualisiert</th>
<th class="px-2 py-1.5 font-medium text-right">Gefiltert</th>
<th class="px-2 py-1.5 font-medium">Hinweis</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
{% for quelle in quellen_zeilen %}
<tr>
<td class="px-2 py-1.5">
<a href="{{ quelle.url }}" target="_blank" rel="noopener noreferrer"
class="font-medium text-slate-700 hover:text-emerald-700">{{ quelle.anzeigename }}</a>
<span class="ml-1 text-[10px] text-slate-300">{{ quelle.name }}</span>
</td>
<td class="px-2 py-1.5">
<form method="post" action="/neuheiten/quellen/{{ quelle.name }}/schalter" class="inline-flex items-center gap-2">
{% if sync_aktiv %}
<input type="hidden" name="aktiv" value="{{ '0' if quelle.aktiv else '1' }}">
<button type="submit"
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors {{ 'bg-emerald-500' if quelle.aktiv else 'bg-slate-300' }}"
title="{{ 'Quelle ausschalten' if quelle.aktiv else 'Quelle einschalten' }}">
<span class="inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform {{ 'translate-x-4' if quelle.aktiv else 'translate-x-0.5' }}"></span>
</button>
{% else %}
<span class="text-xs text-slate-400">{{ 'an' if quelle.aktiv else 'aus' }} (Env)</span>
{% endif %}
</form>
<form method="post" action="/neuheiten/quellen-sync" class="inline">
<input type="hidden" name="quelle" value="{{ quelle.name }}">
<button type="submit"
class="ml-2 text-xs text-emerald-700 hover:text-emerald-900 underline decoration-dotted"
title="Nur diese Quelle jetzt synchronisieren">jetzt sync</button>
</form>
</td>
<td class="px-2 py-1.5 whitespace-nowrap text-slate-500">
{{ quelle.letzte_laufzeit.strftime('%d.%m.%Y %H:%M') if quelle.letzte_laufzeit else '—' }}
{% if quelle.dauer_sekunden %}<span class="text-slate-300">({{ '%.1f' | format(quelle.dauer_sekunden) }}&nbsp;s)</span>{% endif %}
</td>
<td class="px-2 py-1.5 text-right">{{ quelle.neu }}</td>
<td class="px-2 py-1.5 text-right">{{ quelle.aktualisiert }}</td>
<td class="px-2 py-1.5 text-right">{{ quelle.gefiltert }}</td>
<td class="px-2 py-1.5">
{% if quelle.fehlermeldung %}<span class="text-red-600">{{ quelle.fehlermeldung }}</span>
{% elif quelle.unveraendert %}<span class="text-slate-400">unverändert (304) — übersprungen</span>
{% elif not quelle.aktiv %}<span class="text-slate-300">abgeschaltet</span>
{% else %}—{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<p class="mt-2 text-[11px] text-slate-400">
Quellen-Sync:
{% if quellen_sync_aktiv %}<span class="text-emerald-700 font-medium">täglich aktiv</span>{% else %}<span class="text-slate-500">ausgeschaltet</span>{% endif %}
&middot; max. 1 Anfrage/Sekunde je Quelle &middot; Duplikate gegen BGG werden per Titel-Match gefiltert
</p>
</div>
<form method="get" action="/neuheiten"
hx-get="/neuheiten" hx-target="#neuheiten-liste" hx-swap="outerHTML"
hx-trigger="input changed delay:300ms from:find input[name='q'], change from:find select[name='status'], change from:find select[name='jahr']"
class="grid grid-cols-1 sm:flex sm:flex-wrap gap-3 mb-4">
<input type="text" name="q" value="{{ q }}" placeholder="Suche in Titel, Verlag, Autor …"
class="w-full min-h-[44px] border border-slate-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500 sm:w-80">
<select name="status" class="w-full sm:w-auto min-h-[44px] md:min-h-0 border border-slate-300 rounded-lg px-3 py-2 text-sm bg-white">
<option value="">Alle Status</option>
{% for option in status_optionen %}
<option value="{{ option }}" {% if option == status_filter %}selected{% endif %}>{{ option }}</option>
{% endfor %}
</select>
<select name="jahr" class="w-full sm:w-auto min-h-[44px] md:min-h-0 border border-slate-300 rounded-lg px-3 py-2 text-sm bg-white">
<option value="{{ jahr_standard }}">ab {{ jahr_standard }}</option>
{% for jahr in jahr_optionen %}
<option value="{{ jahr }}" {% if jahr|string == jahr_filter|string %}selected{% endif %}>{{ jahr }}</option>
{% endfor %}
<option value="" {% if not jahr_filter %}selected{% endif %}>Alle Jahre</option>
</select>
<noscript>
<button type="submit" class="bg-slate-200 hover:bg-slate-300 px-4 py-2 rounded-lg text-sm min-h-[44px] md:min-h-0">Filtern</button>
</noscript>
</form>
{% include "neuheiten/_liste.html" %}
{% endblock %}