Files
dogfather-universe/assets/js/main.js
T
DogFatherGitandClaude Opus 5 100fcdcbd5 Login oben sichtbar + Musik laeuft beim Seitenwechsel weiter
Login:
- Kleiner "🔒 Login"-Knopf oben rechts in der Navigation, auf jeder
  Seite sichtbar. Fuehrt direkt zum Postfach mit der Code-Eingabe.

Nahtloser Seitenwechsel:
- Klicks auf interne Links tauschen nur noch <main> aus, statt die
  ganze Seite neu zu laden. Kopfzeile, Fussleiste und Musik-Knopf
  bleiben stehen — der Spotify-Player laeuft dadurch weiter.
- Postfach und Zugaenge sind bewusst ausgenommen und laden normal:
  an Login, Sitzung und Rechten aendert sich nichts.
- Seiteneigene Skripte werden der Reihe nach nachgeladen; spaete
  DOMContentLoaded-Anmeldungen laufen sofort, damit Inhalte wie die
  Team-Vorschau auch nach einem Wechsel erscheinen.
- Faellt bei einem Fehler automatisch auf normales Laden zurueck.
- Zurueck-/Vorwaerts-Knopf und geteilte Links funktionieren weiter.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-07-31 06:03:13 +02:00

430 lines
17 KiB
JavaScript

/* =====================================================================
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";
/* ---------- Navigationsstruktur (an einer Stelle pflegen) ---------- */
const NAV = [
{ label: "Start", href: "index.html" },
{
label: "Dogfather",
children: [
{ label: "Die Person", href: "dogfather.html" },
{ label: "Streamer", href: "streamer.html" },
{ label: "Streamplan", href: "streamplan.html" },
{ label: "Zeitreise", href: "zeitreise.html" },
],
},
{
label: "HasiDog",
children: [
{ label: "HasiDog-Welt", href: "hasidog.html" },
{ label: "Casper", href: "casper.html" },
{ label: "Community", href: "community.html" },
{ label: "Stimmen", href: "stimmen.html" },
],
},
{
label: "Manager",
children: [
{ label: "Manager Dogfather", href: "manager.html" },
{ label: "Werte", href: "werte.html" },
{ label: "Scout-Team", href: "team-scouts.html" },
{ label: "Modi-Team", href: "team-modis.html" },
{ label: "VanVan", href: "vanvan.html" },
{ label: "Creator", href: "creator.html" },
{ label: "Bewerben", href: "bewerben.html" },
],
},
{
label: "Medien",
children: [
{ label: "Galerie", href: "medien.html" },
{ label: "Videos", href: "videos.html" },
{ label: "Aktionen", href: "aktionen.html" },
],
},
{ label: "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;
}
function renderNavItem(item) {
if (!item.children) {
return `<a class="nav-flat${isActive(item.href) ? " active" : ""}" href="${item.href}">${item.label}</a>`;
}
const activeChild = item.children.some((c) => isActive(c.href));
return `
<div class="nav-group${activeChild ? " open-hint" : ""}">
<button type="button" aria-expanded="false">${item.label} <span aria-hidden="true">▾</span></button>
<div class="nav-dropdown">
${item.children
.map(
(c) =>
`<a href="${c.href}"${isActive(c.href) ? ' class="active"' : ""}>${c.label}</a>`
)
.join("")}
</div>
</div>`;
}
function renderHeader() {
const el = document.getElementById("site-header");
if (!el) return;
el.innerHTML = `
<header class="site-header">
<div class="nav-bar">
<a class="brand" href="index.html">
<img class="brand-logo" src="assets/img/logo-dogfather.png" alt="Dogfather Logo" />
<span>DOGFATHER UNIVERSE
<small>Streamer · Community · HasiDog · Manager</small>
</span>
</a>
<button class="nav-toggle" type="button" aria-label="Menü öffnen" aria-expanded="false">☰</button>
<nav class="nav-links">
${NAV.map(renderNavItem).join("")}
<a class="nav-cta" href="bewerben.html">Bewerben 🩵</a>
<!-- Interner Einstieg: klein, aber auf JEDER Seite oben sichtbar,
damit Dogi und das Team nie danach suchen müssen. -->
<a class="nav-login" href="postfach.html" title="Interner Login — Postfach &amp; Zugänge">
<span aria-hidden="true">🔒</span> Login
</a>
</nav>
</div>
</header>`;
const toggle = el.querySelector(".nav-toggle");
const links = el.querySelector(".nav-links");
toggle.addEventListener("click", () => {
const open = links.classList.toggle("open");
toggle.setAttribute("aria-expanded", String(open));
});
el.querySelectorAll(".nav-group > button").forEach((btn) => {
btn.addEventListener("click", () => {
const group = btn.parentElement;
const wasOpen = group.classList.contains("open");
el.querySelectorAll(".nav-group.open").forEach((g) => g.classList.remove("open"));
if (!wasOpen) group.classList.add("open");
btn.setAttribute("aria-expanded", String(!wasOpen));
});
});
}
/* Klick außerhalb schließt offene Menüs. Bewusst NUR EINMAL angemeldet —
renderHeader() läuft bei jedem Seitenwechsel erneut, und ein Zuhörer
pro Wechsel würde sich sonst endlos aufsummieren. */
document.addEventListener("click", (e) => {
const kopf = document.getElementById("site-header");
if (kopf && !kopf.contains(e.target)) {
kopf.querySelectorAll(".nav-group.open").forEach((g) => g.classList.remove("open"));
}
});
function renderFooter() {
const el = document.getElementById("site-footer");
if (!el) return;
const year = new Date().getFullYear();
el.innerHTML = `
<footer class="site-footer">
<div class="container footer-grid">
<div>
<div class="brand"><span class="heart">🩵</span> DOGFATHER UNIVERSE</div>
<p class="small" style="margin-top:.8rem;max-width:32ch;">
Streamer. Community. HasiDog. Manager. Gemeinsam stark.
</p>
</div>
<div>
<h4>Dogfather</h4>
<ul>
<li><a href="dogfather.html">Die Person</a></li>
<li><a href="streamer.html">Streamer</a></li>
<li><a href="zeitreise.html">Zeitreise</a></li>
</ul>
</div>
<div>
<h4>HasiDog &amp; Manager</h4>
<ul>
<li><a href="hasidog.html">HasiDog-Welt</a></li>
<li><a href="manager.html">Manager Dogfather</a></li>
<li><a href="team-scouts.html">Scouts</a></li>
<li><a href="team-modis.html">Modis</a></li>
</ul>
</div>
<div>
<h4>Mehr</h4>
<ul>
<li><a href="links.html">Alle Links</a></li>
<li><a href="bewerben.html">Bewerben</a></li>
<li><a href="kontakt.html">Kontakt</a></li>
</ul>
<h4 style="margin-top:1.4rem;">Intern 🔒</h4>
<ul>
<li><a href="postfach.html">Bewerbungs-Postfach</a></li>
<li><a href="zugaenge.html">Zugänge &amp; Rollen</a></li>
</ul>
</div>
</div>
<div class="container footer-bottom">
<span>© ${year} Dogfather Universe. Team Dogi ist Familie 🩵</span>
<span><a href="kontakt.html">Impressum &amp; Datenschutz</a> · <a href="postfach.html">🔒 Interner Login</a></span>
</div>
</footer>`;
}
function renderFog() {
if (document.querySelector(".fog-layer")) return;
const layer = document.createElement("div");
layer.className = "fog-layer";
layer.setAttribute("aria-hidden", "true");
const blobs = 6;
let html = "";
for (let i = 0; i < blobs; i++) {
const size = 220 + Math.round(Math.random() * 260);
const top = Math.round(Math.random() * 100);
const left = Math.round(Math.random() * 100);
const delay = (Math.random() * 10).toFixed(1);
html += `<span style="width:${size}px;height:${size}px;top:${top}%;left:${left}%;animation-delay:${delay}s;"></span>`;
}
layer.innerHTML = html;
document.body.prepend(layer);
}
/* ===================================================================
Musik-Knopf (unten rechts, auf jeder Seite)
Kleiner schwebender Knopf, der ein Feld mit den Spotify-Playern von
Hasidog und Van-Van aufklappt. Genauso gelöst wie im Analyse-Tool.
Zwei bewusste Entscheidungen:
1. Das Feld hängt direkt am <body>, nicht in einem Abschnitt — so
kann kein Neuaufbau der Seite den laufenden Player unterbrechen.
2. Die Player werden ERST beim ersten Öffnen geladen (src wird dann
gesetzt). Sonst würde jede Seite zwei fremde Einbettungen laden,
auch wenn niemand Musik hören will.
=================================================================== */
const SPOTIFY_ARTISTS = [
{ name: "Hasidog", id: "2INiE1VVJGUQdEURsltuJU" },
{ name: "Van-Van", id: "4J9PcKZBkh6V8dVY3CGliw" },
];
function renderMusicWidget() {
if (document.getElementById("music-widget")) return;
const widget = document.createElement("div");
widget.className = "music-widget";
widget.id = "music-widget";
widget.innerHTML = `
<div class="music-panel" id="music-panel" hidden>
<div class="music-panel-head">
<strong>Musik von Team Dogi 🎵</strong>
<button class="music-close" id="music-close" type="button" aria-label="Musik-Feld schließen">✕</button>
</div>
<p class="small muted" style="margin:.2rem 0 .6rem;">Läuft weiter, solange du auf dieser Seite bleibst. 🩵</p>
${SPOTIFY_ARTISTS.map(
(a) => `
<p class="music-artist-label">${a.name}</p>
<iframe data-src="https://open.spotify.com/embed/artist/${a.id}?utm_source=generator&amp;theme=0"
height="152" loading="lazy" title="${a.name} auf Spotify anhören"
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
allowfullscreen></iframe>`
).join("")}
</div>
<button class="music-toggle" id="music-toggle" type="button"
aria-expanded="false" aria-controls="music-panel"
title="Musik von Hasidog &amp; Van-Van">🎵</button>`;
document.body.appendChild(widget);
const panel = document.getElementById("music-panel");
const toggle = document.getElementById("music-toggle");
let geladen = false;
function oeffnen(auf) {
panel.hidden = !auf;
toggle.setAttribute("aria-expanded", String(auf));
widget.classList.toggle("open", auf);
if (auf && !geladen) {
// Player erst jetzt wirklich laden
panel.querySelectorAll("iframe[data-src]").forEach((f) => {
f.src = f.dataset.src;
f.removeAttribute("data-src");
});
geladen = true;
}
}
toggle.addEventListener("click", () => oeffnen(panel.hidden));
document.getElementById("music-close").addEventListener("click", () => oeffnen(false));
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && !panel.hidden) oeffnen(false);
});
}
/* ===================================================================
Nahtloser Seitenwechsel — damit die Musik nicht abreißt
Normalerweise lädt der Browser bei jedem Klick die ganze Seite neu,
wodurch der Spotify-Player jedes Mal von vorn startet. Stattdessen
holen wir die Zielseite im Hintergrund und tauschen nur den
Inhaltsbereich <main> aus. Kopfzeile, Fußzeile und der Musik-Knopf
bleiben stehen — die Musik läuft einfach weiter.
Wichtig für die Sicherheit: Die internen Seiten (Postfach, Zugänge)
sind bewusst AUSGENOMMEN und laden weiterhin ganz normal. An Login,
Sitzung und Rechten ändert sich dadurch nichts.
Absicherung: Geht beim Holen irgendetwas schief, wird ganz normal
zur Zielseite navigiert. Schlimmstenfalls ist es also wie vorher.
=================================================================== */
const KEIN_NAHTLOSER_WECHSEL = ["postfach.html", "zugaenge.html"];
/* Seiteneigene Skripte melden sich fast alle über "DOMContentLoaded" an.
Nach dem ersten Laden ist dieses Ereignis längst vorbei — ein später
nachgeladenes Skript würde also nie starten. Deshalb: Anmeldungen, die
NACH dem Laden eintreffen, sofort ausführen. Während des normalen
Erstaufbaus (readyState === "loading") bleibt alles wie gehabt. */
const anmeldenOriginal = document.addEventListener.bind(document);
document.addEventListener = function (typ, fn, optionen) {
if (typ === "DOMContentLoaded" && document.readyState !== "loading" && typeof fn === "function") {
setTimeout(() => fn(new Event("DOMContentLoaded")), 0);
return;
}
return anmeldenOriginal(typ, fn, optionen);
};
/** Skripte der neuen Seite der Reihe nach ausführen. */
async function skripteUebernehmen(neuesDokument) {
const schonGeladen = new Set(
[...document.querySelectorAll("script[src]")].map((s) => s.getAttribute("src"))
);
for (const alt of [...neuesDokument.querySelectorAll("script")]) {
const src = alt.getAttribute("src");
if (src) {
// Schon vorhanden (z.B. main.js oder eine bereits geladene Datendatei):
// nicht erneut ausführen — sonst gäbe es doppelte Deklarationen.
if (schonGeladen.has(src)) continue;
schonGeladen.add(src);
await new Promise((fertig) => {
const neu = document.createElement("script");
neu.src = src;
neu.async = false;
neu.onload = neu.onerror = fertig; // auch bei Fehler weitermachen
document.body.appendChild(neu);
});
} else {
// Seiteneigenes Skript (z.B. Team-Vorschau der Startseite).
// Läuft erst, wenn die Datendateien davor fertig geladen sind.
const neu = document.createElement("script");
neu.textContent = alt.textContent;
document.body.appendChild(neu);
}
}
}
let ladeVorgang = 0;
async function seiteLaden(url, mitVerlauf) {
const lauf = ++ladeVorgang;
document.body.classList.add("seite-laedt");
let dok;
try {
const antwort = await fetch(url, { credentials: "same-origin" });
if (!antwort.ok) throw new Error("HTTP " + antwort.status);
dok = new DOMParser().parseFromString(await antwort.text(), "text/html");
} catch {
location.href = url; // Rückfall: ganz normal navigieren
return;
}
if (lauf !== ladeVorgang) return; // zwischenzeitlich woanders hingeklickt
const neuerInhalt = dok.querySelector("main");
const alterInhalt = document.querySelector("main");
if (!neuerInhalt || !alterInhalt) {
location.href = url;
return;
}
alterInhalt.replaceWith(neuerInhalt);
// Kopf der Seite mitziehen: Titel, Farbwelt, Beschreibung
document.title = dok.title;
const themaNeu = dok.documentElement.getAttribute("data-theme");
if (themaNeu) document.documentElement.setAttribute("data-theme", themaNeu);
const beschreibungNeu = dok.querySelector('meta[name="description"]');
const beschreibungAlt = document.querySelector('meta[name="description"]');
if (beschreibungNeu && beschreibungAlt) {
beschreibungAlt.setAttribute("content", beschreibungNeu.getAttribute("content"));
}
if (mitVerlauf) history.pushState({ nahtlos: true }, "", url);
currentFile = (new URL(url, location.href).pathname.split("/").pop() || "index.html");
renderHeader(); // markiert den richtigen Menüpunkt als aktiv
window.scrollTo(0, 0);
document.body.classList.remove("seite-laedt");
await skripteUebernehmen(dok);
}
function nahtloseNavigationAktivieren() {
// Ohne History-API oder auf file:// lieber gar nicht erst versuchen
if (!window.history || !window.history.pushState || location.protocol === "file:") return;
if (KEIN_NAHTLOSER_WECHSEL.includes(currentFile)) return;
document.addEventListener("click", (e) => {
if (e.defaultPrevented || e.button !== 0) return;
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; // neuer Tab o.ä.
const link = e.target.closest("a");
if (!link) return;
const href = link.getAttribute("href");
if (!href || href.startsWith("#") || href.startsWith("mailto:") || href.startsWith("tel:")) return;
if (link.target && link.target !== "_self") return;
if (link.hasAttribute("download")) return;
const ziel = new URL(href, location.href);
if (ziel.origin !== location.origin) return; // fremde Seite
if (!ziel.pathname.endsWith(".html")) return; // z.B. Bilddatei
const datei = ziel.pathname.split("/").pop();
if (KEIN_NAHTLOSER_WECHSEL.includes(datei)) return; // interner Bereich
if (ziel.pathname === location.pathname && ziel.search === location.search) return;
e.preventDefault();
seiteLaden(ziel.href, true);
});
window.addEventListener("popstate", () => {
// Zurück-/Vorwärts-Knopf im Browser
const datei = location.pathname.split("/").pop() || "index.html";
if (KEIN_NAHTLOSER_WECHSEL.includes(datei)) {
location.reload();
return;
}
seiteLaden(location.href, false);
});
}
document.addEventListener("DOMContentLoaded", () => {
renderHeader();
renderFooter();
renderFog();
renderMusicWidget();
nahtloseNavigationAktivieren();
});
})();