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]>
This commit is contained in:
2026-08-21 18:17:47 +02:00
co-authored by Claude Opus 5
parent d0613f86c1
commit e2bd436084
8 changed files with 324 additions and 3 deletions
+43
View File
@@ -0,0 +1,43 @@
/* =====================================================================
data-stimmen-avatare.js — Auswahlbilder für die "Stimmen" (21.08.2026,
Nutzer-Wunsch: "ich soll auch paar geile bilder zum aussuchen haben als
profilbild falls die leute kein bild mit schicken, das soll denen auch
als möglichkeit gegeben sein").
EINE gemeinsame Liste für beide Seiten: stimmen.html (dort wählt die
einreichende Person selbst) und verwaltung.html (dort kann Dogi/VanVan
das Bild nachträglich ändern). Getrennte Listen wären auseinander-
gelaufen, sobald irgendwann ein Bild dazukommt oder wegfällt.
Bewusst nur Figuren, Maskottchen und das Logo — KEINE echten Personen
aus dem Team. Sonst könnte sich eine fremde Person mit dem Gesicht von
Dogi, VanVan oder einem Modi als Absender darstellen.
WICHTIG: Die "id"-Werte müssen mit der Liste
ERLAUBTE_AVATARE in server-internal/routes/testimonials.js
übereinstimmen — der Server nimmt nur bekannte Ids an, damit über das
öffentliche Formular kein beliebiger Pfad eingeschleust werden kann.
===================================================================== */
window.STIMMEN_AVATARE = [
{ id: "husky-hoodie", name: "DogFather-Husky", datei: "/assets/img/stimmen-avatare/stimme-husky-hoodie.png" },
{ id: "hase-hoodie", name: "HasiDog", datei: "/assets/img/stimmen-avatare/stimme-hase-hoodie.png" },
{ id: "husky-kopf", name: "Husky-Kopf", datei: "/assets/img/stimmen-avatare/stimme-husky-kopf.png" },
{ id: "husky-kosmos", name: "Kosmos-Husky", datei: "/assets/img/stimmen-avatare/stimme-husky-kosmos.png" },
{ id: "hase-kosmos", name: "Kosmos-Hase", datei: "/assets/img/stimmen-avatare/stimme-hase-kosmos.png" },
{ id: "casper", name: "Casper", datei: "/assets/img/stimmen-avatare/stimme-casper.png" },
{ id: "casper-blick", name: "Casper (Blick)", datei: "/assets/img/stimmen-avatare/stimme-casper-blick.png" },
{ id: "logo", name: "DogFather-Logo", datei: "/assets/img/stimmen-avatare/stimme-logo.png" },
];
/* Liefert die Bilddatei zu einer gespeicherten Auswahl.
- bekannte Id -> das zugehörige Auswahlbild
- "/uploads/…" -> ein von der Verwaltung hochgeladenes eigenes Foto
(kommt als vollständige Adresse vom Server, siehe testimonials.js)
- sonst -> null, dann zeigt die Karte wie bisher den Anfangs-
buchstaben des Namens. */
window.stimmeAvatarBild = function (wert) {
if (!wert) return null;
if (/^https?:\/\//i.test(wert)) return wert;
const treffer = (window.STIMMEN_AVATARE || []).find((a) => a.id === wert);
return treffer ? treffer.datei : null;
};
+11
View File
@@ -40,6 +40,17 @@ window.I18N_PAGE = {
de: "Deine Nachricht", "de-CH": "Dini Nachricht",
en: "Your Message", fr: "Ton message", pt: "Sua mensagem",
},
st_form_avatar_label: {
de: "Dein Profilbild (optional)", "de-CH": "Dis Profilbild (optional)",
en: "Your profile picture (optional)", fr: "Ta photo de profil (facultatif)", pt: "Sua foto de perfil (opcional)",
},
st_form_avatar_hinweis: {
de: "Ohne Auswahl erscheint einfach der erste Buchstabe deines Namens.",
"de-CH": "Ohni Uuswahl erschiint eifach de erscht Buechstab vo dim Name.",
en: "If you don't pick one, the first letter of your name is shown instead.",
fr: "Sans choix, la première lettre de ton prénom s'affichera.",
pt: "Sem escolha, aparece apenas a primeira letra do seu nome.",
},
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ê …",
+43 -2
View File
@@ -9,6 +9,38 @@
(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");
@@ -28,7 +60,7 @@
const res = await fetch(`${API_BASE_URL}/testimonials/submit`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, tiktokUsername, nachricht }),
body: JSON.stringify({ name, tiktokUsername, nachricht, avatar: gewaehlterAvatar || null }),
});
const data = await res.json();
if (!data.ok) {
@@ -74,7 +106,16 @@
(t) => `<div class="stimmen-karte">
<p class="stimmen-karte-text">${escapeHtml(t.nachricht)}</p>
<div class="stimmen-karte-fuss">
<span class="stimmen-karte-avatar" aria-hidden="true">${escapeHtml(erstesZeichen(t.name))}</span>
${
/* 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>` : ""}