From e2bd436084c1450fe74ce38033bb067912a769f8 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Fri, 21 Aug 2026 18:17:47 +0200 Subject: [PATCH] =?UTF-8?q?Arbeit=20der=20parallelen=20Session=20gesichert?= =?UTF-8?q?=20(Profilbilder=20f=C3=BCr=20Stimmen,=20Diene-Zugang)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- assets/css/main.css | 55 ++++++++++++++ assets/js/data-stimmen-avatare.js | 43 +++++++++++ assets/js/i18n-stimmen.js | 11 +++ assets/js/stimmen.js | 45 ++++++++++- gate.html | 30 ++++++++ server/gate.js | 8 +- stimmen.html | 16 ++++ verwaltung.html | 119 ++++++++++++++++++++++++++++++ 8 files changed, 324 insertions(+), 3 deletions(-) create mode 100644 assets/js/data-stimmen-avatare.js diff --git a/assets/css/main.css b/assets/css/main.css index f642de3c..696338b9 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -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 { diff --git a/assets/js/data-stimmen-avatare.js b/assets/js/data-stimmen-avatare.js new file mode 100644 index 00000000..be07da50 --- /dev/null +++ b/assets/js/data-stimmen-avatare.js @@ -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; +}; diff --git a/assets/js/i18n-stimmen.js b/assets/js/i18n-stimmen.js index 8c9a5ef2..33cd52d9 100644 --- a/assets/js/i18n-stimmen.js +++ b/assets/js/i18n-stimmen.js @@ -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ê …", diff --git a/assets/js/stimmen.js b/assets/js/stimmen.js index c6383eb3..b372eb43 100644 --- a/assets/js/stimmen.js +++ b/assets/js/stimmen.js @@ -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 `; + + wahlEl.innerHTML = + kachel("", ``, "Kein Bild") + + liste.map((a) => kachel(a.id, ``, 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) => `

${escapeHtml(t.nachricht)}

- + ${ + /* 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)) + ? `` + : `` + } ${escapeHtml(t.name)} ${t.tiktokUsername ? `@${escapeHtml(t.tiktokUsername)}` : ""} diff --git a/gate.html b/gate.html index 2b7f08f3..80531a9e 100644 --- a/gate.html +++ b/gate.html @@ -222,6 +222,25 @@ background: linear-gradient(120deg, #ff8fc6 0%, #c084fc 30%, #ffd76a 55%, #ff8fc6 80%, #c084fc 100%); background-size: 340% 340%; } + /* Dienes Kachel (21.08.2026, Nutzer-Wunsch: "mach eine 3 Zugangskiste ... + ich will nämlich dass Diene schon jetzt auch die Seite sieht, damit wir + mit mehreren Augen die letzten kleinen Details verbessern"). Wieder eine + EIGENE Farbwelt (Smaragd/Mint/Gold) statt einer Umfärbung der beiden + anderen -- so ist auf einen Blick klar, welche Kachel wem gehört. */ + .gate-card--diene { + background: + linear-gradient(180deg, rgba(13,24,20,.92), rgba(8,15,13,.96)) padding-box, + linear-gradient(110deg, #6ee7b7, #34d399, #d4af37, #6ee7b7) border-box; + box-shadow: 0 30px 70px -25px rgba(0,0,0,.75), 0 0 50px -20px rgba(52,211,153,.35); + } + .gate-card-icon--diene { background-image: url('assets/img/avatar-diene.jpg'); } + .gate-card--diene .gate-card-icon { border-color: rgba(110,231,183,.65); } + .gate-card--diene .gate-card-name { color: #c7f5e3; } + .gate-card--diene input:focus { border-color: rgba(52,211,153,.7); box-shadow: 0 0 0 4px rgba(52,211,153,.16); } + .gate-card-btn--diene { + background: linear-gradient(120deg, #6ee7b7 0%, #34d399 30%, #ffd76a 55%, #6ee7b7 80%, #34d399 100%); + background-size: 340% 340%; + } .gate-card .gate-fehler { margin-top: .7rem; } @media (max-width: 480px) { .gate-cards { flex-direction: column; align-items: center; } @@ -275,6 +294,16 @@

+ +
+ +

Diene

+ + +

+
@@ -374,6 +403,7 @@ } bindeGateKachel("gate-form-dogi", "gate-code-dogi", "gate-fehler-dogi"); bindeGateKachel("gate-form-vanvan", "gate-code-vanvan", "gate-fehler-vanvan"); + bindeGateKachel("gate-form-diene", "gate-code-diene", "gate-fehler-diene"); /* Service-Worker-Registrierung — auf allen anderen Seiten macht das main.js, diese Seite ist aber bewusst eigenstaendig und laedt main.js nicht. Ohne registrierten Service diff --git a/server/gate.js b/server/gate.js index 547c39e1..f12c04e0 100644 --- a/server/gate.js +++ b/server/gate.js @@ -194,6 +194,12 @@ export async function gateAuthHandler(req, res) { let role = null; if (process.env.SITE_ACCESS_CODE_DOGI && timingSafeEqual(code, process.env.SITE_ACCESS_CODE_DOGI)) role = "dogi"; else if (process.env.SITE_ACCESS_CODE_VANVAN && timingSafeEqual(code, process.env.SITE_ACCESS_CODE_VANVAN)) role = "vanvan"; + // Dritter Zugang, 21.08.2026 (Nutzer-Wunsch: "ich will nämlich dass Diene + // schon jetzt auch die Seite sieht, damit wir mit mehreren Augen die + // letzten kleinen Details verbessern"). Eigener Code + eigene Rolle statt + // Weitergabe eines bestehenden Codes -- so lässt sich der Zugang später + // einzeln wieder entziehen, ohne Dogi oder VanVan auszusperren. + else if (process.env.SITE_ACCESS_CODE_DIENE && timingSafeEqual(code, process.env.SITE_ACCESS_CODE_DIENE)) role = "diene"; if (!role) { recordGateFailedAttempt(clientKey); @@ -211,7 +217,7 @@ export async function gateAuthHandler(req, res) { export function gateMiddleware(req, res, next) { const pfad = req.path; - const codesKonfiguriert = !!(process.env.SITE_ACCESS_SECRET && (process.env.SITE_ACCESS_CODE_DOGI || process.env.SITE_ACCESS_CODE_VANVAN)); + const codesKonfiguriert = !!(process.env.SITE_ACCESS_SECRET && (process.env.SITE_ACCESS_CODE_DOGI || process.env.SITE_ACCESS_CODE_VANVAN || process.env.SITE_ACCESS_CODE_DIENE)); if (!codesKonfiguriert) return next(); if (pfad === "/gate-auth" && req.method === "POST") { diff --git a/stimmen.html b/stimmen.html index 05e25491..db4ae117 100644 --- a/stimmen.html +++ b/stimmen.html @@ -67,6 +67,21 @@ + +
+ Dein Profilbild (optional) +

Ohne Auswahl erscheint einfach der erste Buchstabe deines Namens.

+
+

Danke dir! Deine Nachricht wurde eingereicht und wird von Dogi persönlich geprüft. 🩵

@@ -87,6 +102,7 @@ + diff --git a/verwaltung.html b/verwaltung.html index 4950cd37..96bbc511 100644 --- a/verwaltung.html +++ b/verwaltung.html @@ -69,6 +69,15 @@ Sättigung und weniger Helligkeit → sticht jetzt genauso klar hervor wie die anderen drei Akzentfarben. ===================================================================== */ + /* Profilbild-Auswahl in "Stimmen verwalten" (21.08.2026). Die Kacheln + selbst kommen aus main.css (.stimmen-avatar-*) — hier nur die Einbettung + in die Verwaltungs-Karte, etwas kleiner als auf der oeffentlichen Seite, + damit eine Einreichung nicht unnoetig hoch wird. */ + .vw-stimme-avatar-block { margin-top: 1rem; padding-top: .9rem; border-top: 1px solid var(--border); } + .vw-stimme-avatar-wahl { grid-template-columns: repeat(auto-fill, minmax(46px, 1fr)); max-width: 520px; } + .vw-stimme-upload { display: block; margin-top: .8rem; color: var(--text-muted); } + .vw-stimme-upload input { display: block; margin-top: .3rem; } + #supp-root, #vw-stats-root, #session-bar { --accent: #7dd3fc; --accent-2: #38bdf8; --accent-contrast: #1a1408; } body { position: relative; } /* Nutzer-Wunsch 04.08.2026 (3. Runde): "alles bisschen größer, Schrift @@ -1211,6 +1220,7 @@ +