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]>
135 lines
6.2 KiB
JavaScript
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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[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();
|
|
})();
|