P1: Einheitliche klassische Pagination (Neuheiten, Planung, Audit-Log)
- Infinite Scroll ersetzt durch Zurück/Weiter + Seitenzahlen-Fenster (funktioniert ohne JavaScript) - Gemeinsames Makro seitennavigation.html (aria-current auf aktiver Seite, deaktivierte Buttons als span) - Neuheiten: Pagination bleibt beim HTMX-Filter-Swap innerhalb von #neuheiten-liste konsistent; Filter/Sortierung/Jahr bleiben in den Seiten-URLs erhalten - Audit-Log: gleiche Optik statt reiner Text-Links
This commit is contained in:
@@ -285,6 +285,7 @@ class AuditLogPlugin(BasePlugin):
|
||||
"ende": offset + len(eintraege),
|
||||
"url_zurueck": _seiten_url(seite - 1),
|
||||
"url_weiter": _seiten_url(seite + 1),
|
||||
"seiten_url": _seiten_url,
|
||||
"url_ohne_seite": f"/audit-log?{basis}" if basis else "/audit-log",
|
||||
"benutzer_auswahl": benutzer_auswahl,
|
||||
"aktionen_auswahl": aktionen_auswahl,
|
||||
|
||||
@@ -117,16 +117,7 @@
|
||||
0 Einträge
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if gesamt_seiten > 1 %}
|
||||
<div class="flex items-center gap-2">
|
||||
{% if seite > 1 %}
|
||||
<a href="{{ url_zurueck }}" class="border border-slate-300 hover:bg-slate-50 rounded-lg px-3 py-1">← Zurück</a>
|
||||
{% endif %}
|
||||
<span>Seite {{ seite }} von {{ gesamt_seiten }}</span>
|
||||
{% if seite < gesamt_seiten %}
|
||||
<a href="{{ url_weiter }}" class="border border-slate-300 hover:bg-slate-50 rounded-lg px-3 py-1">Weiter →</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% from "seitennavigation.html" import seitennavigation %}
|
||||
{{ seitennavigation(seiten_url, seite, gesamt_seiten) }}
|
||||
{% endblock %}
|
||||
|
||||
@@ -27,7 +27,8 @@ import logging
|
||||
import os
|
||||
import threading
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from urllib.parse import quote
|
||||
from math import ceil
|
||||
from urllib.parse import quote, urlencode
|
||||
|
||||
from fastapi import Depends, Form, HTTPException, Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
@@ -113,6 +114,7 @@ class NeuheitenPlugin(BasePlugin):
|
||||
abwaerts = richtung == "ab" if sort in SORTIERBAR else False
|
||||
spalte_sortiert = spalte.desc() if abwaerts else spalte.asc()
|
||||
pro_seite = 50
|
||||
seite_aktuell = max(seite_nr, 1)
|
||||
|
||||
with self.context.session_factory() as db:
|
||||
abfrage = select(Neuheit)
|
||||
@@ -148,9 +150,11 @@ class NeuheitenPlugin(BasePlugin):
|
||||
) if j is not None
|
||||
]
|
||||
gesamt = db.scalar(select(func.count()).select_from(abfrage.subquery())) or 0
|
||||
gesamt_seiten = max(1, ceil(gesamt / pro_seite))
|
||||
seite_aktuell = min(seite_aktuell, gesamt_seiten)
|
||||
eintraege = db.scalars(
|
||||
abfrage.order_by(spalte_sortiert, Neuheit.id.asc())
|
||||
.offset((max(seite_nr, 1) - 1) * pro_seite)
|
||||
.offset((seite_aktuell - 1) * pro_seite)
|
||||
.limit(pro_seite)
|
||||
).all()
|
||||
status_optionen = [
|
||||
@@ -160,7 +164,18 @@ class NeuheitenPlugin(BasePlugin):
|
||||
]
|
||||
|
||||
quellen_zeilen = self._quellen_uebersicht(db)
|
||||
hat_weitere = max(seite_nr, 1) * pro_seite < gesamt
|
||||
|
||||
def _seiten_url(nr: int) -> str:
|
||||
parameter = {
|
||||
"q": q,
|
||||
"status": status,
|
||||
"sort": sort if sort in SORTIERBAR else STANDARD_SORTIERUNG[0],
|
||||
"richtung": "ab" if abwaerts else "auf",
|
||||
"seite_nr": str(nr),
|
||||
}
|
||||
if jahr is not None:
|
||||
parameter["jahr"] = str(jahr)
|
||||
return f"/neuheiten?{urlencode(parameter)}"
|
||||
kontext = {
|
||||
"user": user,
|
||||
"titel": self.title,
|
||||
@@ -182,8 +197,9 @@ class NeuheitenPlugin(BasePlugin):
|
||||
"suchbegriffe": ", ".join(self._suchbegriffe) or "—",
|
||||
"sync_aktiv": self._scheduler is not None and self._scheduler.running,
|
||||
"anzahl": gesamt,
|
||||
"hat_weitere": hat_weitere,
|
||||
"naechste_seite": max(seite_nr, 1) + 1,
|
||||
"seite": seite_aktuell,
|
||||
"gesamt_seiten": gesamt_seiten,
|
||||
"seiten_url": _seiten_url,
|
||||
"quellen_zeilen": quellen_zeilen,
|
||||
"quellen_sync_aktiv": (
|
||||
os.environ.get("SPIELE_NEUHEITEN_QUELLEN_SYNC_AKTIV", "1").strip()
|
||||
|
||||
@@ -122,11 +122,6 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% from "seitennavigation.html" import seitennavigation %}
|
||||
{{ seitennavigation(seiten_url, seite, gesamt_seiten) }}
|
||||
</div>
|
||||
{% if hat_weitere %}
|
||||
<div id="lade-mehr"
|
||||
hx-get="/neuheiten?seite_nr={{ naechste_seite }}&q={{ q }}&status={{ status_filter }}&sort={{ sort }}&richtung={{ richtung }}"
|
||||
hx-trigger="revealed delay:150ms" hx-swap="outerHTML">
|
||||
<div class="py-6 text-center text-sm text-slate-400">Lade weitere Einträge …</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -24,7 +24,8 @@ der Eintrage-Vorgang bricht.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from urllib.parse import quote
|
||||
from math import ceil
|
||||
from urllib.parse import quote, urlencode
|
||||
|
||||
from fastapi import Depends, Form, Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
@@ -299,13 +300,22 @@ class PlanungPlugin(BasePlugin):
|
||||
gesamt = db.scalar(
|
||||
select(func.count()).select_from(abfrage.subquery())
|
||||
) or 0
|
||||
gesamt_seiten = max(1, ceil(gesamt / pro_seite))
|
||||
seite_aktuell = min(max(seite_nr, 1), gesamt_seiten)
|
||||
eintraege = db.scalars(
|
||||
abfrage.order_by(
|
||||
Planungseintrag.aktualisiert_am.desc(), Planungseintrag.id.desc()
|
||||
)
|
||||
.offset((max(seite_nr, 1) - 1) * pro_seite)
|
||||
.offset((seite_aktuell - 1) * pro_seite)
|
||||
.limit(pro_seite)
|
||||
).all()
|
||||
|
||||
def _seiten_url(nr: int) -> str:
|
||||
parameter = {"seite_nr": str(nr)}
|
||||
if status in GUELTIGE_STATUS:
|
||||
parameter["status"] = status
|
||||
return f"/planung?{urlencode(parameter)}"
|
||||
|
||||
namen = {
|
||||
benutzer.id: (benutzer.display_name or benutzer.username)
|
||||
for benutzer in db.scalars(select(User)).all()
|
||||
@@ -326,8 +336,9 @@ class PlanungPlugin(BasePlugin):
|
||||
"meldung": meldung[:300],
|
||||
"fehler": fehler[:300],
|
||||
"anzahl": gesamt,
|
||||
"hat_weitere": max(seite_nr, 1) * pro_seite < gesamt,
|
||||
"naechste_seite": max(seite_nr, 1) + 1,
|
||||
"seite": seite_aktuell,
|
||||
"gesamt_seiten": gesamt_seiten,
|
||||
"seiten_url": _seiten_url,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -217,13 +217,8 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if hat_weitere %}
|
||||
<div id="planung-lade-mehr"
|
||||
hx-get="/planung?seite_nr={{ naechste_seite }}&status={{ status_filter }}"
|
||||
hx-trigger="revealed delay:150ms" hx-swap="outerHTML">
|
||||
<div class="py-6 text-center text-sm text-slate-400">Lade weitere Einträge …</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% from "seitennavigation.html" import seitennavigation %}
|
||||
{{ seitennavigation(seiten_url, seite, gesamt_seiten) }}
|
||||
<!-- Lösch-Bestätigungsdialog (Progressive Enhancement: ohne JavaScript
|
||||
sendet das Formular sofort, mit Alpine öffnet sich dieser Dialog). -->
|
||||
<div x-show="loeschFormular !== null" x-cloak
|
||||
|
||||
61
src/redaktionskern/templates/seitennavigation.html
Normal file
61
src/redaktionskern/templates/seitennavigation.html
Normal file
@@ -0,0 +1,61 @@
|
||||
{# Klassische Seitennavigation: Zurück/Weiter + Seitenzahlen.
|
||||
Parameter: seiten_url (Funktion nr → URL), seite, gesamt_seiten.
|
||||
Funktioniert ohne JavaScript (einfache Links). #}
|
||||
{% from "icons.html" import icon_chevron_links, icon_chevron_rechts %}
|
||||
{% macro seitennavigation(seiten_url, seite, gesamt_seiten) %}
|
||||
{% if gesamt_seiten > 1 %}
|
||||
<nav class="flex flex-wrap items-center justify-between gap-3 mt-4" aria-label="Seitennavigation">
|
||||
<div>
|
||||
{% if seite > 1 %}
|
||||
<a href="{{ seiten_url(seite - 1) }}" rel="prev"
|
||||
class="inline-flex items-center gap-1 px-3 py-2 rounded-lg border border-slate-300 text-sm font-medium text-slate-700 hover:bg-slate-50">
|
||||
{{ icon_chevron_links("h-4 w-4") }} Zurück
|
||||
</a>
|
||||
{% else %}
|
||||
<span aria-disabled="true"
|
||||
class="inline-flex items-center gap-1 px-3 py-2 rounded-lg border border-slate-200 text-sm font-medium text-slate-300 cursor-not-allowed">
|
||||
{{ icon_chevron_links("h-4 w-4") }} Zurück
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
<span class="hidden sm:inline text-sm text-slate-500 mr-2">Seite {{ seite }} von {{ gesamt_seiten }}</span>
|
||||
<span class="sm:hidden sr-only">Seite {{ seite }} von {{ gesamt_seiten }}</span>
|
||||
{% set von = [seite - 2, 1] | max %}
|
||||
{% set bis = [seite + 2, gesamt_seiten] | min %}
|
||||
{% if von > 1 %}
|
||||
<a href="{{ seiten_url(1) }}"
|
||||
class="px-3 py-2 rounded-lg border border-slate-300 text-sm text-slate-700 hover:bg-slate-50">1</a>
|
||||
{% if von > 2 %}<span class="px-1 text-slate-400" aria-hidden="true">…</span>{% endif %}
|
||||
{% endif %}
|
||||
{% for nr in range(von, bis + 1) %}
|
||||
{% if nr == seite %}
|
||||
<span aria-current="page"
|
||||
class="px-3 py-2 rounded-lg bg-emerald-600 text-white text-sm font-semibold">{{ nr }}</span>
|
||||
{% else %}
|
||||
<a href="{{ seiten_url(nr) }}"
|
||||
class="px-3 py-2 rounded-lg border border-slate-300 text-sm text-slate-700 hover:bg-slate-50">{{ nr }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if bis < gesamt_seiten %}
|
||||
{% if bis < gesamt_seiten - 1 %}<span class="px-1 text-slate-400" aria-hidden="true">…</span>{% endif %}
|
||||
<a href="{{ seiten_url(gesamt_seiten) }}"
|
||||
class="px-3 py-2 rounded-lg border border-slate-300 text-sm text-slate-700 hover:bg-slate-50">{{ gesamt_seiten }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
{% if seite < gesamt_seiten %}
|
||||
<a href="{{ seiten_url(seite + 1) }}" rel="next"
|
||||
class="inline-flex items-center gap-1 px-3 py-2 rounded-lg border border-slate-300 text-sm font-medium text-slate-700 hover:bg-slate-50">
|
||||
Weiter {{ icon_chevron_rechts("h-4 w-4") }}
|
||||
</a>
|
||||
{% else %}
|
||||
<span aria-disabled="true"
|
||||
class="inline-flex items-center gap-1 px-3 py-2 rounded-lg border border-slate-200 text-sm font-medium text-slate-300 cursor-not-allowed">
|
||||
Weiter {{ icon_chevron_rechts("h-4 w-4") }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
Reference in New Issue
Block a user