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:
+26
-1
@@ -191,6 +191,27 @@ header.site-header {
|
|||||||
font-size: .88rem;
|
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 {
|
.nav-toggle {
|
||||||
display: none;
|
display: none;
|
||||||
background: none;
|
background: none;
|
||||||
@@ -220,7 +241,8 @@ header.site-header {
|
|||||||
.nav-flat { display: block; padding: .9rem 0; border-bottom: 1px solid var(--border); }
|
.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-dropdown { position: static; transform: none; box-shadow: none; width: 100%; }
|
||||||
.nav-group.open .nav-dropdown { display: flex; }
|
.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 ---------- */
|
/* ---------- 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
|
MUSIK-KNOPF — kleiner schwebender Knopf unten rechts, auf jeder Seite
|
||||||
|
|
||||||
|
|||||||
+166
-6
@@ -52,7 +52,8 @@
|
|||||||
{ label: "Links", href: "links.html" },
|
{ 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) {
|
function isActive(href) {
|
||||||
return href === currentFile;
|
return href === currentFile;
|
||||||
@@ -93,6 +94,11 @@
|
|||||||
<nav class="nav-links">
|
<nav class="nav-links">
|
||||||
${NAV.map(renderNavItem).join("")}
|
${NAV.map(renderNavItem).join("")}
|
||||||
<a class="nav-cta" href="bewerben.html">Bewerben 🩵</a>
|
<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 & Zugänge">
|
||||||
|
<span aria-hidden="true">🔒</span> Login
|
||||||
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>`;
|
</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() {
|
function renderFooter() {
|
||||||
const el = document.getElementById("site-footer");
|
const el = document.getElementById("site-footer");
|
||||||
if (!el) return;
|
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", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
renderHeader();
|
renderHeader();
|
||||||
renderFooter();
|
renderFooter();
|
||||||
renderFog();
|
renderFog();
|
||||||
renderMusicWidget();
|
renderMusicWidget();
|
||||||
|
nahtloseNavigationAktivieren();
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user