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
+119
View File
@@ -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 @@
<script src="assets/js/data-scouts.js?v=20260821p"></script>
<script src="assets/js/data-creator.js?v=20260821p"></script>
<script src="assets/js/data-manager.js?v=20260821p"></script>
<script src="assets/js/data-stimmen-avatare.js?v=20260821p"></script>
<script src="assets/js/main.js?v=20260821p"></script>
<script src="assets/js/admin-auth.js?v=20260821p"></script>
<script>
@@ -1689,6 +1699,43 @@
stehen nur die sinnvollen Aktionen zur Verfügung. */
const TESTIMONIAL_STATUS_LABELS = { eingereicht: "🟡 Neu", freigegeben: "🟢 Freigegeben", abgelehnt: "⚪ Abgelehnt" };
/* Profilbild-Auswahl je Einreichung (21.08.2026, Nutzer-Wunsch: "ich soll
auch paar geile bilder zum aussuchen haben als profilbild"). Nutzt
dieselbe Liste wie die öffentliche Seite (data-stimmen-avatare.js), damit
Dogi genau die Bilder sieht, die es auch wirklich gibt. Zusätzlich zu den
Auswahlbildern kann hier ein eigenes Foto hochgeladen werden — das darf
bewusst NUR die Verwaltung, nicht das öffentliche Formular. */
function avatarWahlHtml(t) {
const liste = window.STIMMEN_AVATARE || [];
const aktiv = (wert) => (t.avatar === wert ? " aktiv" : "");
const eigenes = t.avatar && /^https?:\/\//i.test(t.avatar);
return `
<div class="vw-stimme-avatar-block">
<span class="small" style="color:var(--text-muted);display:block;margin-bottom:.45rem;">Profilbild</span>
<div class="stimmen-avatar-wahl vw-stimme-avatar-wahl">
<button type="button" class="stimmen-avatar-kachel${aktiv(null) || (t.avatar ? "" : " aktiv")}" data-avatar="" title="Kein Bild (Anfangsbuchstabe)">
<span class="stimmen-avatar-keins" aria-hidden="true">✦</span>
</button>
${liste
.map(
(a) =>
`<button type="button" class="stimmen-avatar-kachel${aktiv(a.id)}" data-avatar="${a.id}" title="${escapeHtml(a.name)}"><img src="${a.datei}" alt="" loading="lazy" /></button>`
)
.join("")}
${
eigenes
? `<button type="button" class="stimmen-avatar-kachel aktiv" data-avatar="${escapeHtml(t.avatar)}" title="Eigenes Foto"><img src="${escapeHtml(t.avatar)}" alt="" loading="lazy" /></button>`
: ""
}
</div>
<label class="small vw-stimme-upload">
Eigenes Foto hochladen (max. 5 MB, JPG/PNG/WebP)
<input type="file" class="testimonial-foto-input" accept="image/jpeg,image/png,image/webp" />
</label>
<p class="small testimonial-avatar-status" style="margin:.35rem 0 0;"></p>
</div>`;
}
function testimonialKarteHtml(t) {
return `<div class="card vw-card vw-shine-card" style="margin-bottom:1rem;padding:1.3rem 1.4rem;" data-testimonial-id="${t.id}">
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:1rem;flex-wrap:wrap;">
@@ -1703,6 +1750,7 @@
</div>
</div>
<p style="margin:.8rem 0 0;">${escapeHtml(t.nachricht)}</p>
${avatarWahlHtml(t)}
</div>`;
}
@@ -1733,6 +1781,77 @@
await ladeUndRenderTestimonials();
});
});
/* Profilbild anklicken = sofort speichern. Bewusst OHNE die ganze Liste
neu zu laden: sonst springt die Ansicht bei jedem Klick nach oben und
man verliert die Stelle, an der man gerade arbeitet. Stattdessen wird
nur die Markierung innerhalb dieser einen Karte umgesetzt. */
listeEl.querySelectorAll(".vw-stimme-avatar-wahl").forEach((wahl) => {
wahl.addEventListener("click", async (e) => {
const kachel = e.target.closest(".stimmen-avatar-kachel");
if (!kachel) return;
const karte = kachel.closest("[data-testimonial-id]");
const statusEl = karte.querySelector(".testimonial-avatar-status");
const avatar = kachel.dataset.avatar || "";
const res = await window.AdminAuth.apiCall(API_BASE_URL, "/admin/testimonials/avatar", {
id: karte.dataset.testimonialId,
avatar,
});
if (!res.ok) {
statusEl.textContent = res.error || "Speichern fehlgeschlagen.";
statusEl.style.color = "#ff6b6b";
return;
}
wahl.querySelectorAll(".stimmen-avatar-kachel").forEach((b) => b.classList.remove("aktiv"));
kachel.classList.add("aktiv");
statusEl.textContent = "✅ Profilbild gespeichert.";
statusEl.style.color = "#baf7d6";
});
});
/* Eigenes Foto: wird sofort bei der Auswahl hochgeladen UND direkt als
Bild dieser Stimme gesetzt — ein zusätzlicher "Speichern"-Klick wäre
hier nur eine Stolperfalle (dieselbe Logik wie bei den Event-Bildern). */
listeEl.querySelectorAll(".testimonial-foto-input").forEach((input) => {
input.addEventListener("change", async () => {
const datei = input.files?.[0];
if (!datei) return;
const karte = input.closest("[data-testimonial-id]");
const statusEl = karte.querySelector(".testimonial-avatar-status");
statusEl.style.color = "";
statusEl.textContent = "Foto wird hochgeladen …";
const form = new FormData();
form.append("image", datei);
const session = window.AdminAuth.getSession();
try {
const res = await fetch(`${API_BASE_URL}/admin/testimonials/upload-avatar`, {
method: "POST",
headers: session?.token ? { Authorization: `Bearer ${session.token}` } : {},
body: form,
});
const data = await res.json();
if (!data.ok) {
statusEl.textContent = data.error || "Upload fehlgeschlagen.";
statusEl.style.color = "#ff6b6b";
return;
}
const gesetzt = await window.AdminAuth.apiCall(API_BASE_URL, "/admin/testimonials/avatar", {
id: karte.dataset.testimonialId,
avatar: data.url,
});
if (!gesetzt.ok) {
statusEl.textContent = gesetzt.error || "Foto konnte nicht gesetzt werden.";
statusEl.style.color = "#ff6b6b";
return;
}
await ladeUndRenderTestimonials();
} catch {
statusEl.textContent = "Upload fehlgeschlagen (keine Verbindung).";
statusEl.style.color = "#ff6b6b";
}
});
});
}
/* ===== "Team verwalten" — Modis/Scouts/Creator/Manager (21.08.2026) =====