/* ===================================================================== DOGFATHER UNIVERSE — main.js Gemeinsame Navigation, mobiles Menü, Footer, Nebel-Effekt. Läuft rein clientseitig (kein Build-Tool, kein Fetch von Partials), damit jede Seite auch direkt per Doppelklick (file://) funktioniert. ===================================================================== */ (function () { "use strict"; /* =================================================================== Mehrsprachigkeit (i18n) Fünf Sprachen: Deutsch (Standard, IMMER die Sprache beim ersten Besuch), Englisch, echtes Schweizerdeutsch (Dialekt, nicht nur Hochdeutsch mit ß→ss — Wunsch von Dogi; da Dialekt keine einheitliche Schriftform hat, wurde eine gängige, Zürich-nahe Schreibweise gewählt), Französisch, Portugiesisch. Rein clientseitig, kein Build-Tool: - I18N_CORE deckt die sitewide gemeinsamen Textstücke ab (Navigation, Fußzeile, Musik-Widget) — läuft auf JEDER Seite, weil main.js überall eingebunden ist. - Seiteneigene Inhalte (z.B. die Startseite) bekommen zusätzlich ein eigenes Wörterbuch über window.I18N_PAGE (siehe assets/js/i18n-*.js), das per data-i18n="schluessel" auf Elemente gemappt wird. Markennamen (DogFather, HasiDog, Casper, VanVan, Team Dogi, Modi-Team, Scout-Team, Streamer, Creator, Manager, Community) bleiben bewusst in JEDER Sprache unübersetzt — das ist die Marke. Gewählte Sprache landet in localStorage und bleibt beim Klicken durch die Seite erhalten. Ein komplett neuer Besuch (keine Speicherung) startet immer auf Deutsch — nie automatische Browsersprache-Erkennung. =================================================================== */ /* Bewusst KEINE Flaggen-Emojis: Windows rendert Flaggen-Emojis grundsätzlich nicht als Bild, sondern als nackten Buchstaben-Code (z.B. "DE") — zusammen mit dem eigenen Kürzel wäre das "DE DE", "GB EN" (Flagge≠Sprachcode bei Englisch) usw. Ein einzelnes Globus-Symbol im Knopf plus volle Sprachnamen im Dropdown ist eindeutig und funktioniert auf jedem Betriebssystem gleich. */ /* Pro Sprache eine kräftige, gut leuchtende Akzentfarbe (glut) — für Schrift-Leuchten, Knopf-Aurora und die glänzende Kugel (deren Farbverlauf wird rein in CSS aus dieser einen Farbe gebaut, siehe .sprach-kugel in main.css). Angelehnt an die jeweilige Landesfarbe, aber bewusst gesättigter/heller (nicht die originalen, oft eher stumpfen Flaggenfarben), damit der Polarlicht-Effekt gut trägt. */ const SPRACHEN = [ // "glut" = einzelne Leuchtfarbe (Text-Glow, Dropdown-Zeile). // "farbstopps" = mehrere Landesfarben, aus denen Knopf-Aurora und // Kugel-Verlauf gemischt werden — daher wirklich "gemischte Farben // wie bei den jeweiligen Nationalflaggen", nicht nur ein Farbton. { code: "de", kurz: "DE", name: "Deutsch", glut: "#ffb300", farbstopps: ["#1a1a1a", "#dd0000", "#ffce00"] }, { code: "de-CH", kurz: "CH", name: "Schwiizerdütsch", glut: "#ff3b3b", farbstopps: ["#d52b1e", "#ffffff", "#d52b1e"] }, { code: "en", kurz: "EN", name: "English", glut: "#2f6bff", farbstopps: ["#00247d", "#ffffff", "#cf142b"] }, { code: "fr", kurz: "FR", name: "Français", glut: "#00c2ff", farbstopps: ["#0055a4", "#ffffff", "#ef4135"] }, { code: "pt", kurz: "PT", name: "Português", glut: "#20d97a", farbstopps: ["#046a38", "#ffcc00", "#da291c"] }, ]; const SPRACHE_SCHLUESSEL = "dogi-sprache"; function getSprache() { const gespeichert = localStorage.getItem(SPRACHE_SCHLUESSEL); return SPRACHEN.some((s) => s.code === gespeichert) ? gespeichert : "de"; } function setSprache(code) { if (!SPRACHEN.some((s) => s.code === code)) return; localStorage.setItem(SPRACHE_SCHLUESSEL, code); document.documentElement.setAttribute("lang", code); renderHeader(); renderFooter(); applyTranslations(); applyMusicWidgetSprache(); // Seiteneigene Skripte (z.B. der Live-Status auf index.html) können // darauf reagieren, um dynamisch gesetzten Text (nicht über // data-i18n) ebenfalls neu zu übersetzen. document.dispatchEvent(new CustomEvent("dogi-sprache-geaendert", { detail: { sprache: code } })); } const I18N_CORE = { nav_start: { de: "Start", "de-CH": "Start", en: "Home", fr: "Accueil", pt: "Início" }, nav_dogfather: { de: "DogFather", "de-CH": "DogFather", en: "DogFather", fr: "DogFather", pt: "DogFather" }, nav_dogfather_person: { de: "Die Person", "de-CH": "D Person", en: "The Person", fr: "La personne", pt: "A pessoa" }, nav_dogfather_streamer: { de: "Streamer", "de-CH": "Streamer", en: "Streamer", fr: "Streamer", pt: "Streamer" }, nav_dogfather_casper: { de: "Casper", "de-CH": "Casper", en: "Casper", fr: "Casper", pt: "Casper" }, nav_dogfather_vanvan: { de: "VanVan", "de-CH": "VanVan", en: "VanVan", fr: "VanVan", pt: "VanVan" }, nav_dogfather_zeitreise: { de: "Zeitreise", "de-CH": "Zytreis", en: "Timeline", fr: "Chronologie", pt: "Linha do tempo" }, nav_dogfather_ttmuddi: { de: "TT-Muddi", "de-CH": "TT-Muddi", en: "TT-Muddi", fr: "TT-Muddi", pt: "TT-Muddi" }, nav_hasidog: { de: "HasiDog", "de-CH": "HasiDog", en: "HasiDog", fr: "HasiDog", pt: "HasiDog" }, nav_hasidog_welt: { de: "HasiDog-Welt", "de-CH": "HasiDog-Wält", en: "HasiDog World", fr: "Monde HasiDog", pt: "Mundo HasiDog" }, nav_hasidog_modi: { de: "Modi-Team", "de-CH": "Modi-Team", en: "Modi-Team", fr: "Modi-Team", pt: "Modi-Team" }, nav_hasidog_community: { de: "Community", "de-CH": "Community", en: "Community", fr: "Communauté", pt: "Comunidade" }, nav_hasidog_stimmen: { de: "Stimmen", "de-CH": "Stimme", en: "Voices", fr: "Témoignages", pt: "Depoimentos" }, nav_hasidog_streamplan: { de: "Streamplan", "de-CH": "Streamplan", en: "Stream schedule", fr: "Programme des streams", pt: "Programação das lives" }, nav_manager: { de: "Manager", "de-CH": "Manager", en: "Manager", fr: "Manager", pt: "Manager" }, nav_manager_dogfather: { de: "Manager DogFather", "de-CH": "Manager DogFather", en: "Manager DogFather", fr: "Manager DogFather", pt: "Manager DogFather" }, nav_manager_scouts: { de: "Scout-Team", "de-CH": "Scout-Team", en: "Scout Team", fr: "Équipe Scouts", pt: "Equipe Scouts" }, nav_manager_creator: { de: "Creator", "de-CH": "Creator", en: "Creator", fr: "Creator", pt: "Creator" }, nav_manager_werte: { de: "Werte", "de-CH": "Wärt", en: "Values", fr: "Valeurs", pt: "Valores" }, nav_manager_bewerben: { de: "Bewerben", "de-CH": "Bewärbe", en: "Apply", fr: "Postuler", pt: "Candidatar-se" }, nav_medien: { de: "Medien", "de-CH": "Medie", en: "Media", fr: "Médias", pt: "Mídia" }, nav_medien_dogfather: { de: "DogFather", "de-CH": "DogFather", en: "DogFather", fr: "DogFather", pt: "DogFather" }, nav_medien_hasidog: { de: "HasiDog", "de-CH": "HasiDog", en: "HasiDog", fr: "HasiDog", pt: "HasiDog" }, nav_medien_casper: { de: "Casper", "de-CH": "Casper", en: "Casper", fr: "Casper", pt: "Casper" }, nav_links: { de: "Links", "de-CH": "Links", en: "Links", fr: "Liens", pt: "Links" }, nav_cta_bewerben: { de: "Bewerben 🩵", "de-CH": "Bewärbe 🩵", en: "Apply 🩵", fr: "Postuler 🩵", pt: "Candidatar-se 🩵" }, nav_cta_abo_anonym: { de: "DogiCrew 👑", "de-CH": "DogiCrew 👑", en: "DogiCrew 👑", fr: "DogiCrew 👑", pt: "DogiCrew 👑" }, nav_cta_abo_aktivieren: { de: "Abo aktivieren 👑", "de-CH": "Abo aktiviere 👑", en: "Activate Subscription 👑", fr: "Activer l'abonnement 👑", pt: "Ativar assinatura 👑" }, nav_cta_abo_bereich: { de: "Supporter-Bereich 👑", "de-CH": "Supporter-Bereich 👑", en: "Supporter Area 👑", fr: "Espace Supporter 👑", pt: "Área do Apoiador 👑" }, nav_toggle_aria: { de: "Menü öffnen", "de-CH": "Menü uftue", en: "Open menu", fr: "Ouvrir le menu", pt: "Abrir menu" }, sprache_aria: { de: "Sprache wählen", "de-CH": "Sprach uswähle", en: "Choose language", fr: "Choisir la langue", pt: "Escolher idioma" }, brand_tagline: { de: "Streamer · Community · HasiDog · Manager", "de-CH": "Streamer · Community · HasiDog · Manager", en: "Streamer · Community · HasiDog · Manager", fr: "Streamer · Communauté · HasiDog · Manager", pt: "Streamer · Comunidade · HasiDog · Manager", }, footer_tagline: { de: "Streamer. Community. HasiDog. Manager. Gemeinsam stark.", "de-CH": "Streamer. Community. HasiDog. Manager. Zäme stark.", en: "Streamer. Community. HasiDog. Manager. Stronger together.", fr: "Streamer. Communauté. HasiDog. Manager. Plus forts ensemble.", pt: "Streamer. Comunidade. HasiDog. Manager. Mais fortes juntos.", }, footer_h4_hasidog_manager: { de: "HasiDog & Manager", "de-CH": "HasiDog & Manager", en: "HasiDog & Manager", fr: "HasiDog et Manager", pt: "HasiDog e Manager" }, footer_h4_mehr: { de: "Mehr", "de-CH": "Meh", en: "More", fr: "Plus", pt: "Mais" }, footer_scouts: { de: "Scouts", "de-CH": "Scouts", en: "Scouts", fr: "Scouts", pt: "Scouts" }, footer_alle_links: { de: "Alle Links", "de-CH": "Alli Links", en: "All links", fr: "Tous les liens", pt: "Todos os links" }, footer_kontakt: { de: "Kontakt", "de-CH": "Kontakt", en: "Contact", fr: "Contact", pt: "Contato" }, footer_copyright: { de: "© {jahr} DogFather Universe. Team Dogi ist Familie 🩵", "de-CH": "© {jahr} DogFather Universe. Team Dogi isch Familie 🩵", en: "© {jahr} DogFather Universe. Team Dogi is family 🩵", fr: "© {jahr} DogFather Universe. Team Dogi, c'est la famille 🩵", pt: "© {jahr} DogFather Universe. Team Dogi é família 🩵", }, footer_impressum: { de: "Impressum & Datenschutz", "de-CH": "Impressum & Datenschutz", en: "Legal notice & privacy", fr: "Mentions légales et confidentialité", pt: "Aviso legal e privacidade" }, music_title: { de: "Musik von Team Dogi 🎵", "de-CH": "Musig vo Team Dogi 🎵", en: "Music from Team Dogi 🎵", fr: "Musique de Team Dogi 🎵", pt: "Música do Team Dogi 🎵" }, music_close_aria: { de: "Musik-Feld schließen", "de-CH": "Musig-Feld zuemache", en: "Close music panel", fr: "Fermer le panneau musique", pt: "Fechar painel de música" }, music_intro: { de: "Läuft weiter, solange du auf dieser Seite bleibst. 🩵", "de-CH": "Laift wiiter, solang du uf dere Site bliibsch. 🩵", en: "Keeps playing as long as you stay on this page. 🩵", fr: "Continue de jouer tant que tu restes sur cette page. 🩵", pt: "Continua tocando enquanto você ficar nesta página. 🩵", }, music_toggle_title: { de: "Musik von Hasidog & Van-Van", "de-CH": "Musig vo Hasidog & Van-Van", en: "Music from Hasidog & Van-Van", fr: "Musique de Hasidog et Van-Van", pt: "Música de Hasidog e Van-Van" }, music_spotify_suffix: { de: "auf Spotify anhören", "de-CH": "uf Spotify alose", en: "listen on Spotify", fr: "écouter sur Spotify", pt: "ouvir no Spotify" }, }; function t(schluessel) { const eintrag = I18N_CORE[schluessel]; if (!eintrag) return schluessel; const sprache = getSprache(); return eintrag[sprache] || eintrag.de || schluessel; } /* Elemente mit data-i18n="schluessel" übersetzen — Wörterbuch kommt aus window.I18N_PAGE (seiteneigen, z.B. assets/js/i18n-index.js). Fehlt der Schlüssel oder die Sprache, bleibt der bestehende (deutsche) Text einfach stehen, statt kaputtzugehen. */ function applyTranslations() { const sprache = getSprache(); document.documentElement.setAttribute("lang", sprache); const seitenWoerterbuch = window.I18N_PAGE || {}; document.querySelectorAll("[data-i18n]").forEach((el) => { const eintrag = seitenWoerterbuch[el.getAttribute("data-i18n")]; if (!eintrag) return; const text = eintrag[sprache] || eintrag.de; if (text) el.innerHTML = text; }); // data-i18n-attr="alt:schluessel;title:andererSchluessel" — für Attribute // statt Textinhalt (z.B. Bild-Alt-Texte). document.querySelectorAll("[data-i18n-attr]").forEach((el) => { el.getAttribute("data-i18n-attr") .split(";") .map((teil) => teil.trim()) .filter(Boolean) .forEach((teil) => { const [attr, schluessel] = teil.split(":"); const eintrag = seitenWoerterbuch[schluessel]; if (!eintrag) return; const text = eintrag[sprache] || eintrag.de; if (text) el.setAttribute(attr, text); }); }); } /* ---------- Navigationsstruktur (an einer Stelle pflegen) ---------- */ const NAV = [ { key: "nav_start", href: "index.html" }, { key: "nav_dogfather", children: [ { key: "nav_dogfather_person", href: "dogfather.html" }, { key: "nav_dogfather_streamer", href: "streamer.html" }, { key: "nav_dogfather_casper", href: "casper.html" }, { key: "nav_dogfather_vanvan", href: "vanvan.html" }, { key: "nav_dogfather_ttmuddi", href: "tt-muddi.html" }, { key: "nav_dogfather_zeitreise", href: "zeitreise.html" }, ], }, { key: "nav_hasidog", children: [ { key: "nav_hasidog_welt", href: "hasidog.html" }, { key: "nav_hasidog_modi", href: "team-modis.html" }, { key: "nav_hasidog_community", href: "community.html" }, { key: "nav_hasidog_stimmen", href: "stimmen.html" }, // Streamplan zurück unter HasiDog — Wunsch Dogi. { key: "nav_hasidog_streamplan", href: "streamplan.html" }, ], }, { key: "nav_manager", children: [ { key: "nav_manager_dogfather", href: "manager.html" }, { key: "nav_manager_scouts", href: "team-scouts.html" }, { key: "nav_manager_creator", href: "creator.html" }, { key: "nav_manager_werte", href: "werte.html" }, { key: "nav_manager_bewerben", href: "bewerben.html" }, ], }, { // Medien ist bewusst nach den 3 Welten aufgeteilt statt nach // Foto/Video/Aktionen getrennt zu sein — Wunsch von Dogi: pro Welt // (DogFather/HasiDog/Casper) gibt es EINE Seite mit allem // kategorisiert (Fotos, Videos, Events), siehe medien-*.html. key: "nav_medien", children: [ { key: "nav_medien_dogfather", href: "medien-dogfather.html" }, { key: "nav_medien_hasidog", href: "medien-hasidog.html" }, { key: "nav_medien_casper", href: "medien-casper.html" }, ], }, { key: "nav_links", href: "links.html" }, ]; /* Aktuelle Datei — wird beim nahtlosen Seitenwechsel mit aktualisiert. */ let currentFile = (location.pathname.split("/").pop() || "index.html"); function isActive(href) { return href === currentFile; } /* Ordnet jedem Nav-Punkt die Farbklasse SEINER Welt zu (siehe .nav-item-* in main.css, Farben aus theme-dogfather/-hasidog/ -spicymedia.css) — für den augenschonenden Polarlicht-Schimmer pro Kategorie. "Links" bekommt bewusst keine Farbe (keine eigene Welt, sitzt direkt neben dem schon bunten Bewerben-Knopf). */ const NAV_FARBKLASSE = { nav_start: "nav-item-start", nav_dogfather: "nav-item-dogfather", nav_hasidog: "nav-item-hasidog", nav_manager: "nav-item-manager", nav_medien: "nav-item-medien", nav_links: "nav-item-links", }; function renderNavItem(item) { const farbklasse = NAV_FARBKLASSE[item.key] ? ` ${NAV_FARBKLASSE[item.key]}` : ""; if (!item.children) { return `${t(item.key)}`; } const activeChild = item.children.some((c) => isActive(c.href)); // Farbklasse sitzt am äußeren .nav-group, nicht am Button — so // erben Button-Aurora UND Dropdown (beides Kinder von .nav-group) // dieselben --brand-1/--brand-2-Variablen für einen einheitlichen // Farbklang zwischen Kategorie-Knopf und aufgeklapptem Menü. return `
`; } /* Sprachumschalter — eigener nav-group-artiger Dropdown, zeigt die aktuell gewählte Sprache (Flagge + Kürzel) und listet beim Klick die übrigen 4 Sprachen auf. */ /* Aus den 2-3 Landesfarben einer Sprache ein Aurora-Wolkenbild bauen (mehrere versetzte, weiche Farbflecken — dieselbe Machart wie beim Bewerben-Knopf, nur mit den Farben DIESER Sprache) plus eine Stopp-Liste für den conic-gradient der Kugel. */ function sprachAurora(farben) { const positionen = ["22% 30%", "72% 28%", "48% 78%"]; return farben .map((f, i) => `radial-gradient(circle at ${positionen[i % positionen.length]}, ${f}, transparent 60%)`) .join(", "); } function sprachKugelStopps(farben) { return [...farben, farben[0]].join(", "); } function renderSprachSchalter() { const aktuell = getSprache(); const aktiv = SPRACHEN.find((s) => s.code === aktuell) || SPRACHEN[0]; const styleFuer = (s) => `--sprach-glut:${s.glut};--sprach-aurora:${sprachAurora(s.farbstopps)};--sprach-stopps:${sprachKugelStopps(s.farbstopps)};`; return ` `; } /* =================================================================== Vorschau-Modus für Dogi + VanVan (Zugangsschranke, siehe gate-worker.js) Solange die Seite noch nicht öffentlich ist, kommen nur Dogi und VanVan per Zugangscode rein (gate-worker.js setzt dabei ein lesbares Cookie "dogi_role=dogi"/"vanvan" — NICHT das eigentliche Sicherheits-Cookie, das bleibt HttpOnly). Damit die beiden die Seite auch "als Abonnent" bzw. "als Nicht-Abonnent" ansehen können, OHNE ein echtes Abo abschließen zu müssen, gibt es einen kleinen Umschalter unten links, der NUR erscheint, wenn das Rollen-Cookie gesetzt ist. Die Wahl landet in localStorage und wird von renderAboButton() sowie von supporter.js/ supporter.html (Mock-Daten statt echtem API-Aufruf) ausgelesen. =================================================================== */ const VORSCHAU_SCHLUESSEL = "dogiPreviewMode"; // "abonnent" | "nicht-abonnent" | leer = echter Stand function getCookieWert(name) { const treffer = document.cookie.match(new RegExp(`(?:^|;\\s*)${name}=([^;]+)`)); return treffer ? decodeURIComponent(treffer[1]) : null; } function getVorschauRolle() { return getCookieWert("dogi_role"); // "dogi" | "vanvan" | null } /* Realistischer Anzeigename fürs Vorschau-Konto — Nutzer-Feedback 03.08.2026: "ich will in der Vorschau alles sehen wie es fertig sein wird, nix da stehen haben von Vorschau, das bringt mir nix." Statt dem Wort "Vorschau" irgendwo im Text zeigt die Vorschau jetzt einfach den Namen der Person, die gerade eingeloggt ist (Dogi oder VanVan) — sieht dadurch exakt so aus wie ein echtes, fertiges Konto. */ function vorschauAnzeigename() { const rolle = getVorschauRolle(); return rolle === "vanvan" ? "VanVan" : "Dogi"; } window.dogiVorschauName = vorschauAnzeigename; // öffentlich, für supporter.html function getVorschauModus() { if (!getVorschauRolle()) return null; // Umschalter nur für Dogi/VanVan gültig const wert = localStorage.getItem(VORSCHAU_SCHLUESSEL); return wert === "abonnent" || wert === "nicht-abonnent" ? wert : null; } function setVorschauModus(modus) { if (modus) localStorage.setItem(VORSCHAU_SCHLUESSEL, modus); else localStorage.removeItem(VORSCHAU_SCHLUESSEL); renderHeader(); renderVorschauSchalter(); document.dispatchEvent(new CustomEvent("dogi-vorschau-geaendert", { detail: { modus } })); } window.dogiVorschauModus = getVorschauModus; // öffentlich, für supporter.html/abonnieren.html /* Aufklappbarer Kreisknopf unten links, spiegelbildlich zum Musik-Knopf unten rechts (gleiche Bauart, siehe .vorschau-toggle/.vorschau-panel in main.css) — Nutzer-Feedback 03.08.2026: statt einer dauerhaft offenen Text-Knopfleiste "so ein Button wie bei VanVan", der sich erst bei Klick öffnet. Nur EINMAL erzeugt (wie beim Musik-Widget), damit ein offenes Panel beim nahtlosen Seitenwechsel nicht jedes Mal neu gebaut wird — nur der Inhalt (aktive Auswahl) wird bei jedem Aufruf aktuell gehalten. */ function renderVorschauSchalter() { if (!getVorschauRolle()) return; // öffentliche Besucher sehen nie etwas let widget = document.getElementById("vorschau-widget"); if (!widget) { widget = document.createElement("div"); widget.className = "vorschau-widget"; widget.id = "vorschau-widget"; widget.innerHTML = `${t("music_intro")}
${SPOTIFY_ARTISTS.map( (a) => `${a.name}
` ).join("")}