Files
dogfather-universe/assets/js/stimmen.js
T
DogFatherGitandClaude Opus 5 e2bd436084 Arbeit der parallelen Session gesichert (Profilbilder für Stimmen, Diene-Zugang)
Wie beim vorherigen Mal lag das nur als Arbeitskopie auf dem Server, nicht
in git. Unverändert übernommen, bevor darauf aufgebaut wird:
- Profilbild-Auswahl für die Stimmen (data-stimmen-avatare.js neu,
  stimmen.js, i18n-stimmen.js, stimmen.html, verwaltung.html, main.css)
- Dritte Zugangs-Kachel für Diene (gate.html, server/gate.js)

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-21 18:17:47 +02:00

135 lines
6.2 KiB
JavaScript

/* =====================================================================
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";
/* ---------- Profilbild-Auswahl (21.08.2026) ----------
Baut die Kacheln aus der gemeinsamen Liste in data-stimmen-avatare.js
auf. Bewusst echte <button>-Elemente statt <div>: so ist die Auswahl
auch mit der Tastatur bedienbar und Screenreader melden sie als
Schaltfläche. "" = bewusst kein Bild (erste Kachel). */
let gewaehlterAvatar = "";
const wahlEl = document.getElementById("stimmen-avatar-wahl");
if (wahlEl) {
const liste = window.STIMMEN_AVATARE || [];
const kachel = (id, inhalt, titel) =>
`<button type="button" class="stimmen-avatar-kachel" data-avatar="${id}" role="radio" aria-checked="false" title="${titel}" aria-label="${titel}">${inhalt}</button>`;
wahlEl.innerHTML =
kachel("", `<span class="stimmen-avatar-keins" aria-hidden="true">✦</span>`, "Kein Bild") +
liste.map((a) => kachel(a.id, `<img src="${a.datei}" alt="" loading="lazy" />`, a.name)).join("");
const markiere = () => {
wahlEl.querySelectorAll(".stimmen-avatar-kachel").forEach((b) => {
const aktiv = b.dataset.avatar === gewaehlterAvatar;
b.classList.toggle("aktiv", aktiv);
b.setAttribute("aria-checked", aktiv ? "true" : "false");
});
};
wahlEl.addEventListener("click", (e) => {
const btn = e.target.closest(".stimmen-avatar-kachel");
if (!btn) return;
gewaehlterAvatar = btn.dataset.avatar;
markiere();
});
markiere();
}
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, avatar: gewaehlterAvatar || null }),
});
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]));
}
/* Erstes sichtbares Zeichen des Namens fürs Avatar-Medaillon. [...name]
statt name[0], damit auch Namen, die mit einem Emoji beginnen, nicht
mitten in einem Zeichen abgeschnitten werden (ein Emoji besteht in
JavaScript aus mehreren "Zeichen" — name[0] lieferte sonst nur ein
kaputtes Halb-Zeichen). */
function erstesZeichen(name) {
const zeichen = [...String(name || "").trim()];
return zeichen.length ? zeichen[0].toUpperCase() : "?";
}
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">
${
/* Gewähltes Bild, sonst wie bisher der Anfangsbuchstabe.
escapeHtml auch auf der Bildadresse: sie stammt zwar aus
unserer eigenen Liste bzw. vom Server, aber sie landet
hier in einem HTML-Attribut — da gehört sie grundsätzlich
entschärft, unabhängig von der Herkunft. */
(window.stimmeAvatarBild && window.stimmeAvatarBild(t.avatar))
? `<span class="stimmen-karte-avatar stimmen-karte-avatar-bild" aria-hidden="true"><img src="${escapeHtml(window.stimmeAvatarBild(t.avatar))}" alt="" loading="lazy" /></span>`
: `<span class="stimmen-karte-avatar" aria-hidden="true">${escapeHtml(erstesZeichen(t.name))}</span>`
}
<span class="stimmen-karte-namen">
<span class="stimmen-karte-name">${escapeHtml(t.name)}</span>
${t.tiktokUsername ? `<span class="stimmen-karte-tiktok">@${escapeHtml(t.tiktokUsername)}</span>` : ""}
</span>
</div>
</div>`
)
.join("");
root.hidden = false;
})
.catch(() => {
/* Abschnitt bleibt einfach unsichtbar -- kein Fehler nötig. */
});
}
ladeEchteStimmen();
})();