Musik-Button (Van-Van Spotify) unten rechts, seitenweit persistent + Chamäleon-Farben
Vorschau-Umschalter (🎭) nach unten links verschoben, damit unten rechts Platz für den neuen Musik-Button ist. Astro ClientRouter (View Transitions) eingeführt, damit der Spotify-Player beim Seitenwechsel als echtes, unverändertes DOM-Element bestehen bleibt (transition:persist) statt neu zu laden — die Musik läuft beim Weiterklicken einfach weiter. Der Button-Farbverlauf passt sich per Canvas-Farbanalyse an das Hauptbild der jeweiligen Seite an, mit einem seiten-spezifischen Fallback-Farbpaar für bildlose Seiten (Warenkorb, Rechtstexte, ...). Da Astro Skripte bei Client-seitigen Übergängen nicht automatisch neu ausführt, musste das komplette Header-Skript (Warenkorb-Zähler, Konto-Menü, Hamburger, Installations- Button) auf das "astro:page-load"-Ereignis umgestellt werden; echte Dauer-Listener (document/window) wurden dabei bewusst nur einmal registriert, um Duplikate bei jeder Navigation zu vermeiden. Ausführlich mit einem headless-Chrome-Testskript verifiziert: Iframe-Persistenz über mehrere Navigationen hinweg, keine doppelten Klick-Listener, keine Konsolenfehler. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
d3efaf8aba
commit
b888e682d8
+226
-45
@@ -4,6 +4,13 @@ import { categories } from "../data/categories";
|
|||||||
import { defaultLocale, localeMeta, localePrefix, type Locale } from "../i18n/config";
|
import { defaultLocale, localeMeta, localePrefix, type Locale } from "../i18n/config";
|
||||||
import { useTranslations } from "../i18n/ui";
|
import { useTranslations } from "../i18n/ui";
|
||||||
import LanguageSwitcher from "../components/LanguageSwitcher.astro";
|
import LanguageSwitcher from "../components/LanguageSwitcher.astro";
|
||||||
|
// ClientRouter (Astros eingebaute View-Transitions-Technik) ausschließlich für EIN Ziel aktiviert:
|
||||||
|
// den Musik-Button unten rechts (siehe transition:persist weiter unten) soll beim Seitenwechsel
|
||||||
|
// nicht neu erzeugt werden, damit ein einmal gestarteter Spotify-Player weiterläuft, statt bei
|
||||||
|
// jedem Klick auf einen Link neu zu laden — genau wie beim Dogfather-Universe-Projekt, nur mit
|
||||||
|
// Astros nativer, offiziell unterstützter Lösung dafür statt eines selbstgebauten PJAX-Systems
|
||||||
|
// (das mit Astros gebündelten, gehashten Skript-Dateien nicht zuverlässig zusammenspielen würde).
|
||||||
|
import { ClientRouter } from "astro:transitions";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -56,6 +63,7 @@ const p = (href: string) => `${localePrefix(lang)}${href}`;
|
|||||||
<style>
|
<style>
|
||||||
:root { --font-head-override: "Fraunces", serif; }
|
:root { --font-head-override: "Fraunces", serif; }
|
||||||
</style>
|
</style>
|
||||||
|
<ClientRouter fallback="swap" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="topbar">🩵 {t.topbar}</div>
|
<div class="topbar">🩵 {t.topbar}</div>
|
||||||
@@ -194,13 +202,49 @@ const p = (href: string) => `${localePrefix(lang)}${href}`;
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Musik-Button: schwebt rechts unten auf JEDER Seite, spielt Van-Vans Musik von Spotify
|
||||||
|
(genau wie beim Dogfather-Universe-Projekt — gleiche Spotify-Artist-ID, gleiches Grund-
|
||||||
|
prinzip). transition:persist sorgt dafür, dass dieses Element (inkl. laufendem Spotify-
|
||||||
|
iframe) beim Seitenwechsel NICHT neu erzeugt wird, sondern unverändert in die neue Seite
|
||||||
|
übernommen wird — die Musik läuft also beim Klicken durch die Seite einfach weiter, statt
|
||||||
|
bei jeder Navigation neu zu starten. Die Farbe des Knopfs passt sich "chamäleonartig" an
|
||||||
|
die jeweilige Seite an (siehe adaptMusicColors() im Skript unten). */}
|
||||||
|
<div id="vandiy-music-widget" transition:persist transition:name="vandiy-music-widget">
|
||||||
|
<div class="music-panel" id="music-panel" hidden>
|
||||||
|
<div class="music-panel-head">
|
||||||
|
<strong>🎵 Musik von Van-Van</strong>
|
||||||
|
<button type="button" class="music-close" id="music-close" aria-label="Musik-Player schließen">✕</button>
|
||||||
|
</div>
|
||||||
|
<p class="small music-panel-intro">Hör beim Stöbern Van-Vans Musik auf Spotify — läuft einfach weiter, auch wenn du auf der Seite weiterklickst.</p>
|
||||||
|
<iframe
|
||||||
|
id="music-iframe"
|
||||||
|
data-src="https://open.spotify.com/embed/artist/4J9PcKZBkh6V8dVY3CGliw?utm_source=generator&theme=0"
|
||||||
|
height="352"
|
||||||
|
loading="lazy"
|
||||||
|
title="Van-Van auf Spotify"
|
||||||
|
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
||||||
|
allowfullscreen
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="music-toggle" id="music-toggle" aria-expanded="false" aria-controls="music-panel" title="Musik von Van-Van">
|
||||||
|
<span>🎵</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { cartCount } from "../scripts/cart";
|
import { cartCount } from "../scripts/cart";
|
||||||
import { getAccount, logout, anzeigeName } from "../scripts/account";
|
import { getAccount, logout, anzeigeName } from "../scripts/account";
|
||||||
|
|
||||||
// Vorschau-Umschalter: Sichtbarkeit hängt allein vom (nicht-httpOnly) Rollen-Cookie ab, das
|
// WICHTIG seit Einführung von <ClientRouter /> (siehe Kopf dieser Datei, nur für den
|
||||||
// die Gate-Middleware nach erfolgreicher Zugangscode-Eingabe setzt.
|
// persistenten Musik-Button gedacht): Astro führt dieses Skript bei einem Seitenwechsel NICHT
|
||||||
(function initPreviewSwitcher() {
|
// automatisch erneut aus (nur bei einem waschechten ersten Laden) — alles, was auf JEDER
|
||||||
|
// Seite neu laufen muss (Warenkorb-Zähler, Konto-Menü, Hamburger, Musik-Farbe …), gehört
|
||||||
|
// deshalb in initPage() weiter unten, aufgerufen über das Astro-Ereignis "astro:page-load"
|
||||||
|
// (feuert zuverlässig sowohl beim allerersten Laden als auch nach jedem Seitenwechsel).
|
||||||
|
// Reines Browser-/Session-Setup (Installations-Prompt, Service Worker) bleibt bewusst AUSSERHALB
|
||||||
|
// von initPage(), weil es nur einmal pro Sitzung registriert werden darf.
|
||||||
|
|
||||||
|
function initPreviewSwitcher() {
|
||||||
const match = document.cookie.match(/(?:^|;\s*)vandiy_gate_role=([^;]+)/);
|
const match = document.cookie.match(/(?:^|;\s*)vandiy_gate_role=([^;]+)/);
|
||||||
const role = match ? decodeURIComponent(match[1]) : null;
|
const role = match ? decodeURIComponent(match[1]) : null;
|
||||||
if (!role) return;
|
if (!role) return;
|
||||||
@@ -230,9 +274,10 @@ const p = (href: string) => `${localePrefix(lang)}${href}`;
|
|||||||
const open = panel.classList.toggle("open");
|
const open = panel.classList.toggle("open");
|
||||||
toggle.setAttribute("aria-expanded", open ? "true" : "false");
|
toggle.setAttribute("aria-expanded", open ? "true" : "false");
|
||||||
});
|
});
|
||||||
document.addEventListener("click", (e) => {
|
// Kein document.addEventListener hier drin (würde sich bei jedem Seitenwechsel erneut
|
||||||
if (!widget.contains(e.target as Node)) panel.classList.remove("open");
|
// aufaddieren, da #vandiy-preview-switcher zwar bei jeder Seite neu erzeugt wird, `document`
|
||||||
});
|
// selbst aber die ganze Sitzung über bestehen bleibt) — das Schließen bei Klick außerhalb
|
||||||
|
// übernimmt stattdessen ein einziger, dauerhafter Listener weiter unten mit Live-Abfrage.
|
||||||
|
|
||||||
const heute = new Date().toISOString().slice(0, 10);
|
const heute = new Date().toISOString().slice(0, 10);
|
||||||
|
|
||||||
@@ -287,29 +332,26 @@ const p = (href: string) => `${localePrefix(lang)}${href}`;
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})();
|
}
|
||||||
|
|
||||||
function updateCount() {
|
function updateCount() {
|
||||||
const el = document.getElementById("cart-count");
|
const el = document.getElementById("cart-count");
|
||||||
if (el) el.textContent = String(cartCount());
|
if (el) el.textContent = String(cartCount());
|
||||||
}
|
}
|
||||||
updateCount();
|
|
||||||
window.addEventListener("cart:changed", updateCount);
|
|
||||||
window.addEventListener("storage", updateCount);
|
|
||||||
|
|
||||||
// Konto-Bereich der Kopfzeile: "Anmelden"-Link vs. Konto-Menü, je nachdem ob auf diesem
|
// Konto-Bereich der Kopfzeile: "Anmelden"-Link vs. Konto-Menü, je nachdem ob auf diesem
|
||||||
// Gerät gerade ein (Demo-)Konto angemeldet ist (siehe scripts/account.ts). Läuft auf JEDER
|
// Gerät gerade ein (Demo-)Konto angemeldet ist (siehe scripts/account.ts). Läuft auf JEDER
|
||||||
// Seite, weil Layout.astro von allen Seiten eingebunden wird — daher überall konsistent.
|
// Seite, weil Layout.astro von allen Seiten eingebunden wird — daher überall konsistent.
|
||||||
const loginBtn = document.getElementById("account-login-btn");
|
// Fragt Elemente bewusst live per document.getElementById ab (statt sie einmal zu speichern),
|
||||||
const menu = document.getElementById("account-menu");
|
// damit sie sowohl beim erneuten Aufruf aus initPage() als auch aus den dauerhaften
|
||||||
const menuBtn = document.getElementById("account-menu-btn");
|
// window/document-Listenern weiter unten immer die AKTUELLEN (nach Seitenwechsel neuen)
|
||||||
const dropdown = document.getElementById("account-dropdown");
|
// Elemente treffen.
|
||||||
const avatar = document.getElementById("account-avatar");
|
|
||||||
const usernameLabel = document.getElementById("account-username-label");
|
|
||||||
const logoutBtn = document.getElementById("account-logout-btn");
|
|
||||||
|
|
||||||
function renderAccountArea() {
|
function renderAccountArea() {
|
||||||
const a = getAccount();
|
const a = getAccount();
|
||||||
|
const loginBtn = document.getElementById("account-login-btn");
|
||||||
|
const menu = document.getElementById("account-menu");
|
||||||
|
const avatar = document.getElementById("account-avatar");
|
||||||
|
const usernameLabel = document.getElementById("account-username-label");
|
||||||
if (loginBtn) loginBtn.style.display = a.loggedIn ? "none" : "";
|
if (loginBtn) loginBtn.style.display = a.loggedIn ? "none" : "";
|
||||||
if (menu) menu.style.display = a.loggedIn ? "flex" : "none";
|
if (menu) menu.style.display = a.loggedIn ? "flex" : "none";
|
||||||
if (!a.loggedIn) return;
|
if (!a.loggedIn) return;
|
||||||
@@ -324,9 +366,18 @@ const p = (href: string) => `${localePrefix(lang)}${href}`;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function closeAccountMenu() {
|
||||||
|
document.getElementById("account-menu")?.classList.remove("open");
|
||||||
|
document.getElementById("account-menu-btn")?.setAttribute("aria-expanded", "false");
|
||||||
|
}
|
||||||
|
// Nur das Verdrahten der Listener an die (bei jedem Seitenwechsel neu erzeugten) Kopfzeilen-
|
||||||
|
// Elemente selbst — sicher, wiederholt aufzurufen: alte Elemente samt ihren Listenern
|
||||||
|
// verschwinden zusammen mit dem Seitenwechsel, keine Ansammlung.
|
||||||
|
function initAccountArea() {
|
||||||
renderAccountArea();
|
renderAccountArea();
|
||||||
window.addEventListener("account:changed", renderAccountArea);
|
const menu = document.getElementById("account-menu");
|
||||||
window.addEventListener("storage", renderAccountArea);
|
const menuBtn = document.getElementById("account-menu-btn");
|
||||||
|
const logoutBtn = document.getElementById("account-logout-btn");
|
||||||
|
|
||||||
menuBtn?.addEventListener("click", (e) => {
|
menuBtn?.addEventListener("click", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -344,49 +395,34 @@ const p = (href: string) => `${localePrefix(lang)}${href}`;
|
|||||||
menuBtn?.setAttribute("aria-expanded", "true");
|
menuBtn?.setAttribute("aria-expanded", "true");
|
||||||
});
|
});
|
||||||
menu?.addEventListener("mouseleave", () => {
|
menu?.addEventListener("mouseleave", () => {
|
||||||
accountCloseTimer = setTimeout(() => {
|
accountCloseTimer = setTimeout(closeAccountMenu, 200);
|
||||||
menu.classList.remove("open");
|
|
||||||
menuBtn?.setAttribute("aria-expanded", "false");
|
|
||||||
}, 200);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("click", () => {
|
|
||||||
menu?.classList.remove("open");
|
|
||||||
menuBtn?.setAttribute("aria-expanded", "false");
|
|
||||||
});
|
|
||||||
document.addEventListener("keydown", (e) => {
|
|
||||||
if (e.key === "Escape") {
|
|
||||||
menu?.classList.remove("open");
|
|
||||||
menuBtn?.setAttribute("aria-expanded", "false");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
logoutBtn?.addEventListener("click", () => {
|
logoutBtn?.addEventListener("click", () => {
|
||||||
logout();
|
logout();
|
||||||
menu?.classList.remove("open");
|
closeAccountMenu();
|
||||||
menuBtn?.setAttribute("aria-expanded", "false");
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initHamburger() {
|
||||||
const hamburger = document.getElementById("hamburger");
|
const hamburger = document.getElementById("hamburger");
|
||||||
const mobileNav = document.getElementById("mobile-nav");
|
const mobileNav = document.getElementById("mobile-nav");
|
||||||
hamburger?.addEventListener("click", () => {
|
hamburger?.addEventListener("click", () => {
|
||||||
const open = mobileNav?.classList.toggle("open");
|
const open = mobileNav?.classList.toggle("open");
|
||||||
hamburger.setAttribute("aria-expanded", String(!!open));
|
hamburger.setAttribute("aria-expanded", String(!!open));
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// "Auf dem Gerät installieren" — zeigt einen echten Button in der Kopfzeile, sobald der
|
// "Auf dem Gerät installieren" — zeigt einen echten Button in der Kopfzeile, sobald der
|
||||||
// Browser eine Installation anbietet (Chrome/Edge auf PC & Handy), statt sich darauf zu
|
// Browser eine Installation anbietet (Chrome/Edge auf PC & Handy), statt sich darauf zu
|
||||||
// verlassen, dass jemand das winzige Icon im Browser selbst findet. Läuft bereits als
|
// verlassen, dass jemand das winzige Icon im Browser selbst findet. Läuft bereits als
|
||||||
// installierte App? Dann gibt es sowieso nichts mehr zu installieren — Button bleibt versteckt.
|
// installierte App? Dann gibt es sowieso nichts mehr zu installieren — Button bleibt versteckt.
|
||||||
|
// installPromptEvent lebt AUSSERHALB von initPage() (Modul-Gültigkeitsbereich, überlebt jeden
|
||||||
|
// Seitenwechsel unverändert), weil das Ereignis nur EINMAL pro Sitzung vom Browser kommt, der
|
||||||
|
// "Installieren"-Knopf selbst aber auf jeder neu geladenen Seite neu gefunden werden muss.
|
||||||
|
let installPromptEvent: any = null;
|
||||||
|
function initInstallButton() {
|
||||||
const installBtn = document.getElementById("install-btn");
|
const installBtn = document.getElementById("install-btn");
|
||||||
const laeuftBereitsAlsApp = window.matchMedia("(display-mode: standalone)").matches;
|
|
||||||
let installPromptEvent = null;
|
|
||||||
if (!laeuftBereitsAlsApp) {
|
|
||||||
window.addEventListener("beforeinstallprompt", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
installPromptEvent = e;
|
|
||||||
if (installBtn) installBtn.style.display = "flex";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
installBtn?.addEventListener("click", async () => {
|
installBtn?.addEventListener("click", async () => {
|
||||||
if (!installPromptEvent) return;
|
if (!installPromptEvent) return;
|
||||||
installBtn.style.display = "none";
|
installBtn.style.display = "none";
|
||||||
@@ -394,7 +430,152 @@ const p = (href: string) => `${localePrefix(lang)}${href}`;
|
|||||||
await installPromptEvent.userChoice;
|
await installPromptEvent.userChoice;
|
||||||
installPromptEvent = null;
|
installPromptEvent = null;
|
||||||
});
|
});
|
||||||
|
if (installPromptEvent && installBtn) installBtn.style.display = "flex";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Musik-Button: #vandiy-music-widget bleibt dank transition:persist bei jedem Seitenwechsel
|
||||||
|
// dasselbe DOM-Element (siehe Markup oben) — der "wired"-Schutz verhindert, dass sich Klick-
|
||||||
|
// Listener bei jedem erneuten initPage()-Aufruf aufaddieren, während die Farbanpassung
|
||||||
|
// bewusst bei JEDER Seite neu läuft.
|
||||||
|
function initMusicWidget() {
|
||||||
|
const widget = document.getElementById("vandiy-music-widget");
|
||||||
|
const toggle = document.getElementById("music-toggle");
|
||||||
|
const panel = document.getElementById("music-panel") as HTMLElement | null;
|
||||||
|
const closeBtn = document.getElementById("music-close");
|
||||||
|
const iframe = document.getElementById("music-iframe") as HTMLIFrameElement | null;
|
||||||
|
if (!widget || !toggle || !panel) return;
|
||||||
|
|
||||||
|
if (!widget.dataset.wired) {
|
||||||
|
widget.dataset.wired = "1";
|
||||||
|
const openPanel = () => {
|
||||||
|
panel.hidden = false;
|
||||||
|
widget.classList.add("open");
|
||||||
|
toggle.setAttribute("aria-expanded", "true");
|
||||||
|
if (iframe && !iframe.getAttribute("src")) {
|
||||||
|
const src = iframe.dataset.src;
|
||||||
|
if (src) iframe.setAttribute("src", src);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const closePanel = () => {
|
||||||
|
panel.hidden = true;
|
||||||
|
widget.classList.remove("open");
|
||||||
|
toggle.setAttribute("aria-expanded", "false");
|
||||||
|
};
|
||||||
|
toggle.addEventListener("click", () => { panel.hidden ? openPanel() : closePanel(); });
|
||||||
|
closeBtn?.addEventListener("click", closePanel);
|
||||||
|
document.addEventListener("click", (e) => {
|
||||||
|
if (!panel.hidden && !widget.contains(e.target as Node)) closePanel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
adaptMusicColors(toggle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Chamäleon"-Farblogik: nimmt das erste sinnvolle Bild im Hauptinhalt der aktuellen Seite,
|
||||||
|
// liest per Canvas Durchschnitts- und auffälligste Farbe aus und färbt den Musik-Knopf
|
||||||
|
// entsprechend ein (siehe --music-c1/--music-c2 im CSS). Fremd-Domain-Bilder oder Seiten ganz
|
||||||
|
// ohne passendes Bild (Warenkorb, Konto, Rechtstexte …) fallen sauber auf ein festes, aber je
|
||||||
|
// nach URL-Pfad unterschiedliches Markenfarben-Paar zurück.
|
||||||
|
async function adaptMusicColors(toggle: HTMLElement) {
|
||||||
|
const FALLBACK_PALETTE: [string, string][] = [
|
||||||
|
["#5fd3ec", "#9b7fd6"],
|
||||||
|
["#9b7fd6", "#e3b23c"],
|
||||||
|
["#e3b23c", "#e7879a"],
|
||||||
|
["#e7879a", "#5fd3ec"],
|
||||||
|
["#6ecb8f", "#5fd3ec"],
|
||||||
|
];
|
||||||
|
function fallbackColors(): [string, string] {
|
||||||
|
let hash = 0;
|
||||||
|
const path = location.pathname;
|
||||||
|
for (let i = 0; i < path.length; i++) hash = (hash * 31 + path.charCodeAt(i)) >>> 0;
|
||||||
|
return FALLBACK_PALETTE[hash % FALLBACK_PALETTE.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
let c1: string | null = null;
|
||||||
|
let c2: string | null = null;
|
||||||
|
const img = document.querySelector<HTMLImageElement>("main img[src]");
|
||||||
|
if (img) {
|
||||||
|
try {
|
||||||
|
if (!img.complete) {
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
img.addEventListener("load", () => resolve(), { once: true });
|
||||||
|
img.addEventListener("error", () => reject(), { once: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = 24;
|
||||||
|
canvas.height = 24;
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (ctx) {
|
||||||
|
ctx.drawImage(img, 0, 0, 24, 24);
|
||||||
|
const { data } = ctx.getImageData(0, 0, 24, 24);
|
||||||
|
let r = 0, g = 0, b = 0, n = 0;
|
||||||
|
let bestSpread = -1, sr = 0, sg = 0, sb = 0;
|
||||||
|
for (let i = 0; i < data.length; i += 4) {
|
||||||
|
if (data[i + 3] < 40) continue; // durchsichtige Pixel ignorieren
|
||||||
|
r += data[i]; g += data[i + 1]; b += data[i + 2]; n++;
|
||||||
|
const spread = Math.max(data[i], data[i + 1], data[i + 2]) - Math.min(data[i], data[i + 1], data[i + 2]);
|
||||||
|
if (spread > bestSpread) { bestSpread = spread; sr = data[i]; sg = data[i + 1]; sb = data[i + 2]; }
|
||||||
|
}
|
||||||
|
if (n > 0) {
|
||||||
|
c1 = `rgb(${Math.round(r / n)}, ${Math.round(g / n)}, ${Math.round(b / n)})`;
|
||||||
|
c2 = `rgb(${sr}, ${sg}, ${sb})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fremd-Domain-Bild (CORS) oder Ladefehler -> unten auf Fallback-Palette zurückfallen.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!c1 || !c2) { [c1, c2] = fallbackColors(); }
|
||||||
|
toggle.style.setProperty("--music-c1", c1);
|
||||||
|
toggle.style.setProperty("--music-c2", c2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dauerhafte document/window-Listener — bewusst NUR EINMAL registriert (nicht in initPage()),
|
||||||
|
// weil `document`/`window` selbst bei einem Seitenwechsel NICHT neu erzeugt werden. Würden
|
||||||
|
// diese stattdessen bei jedem astro:page-load neu angehängt, würden sie sich mit jeder
|
||||||
|
// Navigation zusätzlich aufaddieren. Fragen die betroffenen Elemente deshalb erst IM
|
||||||
|
// Ereignisfall live ab, statt sie beim Registrieren einmalig festzuhalten.
|
||||||
|
window.addEventListener("account:changed", renderAccountArea);
|
||||||
|
window.addEventListener("storage", renderAccountArea);
|
||||||
|
document.addEventListener("click", (e) => {
|
||||||
|
closeAccountMenu();
|
||||||
|
const previewWidget = document.getElementById("vandiy-preview-switcher");
|
||||||
|
const previewPanel = document.getElementById("vandiy-preview-panel");
|
||||||
|
if (previewWidget && previewPanel && !previewWidget.contains(e.target as Node)) {
|
||||||
|
previewPanel.classList.remove("open");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Escape") closeAccountMenu();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Läuft zuverlässig sowohl beim allerersten Laden als auch nach jedem Seitenwechsel (siehe
|
||||||
|
// Kommentar oben) — alles, was auf JEDER Seite frisch eingerichtet werden muss, steht hier.
|
||||||
|
document.addEventListener("astro:page-load", () => {
|
||||||
|
initPreviewSwitcher();
|
||||||
|
updateCount();
|
||||||
|
initAccountArea();
|
||||||
|
initHamburger();
|
||||||
|
initInstallButton();
|
||||||
|
initMusicWidget();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reines Browser-/Sitzungs-Setup — läuft nur EINMAL (dieses Skript selbst wird von Astro nur
|
||||||
|
// beim allerersten Laden ausgeführt, siehe Kommentar oben), absichtlich AUSSERHALB von
|
||||||
|
// initPage(), weil sich diese Ereignisse sonst bei jedem Seitenwechsel erneut registrieren
|
||||||
|
// und mehrfach feuern würden.
|
||||||
|
const laeuftBereitsAlsApp = window.matchMedia("(display-mode: standalone)").matches;
|
||||||
|
if (!laeuftBereitsAlsApp) {
|
||||||
|
window.addEventListener("beforeinstallprompt", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
installPromptEvent = e;
|
||||||
|
const installBtn = document.getElementById("install-btn");
|
||||||
|
if (installBtn) installBtn.style.display = "flex";
|
||||||
|
});
|
||||||
|
}
|
||||||
window.addEventListener("appinstalled", () => {
|
window.addEventListener("appinstalled", () => {
|
||||||
|
const installBtn = document.getElementById("install-btn");
|
||||||
if (installBtn) installBtn.style.display = "none";
|
if (installBtn) installBtn.style.display = "none";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+95
-6
@@ -2650,10 +2650,11 @@ a:focus-visible, button:focus-visible {
|
|||||||
.abo-notif-date { color: var(--c-text-muted); }
|
.abo-notif-date { color: var(--c-text-muted); }
|
||||||
@media (prefers-reduced-motion: reduce) { .abo-plan-card, .abo-plan-card.is-current-plan, .abo-reward-ready { animation: none; transition: none; } }
|
@media (prefers-reduced-motion: reduce) { .abo-plan-card, .abo-plan-card.is-current-plan, .abo-reward-ready { animation: none; transition: none; } }
|
||||||
|
|
||||||
/* Vorschau-Umschalter (nur für qciga/vanvan sichtbar, siehe Layout.astro) — schwebt rechts unten
|
/* Vorschau-Umschalter (nur für qciga/vanvan sichtbar, siehe Layout.astro) — schwebt LINKS unten
|
||||||
über jeder Seite, damit man ohne Seitenwechsel zwischen normalem Besucher/angemeldeter Kundin/
|
über jeder Seite (rechts unten ist der Musik-Button, siehe weiter unten), damit man ohne
|
||||||
Abonnentin hin- und herschalten kann. */
|
Seitenwechsel zwischen normalem Besucher/angemeldeter Kundin/Abonnentin hin- und herschalten
|
||||||
#vandiy-preview-switcher { position: fixed; right: 1.2rem; bottom: 1.2rem; z-index: 200; }
|
kann. */
|
||||||
|
#vandiy-preview-switcher { position: fixed; left: 1.2rem; bottom: 1.2rem; z-index: 200; }
|
||||||
#vandiy-preview-toggle {
|
#vandiy-preview-toggle {
|
||||||
width: 52px;
|
width: 52px;
|
||||||
height: 52px;
|
height: 52px;
|
||||||
@@ -2671,7 +2672,7 @@ a:focus-visible, button:focus-visible {
|
|||||||
#vandiy-preview-panel {
|
#vandiy-preview-panel {
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
left: 0;
|
||||||
bottom: calc(100% + 0.7rem);
|
bottom: calc(100% + 0.7rem);
|
||||||
min-width: 240px;
|
min-width: 240px;
|
||||||
padding: 0.8rem;
|
padding: 0.8rem;
|
||||||
@@ -2696,7 +2697,95 @@ a:focus-visible, button:focus-visible {
|
|||||||
transition: background 0.15s var(--ease);
|
transition: background 0.15s var(--ease);
|
||||||
}
|
}
|
||||||
#vandiy-preview-panel button[data-preview]:hover { background: color-mix(in srgb, var(--c-accent) 14%, transparent); }
|
#vandiy-preview-panel button[data-preview]:hover { background: color-mix(in srgb, var(--c-accent) 14%, transparent); }
|
||||||
@media (max-width: 500px) { #vandiy-preview-switcher { right: 0.7rem; bottom: 0.7rem; } }
|
@media (max-width: 500px) { #vandiy-preview-switcher { left: 0.7rem; bottom: 0.7rem; } }
|
||||||
|
|
||||||
|
/* Musik-Button: schwebt rechts unten auf JEDER Seite (Vorschau-Umschalter ist links unten, siehe
|
||||||
|
oben) — spielt Van-Vans Musik von Spotify, läuft dank transition:persist (Layout.astro) beim
|
||||||
|
Seitenwechsel einfach weiter statt neu zu starten. Gleiches Gestaltungsprinzip wie beim
|
||||||
|
Dogfather-Universe-Projekt: dunkler Knopf mit wandernder, weichgezeichneter Farbwolke
|
||||||
|
("Aurora") + durchlaufendem Lichtschimmer, deren Farben sich "chamäleonartig" an die jeweilige
|
||||||
|
Seite anpassen (siehe --music-c1/--music-c2, live per JS gesetzt — adaptMusicColors() im
|
||||||
|
Skript in Layout.astro). Ohne JS/Bild fällt der Knopf sauber auf die normalen Markenfarben
|
||||||
|
zurück. */
|
||||||
|
#vandiy-music-widget { position: fixed; right: 1.2rem; bottom: 1.2rem; z-index: 200; display: flex; flex-direction: column; align-items: flex-end; gap: 0.8rem; }
|
||||||
|
.music-toggle {
|
||||||
|
position: relative;
|
||||||
|
isolation: isolate;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 54px;
|
||||||
|
height: 54px;
|
||||||
|
flex: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 1.45rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.55);
|
||||||
|
background: var(--c-black);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--music-c1, var(--c-accent)) 55%, transparent);
|
||||||
|
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.45), 0 0 24px color-mix(in srgb, var(--music-c1, var(--c-accent)) 35%, transparent);
|
||||||
|
transition: transform 0.25s var(--ease), box-shadow 0.25s var(--ease), border-color 1.1s ease;
|
||||||
|
}
|
||||||
|
.music-toggle::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: -50%;
|
||||||
|
z-index: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 25% 30%, color-mix(in srgb, var(--music-c1, var(--c-accent)) 92%, transparent), transparent 58%),
|
||||||
|
radial-gradient(circle at 75% 72%, color-mix(in srgb, var(--music-c2, var(--c-purple-glow)) 92%, transparent), transparent 58%);
|
||||||
|
filter: blur(7px) saturate(1.3);
|
||||||
|
animation: music-aurora 10s ease-in-out infinite alternate;
|
||||||
|
transition: background 1.1s ease;
|
||||||
|
}
|
||||||
|
.music-toggle::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(115deg, transparent 35%, rgba(255, 255, 255, 0.35) 49%, transparent 63%);
|
||||||
|
background-size: 240% 100%;
|
||||||
|
mix-blend-mode: overlay;
|
||||||
|
animation: music-shimmer 5s linear infinite;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.music-toggle span { position: relative; z-index: 2; }
|
||||||
|
.music-toggle:hover, .music-toggle:focus-visible {
|
||||||
|
transform: translateY(-2px) scale(1.06);
|
||||||
|
box-shadow: 0 16px 38px rgba(0, 0, 0, 0.5), 0 0 34px color-mix(in srgb, var(--music-c1, var(--c-accent)) 55%, transparent);
|
||||||
|
}
|
||||||
|
#vandiy-music-widget.open .music-toggle { transform: rotate(12deg); }
|
||||||
|
@keyframes music-aurora {
|
||||||
|
0% { transform: translate(-4%, -3%) rotate(0deg); }
|
||||||
|
100% { transform: translate(4%, 3%) rotate(8deg); }
|
||||||
|
}
|
||||||
|
@keyframes music-shimmer {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -60% 0; }
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) { .music-toggle::before, .music-toggle::after { animation: none; } }
|
||||||
|
|
||||||
|
.music-panel {
|
||||||
|
width: min(320px, calc(100vw - 2.4rem));
|
||||||
|
padding: 1rem 1.1rem 1.2rem;
|
||||||
|
border-radius: var(--radius-l);
|
||||||
|
border: 1px solid rgba(244, 241, 247, 0.14);
|
||||||
|
background: var(--c-anthracite-light);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
animation: music-panel-in 0.22s ease-out;
|
||||||
|
}
|
||||||
|
.music-panel[hidden] { display: none; }
|
||||||
|
@keyframes music-panel-in { from { opacity: 0; transform: translateY(10px) scale(0.97); } to { opacity: 1; transform: none; } }
|
||||||
|
.music-panel-head { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; }
|
||||||
|
.music-panel-intro { margin: 0.4rem 0 0.7rem; color: var(--c-text-muted); }
|
||||||
|
.music-close { background: none; border: 0; color: var(--c-text-muted); font-size: 1rem; line-height: 1; padding: 0.2rem 0.3rem; cursor: pointer; border-radius: 6px; }
|
||||||
|
.music-close:hover { color: var(--c-accent); }
|
||||||
|
.music-panel iframe { display: block; width: 100%; border: 0; border-radius: 12px; }
|
||||||
|
@media (max-width: 500px) { #vandiy-music-widget { right: 0.7rem; bottom: 0.7rem; } }
|
||||||
|
@media print { #vandiy-music-widget { display: none !important; } }
|
||||||
|
|
||||||
/* Interne Bestellverwaltung (/verwaltung/) — Status-Kacheln, Filterleiste, Auswertungs-Balken. */
|
/* Interne Bestellverwaltung (/verwaltung/) — Status-Kacheln, Filterleiste, Auswertungs-Balken. */
|
||||||
.verwaltung-stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 0.9rem; margin: 1.2rem 0; }
|
.verwaltung-stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 0.9rem; margin: 1.2rem 0; }
|
||||||
|
|||||||
Reference in New Issue
Block a user