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
+252
-71
@@ -4,6 +4,13 @@ import { categories } from "../data/categories";
|
||||
import { defaultLocale, localeMeta, localePrefix, type Locale } from "../i18n/config";
|
||||
import { useTranslations } from "../i18n/ui";
|
||||
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 {
|
||||
title: string;
|
||||
@@ -56,6 +63,7 @@ const p = (href: string) => `${localePrefix(lang)}${href}`;
|
||||
<style>
|
||||
:root { --font-head-override: "Fraunces", serif; }
|
||||
</style>
|
||||
<ClientRouter fallback="swap" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="topbar">🩵 {t.topbar}</div>
|
||||
@@ -194,13 +202,49 @@ const p = (href: string) => `${localePrefix(lang)}${href}`;
|
||||
</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>
|
||||
import { cartCount } from "../scripts/cart";
|
||||
import { getAccount, logout, anzeigeName } from "../scripts/account";
|
||||
|
||||
// Vorschau-Umschalter: Sichtbarkeit hängt allein vom (nicht-httpOnly) Rollen-Cookie ab, das
|
||||
// die Gate-Middleware nach erfolgreicher Zugangscode-Eingabe setzt.
|
||||
(function initPreviewSwitcher() {
|
||||
// WICHTIG seit Einführung von <ClientRouter /> (siehe Kopf dieser Datei, nur für den
|
||||
// persistenten Musik-Button gedacht): Astro führt dieses Skript bei einem Seitenwechsel NICHT
|
||||
// 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 role = match ? decodeURIComponent(match[1]) : null;
|
||||
if (!role) return;
|
||||
@@ -230,9 +274,10 @@ const p = (href: string) => `${localePrefix(lang)}${href}`;
|
||||
const open = panel.classList.toggle("open");
|
||||
toggle.setAttribute("aria-expanded", open ? "true" : "false");
|
||||
});
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!widget.contains(e.target as Node)) panel.classList.remove("open");
|
||||
});
|
||||
// Kein document.addEventListener hier drin (würde sich bei jedem Seitenwechsel erneut
|
||||
// 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);
|
||||
|
||||
@@ -287,29 +332,26 @@ const p = (href: string) => `${localePrefix(lang)}${href}`;
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
function updateCount() {
|
||||
const el = document.getElementById("cart-count");
|
||||
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
|
||||
// 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.
|
||||
const loginBtn = document.getElementById("account-login-btn");
|
||||
const menu = document.getElementById("account-menu");
|
||||
const menuBtn = document.getElementById("account-menu-btn");
|
||||
const dropdown = document.getElementById("account-dropdown");
|
||||
const avatar = document.getElementById("account-avatar");
|
||||
const usernameLabel = document.getElementById("account-username-label");
|
||||
const logoutBtn = document.getElementById("account-logout-btn");
|
||||
|
||||
// Fragt Elemente bewusst live per document.getElementById ab (statt sie einmal zu speichern),
|
||||
// damit sie sowohl beim erneuten Aufruf aus initPage() als auch aus den dauerhaften
|
||||
// window/document-Listenern weiter unten immer die AKTUELLEN (nach Seitenwechsel neuen)
|
||||
// Elemente treffen.
|
||||
function renderAccountArea() {
|
||||
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 (menu) menu.style.display = a.loggedIn ? "flex" : "none";
|
||||
if (!a.loggedIn) return;
|
||||
@@ -324,77 +366,216 @@ const p = (href: string) => `${localePrefix(lang)}${href}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
renderAccountArea();
|
||||
window.addEventListener("account:changed", renderAccountArea);
|
||||
window.addEventListener("storage", renderAccountArea);
|
||||
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();
|
||||
const menu = document.getElementById("account-menu");
|
||||
const menuBtn = document.getElementById("account-menu-btn");
|
||||
const logoutBtn = document.getElementById("account-logout-btn");
|
||||
|
||||
menuBtn?.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
const open = menu?.classList.toggle("open");
|
||||
menuBtn.setAttribute("aria-expanded", open ? "true" : "false");
|
||||
});
|
||||
menuBtn?.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
const open = menu?.classList.toggle("open");
|
||||
menuBtn.setAttribute("aria-expanded", open ? "true" : "false");
|
||||
});
|
||||
|
||||
// Öffnet zusätzlich automatisch beim Drüberfahren mit der Maus (Desktop) — kein Klick mehr
|
||||
// nötig. Kurze Schließverzögerung beim Verlassen, damit der Wechsel von Button zu Menü nicht
|
||||
// sofort wieder zuklappt. Klick bleibt als Fallback für Touch-Geräte/Tastatur erhalten.
|
||||
let accountCloseTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
menu?.addEventListener("mouseenter", () => {
|
||||
if (accountCloseTimer) { clearTimeout(accountCloseTimer); accountCloseTimer = null; }
|
||||
menu.classList.add("open");
|
||||
menuBtn?.setAttribute("aria-expanded", "true");
|
||||
});
|
||||
menu?.addEventListener("mouseleave", () => {
|
||||
accountCloseTimer = setTimeout(() => {
|
||||
menu.classList.remove("open");
|
||||
menuBtn?.setAttribute("aria-expanded", "false");
|
||||
}, 200);
|
||||
});
|
||||
// Öffnet zusätzlich automatisch beim Drüberfahren mit der Maus (Desktop) — kein Klick mehr
|
||||
// nötig. Kurze Schließverzögerung beim Verlassen, damit der Wechsel von Button zu Menü nicht
|
||||
// sofort wieder zuklappt. Klick bleibt als Fallback für Touch-Geräte/Tastatur erhalten.
|
||||
let accountCloseTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
menu?.addEventListener("mouseenter", () => {
|
||||
if (accountCloseTimer) { clearTimeout(accountCloseTimer); accountCloseTimer = null; }
|
||||
menu.classList.add("open");
|
||||
menuBtn?.setAttribute("aria-expanded", "true");
|
||||
});
|
||||
menu?.addEventListener("mouseleave", () => {
|
||||
accountCloseTimer = setTimeout(closeAccountMenu, 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", () => {
|
||||
logout();
|
||||
menu?.classList.remove("open");
|
||||
menuBtn?.setAttribute("aria-expanded", "false");
|
||||
});
|
||||
logoutBtn?.addEventListener("click", () => {
|
||||
logout();
|
||||
closeAccountMenu();
|
||||
});
|
||||
}
|
||||
|
||||
const hamburger = document.getElementById("hamburger");
|
||||
const mobileNav = document.getElementById("mobile-nav");
|
||||
hamburger?.addEventListener("click", () => {
|
||||
const open = mobileNav?.classList.toggle("open");
|
||||
hamburger.setAttribute("aria-expanded", String(!!open));
|
||||
});
|
||||
function initHamburger() {
|
||||
const hamburger = document.getElementById("hamburger");
|
||||
const mobileNav = document.getElementById("mobile-nav");
|
||||
hamburger?.addEventListener("click", () => {
|
||||
const open = mobileNav?.classList.toggle("open");
|
||||
hamburger.setAttribute("aria-expanded", String(!!open));
|
||||
});
|
||||
}
|
||||
|
||||
// "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
|
||||
// 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.
|
||||
const installBtn = document.getElementById("install-btn");
|
||||
// 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");
|
||||
installBtn?.addEventListener("click", async () => {
|
||||
if (!installPromptEvent) return;
|
||||
installBtn.style.display = "none";
|
||||
installPromptEvent.prompt();
|
||||
await installPromptEvent.userChoice;
|
||||
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;
|
||||
let installPromptEvent = null;
|
||||
if (!laeuftBereitsAlsApp) {
|
||||
window.addEventListener("beforeinstallprompt", (e) => {
|
||||
e.preventDefault();
|
||||
installPromptEvent = e;
|
||||
const installBtn = document.getElementById("install-btn");
|
||||
if (installBtn) installBtn.style.display = "flex";
|
||||
});
|
||||
}
|
||||
installBtn?.addEventListener("click", async () => {
|
||||
if (!installPromptEvent) return;
|
||||
installBtn.style.display = "none";
|
||||
installPromptEvent.prompt();
|
||||
await installPromptEvent.userChoice;
|
||||
installPromptEvent = null;
|
||||
});
|
||||
window.addEventListener("appinstalled", () => {
|
||||
const installBtn = document.getElementById("install-btn");
|
||||
if (installBtn) installBtn.style.display = "none";
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user