"Stimmen" verwandelt: Fans reichen selbst Testimonials ein

Nutzer-Wunsch 20.08.2026: "eine richtig geile kachel ... wo die leute mit
namen und tiktok namen mir eine nachricht schreiben können die ich in die
verwaltung kriege, und dann kann ich die ausgewählten über die
verwaltungsseite auf die website hinzufügen ... das wird mega persoenlich
zu den fans, bitte wirklich krass geil speziell."

- Neue, augenschonend gestaltete Einreich-Kachel auf stimmen.html: Name,
  TikTok-Name (optional), Nachricht -- landet NIE automatisch oeffentlich,
  sondern immer erst als Entwurf in der Verwaltung.
  Warmer Babyblau/Rosé-Farbverlauf, wandernder Lichtschein, pulsierendes
  Herz -- ersetzt die 3 ewigen "Hier steht bald..."-Platzhalterkarten.
- Neue Verwaltungs-Sektion "💬 Stimmen verwalten": Warteschlange (offene
  zuerst), Freigeben/Ablehnen/Löschen pro Eintrag.
- Freigegebene Stimmen erscheinen automatisch in einer neuen, spezielleren
  Zitat-Kartenoptik (großes Anführungszeichen, Name + TikTok-Chip) --
  Abschnitt bleibt komplett unsichtbar, solange keine einzige freigegeben
  wurde.
- Backend: neue Tabelle `testimonials` (Migration 0006), routes/
  testimonials.js (oeffentliches Einreichen + Lesen freigegebener,
  admin-Warteschlange + Status/Loeschen mit neuer TESTIMONIALS_MANAGE-
  Berechtigung). 18 automatisierte Tests gegen eine Fake-DB bestanden.
- Alles per Playwright visuell durchgespielt: leerer Zustand, Einreichen
  -> Erfolgsmeldung, freigegebene Stimmen-Anzeige, Verwaltungs-Warteschlange
  mit allen Aktionen.

Backend-Teil (server-internal/, cloudflare-worker/migrations/) noch ohne
Deploy-Zugriff -- Dogi muss ihn manuell auf dogiintern ausrollen.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-20 23:37:31 +02:00
co-authored by Claude Opus 5
parent 519e209bce
commit 399260b805
43 changed files with 693 additions and 275 deletions
+77 -6
View File
@@ -19,10 +19,10 @@
<link rel="apple-touch-icon" href="assets/img/apple-touch-icon-admin.png" />
<meta name="apple-mobile-web-app-title" content="DogiCrew-Verwaltung" />
<meta name="theme-color" content="#0e0a16" />
<link rel="stylesheet" href="assets/css/main.css?v=20260820j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260820j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260820j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260820j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260820k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260820k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260820k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260820k" />
<style>
/* =====================================================================
verwaltung.html — Dogis privater Command-Center-Bereich.
@@ -844,6 +844,20 @@
</div>
</section>
<!-- ===== "Stimmen" verwalten (20.08.2026) — Nutzer-Wunsch: "die
ausgewählten über die verwaltungsseite auf die website hinzufügen".
Eingereichte Nachrichten von stimmen.html landen hier zur Prüfung,
werden NIE automatisch veröffentlicht. ===== -->
<section class="section-tight" id="testimonials-root" style="display:none;padding-top:0;">
<div class="container">
<h2 class="text-center" style="margin-bottom:.3rem;">💬 Stimmen verwalten</h2>
<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>
</section>
<!-- ===== Supporter-Liste ===== -->
<section class="section-tight" id="supp-root" style="display:none;padding-top:0;">
<div class="container" id="liste-ansicht">
@@ -1018,8 +1032,8 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/main.js?v=20260820j"></script>
<script src="assets/js/admin-auth.js?v=20260820j"></script>
<script src="assets/js/main.js?v=20260820k"></script>
<script src="assets/js/admin-auth.js?v=20260820k"></script>
<script>
(function () {
const API_BASE_URL = "https://postfach.dogfather-universe.com";
@@ -1095,6 +1109,10 @@
document.getElementById("special-event-root").style.display = "";
await ladeUndRenderSpecialEvent();
}
if (window.AdminAuth.sessionHasPermission(session, "TESTIMONIALS_MANAGE")) {
document.getElementById("testimonials-root").style.display = "";
await ladeUndRenderTestimonials();
}
const pollsBox = document.getElementById("box-abstimmungen");
if (window.AdminAuth.sessionHasPermission(session, "POLLS_MANAGE")) {
pollsBox.style.display = "";
@@ -1113,6 +1131,7 @@
document.getElementById("supp-root").style.display = "none";
document.getElementById("events-root").style.display = "none";
document.getElementById("special-event-root").style.display = "none";
document.getElementById("testimonials-root").style.display = "none";
document.getElementById("box-abstimmungen").style.display = "none";
}
@@ -1477,6 +1496,58 @@
});
}
/* ===== "Stimmen" — Verwaltung (20.08.2026) =====
Offene Einreichungen zuerst (siehe getTestimonialsAdmin-Sortierung),
jede Karte mit Freigeben/Ablehnen/Löschen — je nach aktuellem Status
stehen nur die sinnvollen Aktionen zur Verfügung. */
const TESTIMONIAL_STATUS_LABELS = { eingereicht: "🟡 Neu", freigegeben: "🟢 Freigegeben", abgelehnt: "⚪ Abgelehnt" };
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;">
<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;">
${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>
</div>`;
}
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>`;
return;
}
const liste = res.testimonials || [];
listeEl.innerHTML = liste.length ? liste.map(testimonialKarteHtml).join("") : `<p class="vw-leer">Noch keine Einreichungen.</p>`;
const aktion = async (btn, status) => {
const id = btn.closest("[data-testimonial-id]").dataset.testimonialId;
btn.disabled = true;
await window.AdminAuth.apiCall(API_BASE_URL, "/admin/testimonials/status", { id, status });
await ladeUndRenderTestimonials();
};
listeEl.querySelectorAll(".testimonial-freigeben-btn").forEach((btn) => btn.addEventListener("click", () => aktion(btn, "freigegeben")));
listeEl.querySelectorAll(".testimonial-ablehnen-btn").forEach((btn) => btn.addEventListener("click", () => aktion(btn, "abgelehnt")));
listeEl.querySelectorAll(".testimonial-loeschen-btn").forEach((btn) => {
btn.addEventListener("click", async () => {
if (!confirm("Diese Einreichung unwiderruflich löschen?")) return;
const id = btn.closest("[data-testimonial-id]").dataset.testimonialId;
btn.disabled = true;
await window.AdminAuth.apiCall(API_BASE_URL, "/admin/testimonials/delete", { id });
await ladeUndRenderTestimonials();
});
});
}
// Nutzer-Wunsch 04.08.2026 (6. Runde): zwei getrennte Kisten/Formulare
// (Dogi + VanVan) statt einem gemeinsamen Feld — beide rufen denselben
// Login-Endpunkt auf, die Fehlermeldung erscheint jeweils nur in der