Verwaltung: Stimmen als kompakte Kacheln, Supporter-Liste auf 4 gekuerzt

Beides auf Wunsch vom 27.08.2026 ein-/ausklappbar:

- Stimmen liegen jetzt in einem Raster (auto-fill, min. 290px) statt
  untereinander -- am Computer stehen 3-4 Kacheln nebeneinander, am Handy
  eine. Zugeklappt ist genau die erste Reihe sichtbar; wie viele Kacheln
  das sind, liest die Logik aus dem Raster selbst aus (getComputedStyle
  liefert bei auto-fill die gebauten Spuren), damit "erste Reihe" auf
  jeder Breite stimmt -- inklusive Neuberechnung beim Groessenwechsel.
- Supporter-Tabelle zeigt nur noch die 4 zuletzt Angemeldeten, Rest per
  Knopf. Knopftext nennt immer die Gesamtzahl, damit nichts versteckt
  wirkt; beim Zuklappen springt die Ansicht sauber zum Listenanfang.
- Neuer Test pruef-verwaltung-kacheln.mjs (18 Pruefungen, Computer +
  Handy, inkl. Groessenwechsel und Ueberlauf-Kontrolle).

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-27 13:17:05 +02:00
co-authored by Claude Opus 5
parent 988a2fdc95
commit 0465a7a14a
2 changed files with 328 additions and 6 deletions
+134 -6
View File
@@ -78,6 +78,36 @@
.vw-stimme-upload { display: block; margin-top: .8rem; color: var(--text-muted); }
.vw-stimme-upload input { display: block; margin-top: .3rem; }
/* ---------- Stimmen: kompakte Kacheln, nur erste Reihe (27.08.2026) ----------
Nutzer-Wunsch: "die kacheln sollen kleiner sein so das auch paar neben
einander stehen, und ich soll immer nur die erste reihe sehen und auf
einen button drücken um die anderen zu sehen und wieder auf dern button
damit die liste wieder zu geht."
Wie viele Kacheln in eine Reihe passen, entscheidet die Bildschirmbreite
(auto-fill). Deshalb wird die Reihenbreite NICHT fest verdrahtet, sondern
die Anzeige-Logik liest die tatsächliche Spaltenzahl aus dem Raster aus —
so stimmt "erste Reihe" am Handy (1 Kachel) genauso wie am großen
Bildschirm (3-4 Kacheln). */
.vw-stimmen-grid { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); align-items: start; }
.vw-stimmen-karte { margin: 0 !important; padding: 1rem 1.05rem 1.1rem !important; }
.vw-stimmen-karte .vw-stimme-kopf { display: flex; justify-content: space-between; align-items: flex-start; gap: .55rem; flex-wrap: wrap; }
.vw-stimmen-karte .btn-row { gap: .4rem; flex-wrap: wrap; }
.vw-stimmen-karte .vw-refresh-btn { padding: .4rem .75rem; font-size: .76rem; }
.vw-stimmen-karte .vw-stimme-text { margin: .75rem 0 0; font-size: .88rem; line-height: 1.55; }
.vw-stimmen-karte .vw-stimme-avatar-block { margin-top: .85rem; padding-top: .75rem; }
.vw-stimmen-karte .vw-stimme-avatar-wahl { grid-template-columns: repeat(auto-fill, minmax(40px, 1fr)); max-width: 100%; }
.vw-stimmen-karte .vw-stimme-upload { margin-top: .6rem; font-size: .76rem; }
/* Ein-/Ausklappen: gilt für die Stimmen-Kacheln UND die Supporter-Liste. */
.vw-eingeklappt { display: none !important; }
.vw-mehr-zeile { display: flex; justify-content: center; margin-top: 1.1rem; }
/* Der Knopftext ist lang ("Alle 7 Stimmen anzeigen (6 weitere)") — am Handy
muss er umbrechen dürfen, sonst schiebt er sich über den Rand hinaus. */
.vw-mehr-btn { cursor: pointer; max-width: 100%; flex-wrap: wrap; justify-content: center; white-space: normal; text-align: center; line-height: 1.35; }
.vw-mehr-btn .pf { display: inline-block; transition: transform .18s ease; }
.vw-mehr-btn[aria-expanded="true"] .pf { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) { .vw-mehr-btn .pf { transition: none; } }
#supp-root, #vw-stats-root, #session-bar { --accent: #7dd3fc; --accent-2: #38bdf8; --accent-contrast: #1a1408; }
body { position: relative; }
/* Nutzer-Wunsch 04.08.2026 (3. Runde): "alles bisschen größer, Schrift
@@ -892,7 +922,10 @@
<p class="small muted text-center" style="max-width:60ch;margin:0 auto 1.4rem;">
Einreichungen von der Stimmen-Seite. Erst nach "Freigeben" erscheinen sie öffentlich.
</p>
<div id="testimonials-liste"></div>
<div id="testimonials-liste" class="vw-stimmen-grid"></div>
<div class="vw-mehr-zeile">
<button type="button" class="vw-refresh-btn vw-mehr-btn" id="stimmen-mehr-btn" aria-controls="testimonials-liste" aria-expanded="false" hidden></button>
</div>
</div>
</section>
@@ -1189,6 +1222,11 @@
<tbody id="supporter-tbody"></tbody>
</table>
</div>
<!-- Nur die 4 zuletzt Angemeldeten stehen offen da, der Rest kommt
auf Knopfdruck (Nutzer-Wunsch 27.08.2026). -->
<div class="vw-mehr-zeile">
<button type="button" class="vw-refresh-btn vw-mehr-btn" id="supporter-mehr-btn" aria-controls="supporter-tbody" aria-expanded="false" hidden></button>
</div>
<p class="vw-fehler" id="liste-fehler"></p>
</div>
@@ -1877,32 +1915,86 @@
}
function testimonialKarteHtml(t) {
return `<div class="card vw-card vw-shine-card" style="margin-bottom:1rem;padding:1.3rem 1.4rem;" data-testimonial-id="${t.id}">
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:1rem;flex-wrap:wrap;">
return `<div class="card vw-card vw-shine-card vw-stimmen-karte" data-testimonial-id="${t.id}">
<div class="vw-stimme-kopf">
<div>
<strong>${escapeHtml(t.name)}</strong>${t.tiktokUsername ? ` <span class="small" style="color:var(--text-muted);">@${escapeHtml(t.tiktokUsername)}</span>` : ""}
<div class="small" style="color:var(--text-muted);">${TESTIMONIAL_STATUS_LABELS[t.status] || t.status} · eingereicht am ${fmtDatum(t.createdAt)}</div>
</div>
<div class="btn-row" style="gap:.5rem;">
<div class="btn-row">
${t.status !== "freigegeben" ? `<button type="button" class="vw-refresh-btn testimonial-freigeben-btn">Freigeben</button>` : ""}
${t.status !== "abgelehnt" ? `<button type="button" class="vw-refresh-btn testimonial-ablehnen-btn">Ablehnen</button>` : ""}
<button type="button" class="vw-refresh-btn testimonial-loeschen-btn" style="color:#ff8a8a;">Löschen</button>
</div>
</div>
<p style="margin:.8rem 0 0;">${escapeHtml(t.nachricht)}</p>
<p class="vw-stimme-text">${escapeHtml(t.nachricht)}</p>
${avatarWahlHtml(t)}
</div>`;
}
/* ---- Nur die erste Reihe zeigen, Rest auf Knopfdruck (27.08.2026) ----
Die Spaltenzahl kommt direkt aus dem Raster (getComputedStyle liefert
bei auto-fill die tatsächlich gebauten Spuren als Pixelliste), NICHT aus
einer geratenen Zahl — sonst wäre "erste Reihe" am Handy falsch. Beim
Zuklappen werden die übrigen Kacheln mit display:none entfernt; die
Spurenliste bleibt davon unberührt, das Zählen funktioniert also auch im
zugeklappten Zustand. */
let stimmenOffen = false;
function stimmenSpalten(grid) {
const spur = getComputedStyle(grid).gridTemplateColumns;
if (!spur || spur === "none") return 1;
return Math.max(1, spur.split(" ").filter(Boolean).length);
}
function stimmenReiheAnwenden() {
const grid = document.getElementById("testimonials-liste");
const btn = document.getElementById("stimmen-mehr-btn");
if (!grid || !btn) return;
const karten = Array.from(grid.querySelectorAll("[data-testimonial-id]"));
if (!karten.length) { btn.hidden = true; return; }
const spalten = Math.min(stimmenSpalten(grid), karten.length);
karten.forEach((k, i) => k.classList.toggle("vw-eingeklappt", !stimmenOffen && i >= spalten));
const rest = karten.length - spalten;
btn.hidden = rest <= 0;
btn.setAttribute("aria-expanded", stimmenOffen ? "true" : "false");
btn.innerHTML = stimmenOffen
? `<span class="pf" aria-hidden="true">▲</span> Weniger anzeigen`
: `<span class="pf" aria-hidden="true">▼</span> Alle ${karten.length} Stimmen anzeigen (${rest} weitere)`;
}
(function initStimmenMehrKnopf() {
const btn = document.getElementById("stimmen-mehr-btn");
if (!btn) return;
btn.addEventListener("click", () => {
stimmenOffen = !stimmenOffen;
stimmenReiheAnwenden();
if (!stimmenOffen) {
const sanft = !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
document.getElementById("testimonials-root").scrollIntoView({ behavior: sanft ? "smooth" : "auto", block: "start" });
}
});
// Wird das Fenster breiter/schmaler, passen andere Kachelzahlen in eine
// Reihe — im zugeklappten Zustand muss neu gerechnet werden.
let timer = null;
window.addEventListener("resize", () => {
if (stimmenOffen) return;
clearTimeout(timer);
timer = setTimeout(stimmenReiheAnwenden, 120);
});
})();
async function ladeUndRenderTestimonials() {
const listeEl = document.getElementById("testimonials-liste");
const res = await window.AdminAuth.apiCall(API_BASE_URL, "/admin/testimonials/list", {});
if (!res.ok) {
listeEl.innerHTML = `<p class="vw-leer">Stimmen konnten nicht geladen werden.</p>`;
stimmenReiheAnwenden();
return;
}
const liste = res.testimonials || [];
listeEl.innerHTML = liste.length ? liste.map(testimonialKarteHtml).join("") : `<p class="vw-leer">Noch keine Einreichungen.</p>`;
stimmenReiheAnwenden();
const aktion = async (btn, status) => {
const id = btn.closest("[data-testimonial-id]").dataset.testimonialId;
@@ -2699,6 +2791,40 @@
.join("");
}
/* Standardmäßig stehen nur die 4 zuletzt Angemeldeten in der Liste, alles
Weitere kommt auf Knopfdruck (Nutzer-Wunsch 27.08.2026: "ich soll immer
nur die ersten 4 sehen also letzten angemeldeten sehen und auf einen
button drücken um die anderen zu sehen"). "Zuletzt angemeldet" ergibt
sich aus der Standard-Sortierung created_at/desc; wird anders sortiert
oder gefiltert, zeigt die Vorschau konsequenterweise die ersten 4 der
dann gültigen Reihenfolge. */
const SUPPORTER_VORSCHAU = 4;
let supporterAlleZeigen = false;
function supporterMehrKnopfSetzen(gesamt) {
const btn = document.getElementById("supporter-mehr-btn");
if (!btn) return;
const rest = gesamt - SUPPORTER_VORSCHAU;
btn.hidden = rest <= 0;
btn.setAttribute("aria-expanded", supporterAlleZeigen ? "true" : "false");
btn.innerHTML = supporterAlleZeigen
? `<span class="pf" aria-hidden="true">▲</span> Weniger anzeigen`
: `<span class="pf" aria-hidden="true">▼</span> Alle ${gesamt} anzeigen (${rest} weitere)`;
}
(function initSupporterMehrKnopf() {
const btn = document.getElementById("supporter-mehr-btn");
if (!btn) return;
btn.addEventListener("click", () => {
supporterAlleZeigen = !supporterAlleZeigen;
renderTabelle();
if (!supporterAlleZeigen) {
const sanft = !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
document.getElementById("supp-root").scrollIntoView({ behavior: sanft ? "smooth" : "auto", block: "start" });
}
});
})();
function renderTabelle() {
const liste = gefilterteSortierteListe();
const tbody = document.getElementById("supporter-tbody");
@@ -2715,9 +2841,11 @@
? "Keine Treffer für diese Filter-/Suchkombination."
: "Noch keine Einträge — sobald sich jemand als Supporter registriert, taucht er hier auf.";
tbody.innerHTML = `<tr><td colspan="8"><p class="vw-leer">${text}</p></td></tr>`;
supporterMehrKnopfSetzen(0);
return;
}
tbody.innerHTML = liste
supporterMehrKnopfSetzen(liste.length);
tbody.innerHTML = (supporterAlleZeigen ? liste : liste.slice(0, SUPPORTER_VORSCHAU))
.map((s) => {
const status = s.sub_status || "none";
const p = s.progress || { cycleNumber: 1, currentCycleMonth: 0 };