0b645fe765
- Remove debug artifact from login page ('Use Link component: HOME')
- Full icon set: 48/72/96/128/144/152/192/384/512 + maskable variants
- Proper manifest: display_override, shortcuts, categories, scope
- Better service worker: offline fallback page, pre-caching, stale-while-revalidate
- Custom install banner (Android beforeinstallprompt + iOS instructions)
- Connection status indicator (online/offline toast)
- Updated index.html with proper favicon sizes and meta tags
99 lines
3.4 KiB
TypeScript
99 lines
3.4 KiB
TypeScript
import { useState, useEffect, useCallback } from "react";
|
|
|
|
interface BeforeInstallPromptEvent extends Event {
|
|
prompt: () => Promise<void>;
|
|
userChoice: Promise<{ outcome: "accepted" | "dismissed" }>;
|
|
}
|
|
|
|
const DISMISSED_KEY = "dumpster-pwa-install-dismissed";
|
|
|
|
export function InstallBanner() {
|
|
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);
|
|
const [visible, setVisible] = useState(false);
|
|
const [isIOS, setIsIOS] = useState(false);
|
|
|
|
useEffect(() => {
|
|
// Check if already dismissed
|
|
if (localStorage.getItem(DISMISSED_KEY)) return;
|
|
|
|
// Detect iOS (no beforeinstallprompt)
|
|
const ua = window.navigator.userAgent;
|
|
const isIOSDevice = /iPad|iPhone|iPod/.test(ua) || (ua.includes("Mac") && "ontouchend" in window);
|
|
const isStandalone = window.matchMedia("(display-mode: standalone)").matches;
|
|
|
|
if (isStandalone) return; // Already installed
|
|
|
|
if (isIOSDevice && !isStandalone) {
|
|
setIsIOS(true);
|
|
// Show iOS instructions after a short delay
|
|
const timer = setTimeout(() => setVisible(true), 3000);
|
|
return () => clearTimeout(timer);
|
|
}
|
|
|
|
const handler = (e: Event) => {
|
|
e.preventDefault();
|
|
setDeferredPrompt(e as BeforeInstallPromptEvent);
|
|
// Show banner after a short delay so it doesn't clash with page load
|
|
setTimeout(() => setVisible(true), 2000);
|
|
};
|
|
|
|
window.addEventListener("beforeinstallprompt", handler);
|
|
return () => window.removeEventListener("beforeinstallprompt", handler);
|
|
}, []);
|
|
|
|
const handleInstall = useCallback(async () => {
|
|
if (!deferredPrompt) return;
|
|
await deferredPrompt.prompt();
|
|
const { outcome } = await deferredPrompt.userChoice;
|
|
if (outcome === "accepted") {
|
|
setVisible(false);
|
|
}
|
|
setDeferredPrompt(null);
|
|
}, [deferredPrompt]);
|
|
|
|
const handleDismiss = useCallback(() => {
|
|
setVisible(false);
|
|
localStorage.setItem(DISMISSED_KEY, "1");
|
|
}, []);
|
|
|
|
if (!visible) return null;
|
|
|
|
return (
|
|
<div className="fixed bottom-4 left-4 right-4 z-50 flex justify-center pointer-events-none">
|
|
<div className="pointer-events-auto bg-gb-bg-s border border-gb-bg-t shadow-2xl max-w-md w-full flex items-center gap-3 p-3">
|
|
<img src="/icons/icon-72.png" alt="" className="w-10 h-10 shrink-0" />
|
|
<div className="flex-1 min-w-0">
|
|
<div className="text-sm text-gb-fg font-mono font-bold">Install Dumpster</div>
|
|
{isIOS ? (
|
|
<div className="text-xs text-gb-fg-f font-mono mt-0.5">
|
|
Tap <span className="text-gb-blue">Share</span> then{" "}
|
|
<span className="text-gb-blue">Add to Home Screen</span>
|
|
</div>
|
|
) : (
|
|
<div className="text-xs text-gb-fg-f font-mono mt-0.5">
|
|
Add to home screen for the full app experience
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-2 shrink-0">
|
|
{!isIOS && deferredPrompt && (
|
|
<button
|
|
onClick={handleInstall}
|
|
className="text-xs font-mono px-3 py-1.5 bg-gb-orange text-gb-bg hover:bg-gb-yellow transition-colors"
|
|
>
|
|
INSTALL
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={handleDismiss}
|
|
className="text-xs font-mono text-gb-fg-f hover:text-gb-red transition-colors px-2 py-1.5"
|
|
title="Dismiss"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|