Files
dekun 6265e56a7f Redesign UI with zen cards, split AI panel, and PWA install support.
Learn uses 64-gua card grid; liuyao/bazi/combined use two input cards plus sticky right AI panel; add manifest, service worker, and install prompt.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 23:24:55 +08:00

97 lines
2.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useState } from "react";
import { Download, X } from "lucide-react";
import { Button } from "@/components/ui/button";
interface BeforeInstallPromptEvent extends Event {
prompt: () => Promise<void>;
userChoice: Promise<{ outcome: "accepted" | "dismissed" }>;
}
export default function PwaProvider() {
const [deferred, setDeferred] = useState<BeforeInstallPromptEvent | null>(
null,
);
const [dismissed, setDismissed] = useState(false);
const [isIos, setIsIos] = useState(false);
const [isStandalone, setIsStandalone] = useState(false);
useEffect(() => {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js").catch(() => {});
}
setIsStandalone(
window.matchMedia("(display-mode: standalone)").matches ||
(window.navigator as Navigator & { standalone?: boolean }).standalone ===
true,
);
const ua = window.navigator.userAgent;
setIsIos(/iPad|iPhone|iPod/.test(ua));
const handler = (e: Event) => {
e.preventDefault();
setDeferred(e as BeforeInstallPromptEvent);
};
window.addEventListener("beforeinstallprompt", handler);
return () => window.removeEventListener("beforeinstallprompt", handler);
}, []);
if (isStandalone || dismissed) {
return null;
}
if (deferred) {
return (
<div className="fixed bottom-4 left-4 right-4 z-50 mx-auto flex max-w-md items-center gap-3 rounded-xl border border-border/60 bg-card/95 p-4 shadow-lg backdrop-blur-md sm:left-auto">
<Download size={20} className="shrink-0 text-primary" />
<div className="min-w-0 flex-1 text-sm">
<p className="font-medium"></p>
<p className="text-xs text-muted-foreground">
App 使
</p>
</div>
<Button
size="sm"
onClick={async () => {
await deferred.prompt();
setDeferred(null);
}}
>
</Button>
<button
type="button"
aria-label="关闭"
className="text-muted-foreground hover:text-foreground"
onClick={() => setDismissed(true)}
>
<X size={16} />
</button>
</div>
);
}
if (isIos) {
return (
<div className="fixed bottom-4 left-4 right-4 z-50 mx-auto max-w-md rounded-xl border border-border/60 bg-card/95 p-4 text-sm shadow-lg backdrop-blur-md sm:left-auto">
<p className="font-medium"> iPhone / iPad</p>
<p className="mt-1 text-xs text-muted-foreground">
Safari
</p>
<button
type="button"
className="mt-2 text-xs text-muted-foreground underline"
onClick={() => setDismissed(true)}
>
</button>
</div>
);
}
return null;
}