"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
+37 -27
View File
@@ -16,37 +16,47 @@ window.I18N_PAGE = {
de: "Was Community und Team sagen.", "de-CH": "Was d Community und s Team säge.",
en: "What the community and the team say.", fr: "Ce que disent la communauté et l'équipe.", pt: "O que a comunidade e a equipe dizem.",
},
st_quote_community: {
de: `„Hier steht bald eine echte Stimme aus der Community. 🩵"`,
"de-CH": `„Da staht bald e echti Stimm us de Community. 🩵"`,
en: `"A real voice from the community will be here soon. 🩵"`,
fr: `« Un vrai témoignage de la communauté arrivera bientôt ici. 🩵 »`,
pt: `„Em breve, uma voz real da comunidade estará aqui. 🩵"`,
/* Eigene Stimme einreichen (20.08.2026) */
st_form_titel: {
de: "Deine Stimme zählt", "de-CH": "Dini Stimm zählt",
en: "Your Voice Matters", fr: "Ta voix compte", pt: "Sua voz importa",
},
st_label_community: {
de: "— Community-Mitglied", "de-CH": "— Community-Mitglied",
en: "— Community Member", fr: "— Membre de la communauté", pt: "— Membro da comunidade",
st_form_text: {
de: "Was bedeutet dir Team Dogi? Schreib uns deine Geschichte — mit etwas Glück liest DogFather sie persönlich und stellt sie hier auf die Seite.",
"de-CH": "Was bedütet dir Team Dogi? Schriib eus dini Gschicht — mit chli Glück list DogFather sie persönlich und stellt sie da uf d Site.",
en: "What does Team Dogi mean to you? Write us your story — with a bit of luck, DogFather will read it personally and feature it here on the site.",
fr: "Que représente Team Dogi pour toi ? Écris-nous ton histoire — avec un peu de chance, DogFather la lira personnellement et la publiera ici.",
pt: "O que a Team Dogi significa para você? Escreva sua história — com um pouco de sorte, o DogFather vai ler pessoalmente e publicar aqui.",
},
st_quote_team: {
de: `„Hier steht bald eine echte Stimme aus dem Team. 🩵"`,
"de-CH": `„Da staht bald e echti Stimm vom Team. 🩵"`,
en: `"A real voice from the team will be here soon. 🩵"`,
fr: `« Un vrai témoignage de l'équipe arrivera bientôt ici. 🩵 »`,
pt: `„Em breve, uma voz real da equipe estará aqui. 🩵"`,
st_form_name_label: {
de: "Dein Name", "de-CH": "Dis Name",
en: "Your Name", fr: "Ton nom", pt: "Seu nome",
},
st_label_team: {
de: "— Team-Mitglied", "de-CH": "— Team-Mitglied",
en: "— Team Member", fr: "— Membre de l'équipe", pt: "— Membro da equipe",
st_form_tiktok_label: {
de: "TikTok-Name (optional)", "de-CH": "TikTok-Name (optional)",
en: "TikTok Username (optional)", fr: "Nom TikTok (facultatif)", pt: "Nome no TikTok (opcional)",
},
st_quote_scout: {
de: `„Hier steht bald eine echte Stimme von Scouts &amp; Creatorn. 🩵"`,
"de-CH": `„Da staht bald e echti Stimm vo Scouts &amp; Creator. 🩵"`,
en: `"A real voice from scouts &amp; creators will be here soon. 🩵"`,
fr: `« Un vrai témoignage de scouts et créateurs arrivera bientôt ici. 🩵 »`,
pt: `„Em breve, uma voz real de scouts e criadores estará aqui. 🩵"`,
st_form_nachricht_label: {
de: "Deine Nachricht", "de-CH": "Dini Nachricht",
en: "Your Message", fr: "Ton message", pt: "Sua mensagem",
},
st_label_scout: {
de: "— Scout/Creator", "de-CH": "— Scout/Creator",
en: "— Scout/Creator", fr: "— Scout/Créateur", pt: "— Scout/Criador",
st_form_nachricht_placeholder: {
de: "Erzähl uns, was dir Team Dogi bedeutet …", "de-CH": "Verzell eus, was dir Team Dogi bedütet …",
en: "Tell us what Team Dogi means to you …", fr: "Dis-nous ce que Team Dogi représente pour toi …", pt: "Conte-nos o que a Team Dogi significa para você …",
},
st_form_absenden: {
de: "💙 Nachricht einreichen", "de-CH": "💙 Nachricht ischicke",
en: "💙 Submit Message", fr: "💙 Envoyer le message", pt: "💙 Enviar mensagem",
},
st_form_erfolg: {
de: "Danke dir! Deine Nachricht wurde eingereicht und wird von Dogi persönlich geprüft. 🩵",
"de-CH": "Danke dir! Dini Nachricht isch ischickt worde und wird vo Dogi persönlich gprüeft. 🩵",
en: "Thank you! Your message has been submitted and will be reviewed by Dogi personally. 🩵",
fr: "Merci ! Ton message a été envoyé et sera examiné personnellement par Dogi. 🩵",
pt: "Obrigado! Sua mensagem foi enviada e será revisada pessoalmente pelo Dogi. 🩵",
},
st_echte_titel: {
de: "Das sagt die Community", "de-CH": "Das seit d Community",
en: "What the Community Says", fr: "Ce que dit la communauté", pt: "O que a comunidade diz",
},
};
+80
View File
@@ -0,0 +1,80 @@
/* =====================================================================
stimmen.js — NUR für stimmen.html (20.08.2026, Nutzer-Wunsch: "eine
richtig geile kachel ... wo die leute mit namen und tiktok namen mir
eine nachricht schreiben können die ich in die verwaltung kriege").
Formular schickt eine Einreichung ab (landet als Entwurf, siehe
server-internal/routes/testimonials.js), zweiter Teil lädt die bereits
freigegebenen Stimmen und zeigt sie an — bleibt komplett unsichtbar,
solange keine einzige freigegeben wurde. ===================================================================== */
(function () {
const API_BASE_URL = "https://postfach.dogfather-universe.com";
const formular = document.getElementById("stimmen-formular");
if (formular) {
const erfolgEl = document.getElementById("stimmen-erfolg");
const fehlerEl = document.getElementById("stimmen-fehler");
formular.addEventListener("submit", async (e) => {
e.preventDefault();
erfolgEl.classList.remove("zeige");
fehlerEl.classList.remove("zeige");
const name = document.getElementById("st-name").value.trim();
const tiktokUsername = document.getElementById("st-tiktok").value.trim();
const nachricht = document.getElementById("st-nachricht").value.trim();
const absendenBtn = formular.querySelector("button[type=submit]");
absendenBtn.disabled = true;
try {
const res = await fetch(`${API_BASE_URL}/testimonials/submit`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, tiktokUsername, nachricht }),
});
const data = await res.json();
if (!data.ok) {
fehlerEl.textContent = data.error || "Einreichen hat leider nicht geklappt. Bitte versuch es nochmal.";
fehlerEl.classList.add("zeige");
return;
}
formular.hidden = true;
erfolgEl.classList.add("zeige");
} catch {
fehlerEl.textContent = "Verbindung zum Server fehlgeschlagen. Bitte versuch es nochmal.";
fehlerEl.classList.add("zeige");
} finally {
absendenBtn.disabled = false;
}
});
}
function escapeHtml(text) {
return String(text ?? "").replace(/[&<>"']/g, (z) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[z]));
}
function ladeEchteStimmen() {
fetch(`${API_BASE_URL}/testimonials`, { cache: "no-store" })
.then((res) => res.json())
.then((data) => {
const liste = data && data.ok ? data.testimonials : [];
if (!liste.length) return; // Abschnitt bleibt hidden, kein Platzhalter mehr
const root = document.getElementById("stimmen-echte-root");
const grid = document.getElementById("stimmen-echte-grid");
grid.innerHTML = liste
.map(
(t) => `<div class="stimmen-karte">
<p class="stimmen-karte-text">${escapeHtml(t.nachricht)}</p>
<div class="stimmen-karte-fuss">
<span class="stimmen-karte-name">${escapeHtml(t.name)}</span>
${t.tiktokUsername ? `<span class="stimmen-karte-tiktok">@${escapeHtml(t.tiktokUsername)}</span>` : ""}
</div>
</div>`
)
.join("");
root.hidden = false;
})
.catch(() => {
/* Abschnitt bleibt einfach unsichtbar -- kein Fehler nötig. */
});
}
ladeEchteStimmen();
})();