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:
@@ -4107,6 +4107,61 @@ body.start-seite .music-toggle:focus-visible {
|
||||
background: linear-gradient(135deg, #ffd0e8, #a8e4f2);
|
||||
box-shadow: 0 0 16px rgba(255, 154, 208, .3);
|
||||
}
|
||||
/* Gewähltes Profilbild statt Anfangsbuchstabe (21.08.2026). Erbt Größe und
|
||||
runde Form von .stimmen-karte-avatar oben, ersetzt nur Füllung/Farbe. */
|
||||
.stimmen-karte-avatar-bild {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background: rgba(255, 255, 255, .05);
|
||||
box-shadow: 0 0 14px rgba(212, 175, 55, .25);
|
||||
}
|
||||
.stimmen-karte-avatar-bild img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
|
||||
/* ---------- Profilbild-Auswahl im Einreichen-Formular (21.08.2026) ----------
|
||||
Nutzer-Wunsch: "paar geile bilder zum aussuchen ... das soll denen auch
|
||||
als möglichkeit gegeben sein". Kachelraster, das sich der Breite anpasst;
|
||||
die getroffene Wahl bekommt einen goldenen Ring statt nur einer leichten
|
||||
Aufhellung — sonst ist auf dunklem Grund kaum erkennbar, was gewählt ist. */
|
||||
.stimmen-avatar-titel {
|
||||
display: block;
|
||||
font-size: .82rem; font-weight: 700; letter-spacing: .04em;
|
||||
text-transform: uppercase; color: var(--text-muted);
|
||||
margin-bottom: .25rem;
|
||||
}
|
||||
.stimmen-avatar-hinweis { margin: 0 0 .7rem; }
|
||||
.stimmen-avatar-wahl {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(58px, 1fr));
|
||||
gap: .6rem;
|
||||
}
|
||||
.stimmen-avatar-kachel {
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
background: rgba(255, 255, 255, .04);
|
||||
border: 2px solid rgba(255, 255, 255, .12);
|
||||
transition: transform .16s ease, border-color .16s ease, box-shadow .16s ease;
|
||||
}
|
||||
.stimmen-avatar-kachel img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.stimmen-avatar-keins {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
width: 100%; height: 100%;
|
||||
font-size: 1.15rem; color: var(--text-muted);
|
||||
}
|
||||
.stimmen-avatar-kachel:hover { transform: translateY(-2px); border-color: rgba(255, 233, 168, .6); }
|
||||
.stimmen-avatar-kachel.aktiv {
|
||||
border-color: #ffe9a8;
|
||||
box-shadow: 0 0 0 2px rgba(212, 175, 55, .45), 0 0 18px rgba(212, 175, 55, .4);
|
||||
}
|
||||
.stimmen-avatar-kachel:focus-visible { outline: 2px solid #ffe9a8; outline-offset: 3px; }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.stimmen-avatar-kachel { transition: none; }
|
||||
.stimmen-avatar-kachel:hover { transform: none; }
|
||||
}
|
||||
|
||||
.stimmen-karte-namen { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
|
||||
.stimmen-karte-name { font-weight: 700; color: var(--text); font-size: .95rem; }
|
||||
.stimmen-karte-tiktok {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
@@ -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>` : ""}
|
||||
|
||||
Reference in New Issue
Block a user