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:
+119
@@ -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) =====
|
||||
|
||||
Reference in New Issue
Block a user