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:
+134
-6
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user