Verwaltungsseite perfektioniert: Team-Verwaltung für Modis/Scouts/Creator/Manager

Nutzer-Wunsch 21.08.2026: "überall wo Modis sind oder Scouts oder
Manager, oder Creator, will ich dass ich die easy über meine
Verwaltungsseite hinzufüge und die automatisch in der Website
hinzugefügt werden ... auch mit Fotos und TikTok Link."

Backend (server-internal, braucht Filipes manuellen Deploy):
- Neue Tabelle team_members (Migration 0007) für alle vier Kategorien
  gemeinsam -- ergänzt, überschreibt NIE die von Hand gepflegten
  Einträge in data-modis.js/data-scouts.js/data-creator.js.
- routes/team.js: öffentliches Lesen (nur aktive Mitglieder, optional
  nach Kategorie gefiltert), TEAM_MANAGE-geschütztes Anlegen/Bearbeiten/
  Löschen, Foto-Upload (gleiches Muster wie "Event des Jahres"),
  automatische Übersetzung von Rolle/Bio/Zitat wie bei anderen
  admin-gepflegten Texten. Slug global eindeutig (mit automatischer
  Kollisionsauflösung), TikTok-Kurzname wird zu voller URL ergänzt.
  27 Tests in isolierter Testumgebung geprüft (kein better-sqlite3
  lokal kompilierbar).

Frontend:
- window.dogiTeamLaden() in main.js: holt admin-gepflegte Mitglieder
  einer (oder aller) Kategorien und bringt sie in exakt dieselbe Form
  wie die bestehenden data-*.js-Einträge.
- team-scouts.html/team-modis.html/creator.html hängen das Ergebnis
  einfach an ihre bestehenden Arrays an und rendern erneut -- exakt
  derselbe Look wie die schon bestehenden Profile (Foto/Avatar-Initiale,
  Rolle, Zitat, TikTok-Button). creator.html hatte bisher ein "return"
  bei leerem CREATORS-Array, wodurch admin-Creator NIE geladen worden
  wären -- gefixt. team-modis.html berücksichtigt Rang/Rang-Bezeichnung
  für die Pyramide.
- Neue Sektion "Weitere Manager" auf manager.html, komplett unsichtbar
  bis der erste Manager angelegt wird (data-manager.js neu, wie
  data-creator.js aktuell leer).
- profil.html sucht jetzt kategorieübergreifend auch in admin-gepflegten
  Profilen, inkl. korrektem Theme/Zurück-Link auch für Manager.

Verwaltung: neue Sektion "Team verwalten" (TEAM_MANAGE-Berechtigung) --
ein Formular für alle vier Kategorien mit Foto-Sofort-Upload,
TikTok-Feld, Bio/Zitat, bedingten Rang-Feldern (nur Modi), Kategorie-
Filterleiste und Bearbeiten/Löschen pro Eintrag.

Beim Testen mit Playwright einen echten Syntaxfehler gefunden und
gefixt (ASCII-Anführungszeichen statt schließendem „" in einem
Statustext), der das GESAMTE Verwaltungs-Skript und damit die komplette
Seite lahmgelegt hätte.

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

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-08-21 03:06:21 +02:00
co-authored by Claude Sonnet 5
parent 8ead1b750b
commit 7060a4a6ae
45 changed files with 1077 additions and 247 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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
</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=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<script src="assets/js/i18n-404.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
<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=20260821i"></script>
<script src="assets/js/supporter.js?v=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<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>
(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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
</head>
<body>
<div id="site-header"></div>
@@ -134,6 +134,6 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/main.js?v=20260821i"></script>
<script src="assets/js/main.js?v=20260821j"></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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
</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=20260821i"></script>
<script src="assets/js/i18n-aktion-detail.js?v=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<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>
document.addEventListener("DOMContentLoaded", () => {
const params = new URLSearchParams(location.search);
+18
View File
@@ -0,0 +1,18 @@
/* =====================================================================
data-manager.js — Manager-Datenobjekt (21.08.2026)
DogFather (Filipe) selbst hat weiterhin seine eigene, feste Seite
(manager.html) — dieses Array ist für WEITERE Manager gedacht, die
künftig dazukommen könnten (siehe zeitreise.html, "Aufbau eines
eigenen Creator-Systems": "Eine langfristige Struktur für Creator,
Scouts und zukünftige Manager"). Noch niemand final geklärt, deshalb
bewusst leer — genau wie data-creator.js, bis echte Namen feststehen.
Einfach nach dem Muster in data-scouts.js Objekte ins Array einfügen,
sobald es so weit ist — manager.html liest automatisch aus.
Felder: siehe data-scouts.js (identisches Muster).
===================================================================== */
const MANAGERS = [];
if (typeof window !== "undefined") window.MANAGERS = MANAGERS;
+7
View File
@@ -39,6 +39,13 @@ window.I18N_PAGE = {
de: "Profil ansehen", "de-CH": "Profil aluege",
en: "View profile", fr: "Voir le profil", pt: "Ver perfil",
},
/* 21.08.2026: Fallback-Kategorie für über die Verwaltung angelegte
Creator ohne eigenes Genre (die Verwaltung fragt aktuell keine
Genre-Kategorie ab) -- verhindert einen "undefined"-Filterknopf. */
crt_kategorie_sonstige: {
de: "Sonstige", "de-CH": "Anders",
en: "Other", fr: "Autre", pt: "Outro",
},
crt_bewerben_btn: {
de: "Als Creator bewerben", "de-CH": "Als Creator bewärbe",
en: "Apply as a creator", fr: "Postuler comme créateur", pt: "Candidatar-se como creator",
+12
View File
@@ -177,4 +177,16 @@ window.I18N_PAGE = {
fr: "Les scouts trouvent et accompagnent de nouveaux talents pour Dogi.",
pt: "Os scouts encontram e acompanham novos talentos para o Dogi.",
},
/* 21.08.2026: Abschnitt für weitere, über die Verwaltung angelegte Manager
(siehe zeitreise.html, "Aufbau eines eigenen Creator-Systems" ->
"zukünftige Manager"). Bleibt komplett unsichtbar, solange keiner
eingetragen ist -- siehe renderWeitereManager() weiter unten. */
mgr_weitere_h2: {
de: "Weitere Manager", "de-CH": "Witeri Manager",
en: "More Managers", fr: "Autres managers", pt: "Outros managers",
},
mgr_weitere_profil_btn: {
de: "Profil ansehen", "de-CH": "Profil aluege",
en: "View profile", fr: "Voir le profil", pt: "Ver perfil",
},
};
+8
View File
@@ -30,6 +30,10 @@ window.I18N_PAGE = {
de: "Manager DogFather — Creator", "de-CH": "Manager DogFather — Creator",
en: "Manager DogFather — Creator", fr: "Manager DogFather — Creator", pt: "Manager DogFather — Creator",
},
pr_eyebrow_manager: {
de: "Manager DogFather — Spicy Media", "de-CH": "Manager DogFather — Spicy Media",
en: "Manager DogFather — Spicy Media", fr: "Manager DogFather — Spicy Media", pt: "Manager DogFather — Spicy Media",
},
pr_back_modis: {
de: "← Zurück zum Modi-Team", "de-CH": "← Zrugg zum Modi-Team",
en: "← Back to the Modi Team", fr: "← Retour à l'équipe Modi", pt: "← Voltar à equipe Modi",
@@ -42,6 +46,10 @@ window.I18N_PAGE = {
de: "← Zurück zu den Creatorn", "de-CH": "← Zrugg zu de Creator",
en: "← Back to the Creators", fr: "← Retour aux Creators", pt: "← Voltar aos Creators",
},
pr_back_manager: {
de: "← Zurück zu Manager DogFather", "de-CH": "← Zrugg zu Manager DogFather",
en: "← Back to Manager DogFather", fr: "← Retour à Manager DogFather", pt: "← Voltar a Manager DogFather",
},
pr_badge_prefix: {
de: "Stufe", "de-CH": "Stuefe",
en: "Level", fr: "Niveau", pt: "Nível",
+51
View File
@@ -1154,6 +1154,57 @@
return feld[sprache] || feld.de || "";
};
/* =====================================================================
dogiTeamLaden() — 21.08.2026, Nutzer-Wunsch: "überall wo Modis sind
oder Scouts oder Manager, oder Creator, will ich dass ich die easy
über meine Verwaltungsseite hinzufüge und die automatisch in der
Website hinzugefügt werden ... auch mit Fotos und TikTok Link."
Holt die über die Verwaltung angelegten Team-Mitglieder einer
Kategorie ("modi" | "scout" | "creator" | "manager") vom Server und
bringt sie in GENAU dieselbe Objektform wie die von Hand gepflegten
Einträge in data-modis.js/data-scouts.js/data-creator.js/
data-manager.js (name/slug/role/photo/bio/quote/social/...) — die
aufrufende Seite hängt das Ergebnis einfach hinten an ihr bestehendes
window.MODIS/SCOUTS/CREATORS/MANAGERS-Array an und ruft ihre eigene,
unveränderte Render-Funktion erneut auf. Kein kategorie-Parameter
liefert ALLE Kategorien zusammen (genutzt von profil.html, das vorab
nicht weiß, zu welcher Kategorie ein slug gehört).
Schlägt die Anfrage fehl (Server nicht erreichbar, Netz weg): leeres
Array statt Fehler -- die von Hand gepflegten Einträge bleiben davon
komplett unberührt sichtbar, nur admin-gepflegte kommen dann halt
(vorübergehend) nicht hinzu. */
const TEAM_API_BASIS = "https://postfach.dogfather-universe.com";
window.dogiTeamLaden = async function (kategorie) {
try {
const url = kategorie ? `${TEAM_API_BASIS}/team?kategorie=${encodeURIComponent(kategorie)}` : `${TEAM_API_BASIS}/team`;
const res = await fetch(url, { cache: "no-store" });
const data = await res.json();
if (!data || !data.ok || !Array.isArray(data.mitglieder)) return [];
return data.mitglieder.map((m) => ({
name: m.name,
slug: m.slug,
kategorie: m.kategorie,
role: m.rolle || "",
photo: m.foto || "",
avatar: m.foto || "",
bio: m.bio || "",
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,
// Markiert admin-gepflegte Einträge (z.B. für profil.html, das
// sonst nicht zwischen fest einprogrammierten und dynamischen
// Personen unterscheiden könnte).
istAdminGepflegt: true,
}));
} catch {
return [];
}
};
/* ===================================================================
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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
</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=20260821i"></script>
<script src="assets/js/i18n-bewerben-creator.js?v=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<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>
</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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
</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=20260821i"></script>
<script src="assets/js/i18n-bewerben-kooperation.js?v=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<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>
</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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
</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=20260821i"></script>
<script src="assets/js/i18n-bewerben-modi.js?v=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<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>
</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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
</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=20260821i"></script>
<script src="assets/js/i18n-bewerben-scout.js?v=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<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>
</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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
</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=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<script src="assets/js/i18n-bewerben.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
</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=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<script src="assets/js/i18n-casper.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
</body>
</html>
@@ -0,0 +1,44 @@
-- =====================================================================
-- Team-Mitglieder (21.08.2026) — Nutzer-Wunsch: "überall wo Modis sind
-- oder Scouts oder Manager, oder Creator, will ich dass ich die easy über
-- meine Verwaltungsseite hinzufüge und die automatisch in der Website
-- hinzugefügt werden ... auch mit Fotos und TikTok Link."
--
-- Eine gemeinsame Tabelle für alle vier Kategorien (statt vier eigenen),
-- weil sie strukturell identisch sind (Name/Rolle/Foto/TikTok/Bio/Zitat) --
-- "kategorie" unterscheidet, wo jemand öffentlich erscheint. rang/
-- rang_label sind nur für Modis relevant (Pyramiden-Hierarchie auf
-- team-modis.html), bei den anderen Kategorien bleiben sie einfach leer.
--
-- Ergänzt die weiterhin bestehenden, von Hand gepflegten Einträge in
-- assets/js/data-modis.js/data-scouts.js/data-creator.js/data-manager.js
-- (z.B. VanVan, Patrick, Bananenstift) -- die Website mischt beide Quellen
-- zu einer gemeinsamen Liste (siehe window.dogiTeamLaden() in main.js).
-- Ein Fehler bei einer Kategorie kann so nie die schon bestehenden,
-- fest einprogrammierten Profile verschwinden lassen.
--
-- slug ist GLOBAL eindeutig (nicht nur pro Kategorie), weil profil.html
-- über alle Kategorien hinweg nach genau einem slug sucht.
-- =====================================================================
CREATE TABLE IF NOT EXISTS team_members (
id TEXT PRIMARY KEY,
kategorie TEXT NOT NULL, -- modi | scout | creator | manager
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
rolle_json TEXT NOT NULL, -- {de, "de-CH", en, fr, pt}
foto_url TEXT,
tiktok_url TEXT,
bio_json TEXT, -- {de, "de-CH", en, fr, pt} oder NULL
zitat_json TEXT, -- {de, "de-CH", en, fr, pt} oder NULL
rang INTEGER, -- nur Modi-Pyramide, sonst NULL
rang_label_json TEXT, -- nur Modi-Pyramide, sonst NULL
reihenfolge INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'aktiv', -- aktiv | pausiert
erstellt_am TEXT NOT NULL,
geaendert_am TEXT NOT NULL,
erstellt_von TEXT,
geaendert_von TEXT
);
CREATE INDEX IF NOT EXISTS idx_team_members_kategorie ON team_members (kategorie, status, reihenfolge);
+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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
</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=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<script src="assets/js/i18n-community.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
</body>
</html>
+27 -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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
</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>
@@ -49,6 +49,7 @@
<span data-i18n="crt_empty">Noch keine Creator-Profile veröffentlicht — bald verfügbar. 🩵</span>
<span data-i18n="crt_filter_alle">alle</span>
<span data-i18n="crt_profil_btn">Profil ansehen</span>
<span data-i18n="crt_kategorie_sonstige">Sonstige</span>
</div>
<!-- TODO: Noch keine Creator-Profile final geklärt. Sobald Namen/Infos feststehen, in assets/js/data-creator.js ergänzen — diese Seite rendert automatisch. -->
<div class="btn-row" style="justify-content:center;">
@@ -59,14 +60,26 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/data-creator.js?v=20260821i"></script>
<script src="assets/js/i18n-creator.js?v=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<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>
document.addEventListener("DOMContentLoaded", () => {
document.addEventListener("DOMContentLoaded", async () => {
const grid = document.getElementById("creator-grid");
const filterBar = document.getElementById("creator-filters");
const list = window.CREATORS || [];
let list = window.CREATORS || [];
// 21.08.2026: über die Verwaltung angelegte Creator ergänzen, BEVOR
// entschieden wird, ob die Seite "leer" ist (Nutzer-Wunsch "überall wo
// ... Creator ... sind, will ich dass ich die easy über meine
// Verwaltungsseite hinzufüge"). Vorher gab es direkt darunter ein
// "return", sobald window.CREATORS leer war (bisher der Normalfall, da
// noch keine echten Creator-Profile feststehen) -- dadurch wären
// admin-gepflegte Creator NIE geladen worden.
if (window.dogiTeamLaden) {
const neue = await window.dogiTeamLaden("creator");
if (neue.length) list = [...list, ...neue];
}
// Texte fürs JS-generierte Grid/Filter kommen aus den versteckten
// data-i18n-Elementen oben — main.js übersetzt sie ganz normal (auch bei
@@ -88,6 +101,11 @@ document.addEventListener("DOMContentLoaded", () => {
return;
}
// 21.08.2026: über die Verwaltung angelegte Creator haben (noch) kein
// eigenes Genre-Feld (category) -- ohne diesen Fallback würde die
// Filterleiste einen kaputten "undefined"-Knopf zeigen.
list = list.map((c) => (c.category ? c : { ...c, category: uebersetzen("crt_kategorie_sonstige") }));
const categories = ["alle", ...new Set(list.map((c) => c.category))];
let aktiverFilter = "alle";
+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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
</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=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<script src="assets/js/i18n-dogfather.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/main.css?v=20260821j" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821j" />
<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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
</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=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<script src="assets/js/i18n-hasidog.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
</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=20260821i"></script>
<script src="assets/js/data-scouts.js?v=20260821i"></script>
<script src="assets/js/i18n-index.js?v=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<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>
// 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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
</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=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<script src="assets/js/i18n-kontakt.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
<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=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<script src="assets/js/i18n-links.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script>
(function () {
document.addEventListener("DOMContentLoaded", function () {
+64 -6
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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
</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>
@@ -96,6 +96,19 @@
</div>
</section>
<!-- "Weitere Manager" (21.08.2026, Nutzer-Wunsch: "überall wo ... Manager
... sind, will ich dass ich die easy über meine Verwaltungsseite
hinzufüge"). Aktuell gibt es nur DogFather selbst als Manager -- diese
Sektion bleibt deshalb komplett unsichtbar (hidden), bis über die
Verwaltung mindestens ein weiterer Manager angelegt wurde. Gleiches
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>
<div id="weitere-manager-grid" class="grid grid-4"></div>
</div>
</section>
<section>
<div class="container">
<h2 class="text-center" data-i18n="mgr_team_h2">Willst du dazugehören?</h2>
@@ -126,7 +139,52 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/i18n-manager.js?v=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<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>
// "Weitere Manager" (21.08.2026) -- siehe Kommentar bei #weitere-manager-root
// oben. Aktuell IMMER window.MANAGERS === [] (siehe data-manager.js), Inhalt
// kommt also ausschließlich über die Verwaltung (dogiTeamLaden). Bleibt
// komplett unsichtbar, solange niemand eingetragen ist.
document.addEventListener("DOMContentLoaded", () => {
const root = document.getElementById("weitere-manager-root");
const grid = document.getElementById("weitere-manager-grid");
if (!root || !grid) return;
const feld = (x) => (window.dogiFeld ? window.dogiFeld(x) : x || "");
const t = (schluessel) => (window.dogiUebersetzen ? window.dogiUebersetzen(schluessel) : schluessel);
const renderWeitereManager = () => {
const liste = window.MANAGERS || [];
if (!liste.length) {
root.hidden = true;
return;
}
grid.innerHTML = liste.map((m) => {
const quoteText = feld(m.quote);
return `
<a class="profile-card" href="profil.html?p=${m.slug}">
<div class="avatar">${(m.avatar || m.photo) ? `<img src="${m.avatar || m.photo}" alt="${m.name}" loading="lazy">` : `<span class="avatar-initial">${m.name.charAt(0).toUpperCase()}</span>`}</div>
<h3>${m.name}</h3>
<div class="role">${feld(m.role)}</div>
${quoteText ? `<p class="quote">„${quoteText}"</p>` : ""}
<span class="btn btn-outline" style="margin-top:.7rem;pointer-events:none;">${t("mgr_weitere_profil_btn")}</span>
</a>`;
}).join("");
root.hidden = false;
};
document.addEventListener("dogi-sprache-geaendert", renderWeitereManager);
if (window.dogiTeamLaden) {
window.dogiTeamLaden("manager").then((neue) => {
if (!neue.length) return;
window.MANAGERS = [...(window.MANAGERS || []), ...neue];
renderWeitereManager();
});
}
});
</script>
</body>
</html>
+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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
</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=20260821i"></script>
<script src="assets/js/data-videos.js?v=20260821i"></script>
<script src="assets/js/medien-welt.js?v=20260821i"></script>
<script src="assets/js/i18n-medien-casper.js?v=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<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>
</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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
</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=20260821i"></script>
<script src="assets/js/data-videos.js?v=20260821i"></script>
<script src="assets/js/data-aktionen.js?v=20260821i"></script>
<script src="assets/js/medien-welt.js?v=20260821i"></script>
<script src="assets/js/i18n-medien-dogfather.js?v=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<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>
</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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
</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=20260821i"></script>
<script src="assets/js/data-videos.js?v=20260821i"></script>
<script src="assets/js/data-aktionen.js?v=20260821i"></script>
<script src="assets/js/medien-welt.js?v=20260821i"></script>
<script src="assets/js/i18n-medien-hasidog.js?v=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<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>
</body>
</html>
+33 -15
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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
<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,18 +130,25 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/data-modis.js?v=20260821i"></script>
<script src="assets/js/data-scouts.js?v=20260821i"></script>
<script src="assets/js/data-creator.js?v=20260821i"></script>
<script src="assets/js/i18n-profil.js?v=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<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>
document.addEventListener("DOMContentLoaded", () => {
document.addEventListener("DOMContentLoaded", async () => {
const slug = new URLSearchParams(location.search).get("p") || "";
const modis = window.MODIS || [];
const scouts = window.SCOUTS || [];
const creators = window.CREATORS || [];
const alle = [...modis, ...scouts, ...creators];
const managers = window.MANAGERS || [];
// 21.08.2026: über die Verwaltung angelegte Mitglieder ALLER Kategorien
// dazuholen -- profil.html weiß vorab nicht, zu welcher Kategorie ein
// slug gehört, deshalb kommt hier (ohne Kategorie-Filter) einfach alles
// zusammen zurück und wird unten normal mitgesucht.
const admins = window.dogiTeamLaden ? await window.dogiTeamLaden() : [];
const alle = [...modis, ...scouts, ...creators, ...managers, ...admins];
const person = alle.find((p) => p.slug === slug);
// Für alle Rahmentexte, die rein per JS gesetzt werden (Name/Rolle/Bio
@@ -170,16 +177,22 @@ document.addEventListener("DOMContentLoaded", () => {
return;
}
const istScout = scouts.includes(person);
const istCreator = creators.includes(person);
const welt = istScout || istCreator ? "spicymedia" : "dogfather";
// 21.08.2026: admin-gepflegte Personen (aus dogiTeamLaden) stecken NICHT
// in den statischen modis/scouts/creators/managers-Arrays selbst (die
// reference-Prüfung .includes(person) würde für sie also fälschlich
// "false" liefern) -- deshalb zusätzlich über das von dogiTeamLaden
// gesetzte person.kategorie-Feld prüfen.
const istScout = scouts.includes(person) || person.kategorie === "scout";
const istCreator = creators.includes(person) || person.kategorie === "creator";
const istManager = managers.includes(person) || person.kategorie === "manager";
const welt = istScout || istCreator || istManager ? "spicymedia" : "dogfather";
document.documentElement.setAttribute("data-theme", welt);
// Passendes Weltmotiv nur noch als weichgezeichnete Stimmung ganz im
// Hintergrund (body.mit-hintergrund) — bewusst KEIN scharfes Banner-Bild
// mehr oben im Kopfbereich, damit das Profil der Person (Foto/Name/Bio)
// sofort beim Öffnen der Seite im Fokus steht, ohne Banner davor.
const bgBasis = istScout || istCreator ? "bg-scouts" : "bg-dogfather";
const bgBasis = istScout || istCreator || istManager ? "bg-scouts" : "bg-dogfather";
document.body.style.setProperty("--page-bg", `url('/assets/img/${bgBasis}.jpg')`);
document.body.style.setProperty("--page-bg-mobile", `url('/assets/img/${bgBasis}-mobile.jpg')`);
@@ -241,6 +254,8 @@ document.addEventListener("DOMContentLoaded", () => {
? t("pr_eyebrow_scout")
: istCreator
? t("pr_eyebrow_creator")
: istManager
? t("pr_eyebrow_manager")
: t("pr_eyebrow_default");
document.getElementById("profil-hero-name").textContent = person.name;
@@ -286,6 +301,9 @@ document.addEventListener("DOMContentLoaded", () => {
} else if (istCreator) {
zurueck.href = "creator.html";
zurueck.textContent = t("pr_back_creator");
} else if (istManager) {
zurueck.href = "manager.html";
zurueck.textContent = t("pr_back_manager");
} else {
zurueck.href = "team-modis.html";
zurueck.textContent = t("pr_back_modis");
+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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
<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=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<script src="assets/js/i18n-rabattcodes.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script>
(function () {
document.addEventListener("DOMContentLoaded", function () {
+1
View File
@@ -30,6 +30,7 @@ const MIGRATION_FILES = [
"0004_supporter_oauth_providers.sql",
"0005_supporter_polls.sql",
"0006_testimonials.sql",
"0007_team_members.sql",
];
function alreadyApplied(name) {
+22
View File
@@ -71,6 +71,9 @@ import {
import {
createPoll, listPolls, closePoll, reopenPoll, deletePoll, getActivePollPublic, voteOnPoll,
} from "./routes/polls.js";
import {
getTeamPublic, getTeamAdmin, saveTeamMember, deleteTeamMember, uploadTeamPhoto,
} from "./routes/team.js";
initDb();
@@ -86,6 +89,10 @@ app.disable("x-powered-by"); // verrät sonst unnötig den eingesetzten Technik-
const UPLOADS_BASIS_DIR = process.env.UPLOADS_DIR || "/var/lib/dogfather-internal/uploads";
const EVENTS_UPLOADS_DIR = `${UPLOADS_BASIS_DIR}/events`;
mkdirSync(EVENTS_UPLOADS_DIR, { recursive: true });
// Team-Fotos (21.08.2026, "Verwaltungsseite perfektionieren"-Auftrag) --
// eigener Unterordner, gleiches Muster wie bei den Event-Bildern oben.
const TEAM_UPLOADS_DIR = `${UPLOADS_BASIS_DIR}/team`;
mkdirSync(TEAM_UPLOADS_DIR, { recursive: true });
const ERLAUBTE_BILD_TYPEN = new Set(["image/jpeg", "image/png", "image/webp"]);
const bildUpload = multer({
@@ -99,6 +106,14 @@ const bildUpload = multer({
limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB
fileFilter: (req, file, cb) => cb(null, ERLAUBTE_BILD_TYPEN.has(file.mimetype)),
});
const teamBildUpload = multer({
storage: multer.diskStorage({
destination: (req, file, cb) => cb(null, TEAM_UPLOADS_DIR),
filename: (req, file, cb) => cb(null, `${crypto.randomUUID()}${extname(file.originalname).toLowerCase()}`),
}),
limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB
fileFilter: (req, file, cb) => cb(null, ERLAUBTE_BILD_TYPEN.has(file.mimetype)),
});
app.use((req, res, next) => {
res.setHeader("X-Content-Type-Options", "nosniff");
@@ -164,6 +179,7 @@ app.get("/events-of-year", wrap(getEventsPublic));
app.get("/special-event", wrap(getSpecialEventPublic));
app.post("/testimonials/submit", wrap(submitTestimonial));
app.get("/testimonials", wrap(getApprovedTestimonialsPublic));
app.get("/team", wrap(getTeamPublic));
app.post("/supporter/register", wrap(registerSupporter));
app.post("/supporter/verify-email", wrap(verifySupporterEmail));
@@ -210,6 +226,12 @@ app.post("/admin/testimonials/list", requireTeamSession, withSession(getTestimon
app.post("/admin/testimonials/status", requireTeamSession, withSession(setTestimonialStatus));
app.post("/admin/testimonials/delete", requireTeamSession, withSession(deleteTestimonial));
// 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/delete", requireTeamSession, withSession(deleteTeamMember));
app.post("/admin/team/upload-photo", requireTeamSession, teamBildUpload.single("image"), withSession(uploadTeamPhoto));
app.post("/admin/roles/list", requireTeamSession, withSession(listRoles));
app.post("/admin/roles/create", requireTeamSession, withSession(createRole));
app.post("/admin/roles/update", requireTeamSession, withSession(updateRole));
+2
View File
@@ -49,6 +49,7 @@ export const PERMISSIONS = {
EVENTS_MANAGE: "Startseite: \"Event des Jahres\" verwalten",
TESTIMONIALS_MANAGE: "Stimmen/Testimonials verwalten",
TEAM_MANAGE: "Team-Mitglieder verwalten (Modis/Scouts/Creator/Manager)",
};
export const PERMISSION_GROUPS = [
@@ -61,6 +62,7 @@ export const PERMISSION_GROUPS = [
{ label: "DogiCrew 🩵", keys: ["SUPPORTER_VIEW", "SUPPORTER_MANAGE_PREMIUMS", "SUPPORTER_MANAGE_COLLECTIONS", "POLLS_MANAGE"] },
{ label: "Startseite", keys: ["EVENTS_MANAGE"] },
{ label: "Stimmen", keys: ["TESTIMONIALS_MANAGE"] },
{ label: "Team", keys: ["TEAM_MANAGE"] },
];
export const OWNER_ONLY_ACTIONS = [
+228
View File
@@ -0,0 +1,228 @@
/* =====================================================================
routes/team.js — Team-Mitglieder-Verwaltung (21.08.2026, Nutzer-Wunsch:
"überall wo Modis sind oder Scouts oder Manager, oder Creator, will ich
dass ich die easy über meine Verwaltungsseite hinzufüge und die
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.
Öffentlicher Lesezugriff (getTeamPublic) ohne Session, nur AKTIVE
Mitglieder -- Schreibzugriff nur mit TEAM_MANAGE-Berechtigung.
===================================================================== */
import { db } from "../db.js";
import { generateId, nowIso } from "../lib/crypto.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";
const KATEGORIEN = ["modi", "scout", "creator", "manager"];
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);
}
function parseJsonFeld(text) {
if (!text) return null;
try {
return JSON.parse(text);
} catch {
return null;
}
}
/** 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. */
function slugAusName(name) {
return String(name || "")
.toLowerCase()
.replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 60);
}
function oeffentlichesFeld(row) {
return {
id: row.id,
kategorie: row.kategorie,
name: row.name,
slug: row.slug,
rolle: parseJsonFeld(row.rolle_json),
foto: row.foto_url || "",
tiktok: row.tiktok_url || "",
bio: parseJsonFeld(row.bio_json),
zitat: parseJsonFeld(row.zitat_json),
rang: row.rang,
rangLabel: parseJsonFeld(row.rang_label_json),
};
}
function vollesFeld(row) {
return {
...oeffentlichesFeld(row),
status: row.status,
reihenfolge: row.reihenfolge,
erstelltAm: row.erstellt_am,
geaendertAm: row.geaendert_am,
};
}
/** Öffentlich, keine Session: nur AKTIVE Mitglieder. Ohne ?kategorie=
kommen alle vier Kategorien zusammen zurück (genutzt von profil.html,
das vorab nicht weiß, zu welcher Kategorie ein slug gehört). */
export async function getTeamPublic(req, res) {
const kategorie = String(req.query?.kategorie || "").trim();
let rows;
if (kategorie && KATEGORIEN.includes(kategorie)) {
rows = db
.prepare(`SELECT * FROM team_members WHERE kategorie = ? AND status = 'aktiv' ORDER BY reihenfolge ASC, erstellt_am ASC`)
.all(kategorie);
} else {
rows = db
.prepare(`SELECT * FROM team_members WHERE status = 'aktiv' ORDER BY reihenfolge ASC, erstellt_am ASC`)
.all();
}
return json(res, { ok: true, mitglieder: rows.map(oeffentlichesFeld) });
}
/** Für die Verwaltung: ALLE Mitglieder aller Kategorien/Status. */
export async function getTeamAdmin(req, res, session) {
if (!can(session, "TEAM_MANAGE")) return forbidden(res);
const rows = db.prepare(`SELECT * FROM team_members ORDER BY kategorie ASC, reihenfolge ASC, erstellt_am ASC`).all();
return json(res, { ok: true, mitglieder: rows.map(vollesFeld) });
}
/** NUR Foto-Upload, kein JSON-Body (multipart, siehe index.js multer-Middleware). */
export async function uploadTeamPhoto(req, res, session) {
if (!can(session, "TEAM_MANAGE")) return forbidden(res);
if (!req.file) return json(res, { ok: false, error: "Keine Bilddatei erhalten." }, 400);
const url = `/uploads/team/${req.file.filename}`;
logAction(session.actor, "team.photo_uploaded", req.file.filename, null);
return json(res, { ok: true, url });
}
export async function saveTeamMember(req, res, session) {
if (!can(session, "TEAM_MANAGE")) return forbidden(res);
const body = req.body || {};
const id = String(body.id || "").trim() || null;
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);
if (!name) return json(res, { ok: false, error: "Name ist Pflicht." }, 400);
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 tiktokRoh = String(body.tiktok || "").trim().slice(0, 200);
let tiktokUrl = null;
if (tiktokRoh) {
if (/^https?:\/\//i.test(tiktokRoh)) {
tiktokUrl = tiktokRoh;
} else {
// Bequemlichkeit: reiner Benutzername (mit oder ohne @) wird
// automatisch zu einer vollen TikTok-URL ergänzt.
tiktokUrl = `https://www.tiktok.com/@${tiktokRoh.replace(/^@/, "")}`;
}
}
const bioDe = String(body.bio || "").trim().slice(0, 800);
const zitatDe = String(body.zitat || "").trim().slice(0, 200);
const fotoUrl = String(body.foto || "").trim().slice(0, 300) || null;
const rang = body.rang !== undefined && body.rang !== null && body.rang !== ""
? Math.max(1, Math.min(99, Number(body.rang) || 0)) : null;
const rangLabelDe = String(body.rangLabel || "").trim().slice(0, 40);
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([
uebersetzeAlleSprachen(rolleDe),
bioDe ? uebersetzeAlleSprachen(bioDe) : Promise.resolve(null),
zitatDe ? uebersetzeAlleSprachen(zitatDe) : Promise.resolve(null),
rangLabelDe ? uebersetzeAlleSprachen(rangLabelDe) : Promise.resolve(null),
]);
const jetzt = nowIso();
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 = ?`
)
.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
);
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);
return json(res, { ok: true, mitglied: vollesFeld(row) });
}
const neueId = generateId();
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) });
}
export async function deleteTeamMember(req, res, session) {
if (!can(session, "TEAM_MANAGE")) return forbidden(res);
const id = String(req.body?.id || "");
const info = db.prepare(`DELETE FROM team_members WHERE id = ?`).run(id);
if (info.changes === 0) return json(res, { ok: false, error: "Nicht gefunden." }, 404);
logAction(session.actor, "team.deleted", id, null);
return json(res, { ok: true });
}
+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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
</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=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<script src="assets/js/stimmen.js?v=20260821i"></script>
<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>
</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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
</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=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<script src="assets/js/i18n-streamer.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
</head>
<body class="mit-hintergrund" style="--page-bg:url('/assets/img/bg-streamplan.jpg?v=20260821i');--page-bg-mobile:url('/assets/img/bg-streamplan-mobile.jpg?v=20260821i');">
<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');">
<div id="site-header"></div>
<main>
<section class="hero page-bg">
<div class="hero-banner"><img src="/assets/img/bg-streamplan.jpg?v=20260821i" alt="" loading="eager" /></div>
<div class="hero-banner"><img src="/assets/img/bg-streamplan.jpg?v=20260821j" 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=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<script src="assets/js/streamplan.js?v=20260821i"></script>
<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>
</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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
<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=20260821i"></script>
<script src="assets/js/supporter.js?v=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<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>
(function () {
const S = window.DogiSupporter;
+30 -11
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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
</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>
@@ -72,24 +72,31 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/data-modis.js?v=20260821i"></script>
<script src="assets/js/i18n-team-modis.js?v=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<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>
document.addEventListener("DOMContentLoaded", () => {
const grid = document.getElementById("modis-grid");
if (!grid || !window.MODIS) return;
const rest = window.MODIS.filter((m) => m.name !== "VanVan").sort((a, b) => a.rank - b.rank);
const feld = (x) => (window.dogiFeld ? window.dogiFeld(x) : x || "");
const t = (schluessel) => (window.dogiUebersetzen ? window.dogiUebersetzen(schluessel) : schluessel);
// Pyramide: eine Ebene pro eindeutigem Rang, oben schmal/groß, nach
// unten breiter/kompakter — genau wie ein Organigramm.
const raenge = [...new Set(rest.map((m) => m.rank))];
const MAX_STUFEN = 5; // fünf definierte Größenstufen im CSS
// 21.08.2026: rest/raenge werden jetzt INNERHALB von renderModis() frisch aus
// window.MODIS berechnet (statt einmalig beim Setup) -- nötig, damit über die
// Verwaltung nachträglich angehängte Modis (siehe dogiTeamLaden weiter unten)
// beim erneuten Aufruf von renderModis() tatsächlich mit auftauchen. Ein
// admin-gepflegter Modi ohne eigenen Rang fällt dabei (?? 999) ganz ans Ende
// der Pyramide, statt die Sortierung der bestehenden Ränge durcheinander zu
// bringen.
const renderModis = () => {
const rest = window.MODIS.filter((m) => m.name !== "VanVan").sort((a, b) => (a.rank ?? 999) - (b.rank ?? 999));
// Pyramide: eine Ebene pro eindeutigem Rang, oben schmal/groß, nach
// unten breiter/kompakter — genau wie ein Organigramm.
const raenge = [...new Set(rest.map((m) => m.rank))];
grid.innerHTML = raenge
.map((rang, i) => {
const stufe = Math.min(i + 1, MAX_STUFEN);
@@ -125,6 +132,18 @@ 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.
if (window.dogiTeamLaden) {
window.dogiTeamLaden("modi").then((neue) => {
if (!neue.length) return;
window.MODIS = [...window.MODIS, ...neue];
renderModis();
});
}
});
</script>
</body>
+20 -7
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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
</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>
@@ -49,9 +49,9 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/data-scouts.js?v=20260821i"></script>
<script src="assets/js/i18n-team-scouts.js?v=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<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>
document.addEventListener("DOMContentLoaded", () => {
const grid = document.getElementById("scouts-grid");
@@ -76,6 +76,19 @@ 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.
if (window.dogiTeamLaden) {
window.dogiTeamLaden("scout").then((neue) => {
if (!neue.length) return;
window.SCOUTS = [...window.SCOUTS, ...neue];
renderScouts();
});
}
});
</script>
</body>
+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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
<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=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<script src="assets/js/i18n-tt-muddi.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
<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=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<script src="assets/js/i18n-vanvan.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
</body>
</html>
+317 -6
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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
<style>
/* =====================================================================
verwaltung.html — Dogis privater Command-Center-Bereich.
@@ -858,6 +858,109 @@
</div>
</section>
<!-- ===== "Team verwalten" (21.08.2026) — Nutzer-Wunsch: "überall wo
Modis sind oder Scouts oder Manager, oder Creator, will ich dass ich
die easy über meine Verwaltungsseite hinzufüge und die 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 -- neue Mitglieder erscheinen automatisch auf
team-modis.html/team-scouts.html/creator.html/manager.html UND auf
profil.html, exakt im selben Look wie die schon bestehenden (siehe
window.dogiTeamLaden() in main.js). Rolle/Bio/Zitat werden wie
"Event des Jahres" nur auf Deutsch eingetippt und beim Speichern
automatisch übersetzt. -->
<section class="section-tight" id="team-root" style="display:none;padding-top:0;">
<div class="container">
<h2 class="text-center" style="margin-bottom:.3rem;">🧑‍🤝‍🧑 Team verwalten</h2>
<p class="small muted text-center" style="max-width:64ch;margin:0 auto 1.4rem;">
Modis, Scouts, Creator und Manager — neue Mitglieder erscheinen automatisch auf der
passenden Seite (mit eigenem Profil, Foto und TikTok-Button), genau wie die schon
bestehenden. Rolle/Vorstellung/Zitat bitte auf Deutsch eintippen, der Rest wird beim
Speichern automatisch übersetzt.
</p>
<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="" />
<div class="field">
<label for="team-kategorie">Kategorie</label>
<select id="team-kategorie">
<option value="modi">Modi (Team Dogi)</option>
<option value="scout">Scout (Spicy Media)</option>
<option value="creator">Creator (Spicy Media)</option>
<option value="manager">Manager (Spicy Media)</option>
</select>
</div>
<div class="field">
<label for="team-name">Name</label>
<input type="text" id="team-name" maxlength="60" placeholder="z. B. Foxy" />
</div>
<div class="field">
<label for="team-rolle">Rolle (Deutsch)</label>
<input type="text" id="team-rolle" maxlength="120" placeholder="z. B. Scout (Manager) — Rechte Hand" />
</div>
<div style="margin-bottom:1rem;text-align:center;">
<img id="team-foto-vorschau" alt="" style="width:140px;height:140px;object-fit:cover;border-radius:50%;background:var(--bg-alt);display:none;" />
</div>
<div class="field">
<label for="team-foto-input">Foto (optional, max. 5 MB, JPG/PNG/WebP)</label>
<input type="file" id="team-foto-input" accept="image/jpeg,image/png,image/webp" />
</div>
<div class="field">
<label for="team-tiktok">TikTok (Benutzername oder Link, optional)</label>
<input type="text" id="team-tiktok" maxlength="200" placeholder="z. B. @foxy123 oder https://tiktok.com/@foxy123" />
</div>
<div class="field">
<label for="team-bio">Kurze Vorstellung (Deutsch, optional)</label>
<textarea id="team-bio" maxlength="800" rows="3"></textarea>
</div>
<div class="field">
<label for="team-zitat">Zitat/Motto (Deutsch, optional)</label>
<input type="text" id="team-zitat" maxlength="200" />
</div>
<div id="team-rang-felder" style="display:none;">
<div class="grid grid-2" style="gap:.8rem;">
<div class="field">
<label for="team-rang">Rang (Pyramiden-Stufe)</label>
<input type="number" id="team-rang" min="1" max="99" />
</div>
<div class="field">
<label for="team-rang-label">Rang-Bezeichnung (Deutsch)</label>
<input type="text" id="team-rang-label" maxlength="40" placeholder="z. B. Rechte Hand" />
</div>
</div>
<p class="small muted" style="margin:.3rem 0 1rem;">
Bestimmt die Ebene in der Modi-Pyramide — ohne Rang landet die Person automatisch
ganz unten.
</p>
</div>
<div class="field">
<label for="team-reihenfolge">Reihenfolge (kleinere Zahl erscheint zuerst)</label>
<input type="number" id="team-reihenfolge" value="0" />
</div>
<label style="display:flex;align-items:center;gap:.6rem;margin:1rem 0;cursor:pointer;">
<input type="checkbox" id="team-aktiv" checked style="width:1.2rem;height:1.2rem;" />
<span>Aktiv — öffentlich sichtbar</span>
</label>
<div class="btn-row">
<button type="button" class="btn btn-primary" id="team-speichern" style="flex:1;">Speichern</button>
<button type="button" class="btn btn-outline" id="team-abbrechen" style="display:none;">Abbrechen</button>
</div>
<p class="small" id="team-status" style="margin-top:.6rem;"></p>
</div>
<div class="btn-row" style="justify-content:center;margin-bottom:1.4rem;" id="team-filter-bar">
<button type="button" class="filter-btn active" data-team-filter="alle">Alle</button>
<button type="button" class="filter-btn" data-team-filter="modi">Modi</button>
<button type="button" class="filter-btn" data-team-filter="scout">Scouts</button>
<button type="button" class="filter-btn" data-team-filter="creator">Creator</button>
<button type="button" class="filter-btn" data-team-filter="manager">Manager</button>
</div>
<div id="team-liste"></div>
</div>
</section>
<!-- ===== Supporter-Liste ===== -->
<section class="section-tight" id="supp-root" style="display:none;padding-top:0;">
<div class="container" id="liste-ansicht">
@@ -1032,8 +1135,8 @@
</main>
<div id="site-footer"></div>
<script src="assets/js/main.js?v=20260821i"></script>
<script src="assets/js/admin-auth.js?v=20260821i"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script src="assets/js/admin-auth.js?v=20260821j"></script>
<script>
(function () {
const API_BASE_URL = "https://postfach.dogfather-universe.com";
@@ -1113,6 +1216,10 @@
document.getElementById("testimonials-root").style.display = "";
await ladeUndRenderTestimonials();
}
if (window.AdminAuth.sessionHasPermission(session, "TEAM_MANAGE")) {
document.getElementById("team-root").style.display = "";
await ladeUndRenderTeam();
}
const pollsBox = document.getElementById("box-abstimmungen");
if (window.AdminAuth.sessionHasPermission(session, "POLLS_MANAGE")) {
pollsBox.style.display = "";
@@ -1132,6 +1239,7 @@
document.getElementById("events-root").style.display = "none";
document.getElementById("special-event-root").style.display = "none";
document.getElementById("testimonials-root").style.display = "none";
document.getElementById("team-root").style.display = "none";
document.getElementById("box-abstimmungen").style.display = "none";
}
@@ -1548,6 +1656,209 @@
});
}
/* ===== "Team verwalten" — Modis/Scouts/Creator/Manager (21.08.2026) =====
Ein Formular für alle vier Kategorien (identische Felder), darunter
eine nach Kategorie filterbare Liste aller bereits angelegten
Mitglieder mit Bearbeiten/Löschen. Rolle/Bio/Zitat werden beim
Speichern serverseitig automatisch übersetzt (siehe routes/team.js) --
genau wie bei "Event des Jahres". Ein hochgeladenes Foto landet SOFORT
bei Auswahl auf dem Server (nicht erst bei "Speichern"), gleiches
Muster wie bei den Event-Bildern weiter oben. */
const TEAM_KATEGORIE_LABELS = { modi: "Modi", scout: "Scout", creator: "Creator", manager: "Manager" };
let teamHochgeladenesFoto = null;
let teamAktiverFilter = "alle";
function teamKarteHtml(m) {
const fotoHtml = m.foto
? `<img src="${m.foto}" alt="${escapeHtml(m.name)}" style="width:56px;height:56px;border-radius:50%;object-fit:cover;flex-shrink:0;">`
: `<span style="width:56px;height:56px;border-radius:50%;background:var(--bg-alt);display:flex;align-items:center;justify-content:center;font-weight:700;flex-shrink:0;">${escapeHtml((m.name || "?").charAt(0).toUpperCase())}</span>`;
return `<div class="card vw-card vw-shine-card" style="margin-bottom:1rem;padding:1.1rem 1.3rem;display:flex;gap:1rem;align-items:center;flex-wrap:wrap;" data-team-id="${m.id}">
${fotoHtml}
<div style="flex:1;min-width:180px;">
<strong>${escapeHtml(m.name)}</strong>
<span class="vw-overview-chip">${TEAM_KATEGORIE_LABELS[m.kategorie] || m.kategorie}</span>
<span class="vw-overview-chip" style="${m.status === "pausiert" ? "opacity:.6;" : ""}">${m.status === "pausiert" ? "⚪ Pausiert" : "🟢 Aktiv"}</span>
<div class="small" style="color:var(--text-muted);">${escapeHtml(m.rolle?.de || "")}</div>
${m.tiktok ? `<a href="${escapeHtml(m.tiktok)}" target="_blank" rel="noopener" class="small">♪ TikTok</a>` : ""}
</div>
<div class="btn-row" style="gap:.5rem;">
<button type="button" class="vw-refresh-btn team-bearbeiten-btn">Bearbeiten</button>
<button type="button" class="vw-refresh-btn team-loeschen-btn" style="color:#ff8a8a;">Löschen</button>
</div>
</div>`;
}
let teamAlleMitglieder = [];
function teamListeZeichnen() {
const listeEl = document.getElementById("team-liste");
const gefiltert = teamAktiverFilter === "alle" ? teamAlleMitglieder : teamAlleMitglieder.filter((m) => m.kategorie === teamAktiverFilter);
listeEl.innerHTML = gefiltert.length ? gefiltert.map(teamKarteHtml).join("") : `<p class="vw-leer">Noch keine Mitglieder in dieser Kategorie.</p>`;
listeEl.querySelectorAll(".team-bearbeiten-btn").forEach((btn) => {
btn.addEventListener("click", () => {
const id = btn.closest("[data-team-id]").dataset.teamId;
const m = teamAlleMitglieder.find((x) => x.id === id);
if (m) teamFormularBefuellen(m);
});
});
listeEl.querySelectorAll(".team-loeschen-btn").forEach((btn) => {
btn.addEventListener("click", async () => {
const id = btn.closest("[data-team-id]").dataset.teamId;
const m = teamAlleMitglieder.find((x) => x.id === id);
if (!confirm(`${m?.name || "Dieses Mitglied"} unwiderruflich löschen?`)) return;
btn.disabled = true;
await window.AdminAuth.apiCall(API_BASE_URL, "/admin/team/delete", { id });
await ladeUndRenderTeam();
});
});
}
async function ladeUndRenderTeam() {
const res = await window.AdminAuth.apiCall(API_BASE_URL, "/admin/team/list", {});
teamAlleMitglieder = res.ok ? (res.mitglieder || []) : [];
teamListeZeichnen();
}
function teamFormularZuruecksetzen() {
document.getElementById("team-id").value = "";
document.getElementById("team-form-titel").textContent = "Neues Mitglied";
document.getElementById("team-kategorie").value = "modi";
document.getElementById("team-name").value = "";
document.getElementById("team-rolle").value = "";
document.getElementById("team-tiktok").value = "";
document.getElementById("team-bio").value = "";
document.getElementById("team-zitat").value = "";
document.getElementById("team-rang").value = "";
document.getElementById("team-rang-label").value = "";
document.getElementById("team-reihenfolge").value = "0";
document.getElementById("team-aktiv").checked = true;
document.getElementById("team-foto-vorschau").style.display = "none";
document.getElementById("team-foto-input").value = "";
document.getElementById("team-abbrechen").style.display = "none";
document.getElementById("team-status").textContent = "";
teamHochgeladenesFoto = null;
teamRangFelderAnpassen();
}
function teamFormularBefuellen(m) {
document.getElementById("team-id").value = m.id;
document.getElementById("team-form-titel").textContent = `„${m.name}“ bearbeiten`;
document.getElementById("team-kategorie").value = m.kategorie;
document.getElementById("team-name").value = m.name;
document.getElementById("team-rolle").value = m.rolle?.de || "";
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-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";
teamHochgeladenesFoto = m.foto || null;
const vorschau = document.getElementById("team-foto-vorschau");
if (teamHochgeladenesFoto) {
vorschau.src = teamHochgeladenesFoto;
vorschau.style.display = "";
} else {
vorschau.style.display = "none";
}
document.getElementById("team-abbrechen").style.display = "";
document.getElementById("team-status").textContent = "";
teamRangFelderAnpassen();
document.getElementById("team-root").scrollIntoView({ behavior: "smooth", block: "start" });
}
function teamRangFelderAnpassen() {
const istModi = document.getElementById("team-kategorie").value === "modi";
document.getElementById("team-rang-felder").style.display = istModi ? "" : "none";
}
document.getElementById("team-kategorie").addEventListener("change", teamRangFelderAnpassen);
document.getElementById("team-abbrechen").addEventListener("click", teamFormularZuruecksetzen);
document.getElementById("team-foto-input").addEventListener("change", async () => {
const input = document.getElementById("team-foto-input");
const datei = input.files?.[0];
if (!datei) return;
const statusEl = document.getElementById("team-status");
statusEl.textContent = "Foto wird hochgeladen …";
statusEl.style.color = "";
const form = new FormData();
form.append("image", datei);
const session = window.AdminAuth.getSession();
try {
const res = await fetch(`${API_BASE_URL}/admin/team/upload-photo`, {
method: "POST",
headers: session?.token ? { Authorization: `Bearer ${session.token}` } : {},
body: form,
});
const data = await res.json();
if (!data.ok) {
statusEl.textContent = data.error || "Foto-Upload fehlgeschlagen.";
statusEl.style.color = "#ff6b6b";
return;
}
teamHochgeladenesFoto = data.url;
const vorschau = document.getElementById("team-foto-vorschau");
vorschau.src = data.url;
vorschau.style.display = "";
statusEl.textContent = "Foto hochgeladen — jetzt noch auf „Speichern“ klicken.";
} catch {
statusEl.textContent = "Foto-Upload fehlgeschlagen (keine Verbindung).";
statusEl.style.color = "#ff6b6b";
}
});
document.getElementById("team-speichern").addEventListener("click", async () => {
const statusEl = document.getElementById("team-status");
const name = document.getElementById("team-name").value.trim();
const rolle = document.getElementById("team-rolle").value.trim();
if (!name || !rolle) {
statusEl.textContent = "Name und Rolle sind Pflicht.";
statusEl.style.color = "#ff6b6b";
return;
}
const btn = document.getElementById("team-speichern");
btn.disabled = true;
statusEl.textContent = "Wird gespeichert und übersetzt …";
statusEl.style.color = "";
const id = document.getElementById("team-id").value || undefined;
const res = await window.AdminAuth.apiCall(API_BASE_URL, "/admin/team/save", {
id,
kategorie: document.getElementById("team-kategorie").value,
name,
rolle,
foto: teamHochgeladenesFoto,
tiktok: document.getElementById("team-tiktok").value.trim(),
bio: document.getElementById("team-bio").value.trim(),
zitat: document.getElementById("team-zitat").value.trim(),
rang: document.getElementById("team-rang").value,
rangLabel: document.getElementById("team-rang-label").value.trim(),
reihenfolge: document.getElementById("team-reihenfolge").value,
status: document.getElementById("team-aktiv").checked ? "aktiv" : "pausiert",
});
btn.disabled = false;
if (!res.ok) {
statusEl.textContent = res.error || "Speichern fehlgeschlagen.";
statusEl.style.color = "#ff6b6b";
return;
}
statusEl.textContent = "✅ Gespeichert.";
statusEl.style.color = "#baf7d6";
teamFormularZuruecksetzen();
await ladeUndRenderTeam();
});
document.getElementById("team-filter-bar").addEventListener("click", (e) => {
const btn = e.target.closest(".filter-btn");
if (!btn) return;
teamAktiverFilter = btn.dataset.teamFilter;
document.querySelectorAll("#team-filter-bar .filter-btn").forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
teamListeZeichnen();
});
teamRangFelderAnpassen();
// 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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
</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=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<script src="assets/js/i18n-werte.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></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=20260821i" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css?v=20260821i" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css?v=20260821i" />
<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" />
</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=20260821i"></script>
<script src="assets/js/main.js?v=20260821i"></script>
<script src="assets/js/i18n-zeitreise.js?v=20260821j"></script>
<script src="assets/js/main.js?v=20260821j"></script>
<script>
/* Wachstums-Reveal: Äste blenden sich beim Scrollen sanft ein — passt
zum "wachsenden Baum"-Konzept dieser Seite. Rein page-lokal, betrifft