From 100fcdcbd55924a0652c75cb7f5ce3c8b56a41e4 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Fri, 31 Jul 2026 06:03:13 +0200 Subject: [PATCH] Login oben sichtbar + Musik laeuft beim Seitenwechsel weiter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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
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 --- assets/css/main.css | 27 ++++++- assets/js/main.js | 172 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 192 insertions(+), 7 deletions(-) diff --git a/assets/css/main.css b/assets/css/main.css index 368108a..c6d454a 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -191,6 +191,27 @@ header.site-header { font-size: .88rem; } +/* Interner Login — dezent, aber auf jeder Seite oben rechts auffindbar */ +.nav-login { + display: inline-flex; + align-items: center; + gap: .3rem; + padding: .5rem .9rem; + border-radius: 999px; + border: 1px solid var(--border); + color: var(--text-muted); + font-weight: 700; + font-size: .82rem; + white-space: nowrap; + transition: color .2s ease, border-color .2s ease, background .2s ease; +} +.nav-login:hover, +.nav-login:focus-visible { + color: var(--accent); + border-color: var(--accent); + background: color-mix(in srgb, var(--accent) 12%, transparent); +} + .nav-toggle { display: none; background: none; @@ -220,7 +241,8 @@ header.site-header { .nav-flat { display: block; padding: .9rem 0; border-bottom: 1px solid var(--border); } .nav-dropdown { position: static; transform: none; box-shadow: none; width: 100%; } .nav-group.open .nav-dropdown { display: flex; } - .nav-cta { align-self: flex-start; margin: 1rem 0; } + .nav-cta { align-self: flex-start; margin: 1rem 0 .6rem; } + .nav-login { align-self: flex-start; } } /* ---------- Buttons ---------- */ @@ -949,6 +971,9 @@ footer.site-footer { } } +/* Kurzes, dezentes Abblenden während ein Seitenwechsel geholt wird */ +body.seite-laedt main { opacity: .55; transition: opacity .15s ease; } + /* ========================================================================== MUSIK-KNOPF — kleiner schwebender Knopf unten rechts, auf jeder Seite diff --git a/assets/js/main.js b/assets/js/main.js index ba8d110..b7966d5 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -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 @@ `; @@ -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
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(); }); })();