- Links: Instagram & Snapchat als Platzhalter ergänzt, Discord auf "Nur auf Anfrage" -> Kontaktseite - Musik-Button auf allen Bewerben-Seiten bekommt jetzt das gleiche Polarlicht-Design wie der Bewerben-Nav-Button - Streamplan im Nav-Menu zurück in die HasiDog-Kategorie verschoben - Neuer Textabschnitt "Der Mensch hinter DogFather" auf dogfather.html und streamer.html ergänzt
446 lines
18 KiB
JavaScript
446 lines
18 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: "Casper", href: "casper.html" },
|
|
{ label: "VanVan", href: "vanvan.html" },
|
|
{ label: "Zeitreise", href: "zeitreise.html" },
|
|
],
|
|
},
|
|
{
|
|
label: "HasiDog",
|
|
children: [
|
|
{ label: "HasiDog-Welt", href: "hasidog.html" },
|
|
{ label: "Modi-Team", href: "team-modis.html" },
|
|
{ label: "Community", href: "community.html" },
|
|
{ label: "Stimmen", href: "stimmen.html" },
|
|
// Streamplan zurück unter HasiDog — Wunsch Dogi.
|
|
{ label: "Streamplan", href: "streamplan.html" },
|
|
],
|
|
},
|
|
{
|
|
label: "Manager",
|
|
children: [
|
|
{ label: "Manager Dogfather", href: "manager.html" },
|
|
{ label: "Scout-Team", href: "team-scouts.html" },
|
|
{ label: "Creator", href: "creator.html" },
|
|
{ label: "Werte", href: "werte.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"><span>Bewerben 🩵</span></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 & 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="casper.html">Casper</a></li>
|
|
<li><a href="vanvan.html">VanVan</a></li>
|
|
<li><a href="zeitreise.html">Zeitreise</a></li>
|
|
</ul>
|
|
</div>
|
|
<div>
|
|
<h4>HasiDog & Manager</h4>
|
|
<ul>
|
|
<li><a href="hasidog.html">HasiDog-Welt</a></li>
|
|
<li><a href="team-modis.html">Modi-Team</a></li>
|
|
<li><a href="community.html">Community</a></li>
|
|
<li><a href="stimmen.html">Stimmen</a></li>
|
|
<li><a href="streamplan.html">Streamplan</a></li>
|
|
<li><a href="manager.html">Manager Dogfather</a></li>
|
|
<li><a href="team-scouts.html">Scouts</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 & 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 & 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&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 & Van-Van"><span>🎵</span></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);
|
|
|
|
// Seitenmotiv mitnehmen: Die Bildvariablen hängen am <body>, damit das
|
|
// Motiv die ganze Seite trägt — beim Wechsel also mit übernehmen.
|
|
const bodyNeu = dok.body;
|
|
document.body.classList.toggle("mit-hintergrund", bodyNeu.classList.contains("mit-hintergrund"));
|
|
["--page-bg", "--page-bg-mobile"].forEach((v) => {
|
|
const wert = bodyNeu.style.getPropertyValue(v);
|
|
if (wert) document.body.style.setProperty(v, wert);
|
|
else document.body.style.removeProperty(v);
|
|
});
|
|
|
|
// 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();
|
|
});
|
|
})();
|