Team-Verwaltung Runde 2: bestehende Mitglieder importieren + Kategorien/Seitentitel editierbar

Nutzer-Wunsch 21.08.2026: "wenn ich eine änderung mache ist es auch für
jeden für die die schon da sind und die neuen" + "ich will ich titel
und namen von kategorien und seiten ändern können in der verwaltung".

Backend (braucht Filipes manuellen Deploy):
- Migration 0008: team_members bekommt intro/bioHtml/extraCta-Spalten
  -- volle Feld-Parität mit den von Hand gepflegten Profilen (Diene/
  Patrick/Bananenstift/Marina nutzen diese Felder).
- routes/team.js: neue importLegacyMember()-Funktion -- übernimmt ein
  bestehendes Profil 1:1 in die Datenbank, OHNE erneut zu übersetzen
  (die vorhandenen, von Hand geschriebenen Übersetzungen bleiben
  erhalten). Idempotent: mehrfacher Import erzeugt keine Duplikate.
- routes/site-texts.js (neu): admin-editierbare Kategorie-Namen und
  Seitentitel, generischer key->{de,...}-Override über app_settings
  (wie "Event des Jahres"), fester Schlüssel-Katalog aus
  Sicherheitsgründen. Leeres Feld setzt auf den Standardtext zurück.
- 27 Tests in isolierter Testumgebung geprüft (kein better-sqlite3
  lokal kompilierbar).

Frontend:
- window.dogiTeamZusammenfuehren() (main.js): admin-Mitglieder
  ÜBERSCHREIBEN jetzt gleichnamige statische Einträge (per slug) statt
  sie zu duplizieren -- eine Bearbeitung wirkt dadurch für alle.
- window.dogiSiteTexteLaden() + applyTranslations() erweitert um
  data-site-text-key -- Kategorie-Seitentitel (team-modis.html/
  team-scouts.html/creator.html/manager.html) sind jetzt live editierbar.
- Verwaltung: "📥 Bestehende Mitglieder importieren"-Knopf (holt VanVan/
  Diene/Funny/Miss/Marina/Ghost/Patrick/Bananenstift aus den data-*.js-
  Dateien, VanVan bewusst ausgenommen -- eigene Sonderkarte + Seite),
  erweitertes Formular (Intro/ausführliche Vorstellung/zweiter Button,
  eingeklappt unter "Erweitert"), neue Sektion "Kategorien &
  Seitentitel" (4 Karten, sofort wirksam auf Website UND Verwaltung).

Beim Testen einen echten UX-Bug gefunden und gefixt: die "Gespeichert"-
Meldung nach dem Speichern eines Mitglieds wurde von der direkt
anschließenden Formular-Zurücksetzung sofort wieder überschrieben und
war nie sichtbar.

Cache-Busting-Version auf 20260821k erhöht.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-08-21 03:47:51 +02:00
co-authored by Claude Sonnet 5
parent 7060a4a6ae
commit 81f55d67db
41 changed files with 894 additions and 306 deletions
+6 -6
View File
@@ -18,10 +18,10 @@
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<meta name="robots" content="noindex" />
<title>Seite nicht gefunden — DOGFATHER UNIVERSE</title>
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
</head>
<body>
<div id="site-header"></div>
@@ -41,7 +41,7 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/i18n-404.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/i18n-404.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
</body>
</html>
+7 -7
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Unterstütze Dogfather dauerhaft mit der DogiCrew 🩵 und sammle exklusive Treueprämien — die Teddy-Kollektion entsteht in Kooperation mit VanVan." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
<style>
/* =====================================================================
Preis-/Anmelde-Kiste — komplett neu gestaltet (Nutzer-Feedback
@@ -454,9 +454,9 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/i18n-abonnieren.js?v=20260821j"></script>
<script src="assets/js/supporter.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/i18n-abonnieren.js?v=20260821k"></script>
<script src="assets/js/supporter.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
<script>
(function () {
const S = window.DogiSupporter;
+5 -5
View File
@@ -20,10 +20,10 @@
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<meta name="robots" content="noindex" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
</head>
<body>
<div id="site-header"></div>
@@ -134,6 +134,6 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821k"></script>
</body>
</html>
+7 -7
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Detailansicht einer Aktion oder eines Projekts von Team Dogi." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
</head>
<body>
<div id="site-header"></div>
@@ -51,9 +51,9 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/data-aktionen.js?v=20260821j"></script>
<script src="assets/js/i18n-aktion-detail.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/data-aktionen.js?v=20260821k"></script>
<script src="assets/js/i18n-aktion-detail.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
<script>
document.addEventListener("DOMContentLoaded", () => {
const params = new URLSearchParams(location.search);
+60
View File
@@ -199,6 +199,22 @@
if (text) el.setAttribute(attr, text);
});
});
// 21.08.2026 (Nutzer-Wunsch: "ich will ich titel und namen von
// kategorien und seiten ändern können in der verwaltung"): admin-
// editierbare Text-Overrides (siehe window.dogiSiteTexteLaden() weiter
// unten). Läuft bewusst NACH den normalen data-i18n-Standardtexten
// oben, damit ein gesetzter Override sie überschreiben kann. Fehlt ein
// Override (Normalfall, noch nichts in der Verwaltung eingetragen),
// bleibt einfach der eingebaute Standardtext stehen -- diese Funktion
// hier macht dann schlicht nichts.
const siteTexte = window.__siteTexteCache || {};
document.querySelectorAll("[data-site-text-key]").forEach((el) => {
const eintrag = siteTexte[el.getAttribute("data-site-text-key")];
if (!eintrag) return;
const text = eintrag[sprache] || eintrag.de;
if (text) el.innerHTML = text;
});
}
/* ---------- Navigationsstruktur (an einer Stelle pflegen) ---------- */
@@ -1052,6 +1068,7 @@
await skripteUebernehmen(dok);
applyTranslations(); // seiteneigene data-i18n-Elemente der neuen Seite übersetzen
if (window.dogiSiteTexteLaden) window.dogiSiteTexteLaden(); // Kategorie-/Seitentitel-Overrides der neuen Seite
}
function nahtloseNavigationAktivieren() {
@@ -1123,6 +1140,7 @@
musikKnopfFarbenAnpassen();
nahtloseNavigationAktivieren();
applyTranslations(); // seiteneigene data-i18n-Elemente (z.B. Startseite) übersetzen
if (window.dogiSiteTexteLaden) window.dogiSiteTexteLaden(); // Kategorie-/Seitentitel-Overrides (siehe Verwaltung)
});
/* Kleine öffentliche Schnittstelle für seiteneigene Inline-Skripte
@@ -1189,12 +1207,19 @@
role: m.rolle || "",
photo: m.foto || "",
avatar: m.foto || "",
// 21.08.2026 (Runde 2, "wenn ich eine änderung mache ist es auch
// für jeden"): volle Feld-Parität mit den von Hand gepflegten
// Profilen, damit ein importiertes/bearbeitetes Profil genau so
// reichhaltig sein kann wie die schon bestehenden.
intro: m.intro || "",
bio: m.bio || "",
bioHtml: m.bioHtml || "",
quote: m.zitat || "",
social: m.tiktok ? { tiktok: m.tiktok } : {},
status: "aktiv", // /team liefert ohnehin nur aktive Mitglieder
rank: m.rang || undefined,
rankLabel: m.rangLabel || undefined,
extraCta: m.extraCta || undefined,
// Markiert admin-gepflegte Einträge (z.B. für profil.html, das
// sonst nicht zwischen fest einprogrammierten und dynamischen
// Personen unterscheiden könnte).
@@ -1205,6 +1230,41 @@
}
};
/* 21.08.2026 (Runde 2, Nutzer-Wunsch: "wenn ich eine änderung mache ist
es auch für jeden für die die schon da sind und die neuen"): fügt eine
per dogiTeamLaden() geladene Liste NICHT einfach hinten an die von Hand
gepflegte Liste an, sondern ÜBERSCHREIBT darin jeden Eintrag mit
demselben slug. So zeigt die Seite nach einem Import + einer
Bearbeitung in der Verwaltung automatisch den aktuellen (DB-)Stand
statt der alten, einprogrammierten Version -- ist das Backend mal
nicht erreichbar, bleibt wenigstens die statische Version sichtbar
(nichts verschwindet einfach). */
window.dogiTeamZusammenfuehren = function (statische, neue) {
if (!neue || !neue.length) return statische;
const neueSlugs = new Set(neue.map((n) => n.slug));
return [...statische.filter((s) => !neueSlugs.has(s.slug)), ...neue];
};
/* 21.08.2026 (Nutzer-Wunsch: "ich will ich titel und namen von
kategorien und seiten ändern können in der verwaltung"): lädt einmalig
alle in der Verwaltung gesetzten Text-Overrides (siehe
server-internal/routes/site-texts.js) und ruft applyTranslations()
danach erneut auf, damit gesetzte Overrides die eingebauten
Standardtexte sofort ersetzen. Ohne Overrides (Normalfall) ändert sich
einfach nichts -- die Seite zeigt weiterhin ihren gewohnten Text. */
window.dogiSiteTexteLaden = async function () {
try {
const res = await fetch(`${TEAM_API_BASIS}/site-texts`, { cache: "no-store" });
const data = await res.json();
if (data && data.ok) {
window.__siteTexteCache = data.texte || {};
applyTranslations();
}
} catch {
/* kein Override verfügbar -> eingebaute Standardtexte bleiben stehen */
}
};
/* ===================================================================
PWA: Service-Worker-Registrierung
Voraussetzung dafür, dass Chrome/Edge auf dem Desktop "App
+7 -7
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Du bist Streamer und suchst einen Manager? Bewirb dich als Creator bei Spicy Media." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
</head>
<body class="mit-hintergrund bewerben-seite" style="--page-bg:url('/assets/img/bg-bewerben-creator.jpg');--page-bg-mobile:url('/assets/img/bg-bewerben-creator-mobile.jpg');">
<div id="site-header"></div>
@@ -70,8 +70,8 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/forms.js?v=20260821j"></script>
<script src="assets/js/i18n-bewerben-creator.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/forms.js?v=20260821k"></script>
<script src="assets/js/i18n-bewerben-creator.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
</body>
</html>
+7 -7
View File
@@ -22,10 +22,10 @@
<meta name="twitter:description" content="Kooperationsanfrage an Manager DogFather / Spicy Media." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
</head>
<body class="mit-hintergrund bewerben-seite" style="--page-bg:url('/assets/img/bg-bewerben.jpg');--page-bg-mobile:url('/assets/img/bg-bewerben-mobile.jpg');">
<div id="site-header"></div>
@@ -68,8 +68,8 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/forms.js?v=20260821j"></script>
<script src="assets/js/i18n-bewerben-kooperation.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/forms.js?v=20260821k"></script>
<script src="assets/js/i18n-bewerben-kooperation.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
</body>
</html>
+7 -7
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Werde Modi bei Team Dogi — DogFathers persönliches Community-Team." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
</head>
<body class="mit-hintergrund bewerben-seite" style="--page-bg:url('/assets/img/bg-dogfather.jpg');--page-bg-mobile:url('/assets/img/bg-dogfather-mobile.jpg');">
<div id="site-header"></div>
@@ -79,8 +79,8 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/forms.js?v=20260821j"></script>
<script src="assets/js/i18n-bewerben-modi.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/forms.js?v=20260821k"></script>
<script src="assets/js/i18n-bewerben-modi.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
</body>
</html>
+7 -7
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Werde Scout für DogFather bei der Spicy Media Agentur — finde neue Talente." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
</head>
<body class="mit-hintergrund bewerben-seite" style="--page-bg:url('/assets/img/bg-scouts.jpg');--page-bg-mobile:url('/assets/img/bg-scouts-mobile.jpg');">
<div id="site-header"></div>
@@ -71,8 +71,8 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/forms.js?v=20260821j"></script>
<script src="assets/js/i18n-bewerben-scout.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/forms.js?v=20260821k"></script>
<script src="assets/js/i18n-bewerben-scout.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
</body>
</html>
+6 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Werde Teil vom Team: Modi bei Team Dogi, Scout oder Creator bei Spicy Media, oder Kooperation anfragen." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
</head>
<body class="mit-hintergrund bewerben-seite" style="--page-bg:url('/assets/img/bg-bewerben-split.jpg');--page-bg-mobile:url('/assets/img/bg-bewerben-split-mobile.jpg');">
<div id="site-header"></div>
@@ -85,7 +85,7 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/i18n-bewerben.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/i18n-bewerben.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
</body>
</html>
+6 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Der Husky an DogFathers Seite — das tierische Gesicht der HasiDog-Welt." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
</head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-casper.jpg');--page-bg-mobile:url('/assets/img/bg-casper-mobile.jpg');">
<div id="site-header"></div>
@@ -85,7 +85,7 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/i18n-casper.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/i18n-casper.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
</body>
</html>
@@ -0,0 +1,18 @@
-- =====================================================================
-- Team-Mitglieder: volle Feld-Parität mit den von Hand gepflegten
-- Profilen (21.08.2026, Nutzer-Wunsch: "wenn ich eine änderung mache
-- ist es auch für jeden für die die schon da sind und die neuen").
--
-- Damit ein bestehendes Profil (Patrick, Bananenstift, Diene, ...) 1:1
-- in die Verwaltung importiert werden kann, OHNE Inhalt zu verlieren,
-- braucht team_members dieselben optionalen Felder wie
-- data-modis.js/data-scouts.js: intro (kurzer Text neben dem Foto),
-- bio_html (ausführliche, mehrabsatzige Vorstellung mit Überschriften),
-- extra_cta (zweiter Button auf der Profilkarte, aktuell nur bei Marina
-- -> manager.html genutzt).
-- =====================================================================
ALTER TABLE team_members ADD COLUMN intro_json TEXT;
ALTER TABLE team_members ADD COLUMN bio_html_json TEXT;
ALTER TABLE team_members ADD COLUMN extra_cta_href TEXT;
ALTER TABLE team_members ADD COLUMN extra_cta_label_json TEXT;
+6 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Team Dogi ist Familie — die Community rund um DogFather und HasiDog." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
</head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-community.jpg');--page-bg-mobile:url('/assets/img/bg-community-mobile.jpg');">
<div id="site-header"></div>
@@ -84,7 +84,7 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/i18n-community.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/i18n-community.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
</body>
</html>
+12 -9
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Content Creator, die Teil von DogFather / Spicy Media sind." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
</head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-creator.jpg');--page-bg-mobile:url('/assets/img/bg-creator-mobile.jpg');">
<div id="site-header"></div>
@@ -32,7 +32,7 @@
<div class="hero-banner"><img src="/assets/img/bg-creator.jpg" alt="" loading="eager" /></div>
<div class="container">
<span class="eyebrow" data-i18n="crt_eyebrow">Manager DogFather — Team</span>
<h1 data-i18n="crt_h1">Creator im Team</h1>
<h1 data-i18n="crt_h1" data-site-text-key="seitentitel_creator">Creator im Team</h1>
<p class="lead worlds" data-i18n="crt_lead">Content Creator, die bei DogFather / Spicy Media mitmachen.</p>
</div>
</section>
@@ -60,9 +60,9 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/data-creator.js?v=20260821j"></script>
<script src="assets/js/i18n-creator.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/data-creator.js?v=20260821k"></script>
<script src="assets/js/i18n-creator.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
<script>
document.addEventListener("DOMContentLoaded", async () => {
const grid = document.getElementById("creator-grid");
@@ -78,7 +78,10 @@ document.addEventListener("DOMContentLoaded", async () => {
// admin-gepflegte Creator NIE geladen worden.
if (window.dogiTeamLaden) {
const neue = await window.dogiTeamLaden("creator");
if (neue.length) list = [...list, ...neue];
// Runde 2 (21.08.2026, "wenn ich eine änderung mache ist es auch für
// jeden"): überschreibt bereits importierte Creator (gleicher slug)
// statt sie zu duplizieren.
list = window.dogiTeamZusammenfuehren(list, neue);
}
// Texte fürs JS-generierte Grid/Filter kommen aus den versteckten
+6 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Wer ist DogFather wirklich? Persönlichkeit, Werte und die Geschichte hinter dem Content." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
</head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-dogfather-person.jpg');--page-bg-mobile:url('/assets/img/bg-dogfather-person-mobile.jpg');">
<div id="site-header"></div>
@@ -158,7 +158,7 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/i18n-dogfather.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/i18n-dogfather.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
</body>
</html>
+2 -2
View File
@@ -26,8 +26,8 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="DogFather" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<style>
/* =====================================================================
Zugangsseite — bewusst kein braves Standard-Login-Formular, sondern
+6 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="HasiDog: Maskottchen, Musikprojekt und die verspielte, familiäre Seite von Team Dogi." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
</head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-hasidog.jpg');--page-bg-mobile:url('/assets/img/bg-hasidog-mobile.jpg');">
<div id="site-header"></div>
@@ -186,7 +186,7 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/i18n-hasidog.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/i18n-hasidog.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
</body>
</html>
+8 -8
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="DOGFATHER UNIVERSE: die Heimat von DogFather als Streamer, der HasiDog-Community und Manager DogFather bei Spicy Media." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
</head>
<body class="start-seite">
<div id="site-header"></div>
@@ -287,10 +287,10 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/data-modis.js?v=20260821j"></script>
<script src="assets/js/data-scouts.js?v=20260821j"></script>
<script src="assets/js/i18n-index.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/data-modis.js?v=20260821k"></script>
<script src="assets/js/data-scouts.js?v=20260821k"></script>
<script src="assets/js/i18n-index.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
<script>
// Gleiche URL wie in assets/js/forms.js / postfach.html — läuft seit
// 05.08.2026 auf dem eigenen Server statt auf Cloudflare Workers.
+6 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Kontakt, Impressum und Datenschutz von DOGFATHER UNIVERSE." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
</head>
<body>
<div id="site-header"></div>
@@ -126,7 +126,7 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/i18n-kontakt.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/i18n-kontakt.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
</body>
</html>
+6 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Alle Social-Media- und Musik-Links von DogFather an einem Ort — plus QR-Codes." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
<style>
/* Rabattcodes: an das Supporter-Abo gekoppelt (Nutzer-Feedback
03.08.2026: "verbinde es mit dem Abo... wenn die Leute drauf drücken
@@ -140,8 +140,8 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/i18n-links.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/i18n-links.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
<script>
(function () {
document.addEventListener("DOMContentLoaded", function () {
+11 -9
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="DogFather als Manager bei der Spicy Media Agency: Scouts, Creator und faire Zusammenarbeit." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
</head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-manager.jpg');--page-bg-mobile:url('/assets/img/bg-manager-mobile.jpg');">
<div id="site-header"></div>
@@ -104,7 +104,7 @@
flaches Karten-Raster wie team-scouts.html. -->
<section class="section-tight" id="weitere-manager-root" hidden>
<div class="container">
<h2 class="text-center" data-i18n="mgr_weitere_h2">Weitere Manager</h2>
<h2 class="text-center" data-i18n="mgr_weitere_h2" data-site-text-key="seitentitel_manager">Weitere Manager</h2>
<div id="weitere-manager-grid" class="grid grid-4"></div>
</div>
</section>
@@ -139,9 +139,9 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/data-manager.js?v=20260821j"></script>
<script src="assets/js/i18n-manager.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/data-manager.js?v=20260821k"></script>
<script src="assets/js/i18n-manager.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
<script>
// "Weitere Manager" (21.08.2026) -- siehe Kommentar bei #weitere-manager-root
// oben. Aktuell IMMER window.MANAGERS === [] (siehe data-manager.js), Inhalt
@@ -180,7 +180,9 @@ document.addEventListener("DOMContentLoaded", () => {
if (window.dogiTeamLaden) {
window.dogiTeamLaden("manager").then((neue) => {
if (!neue.length) return;
window.MANAGERS = [...(window.MANAGERS || []), ...neue];
// Runde 2 (21.08.2026, "wenn ich eine änderung mache ist es auch für
// jeden"): überschreibt bereits importierte Manager statt zu duplizieren.
window.MANAGERS = window.dogiTeamZusammenfuehren(window.MANAGERS || [], neue);
renderWeitereManager();
});
}
+9 -9
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Fotos und Videos rund um Husky Casper an einem Ort." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
</head>
<body class="mit-hintergrund" data-welt="casper" style="--page-bg:url('/assets/img/bg-casper.jpg');--page-bg-mobile:url('/assets/img/bg-casper-mobile.jpg');">
<div id="site-header"></div>
@@ -59,10 +59,10 @@
</div>
<div id="site-footer"></div>
<script src="assets/js/data-media.js?v=20260821j"></script>
<script src="assets/js/data-videos.js?v=20260821j"></script>
<script src="assets/js/medien-welt.js?v=20260821j"></script>
<script src="assets/js/i18n-medien-casper.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/data-media.js?v=20260821k"></script>
<script src="assets/js/data-videos.js?v=20260821k"></script>
<script src="assets/js/medien-welt.js?v=20260821k"></script>
<script src="assets/js/i18n-medien-casper.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
</body>
</html>
+10 -10
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Fotos, Videos und Events aus DogFathers Welt an einem Ort." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
</head>
<body class="mit-hintergrund" data-welt="dogfather" style="--page-bg:url('/assets/img/bg-dogfather.jpg');--page-bg-mobile:url('/assets/img/bg-dogfather-mobile.jpg');">
<div id="site-header"></div>
@@ -65,11 +65,11 @@
</div>
<div id="site-footer"></div>
<script src="assets/js/data-media.js?v=20260821j"></script>
<script src="assets/js/data-videos.js?v=20260821j"></script>
<script src="assets/js/data-aktionen.js?v=20260821j"></script>
<script src="assets/js/medien-welt.js?v=20260821j"></script>
<script src="assets/js/i18n-medien-dogfather.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/data-media.js?v=20260821k"></script>
<script src="assets/js/data-videos.js?v=20260821k"></script>
<script src="assets/js/data-aktionen.js?v=20260821k"></script>
<script src="assets/js/medien-welt.js?v=20260821k"></script>
<script src="assets/js/i18n-medien-dogfather.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
</body>
</html>
+10 -10
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Fotos, Videos und Events aus der HasiDog-Welt an einem Ort." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
</head>
<body class="mit-hintergrund" data-welt="hasidog" style="--page-bg:url('/assets/img/bg-hasidog.jpg');--page-bg-mobile:url('/assets/img/bg-hasidog-mobile.jpg');">
<div id="site-header"></div>
@@ -65,11 +65,11 @@
</div>
<div id="site-footer"></div>
<script src="assets/js/data-media.js?v=20260821j"></script>
<script src="assets/js/data-videos.js?v=20260821j"></script>
<script src="assets/js/data-aktionen.js?v=20260821j"></script>
<script src="assets/js/medien-welt.js?v=20260821j"></script>
<script src="assets/js/i18n-medien-hasidog.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/data-media.js?v=20260821k"></script>
<script src="assets/js/data-videos.js?v=20260821k"></script>
<script src="assets/js/data-aktionen.js?v=20260821k"></script>
<script src="assets/js/medien-welt.js?v=20260821k"></script>
<script src="assets/js/i18n-medien-hasidog.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
</body>
</html>
+10 -10
View File
@@ -16,10 +16,10 @@
<meta name="twitter:card" content="summary_large_image" />
<meta name="robots" content="noindex" /> <!-- generische URL, echte Team-Seiten verlinken hierher -->
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
<style>
.profil-photo { aspect-ratio: 4/5; max-width: 420px; margin: 0 auto; }
.profil-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
@@ -130,12 +130,12 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/data-modis.js?v=20260821j"></script>
<script src="assets/js/data-scouts.js?v=20260821j"></script>
<script src="assets/js/data-creator.js?v=20260821j"></script>
<script src="assets/js/data-manager.js?v=20260821j"></script>
<script src="assets/js/i18n-profil.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/data-modis.js?v=20260821k"></script>
<script src="assets/js/data-scouts.js?v=20260821k"></script>
<script src="assets/js/data-creator.js?v=20260821k"></script>
<script src="assets/js/data-manager.js?v=20260821k"></script>
<script src="assets/js/i18n-profil.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
<script>
document.addEventListener("DOMContentLoaded", async () => {
const slug = new URLSearchParams(location.search).get("p") || "";
+6 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Rabattcodes von Dogis Partnern für Team Dogi — spar bares Geld bei ausgewählten Marken." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
<style>
/* Leere-Zustand-Kachel im "Gutschein"-Look: gestrichelter Rand wie ein
ausgeschnittener Coupon, damit auch OHNE echte Codes schon klar ist,
@@ -131,8 +131,8 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/i18n-rabattcodes.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/i18n-rabattcodes.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
<script>
(function () {
document.addEventListener("DOMContentLoaded", function () {
+1
View File
@@ -31,6 +31,7 @@ const MIGRATION_FILES = [
"0005_supporter_polls.sql",
"0006_testimonials.sql",
"0007_team_members.sql",
"0008_team_members_rich.sql",
];
function alreadyApplied(name) {
+5 -1
View File
@@ -72,8 +72,9 @@ import {
createPoll, listPolls, closePoll, reopenPoll, deletePoll, getActivePollPublic, voteOnPoll,
} from "./routes/polls.js";
import {
getTeamPublic, getTeamAdmin, saveTeamMember, deleteTeamMember, uploadTeamPhoto,
getTeamPublic, getTeamAdmin, saveTeamMember, importLegacyMember, deleteTeamMember, uploadTeamPhoto,
} from "./routes/team.js";
import { getSiteTextsPublic, saveSiteText } from "./routes/site-texts.js";
initDb();
@@ -180,6 +181,7 @@ app.get("/special-event", wrap(getSpecialEventPublic));
app.post("/testimonials/submit", wrap(submitTestimonial));
app.get("/testimonials", wrap(getApprovedTestimonialsPublic));
app.get("/team", wrap(getTeamPublic));
app.get("/site-texts", wrap(getSiteTextsPublic));
app.post("/supporter/register", wrap(registerSupporter));
app.post("/supporter/verify-email", wrap(verifySupporterEmail));
@@ -229,8 +231,10 @@ app.post("/admin/testimonials/delete", requireTeamSession, withSession(deleteTes
// Team-Mitglieder (Modis/Scouts/Creator/Manager) — 21.08.2026
app.post("/admin/team/list", requireTeamSession, withSession(getTeamAdmin));
app.post("/admin/team/save", requireTeamSession, withSession(saveTeamMember));
app.post("/admin/team/import", requireTeamSession, withSession(importLegacyMember));
app.post("/admin/team/delete", requireTeamSession, withSession(deleteTeamMember));
app.post("/admin/team/upload-photo", requireTeamSession, teamBildUpload.single("image"), withSession(uploadTeamPhoto));
app.post("/admin/site-texts/save", requireTeamSession, withSession(saveSiteText));
app.post("/admin/roles/list", requireTeamSession, withSession(listRoles));
app.post("/admin/roles/create", requireTeamSession, withSession(createRole));
+95
View File
@@ -0,0 +1,95 @@
/* =====================================================================
routes/site-texts.js — admin-editierbare Kategorie-Namen & Seitentitel
(21.08.2026, Nutzer-Wunsch: "ich will ich titel und namen von
kategorien und seiten ändern können in der verwaltung").
Bewusst als generischer key->{de,"de-CH",en,fr,pt}-Override gebaut
(wiederverwendet die vorhandene app_settings-Tabelle, key-Präfix
"site_text_"), NICHT als eigene Tabelle -- dieselbe Technik wie
"Event des Jahres". Ein fester, serverseitiger Katalog erlaubter
Keys (ERLAUBTE_KEYS) verhindert, dass beliebige/kaputte Keys
gespeichert werden können. Aktuell nur die vier Team-Kategorien +
ihre Seitentitel belegt -- das Muster ist aber bewusst so gebaut,
dass in einer künftigen Runde weitere Seiten/Überschriften einfach
per neuem Key ergänzt werden können (siehe main.js:
applyTranslations() prüft data-site-text-key genau wie data-i18n).
Öffentlich lesbar (jede Seite braucht das ohne Login), Schreiben nur
mit TEAM_MANAGE (dieselbe Berechtigung wie die Team-Verwaltung,
bewusst keine eigene -- beides gehört zusammen). ===================================================================== */
import { db } from "../db.js";
import { hasPermission } from "../lib/permissions.js";
import { json } from "../lib/http.js";
import { logAction } from "../lib/audit.js";
import { uebersetzeAlleSprachen } from "../lib/translate.js";
function can(session, key) {
if (!session) return false;
if (session.isOwner) return true;
return hasPermission(session.permissions, session.overrides, key);
}
function forbidden(res) {
return json(res, { ok: false, error: "Keine Berechtigung." }, 403);
}
// key -> { label, maxLen } -- label ist nur für Log-/Fehlermeldungen.
const ERLAUBTE_KEYS = {
kategorie_modi: { label: "Kategorie-Name: Modi", maxLen: 40 },
kategorie_scout: { label: "Kategorie-Name: Scout", maxLen: 40 },
kategorie_creator: { label: "Kategorie-Name: Creator", maxLen: 40 },
kategorie_manager: { label: "Kategorie-Name: Manager", maxLen: 40 },
seitentitel_modi: { label: "Seitentitel: Modi-Team", maxLen: 100 },
seitentitel_scout: { label: "Seitentitel: Scout-Team", maxLen: 100 },
seitentitel_creator: { label: "Seitentitel: Creator", maxLen: 100 },
seitentitel_manager: { label: "Seitentitel: Manager", maxLen: 100 },
};
function settingsKey(key) {
return `site_text_${key}`;
}
/** Öffentlich, keine Session: liefert alle aktuell GESETZTEN Overrides
als { key: {de,"de-CH",en,fr,pt} }. Keys ohne eigenen Override
fehlen einfach im Ergebnis -- die aufrufende Seite behält dann ihren
eingebauten Standardtext (siehe main.js). */
export async function getSiteTextsPublic(req, res) {
const rows = db
.prepare(`SELECT key, value FROM app_settings WHERE key LIKE 'site_text_%'`)
.all();
const texte = {};
for (const row of rows) {
const kurzKey = row.key.replace(/^site_text_/, "");
try {
texte[kurzKey] = JSON.parse(row.value);
} catch {
/* kaputter Eintrag -> einfach überspringen, kein Fehler für alle anderen Keys */
}
}
return json(res, { ok: true, texte });
}
export async function saveSiteText(req, res, session) {
if (!can(session, "TEAM_MANAGE")) return forbidden(res);
const body = req.body || {};
const key = String(body.key || "").trim();
const eintrag = ERLAUBTE_KEYS[key];
if (!eintrag) return json(res, { ok: false, error: "Unbekannter Text-Schlüssel." }, 400);
const textDe = String(body.text || "").trim().slice(0, eintrag.maxLen);
if (!textDe) {
// Leeres Feld = Override entfernen -> Seite fällt zurück auf ihren
// eingebauten Standardtext (Dogi kann so jederzeit "zurücksetzen").
db.prepare(`DELETE FROM app_settings WHERE key = ?`).run(settingsKey(key));
logAction(session.actor, "site_text.reset", key, null);
return json(res, { ok: true, text: null });
}
const text = await uebersetzeAlleSprachen(textDe);
db.prepare(
`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
).run(settingsKey(key), JSON.stringify(text), new Date().toISOString());
logAction(session.actor, "site_text.saved", key, { textDe });
return json(res, { ok: true, text });
}
+180 -48
View File
@@ -5,12 +5,20 @@
automatisch in der Website hinzugefügt werden ... auch mit Fotos und
TikTok Link").
Ergänzt (überschreibt NICHT) die von Hand gepflegten Einträge in
assets/js/data-modis.js/data-scouts.js/data-creator.js/data-manager.js.
Die Website mischt beide Quellen zu einer gemeinsamen Liste (siehe
window.dogiTeamLaden() in main.js) -- ein Fehler hier kann die
bestehenden, fest einprogrammierten Profile (VanVan, Patrick, ...) nie
verschwinden lassen.
Update 21.08.2026 (Runde 2, Nutzer-Wunsch: "wenn ich eine änderung
mache ist es auch für jeden für die die schon da sind und die neuen"):
volle Feld-Parität mit den von Hand gepflegten Profilen (intro/bioHtml/
extraCta) PLUS ein Import-Weg für bestehende Profile (importLegacyMember)
-- danach lässt sich JEDES Mitglied (alt oder neu) hier bearbeiten,
und die Website zeigt (siehe window.dogiTeamLaden() in main.js) für
importierte slugs die DB-Version statt der statischen an.
WICHTIG: importLegacyMember übersetzt NICHT automatisch -- die
bestehenden Profile haben von Hand geschriebene, geprüfte Übersetzungen
in allen 5 Sprachen (siehe data-modis.js etc.), die dürfen beim Import
nicht durch Maschinenübersetzung ersetzt werden. saveTeamMember (Neu
anlegen/Bearbeiten in der Verwaltung) übersetzt weiterhin automatisch,
weil dort nur Deutsch eingetippt wird.
Öffentlicher Lesezugriff (getTeamPublic) ohne Session, nur AKTIVE
Mitglieder -- Schreibzugriff nur mit TEAM_MANAGE-Berechtigung.
@@ -23,6 +31,7 @@ import { logAction } from "../lib/audit.js";
import { uebersetzeAlleSprachen } from "../lib/translate.js";
const KATEGORIEN = ["modi", "scout", "creator", "manager"];
const SPRACHEN = ["de", "de-CH", "en", "fr", "pt"];
function can(session, key) {
if (!session) return false;
@@ -43,6 +52,25 @@ function parseJsonFeld(text) {
}
}
/** Prüft, ob ein vom Client geschicktes mehrsprachiges Feld wirklich ein
Objekt mit (mindestens) einem "de"-Text ist -- genutzt vom Import, der
KEINE eigene Übersetzung anstößt und deshalb ein vollständiges Objekt
erwartet, kein einzelnes Deutsch-Textfeld. */
function istGueltigesSprachFeld(x) {
return x && typeof x === "object" && typeof x.de === "string" && x.de.trim().length > 0;
}
/** Baut aus einem client-gesendeten mehrsprachigen Feld ein sauberes,
auf die bekannten 5 Sprachen begrenztes Objekt (verhindert, dass
beliebige zusätzliche Keys in die Datenbank gelangen). */
function saubereSprachen(x, maxLen) {
const out = {};
for (const sprache of SPRACHEN) {
out[sprache] = String(x?.[sprache] ?? x?.de ?? "").slice(0, maxLen);
}
return out;
}
/** Slug aus dem Namen ableiten (Kleinbuchstaben, Umlaute vereinfacht,
alles außer a-z0-9 wird zu "-"), falls die Verwaltung keinen eigenen
Slug mitschickt -- selbes Muster wie an anderen Stellen der Seite. */
@@ -55,7 +83,24 @@ function slugAusName(name) {
.slice(0, 60);
}
function slugFrei(slug, ausgenommeneId) {
const row = db.prepare(`SELECT id FROM team_members WHERE slug = ?`).get(slug);
return !row || row.id === ausgenommeneId;
}
/** Macht einen Slug bei Kollision automatisch eindeutig ("-2", "-3", ...)
statt hart abzulehnen -- Dogi/VanVan sollen nicht wegen eines
Namensdopplers (z.B. zwei Personen mit Spitzname "Foxy") hängen
bleiben. */
function eindeutigerSlug(wunschSlug, ausgenommeneId) {
if (slugFrei(wunschSlug, ausgenommeneId)) return wunschSlug;
let zaehler = 2;
while (!slugFrei(`${wunschSlug}-${zaehler}`, ausgenommeneId)) zaehler++;
return `${wunschSlug}-${zaehler}`;
}
function oeffentlichesFeld(row) {
const extraCtaLabel = parseJsonFeld(row.extra_cta_label_json);
return {
id: row.id,
kategorie: row.kategorie,
@@ -64,10 +109,13 @@ function oeffentlichesFeld(row) {
rolle: parseJsonFeld(row.rolle_json),
foto: row.foto_url || "",
tiktok: row.tiktok_url || "",
intro: parseJsonFeld(row.intro_json),
bio: parseJsonFeld(row.bio_json),
bioHtml: parseJsonFeld(row.bio_html_json),
zitat: parseJsonFeld(row.zitat_json),
rang: row.rang,
rangLabel: parseJsonFeld(row.rang_label_json),
extraCta: row.extra_cta_href && extraCtaLabel ? { href: row.extra_cta_href, label: extraCtaLabel } : null,
};
}
@@ -130,23 +178,9 @@ export async function saveTeamMember(req, res, session) {
const rolleDe = String(body.rolle || "").trim().slice(0, 120);
if (!rolleDe) return json(res, { ok: false, error: "Rolle ist Pflicht." }, 400);
let slug = String(body.slug || "").trim().toLowerCase().replace(/[^a-z0-9-]/g, "").slice(0, 60) || slugAusName(name);
if (!slug) return json(res, { ok: false, error: "Konnte keinen gültigen Slug ableiten -- bitte Namen prüfen." }, 400);
// Slug muss GLOBAL eindeutig sein (profil.html sucht kategorieübergreifend).
// Kollidiert er mit einem ANDEREN Eintrag (nicht dem gerade bearbeiteten),
// wird er automatisch mit "-2", "-3", ... eindeutig gemacht statt hart
// abzulehnen -- Dogi/VanVan sollen nicht wegen eines Namensdopplers
// (z.B. zwei Personen mit Spitzname "Foxy") hängen bleiben.
const slugBelegt = (kandidat) => {
const row = db.prepare(`SELECT id FROM team_members WHERE slug = ?`).get(kandidat);
return row && row.id !== id;
};
if (slugBelegt(slug)) {
let zaehler = 2;
while (slugBelegt(`${slug}-${zaehler}`)) zaehler++;
slug = `${slug}-${zaehler}`;
}
const wunschSlug = String(body.slug || "").trim().toLowerCase().replace(/[^a-z0-9-]/g, "").slice(0, 60) || slugAusName(name);
if (!wunschSlug) return json(res, { ok: false, error: "Konnte keinen gültigen Slug ableiten -- bitte Namen prüfen." }, 400);
const slug = eindeutigerSlug(wunschSlug, id);
const tiktokRoh = String(body.tiktok || "").trim().slice(0, 200);
let tiktokUrl = null;
@@ -160,7 +194,9 @@ export async function saveTeamMember(req, res, session) {
}
}
const introDe = String(body.intro || "").trim().slice(0, 400);
const bioDe = String(body.bio || "").trim().slice(0, 800);
const bioHtmlDe = String(body.bioHtml || "").trim().slice(0, 6000);
const zitatDe = String(body.zitat || "").trim().slice(0, 200);
const fotoUrl = String(body.foto || "").trim().slice(0, 300) || null;
@@ -168,54 +204,150 @@ export async function saveTeamMember(req, res, session) {
? Math.max(1, Math.min(99, Number(body.rang) || 0)) : null;
const rangLabelDe = String(body.rangLabel || "").trim().slice(0, 40);
const extraCtaHref = String(body.extraCtaHref || "").trim().slice(0, 300) || null;
const extraCtaLabelDe = String(body.extraCtaLabel || "").trim().slice(0, 60);
if (extraCtaHref && !/^https?:\/\//i.test(extraCtaHref) && !extraCtaHref.startsWith("/") && !extraCtaHref.endsWith(".html")) {
return json(res, { ok: false, error: "Zweiter Button: Link muss eine Seite (…html) oder http(s)-Adresse sein." }, 400);
}
const reihenfolge = Number.isFinite(Number(body.reihenfolge)) ? Number(body.reihenfolge) : 0;
const status = body.status === "pausiert" ? "pausiert" : "aktiv";
const [rolle, bio, zitat, rangLabel] = await Promise.all([
const [rolle, intro, bio, bioHtml, zitat, rangLabel, extraCtaLabel] = await Promise.all([
uebersetzeAlleSprachen(rolleDe),
introDe ? uebersetzeAlleSprachen(introDe) : Promise.resolve(null),
bioDe ? uebersetzeAlleSprachen(bioDe) : Promise.resolve(null),
bioHtmlDe ? uebersetzeAlleSprachen(bioHtmlDe) : Promise.resolve(null),
zitatDe ? uebersetzeAlleSprachen(zitatDe) : Promise.resolve(null),
rangLabelDe ? uebersetzeAlleSprachen(rangLabelDe) : Promise.resolve(null),
extraCtaLabelDe ? uebersetzeAlleSprachen(extraCtaLabelDe) : Promise.resolve(null),
]);
const jetzt = nowIso();
const werte = {
id: id || generateId(),
kategorie, name, slug,
rolle_json: JSON.stringify(rolle),
foto_url: fotoUrl,
tiktok_url: tiktokUrl,
intro_json: intro ? JSON.stringify(intro) : null,
bio_json: bio ? JSON.stringify(bio) : null,
bio_html_json: bioHtml ? JSON.stringify(bioHtml) : null,
zitat_json: zitat ? JSON.stringify(zitat) : null,
rang, rang_label_json: rangLabel ? JSON.stringify(rangLabel) : null,
extra_cta_href: extraCtaHref && extraCtaLabel ? extraCtaHref : null,
extra_cta_label_json: extraCtaHref && extraCtaLabel ? JSON.stringify(extraCtaLabel) : null,
reihenfolge, status,
geaendert_am: jetzt, geaendert_von: session.actor,
};
if (id) {
const info = db
.prepare(
`UPDATE team_members SET
kategorie = ?, name = ?, slug = ?, rolle_json = ?, foto_url = ?, tiktok_url = ?,
bio_json = ?, zitat_json = ?, rang = ?, rang_label_json = ?, reihenfolge = ?,
status = ?, geaendert_am = ?, geaendert_von = ?
WHERE id = ?`
kategorie=@kategorie, name=@name, slug=@slug, rolle_json=@rolle_json,
foto_url=@foto_url, tiktok_url=@tiktok_url, intro_json=@intro_json,
bio_json=@bio_json, bio_html_json=@bio_html_json, zitat_json=@zitat_json,
rang=@rang, rang_label_json=@rang_label_json,
extra_cta_href=@extra_cta_href, extra_cta_label_json=@extra_cta_label_json,
reihenfolge=@reihenfolge, status=@status,
geaendert_am=@geaendert_am, geaendert_von=@geaendert_von
WHERE id=@id`
)
.run(
kategorie, name, slug, JSON.stringify(rolle), fotoUrl, tiktokUrl,
bio ? JSON.stringify(bio) : null, zitat ? JSON.stringify(zitat) : null,
rang, rangLabel ? JSON.stringify(rangLabel) : null, reihenfolge,
status, jetzt, session.actor, id
);
.run(werte);
if (info.changes === 0) return json(res, { ok: false, error: "Nicht gefunden." }, 404);
logAction(session.actor, "team.updated", id, { name, kategorie });
const row = db.prepare(`SELECT * FROM team_members WHERE id = ?`).get(id);
} else {
db.prepare(
`INSERT INTO team_members
(id, kategorie, name, slug, rolle_json, foto_url, tiktok_url, intro_json,
bio_json, bio_html_json, zitat_json, rang, rang_label_json,
extra_cta_href, extra_cta_label_json, reihenfolge, status,
erstellt_am, geaendert_am, erstellt_von, geaendert_von)
VALUES
(@id, @kategorie, @name, @slug, @rolle_json, @foto_url, @tiktok_url, @intro_json,
@bio_json, @bio_html_json, @zitat_json, @rang, @rang_label_json,
@extra_cta_href, @extra_cta_label_json, @reihenfolge, @status,
@geaendert_am, @geaendert_am, @geaendert_von, @geaendert_von)`
).run(werte);
logAction(session.actor, "team.created", werte.id, { name, kategorie });
}
const row = db.prepare(`SELECT * FROM team_members WHERE id = ?`).get(werte.id);
return json(res, { ok: true, mitglied: vollesFeld(row) });
}
const neueId = generateId();
/** Importiert ein BESTEHENDES, von Hand gepflegtes Profil (aus
data-modis.js/data-scouts.js/data-creator.js/data-manager.js) 1:1 in
die Datenbank -- danach ist es über die Verwaltung wie jedes andere
Mitglied bearbeitbar, und Änderungen gelten für ALLE (siehe
window.dogiTeamLaden()-Override-Logik in main.js).
WICHTIG: übersetzt NICHTS automatisch (anders als saveTeamMember) --
erwartet für jedes Textfeld ein bereits vollständiges {de,"de-CH",en,
fr,pt}-Objekt, wie es in den data-*.js-Dateien von Hand gepflegt
wird. Ist ein Feld kein gültiges Sprachobjekt, wird es einfach
weggelassen statt einen Fehler zu werfen -- ein unvollständiges
Altprofil soll den Import nicht komplett blockieren.
Idempotent: ist der slug schon vorhanden, passiert nichts (kein
Duplikat, kein Fehler) -- der Button in der Verwaltung kann beliebig
oft geklickt werden, ohne Doppel-Einträge zu erzeugen. */
export async function importLegacyMember(req, res, session) {
if (!can(session, "TEAM_MANAGE")) return forbidden(res);
const body = req.body || {};
const kategorie = String(body.kategorie || "").trim();
if (!KATEGORIEN.includes(kategorie)) return json(res, { ok: false, error: "Ungültige Kategorie." }, 400);
const name = String(body.name || "").trim().slice(0, 60);
const slug = String(body.slug || "").trim().toLowerCase().replace(/[^a-z0-9-]/g, "").slice(0, 60);
if (!name || !slug) return json(res, { ok: false, error: "Name und Slug sind Pflicht." }, 400);
if (!istGueltigesSprachFeld(body.rolle)) return json(res, { ok: false, error: "Rolle fehlt oder unvollständig." }, 400);
if (!slugFrei(slug, null)) {
return json(res, { ok: true, uebersprungen: true, grund: "Bereits importiert." });
}
const rolle = saubereSprachen(body.rolle, 120);
const intro = istGueltigesSprachFeld(body.intro) ? saubereSprachen(body.intro, 2000) : null;
const bio = istGueltigesSprachFeld(body.bio) ? saubereSprachen(body.bio, 800) : null;
const bioHtml = istGueltigesSprachFeld(body.bioHtml) ? saubereSprachen(body.bioHtml, 8000) : null;
const zitat = istGueltigesSprachFeld(body.zitat) ? saubereSprachen(body.zitat, 200) : null;
const rangLabel = istGueltigesSprachFeld(body.rangLabel) ? saubereSprachen(body.rangLabel, 40) : null;
const extraCtaLabel = body.extraCta?.href && istGueltigesSprachFeld(body.extraCta?.label) ? saubereSprachen(body.extraCta.label, 60) : null;
const jetzt = nowIso();
const werte = {
id: generateId(),
kategorie, name, slug,
rolle_json: JSON.stringify(rolle),
foto_url: String(body.foto || "").trim().slice(0, 300) || null,
tiktok_url: body.tiktok?.href || (typeof body.tiktok === "string" ? body.tiktok : null),
intro_json: intro ? JSON.stringify(intro) : null,
bio_json: bio ? JSON.stringify(bio) : null,
bio_html_json: bioHtml ? JSON.stringify(bioHtml) : null,
zitat_json: zitat ? JSON.stringify(zitat) : null,
rang: Number.isFinite(Number(body.rang)) ? Number(body.rang) : null,
rang_label_json: rangLabel ? JSON.stringify(rangLabel) : null,
extra_cta_href: extraCtaLabel ? body.extraCta.href : null,
extra_cta_label_json: extraCtaLabel ? JSON.stringify(extraCtaLabel) : null,
reihenfolge: 0,
status: "aktiv",
geaendert_am: jetzt, geaendert_von: session.actor,
};
db.prepare(
`INSERT INTO team_members
(id, kategorie, name, slug, rolle_json, foto_url, tiktok_url, bio_json, zitat_json,
rang, rang_label_json, reihenfolge, status, erstellt_am, geaendert_am, erstellt_von, geaendert_von)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
).run(
neueId, kategorie, name, slug, JSON.stringify(rolle), fotoUrl, tiktokUrl,
bio ? JSON.stringify(bio) : null, zitat ? JSON.stringify(zitat) : null,
rang, rangLabel ? JSON.stringify(rangLabel) : null, reihenfolge,
status, jetzt, jetzt, session.actor, session.actor
);
logAction(session.actor, "team.created", neueId, { name, kategorie });
const row = db.prepare(`SELECT * FROM team_members WHERE id = ?`).get(neueId);
return json(res, { ok: true, mitglied: vollesFeld(row) });
(id, kategorie, name, slug, rolle_json, foto_url, tiktok_url, intro_json,
bio_json, bio_html_json, zitat_json, rang, rang_label_json,
extra_cta_href, extra_cta_label_json, reihenfolge, status,
erstellt_am, geaendert_am, erstellt_von, geaendert_von)
VALUES
(@id, @kategorie, @name, @slug, @rolle_json, @foto_url, @tiktok_url, @intro_json,
@bio_json, @bio_html_json, @zitat_json, @rang, @rang_label_json,
@extra_cta_href, @extra_cta_label_json, @reihenfolge, @status,
@geaendert_am, @geaendert_am, @geaendert_von, @geaendert_von)`
).run(werte);
logAction(session.actor, "team.imported", werte.id, { name, kategorie, slug });
const row = db.prepare(`SELECT * FROM team_members WHERE id = ?`).get(werte.id);
return json(res, { ok: true, uebersprungen: false, mitglied: vollesFeld(row) });
}
export async function deleteTeamMember(req, res, session) {
+7 -7
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Was die Community und das Team über DogFather sagen." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
</head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-stimmen.jpg');--page-bg-mobile:url('/assets/img/bg-stimmen-mobile.jpg');">
<div id="site-header"></div>
@@ -87,8 +87,8 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/i18n-stimmen.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/stimmen.js?v=20260821j"></script>
<script src="assets/js/i18n-stimmen.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
<script src="assets/js/stimmen.js?v=20260821k"></script>
</body>
</html>
+6 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Die persönliche Geschichte hinter DogFather — Filipe, Streamer, Manager bei Spicy Media und Hundepapa von Husky Casper." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
</head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-streamer.jpg');--page-bg-mobile:url('/assets/img/bg-streamer-mobile.jpg');">
<div id="site-header"></div>
@@ -217,7 +217,7 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/i18n-streamer.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/i18n-streamer.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
</body>
</html>
+9 -9
View File
@@ -19,17 +19,17 @@
<meta name="twitter:description" content="Wann geht DogFather live? Streamplan und Livebereich." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
</head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-streamplan.jpg?v=20260821j');--page-bg-mobile:url('/assets/img/bg-streamplan-mobile.jpg?v=20260821j');">
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-streamplan.jpg?v=20260821k');--page-bg-mobile:url('/assets/img/bg-streamplan-mobile.jpg?v=20260821k');">
<div id="site-header"></div>
<main>
<section class="hero page-bg">
<div class="hero-banner"><img src="/assets/img/bg-streamplan.jpg?v=20260821j" alt="" loading="eager" /></div>
<div class="hero-banner"><img src="/assets/img/bg-streamplan.jpg?v=20260821k" alt="" loading="eager" /></div>
<div class="container">
<span class="eyebrow" data-i18n="sp_eyebrow">Welt 1 — DogFather</span>
<h1 data-i18n="sp_h1">Streamplan &amp; Livebereich</h1>
@@ -126,8 +126,8 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/i18n-streamplan.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/streamplan.js?v=20260821j"></script>
<script src="assets/js/i18n-streamplan.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
<script src="assets/js/streamplan.js?v=20260821k"></script>
</body>
</html>
+7 -7
View File
@@ -9,10 +9,10 @@
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#0b0d10" />
<meta name="robots" content="noindex" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
<style>
/* =====================================================================
Supporter-Bereich — komplett neu gestaltet (Nutzer-Feedback
@@ -416,9 +416,9 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/i18n-supporter.js?v=20260821j"></script>
<script src="assets/js/supporter.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/i18n-supporter.js?v=20260821k"></script>
<script src="assets/js/supporter.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
<script>
(function () {
const S = window.DogiSupporter;
+14 -13
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="VanVan, Diene, Funny, Miss und Marina — das Modi-/Core-Team von Team Dogi." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
</head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-dogfather.jpg');--page-bg-mobile:url('/assets/img/bg-dogfather-mobile.jpg');">
<div id="site-header"></div>
@@ -32,7 +32,7 @@
<div class="hero-banner"><img src="/assets/img/bg-dogfather.jpg" alt="" loading="eager" /></div>
<div class="container">
<span class="eyebrow" data-i18n="tm_eyebrow">🩵 Team Dogi</span>
<h1 data-i18n="tm_h1">Das Modi-Team</h1>
<h1 data-i18n="tm_h1" data-site-text-key="seitentitel_modi">Das Modi-Team</h1>
<p class="lead worlds" data-i18n="tm_lead">Das Rückgrat von Team Dogi — von VanVan bis Marina.</p>
<!-- TikTok-Account der Modi-Gang (Nutzer-Wunsch 20.08.2026, umgezogen 21.08.2026 von
streamer.html hierher: "in die modie seite zu den modis hinzufügen"). Passt hier
@@ -72,9 +72,9 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/data-modis.js?v=20260821j"></script>
<script src="assets/js/i18n-team-modis.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/data-modis.js?v=20260821k"></script>
<script src="assets/js/i18n-team-modis.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
<script>
document.addEventListener("DOMContentLoaded", () => {
const grid = document.getElementById("modis-grid");
@@ -133,14 +133,15 @@ document.addEventListener("DOMContentLoaded", () => {
renderModis();
document.addEventListener("dogi-sprache-geaendert", renderModis);
// 21.08.2026: über die Verwaltung angelegte Modis ergänzen (Nutzer-Wunsch
// "überall wo Modis sind ... will ich dass ich die easy über meine
// Verwaltungsseite hinzufüge"). Rang/Rang-Bezeichnung kommen mit, damit sie
// sich sauber in die bestehende Pyramide einordnen.
// 21.08.2026: über die Verwaltung angelegte/importierte Modis einmischen
// (+ Runde 2: "wenn ich eine änderung mache ist es auch für jeden").
// Rang/Rang-Bezeichnung kommen mit, damit sie sich sauber in die
// bestehende Pyramide einordnen. dogiTeamZusammenfuehren() überschreibt
// bereits importierte Modis (gleicher slug) mit ihrem aktuellen DB-Stand.
if (window.dogiTeamLaden) {
window.dogiTeamLaden("modi").then((neue) => {
if (!neue.length) return;
window.MODIS = [...window.MODIS, ...neue];
window.MODIS = window.dogiTeamZusammenfuehren(window.MODIS, neue);
renderModis();
});
}
+15 -14
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Die Scouts von Team Dogi bei Spicy Media." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
</head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-scouts.jpg');--page-bg-mobile:url('/assets/img/bg-scouts-mobile.jpg');">
<div id="site-header"></div>
@@ -32,7 +32,7 @@
<div class="hero-banner"><img src="/assets/img/bg-scouts.jpg" alt="" loading="eager" /></div>
<div class="container">
<span class="eyebrow" data-i18n="ts_eyebrow">Manager DogFather — Spicy Media</span>
<h1 data-i18n="ts_h1">Mein Scout-Team</h1>
<h1 data-i18n="ts_h1" data-site-text-key="seitentitel_scout">Mein Scout-Team</h1>
<p class="lead worlds" data-i18n="ts_lead">Sie finden und begleiten neue Talente für Dogi.</p>
</div>
</section>
@@ -49,9 +49,9 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/data-scouts.js?v=20260821j"></script>
<script src="assets/js/i18n-team-scouts.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/data-scouts.js?v=20260821k"></script>
<script src="assets/js/i18n-team-scouts.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
<script>
document.addEventListener("DOMContentLoaded", () => {
const grid = document.getElementById("scouts-grid");
@@ -77,15 +77,16 @@ document.addEventListener("DOMContentLoaded", () => {
renderScouts();
document.addEventListener("dogi-sprache-geaendert", renderScouts);
// 21.08.2026: über die Verwaltung angelegte Scouts ergänzen (Nutzer-Wunsch
// "überall wo ... Scouts ... sind, will ich dass ich die easy über meine
// Verwaltungsseite hinzufüge"). Fügt sich einfach hinten an die von Hand
// gepflegte Liste an und rendert danach einmal neu -- die fest
// einprogrammierten Scouts (Patrick, Bananenstift) bleiben unverändert.
// 21.08.2026: über die Verwaltung angelegte/importierte Scouts einmischen
// (Nutzer-Wunsch "überall wo ... Scouts ... sind, will ich dass ich die
// easy über meine Verwaltungsseite hinzufüge" + Runde 2: "wenn ich eine
// änderung mache ist es auch für jeden"). dogiTeamZusammenfuehren()
// überschreibt dabei bereits importierte Scouts (gleicher slug) mit ihrem
// aktuellen DB-Stand statt sie zu duplizieren.
if (window.dogiTeamLaden) {
window.dogiTeamLaden("scout").then((neue) => {
if (!neue.length) return;
window.SCOUTS = [...window.SCOUTS, ...neue];
window.SCOUTS = window.dogiTeamZusammenfuehren(window.SCOUTS, neue);
renderScouts();
});
}
+6 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Eine persönliche Widmung von Dogi an Tili — seine TikTok-Muddi, die erste und bis heute einzige Streamerin, die ihm all die Zeit treu geblieben ist." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
<style>
/* Gleiche Anpassung wie vanvan.html: der site-weit etwas größere
Fließtext wirkt bei einem so langen persönlichen Brief zu wuchtig —
@@ -153,7 +153,7 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/i18n-tt-muddi.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/i18n-tt-muddi.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
</body>
</html>
+6 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="VanVan erzählt ihre Geschichte: wie sie zu Team Dogi kam, wer sie ist und was DogFather und Team Dogi ihr bedeuten." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
<style>
/* Auf Wunsch: der site-weit vergrößerte Fließtext wirkte auf VanVans
eigener Seite zu groß (viel Text am Stück) — hier gezielt wieder
@@ -175,7 +175,7 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/i18n-vanvan.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/i18n-vanvan.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
</body>
</html>
+279 -8
View File
@@ -19,10 +19,10 @@
<link rel="apple-touch-icon" href="assets/img/apple-touch-icon-admin.png" />
<meta name="apple-mobile-web-app-title" content="DogiCrew-Verwaltung" />
<meta name="theme-color" content="#0e0a16" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
<style>
/* =====================================================================
verwaltung.html — Dogis privater Command-Center-Bereich.
@@ -880,6 +880,20 @@
Speichern automatisch übersetzt.
</p>
<!-- 21.08.2026 (Runde 2, Nutzer-Wunsch: "wenn ich eine änderung mache
ist es auch für jeden für die die schon da sind und die neuen"):
holt VanVan, Diene, Funny, Miss, Marina, Ghost, Patrick,
Bananenstift usw. einmalig aus den bestehenden data-*.js-Dateien
in die Datenbank -- danach sind sie hier genauso bearbeitbar wie
jedes neu angelegte Mitglied. Übernimmt die vorhandenen, von Hand
geschriebenen Übersetzungen 1:1 (übersetzt NICHT erneut
maschinell). Mehrfach klickbar, ohne Duplikate zu erzeugen --
schon importierte Personen werden dabei einfach übersprungen. -->
<div class="text-center" style="margin-bottom:1.6rem;">
<button type="button" class="btn btn-outline" id="team-import-btn">📥 Bestehende Mitglieder importieren</button>
<p class="small muted" id="team-import-status" style="margin-top:.5rem;"></p>
</div>
<div class="card vw-card vw-shine-card vw-noise" style="max-width:640px;margin:0 auto 2rem;padding:1.5rem;">
<h3 id="team-form-titel" style="margin-top:0;">Neues Mitglied</h3>
<input type="hidden" id="team-id" value="" />
@@ -919,6 +933,40 @@
<label for="team-zitat">Zitat/Motto (Deutsch, optional)</label>
<input type="text" id="team-zitat" maxlength="200" />
</div>
<!-- 21.08.2026 (Runde 2): dieselbe Feld-Tiefe wie die von Hand
gepflegten Profile (siehe data-modis.js: intro/bioHtml/
extraCta) -- eingeklappt, weil die meisten Mitglieder das
nicht brauchen (nur wenige bestehende Profile wie Diene/
Patrick/Bananenstift/Marina nutzen das überhaupt). -->
<details style="margin-bottom:1.2rem;">
<summary style="cursor:pointer;color:var(--accent);font-weight:600;">Erweitert (optional)</summary>
<div style="margin-top:1rem;">
<div class="field">
<label for="team-intro">Kurzer Intro-Text neben dem Foto (Deutsch, optional, HTML erlaubt)</label>
<textarea id="team-intro" maxlength="2000" rows="2" placeholder="Ersetzt/ergänzt die kurze Vorstellung oben direkt neben dem Foto."></textarea>
</div>
<div class="field">
<label for="team-bio-html">Ausführliche Vorstellung (Deutsch, optional, HTML erlaubt: &lt;p&gt;/&lt;h3&gt;/&lt;strong&gt;/&lt;ul&gt;&lt;li&gt;)</label>
<textarea id="team-bio-html" maxlength="6000" rows="6" placeholder="Erscheint als breiter Lesebereich unterhalb des Steckbriefs, wie bei Diene/Patrick/Bananenstift."></textarea>
</div>
<div class="grid grid-2" style="gap:.8rem;">
<div class="field">
<label for="team-extra-cta-href">Zweiter Button: Ziel (z. B. manager.html)</label>
<input type="text" id="team-extra-cta-href" maxlength="300" placeholder="z. B. manager.html" />
</div>
<div class="field">
<label for="team-extra-cta-label">Zweiter Button: Text (Deutsch)</label>
<input type="text" id="team-extra-cta-label" maxlength="60" placeholder="z. B. Zum Manager-Profil" />
</div>
</div>
<p class="small muted" style="margin:.3rem 0 0;">
Zweiter Button erscheint zusätzlich zu "Profil ansehen" auf der Karte (aktuell nur bei
Marina genutzt, Verlinkung zu manager.html).
</p>
</div>
</details>
<div id="team-rang-felder" style="display:none;">
<div class="grid grid-2" style="gap:.8rem;">
<div class="field">
@@ -961,6 +1009,27 @@
</div>
</section>
<!-- ===== "Kategorien & Seitentitel" (21.08.2026, Nutzer-Wunsch: "ich
will ich titel und namen von kategorien und seiten ändern können in
der verwaltung"). Ändert die Kategorie-Namen (z.B. "Modi") und die
Überschrift der jeweiligen öffentlichen Seite -- wirkt sofort
überall, wo diese Kategorie auftaucht (siehe data-site-text-key in
team-modis.html/team-scouts.html/creator.html/manager.html sowie
hier in der Team-Verwaltung selbst). Leeres Feld = zurück zum
eingebauten Standardtext. Gleiche TEAM_MANAGE-Berechtigung wie die
Team-Verwaltung, weil beides zusammengehört. ===== -->
<section class="section-tight" id="site-texts-root" style="display:none;padding-top:0;">
<div class="container">
<h2 class="text-center" style="margin-bottom:.3rem;">🏷️ Kategorien &amp; Seitentitel</h2>
<p class="small muted text-center" style="max-width:64ch;margin:0 auto 1.4rem;">
Ändert, wie die vier Team-Kategorien überall auf der Seite (und hier in der Verwaltung)
heißen. Deutsch eintippen, der Rest wird automatisch übersetzt. Feld leer lassen und
speichern setzt wieder auf den Standardtext zurück.
</p>
<div class="grid grid-2" id="site-texts-grid" style="gap:1.2rem;max-width:900px;margin:0 auto;"></div>
</div>
</section>
<!-- ===== Supporter-Liste ===== -->
<section class="section-tight" id="supp-root" style="display:none;padding-top:0;">
<div class="container" id="liste-ansicht">
@@ -1135,8 +1204,15 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/admin-auth.js?v=20260821j"></script>
<!-- 21.08.2026: für den "Bestehende Mitglieder importieren"-Knopf in der
Team-Verwaltung -- liest die von Hand gepflegten Profile aus diesen
Dateien aus und bietet an, sie 1:1 in die Datenbank zu übernehmen. -->
<script src="assets/js/data-modis.js?v=20260821k"></script>
<script src="assets/js/data-scouts.js?v=20260821k"></script>
<script src="assets/js/data-creator.js?v=20260821k"></script>
<script src="assets/js/data-manager.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
<script src="assets/js/admin-auth.js?v=20260821k"></script>
<script>
(function () {
const API_BASE_URL = "https://postfach.dogfather-universe.com";
@@ -1219,6 +1295,8 @@
if (window.AdminAuth.sessionHasPermission(session, "TEAM_MANAGE")) {
document.getElementById("team-root").style.display = "";
await ladeUndRenderTeam();
document.getElementById("site-texts-root").style.display = "";
await ladeUndRenderSiteTexte();
}
const pollsBox = document.getElementById("box-abstimmungen");
if (window.AdminAuth.sessionHasPermission(session, "POLLS_MANAGE")) {
@@ -1240,6 +1318,7 @@
document.getElementById("special-event-root").style.display = "none";
document.getElementById("testimonials-root").style.display = "none";
document.getElementById("team-root").style.display = "none";
document.getElementById("site-texts-root").style.display = "none";
document.getElementById("box-abstimmungen").style.display = "none";
}
@@ -1729,6 +1808,10 @@
document.getElementById("team-tiktok").value = "";
document.getElementById("team-bio").value = "";
document.getElementById("team-zitat").value = "";
document.getElementById("team-intro").value = "";
document.getElementById("team-bio-html").value = "";
document.getElementById("team-extra-cta-href").value = "";
document.getElementById("team-extra-cta-label").value = "";
document.getElementById("team-rang").value = "";
document.getElementById("team-rang-label").value = "";
document.getElementById("team-reihenfolge").value = "0";
@@ -1737,6 +1820,7 @@
document.getElementById("team-foto-input").value = "";
document.getElementById("team-abbrechen").style.display = "none";
document.getElementById("team-status").textContent = "";
document.getElementById("team-intro").closest("details").open = false;
teamHochgeladenesFoto = null;
teamRangFelderAnpassen();
}
@@ -1750,10 +1834,18 @@
document.getElementById("team-tiktok").value = m.tiktok || "";
document.getElementById("team-bio").value = m.bio?.de || "";
document.getElementById("team-zitat").value = m.zitat?.de || "";
document.getElementById("team-intro").value = m.intro?.de || "";
document.getElementById("team-bio-html").value = m.bioHtml?.de || "";
document.getElementById("team-extra-cta-href").value = m.extraCta?.href || "";
document.getElementById("team-extra-cta-label").value = m.extraCta?.label?.de || "";
document.getElementById("team-rang").value = m.rang || "";
document.getElementById("team-rang-label").value = m.rangLabel?.de || "";
document.getElementById("team-reihenfolge").value = m.reihenfolge ?? 0;
document.getElementById("team-aktiv").checked = m.status !== "pausiert";
// "Erweitert" automatisch aufklappen, wenn schon etwas drinsteht --
// sonst würde man leicht übersehen, dass z.B. Diene eine lange
// bioHtml-Vorstellung hat, weil sie eingeklappt verborgen bliebe.
document.getElementById("team-intro").closest("details").open = !!(m.intro?.de || m.bioHtml?.de || m.extraCta?.href);
teamHochgeladenesFoto = m.foto || null;
const vorschau = document.getElementById("team-foto-vorschau");
if (teamHochgeladenesFoto) {
@@ -1831,6 +1923,10 @@
tiktok: document.getElementById("team-tiktok").value.trim(),
bio: document.getElementById("team-bio").value.trim(),
zitat: document.getElementById("team-zitat").value.trim(),
intro: document.getElementById("team-intro").value.trim(),
bioHtml: document.getElementById("team-bio-html").value.trim(),
extraCtaHref: document.getElementById("team-extra-cta-href").value.trim(),
extraCtaLabel: document.getElementById("team-extra-cta-label").value.trim(),
rang: document.getElementById("team-rang").value,
rangLabel: document.getElementById("team-rang-label").value.trim(),
reihenfolge: document.getElementById("team-reihenfolge").value,
@@ -1842,10 +1938,15 @@
statusEl.style.color = "#ff6b6b";
return;
}
statusEl.textContent = "✅ Gespeichert.";
statusEl.style.color = "#baf7d6";
teamFormularZuruecksetzen();
await ladeUndRenderTeam();
// WICHTIG: erst NACH teamFormularZuruecksetzen() setzen -- die Funktion
// leert team-status als Teil des normalen Zurücksetzens, ein Erfolgs-
// text davor wäre also sofort wieder verschwunden, bevor er je zu sehen
// war (mit Playwright gefunden: der Status blieb nach dem Speichern
// immer leer).
document.getElementById("team-status").textContent = "✅ Gespeichert.";
document.getElementById("team-status").style.color = "#baf7d6";
});
document.getElementById("team-filter-bar").addEventListener("click", (e) => {
@@ -1859,6 +1960,176 @@
teamRangFelderAnpassen();
/* "Bestehende Mitglieder importieren" (21.08.2026, Runde 2, Nutzer-Wunsch:
"wenn ich eine änderung mache ist es auch für jeden für die die schon
da sind und die neuen"). Liest VanVan/Diene/Funny/Miss/Marina/Ghost
(window.MODIS, VanVan bewusst ausgenommen -- sie hat ihre eigene,
abweichend gebaute Hervorhebungs-Karte + eigene Seite vanvan.html,
passt nicht ins generische Schema), Patrick/Bananenstift
(window.SCOUTS) sowie window.CREATORS/MANAGERS (aktuell leer) aus und
schickt jede Person 1:1 an /admin/team/import. Übernimmt die
VORHANDENEN, von Hand geschriebenen Übersetzungen -- KEINE erneute
Maschinenübersetzung. Mehrfach klickbar (idempotent, siehe
routes/team.js): bereits importierte Personen werden serverseitig
einfach übersprungen, nie dupliziert. */
function zuSprachObjekt(feld) {
if (!feld) return null;
if (typeof feld === "string") return { de: feld, "de-CH": feld, en: feld, fr: feld, pt: feld };
if (typeof feld === "object" && feld.de) return feld;
return null;
}
function slugAusNameClient(name) {
return String(name || "")
.toLowerCase()
.replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}
document.getElementById("team-import-btn").addEventListener("click", async () => {
const btn = document.getElementById("team-import-btn");
const statusEl = document.getElementById("team-import-status");
btn.disabled = true;
statusEl.textContent = "Importiere …";
const quellen = [
...(window.MODIS || []).filter((p) => p.name !== "VanVan").map((p) => ({ p, kategorie: "modi" })),
...(window.SCOUTS || []).map((p) => ({ p, kategorie: "scout" })),
...(window.CREATORS || []).map((p) => ({ p, kategorie: "creator" })),
...(window.MANAGERS || []).map((p) => ({ p, kategorie: "manager" })),
];
let importiert = 0, uebersprungen = 0, fehler = 0;
for (const { p, kategorie } of quellen) {
const rolle = zuSprachObjekt(p.role);
if (!rolle) { fehler++; continue; } // Pflichtfeld fehlt -- diese Person überspringen, nicht alle abbrechen
const payload = {
kategorie,
name: p.name,
slug: p.slug || slugAusNameClient(p.name),
rolle,
foto: p.photo || p.avatar || "",
tiktok: p.social?.tiktok || null,
bio: zuSprachObjekt(p.bio),
intro: zuSprachObjekt(p.intro),
bioHtml: zuSprachObjekt(p.bioHtml),
zitat: zuSprachObjekt(p.quote),
rang: p.rank || null,
rangLabel: zuSprachObjekt(p.rankLabel),
extraCta: p.extraCta?.href ? { href: p.extraCta.href, label: zuSprachObjekt(p.extraCta.label) } : null,
};
try {
const res = await window.AdminAuth.apiCall(API_BASE_URL, "/admin/team/import", payload);
if (res.ok && res.uebersprungen) uebersprungen++;
else if (res.ok) importiert++;
else fehler++;
} catch {
fehler++;
}
}
statusEl.textContent = `✅ ${importiert} importiert, ${uebersprungen} schon vorhanden` + (fehler ? `, ${fehler} übersprungen (fehlerhaft)` : "") + ".";
btn.disabled = false;
await ladeUndRenderTeam();
});
/* ===== "Kategorien & Seitentitel" (21.08.2026) =====
Ein Kärtchen pro Kategorie mit zwei Feldern (Kategorie-Name,
Seitentitel) -- speichert beide über denselben admin/site-texts/save-
Endpunkt wie zwei unabhängige Overrides. Aktualisiert nach dem
Speichern sofort die Kategorie-Badges in der Team-Liste, damit man den
Effekt nicht erst durch Neuladen sehen muss. */
const SITE_TEXT_KATEGORIEN = [
{ kat: "modi", label: "Modi", kategorieKey: "kategorie_modi", seitentitelKey: "seitentitel_modi", kategoriePlatzhalter: "Modi", seitentitelPlatzhalter: "Das Modi-Team" },
{ kat: "scout", label: "Scout", kategorieKey: "kategorie_scout", seitentitelKey: "seitentitel_scout", kategoriePlatzhalter: "Scouts", seitentitelPlatzhalter: "Mein Scout-Team" },
{ kat: "creator", label: "Creator", kategorieKey: "kategorie_creator", seitentitelKey: "seitentitel_creator", kategoriePlatzhalter: "Creator", seitentitelPlatzhalter: "Creator im Team" },
{ kat: "manager", label: "Manager", kategorieKey: "kategorie_manager", seitentitelKey: "seitentitel_manager", kategoriePlatzhalter: "Manager", seitentitelPlatzhalter: "Weitere Manager" },
];
function siteTextKarteHtml(eintrag) {
return `<div class="card vw-card vw-shine-card vw-noise" style="padding:1.3rem;" data-site-text-kat="${eintrag.kat}">
<h3 style="margin-top:0;">${eintrag.label}</h3>
<div class="field">
<label>Kategorie-Name</label>
<input type="text" class="site-text-kategorie-input" maxlength="40" placeholder="${eintrag.kategoriePlatzhalter}" />
</div>
<div class="field">
<label>Seitentitel</label>
<input type="text" class="site-text-seitentitel-input" maxlength="100" placeholder="${eintrag.seitentitelPlatzhalter}" />
</div>
<button type="button" class="btn btn-outline site-text-speichern-btn" style="width:100%;">Speichern</button>
<p class="small site-text-status" style="margin-top:.5rem;"></p>
</div>`;
}
async function teamKategorieLabelsAktualisieren(texteVorhanden) {
let texte = texteVorhanden;
if (!texte) {
try {
const res = await fetch(`${API_BASE_URL}/site-texts`, { cache: "no-store" });
const data = await res.json();
texte = data.ok ? data.texte || {} : {};
} catch {
texte = {};
}
}
Object.keys(TEAM_KATEGORIE_LABELS).forEach((kat) => {
const override = texte[`kategorie_${kat}`]?.de;
if (override) TEAM_KATEGORIE_LABELS[kat] = override;
});
document.querySelectorAll("#team-filter-bar .filter-btn[data-team-filter]").forEach((btn) => {
const kat = btn.dataset.teamFilter;
if (TEAM_KATEGORIE_LABELS[kat]) btn.textContent = TEAM_KATEGORIE_LABELS[kat];
});
}
async function ladeUndRenderSiteTexte() {
const grid = document.getElementById("site-texts-grid");
grid.innerHTML = SITE_TEXT_KATEGORIEN.map(siteTextKarteHtml).join("");
let aktuelleTexte = {};
try {
const res = await fetch(`${API_BASE_URL}/site-texts`, { cache: "no-store" });
const data = await res.json();
if (data.ok) aktuelleTexte = data.texte || {};
} catch {
/* Formular bleibt leer (= eingebauter Standardtext), Speichern funktioniert trotzdem. */
}
SITE_TEXT_KATEGORIEN.forEach((eintrag) => {
const karte = grid.querySelector(`[data-site-text-kat="${eintrag.kat}"]`);
karte.querySelector(".site-text-kategorie-input").value = aktuelleTexte[eintrag.kategorieKey]?.de || "";
karte.querySelector(".site-text-seitentitel-input").value = aktuelleTexte[eintrag.seitentitelKey]?.de || "";
karte.querySelector(".site-text-speichern-btn").addEventListener("click", async () => {
const statusEl = karte.querySelector(".site-text-status");
const btn = karte.querySelector(".site-text-speichern-btn");
btn.disabled = true;
statusEl.textContent = "Wird gespeichert …";
statusEl.style.color = "";
const kategorieText = karte.querySelector(".site-text-kategorie-input").value.trim();
const seitentitelText = karte.querySelector(".site-text-seitentitel-input").value.trim();
const [r1, r2] = await Promise.all([
window.AdminAuth.apiCall(API_BASE_URL, "/admin/site-texts/save", { key: eintrag.kategorieKey, text: kategorieText }),
window.AdminAuth.apiCall(API_BASE_URL, "/admin/site-texts/save", { key: eintrag.seitentitelKey, text: seitentitelText }),
]);
btn.disabled = false;
if (!r1.ok || !r2.ok) {
statusEl.textContent = r1.error || r2.error || "Speichern fehlgeschlagen.";
statusEl.style.color = "#ff6b6b";
return;
}
statusEl.textContent = "✅ Gespeichert — wirkt sofort auf der Website.";
statusEl.style.color = "#baf7d6";
await teamKategorieLabelsAktualisieren();
teamListeZeichnen();
});
});
await teamKategorieLabelsAktualisieren(aktuelleTexte);
teamListeZeichnen();
}
// Nutzer-Wunsch 04.08.2026 (6. Runde): zwei getrennte Kisten/Formulare
// (Dogi + VanVan) statt einem gemeinsamen Feld — beide rufen denselben
// Login-Endpunkt auf, die Fehlermeldung erscheint jeweils nur in der
+6 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Die Werte, nach denen DogFather als Manager bei Spicy Media arbeitet." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
</head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-werte.jpg');--page-bg-mobile:url('/assets/img/bg-werte-mobile.jpg');">
<div id="site-header"></div>
@@ -113,7 +113,7 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/i18n-werte.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/i18n-werte.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
</body>
</html>
+6 -6
View File
@@ -19,10 +19,10 @@
<meta name="twitter:description" content="Die Geschichte von DogFather in einer interaktiven Zeitleiste." />
<meta name="twitter:image" content="https://dogfather-universe.com/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821k" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821k" />
</head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-zeitreise.jpg');--page-bg-mobile:url('/assets/img/bg-zeitreise-mobile.jpg');">
<div id="site-header"></div>
@@ -237,8 +237,8 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/i18n-zeitreise.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/i18n-zeitreise.js?v=20260821k"></script>
<script src="assets/js/main.js?v=20260821k"></script>
<script>
/* Wachstums-Reveal: Äste blenden sich beim Scrollen sanft ein — passt
zum "wachsenden Baum"-Konzept dieser Seite. Rein page-lokal, betrifft