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]>
This commit is contained in:
2026-07-31 06:03:13 +02:00
co-authored by Claude Opus 5
parent 8fe5105255
commit 100fcdcbd5
2 changed files with 192 additions and 7 deletions
+166 -6
View File
@@ -52,7 +52,8 @@
{ label: "Links", href: "links.html" },
];
const currentFile = (location.pathname.split("/").pop() || "index.html");
/* Aktuelle Datei — wird beim nahtlosen Seitenwechsel mit aktualisiert. */
let currentFile = (location.pathname.split("/").pop() || "index.html");
function isActive(href) {
return href === currentFile;
@@ -93,6 +94,11 @@
<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>`;
@@ -114,13 +120,18 @@
});
});
document.addEventListener("click", (e) => {
if (!el.contains(e.target)) {
el.querySelectorAll(".nav-group.open").forEach((g) => g.classList.remove("open"));
}
});
}
/* 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;
@@ -260,10 +271,159 @@
});
}
/* ===================================================================
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();
});
})();