Files
root a1daa040ef s
2026-06-07 18:06:52 +08:00

174 lines
8.0 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { Link, useTranslation } from "@/app/digital/i18n/navigation";
import DigitalThemeIcon, { type IconName } from "@/app/digital/components/DigitalThemeIcon";
type BeforeInstallPromptEvent = Event & {
prompt: () => Promise<void>;
userChoice: Promise<{ outcome: "accepted" | "dismissed"; platform: string }>;
};
export default function AppDownloadContent() {
const { t } = useTranslation("appDownload");
const [installPrompt, setInstallPrompt] = useState<BeforeInstallPromptEvent | null>(null);
const [installState, setInstallState] = useState<"idle" | "available" | "installed" | "manual">("idle");
useEffect(() => {
window.history.scrollRestoration = "manual";
window.scrollTo(0, 0);
return () => {
window.history.scrollRestoration = "auto";
};
}, []);
useEffect(() => {
const onBeforeInstallPrompt = (event: Event) => {
event.preventDefault();
setInstallPrompt(event as BeforeInstallPromptEvent);
setInstallState("available");
};
const onInstalled = () => {
setInstallPrompt(null);
setInstallState("installed");
};
window.addEventListener("beforeinstallprompt", onBeforeInstallPrompt);
window.addEventListener("appinstalled", onInstalled);
return () => {
window.removeEventListener("beforeinstallprompt", onBeforeInstallPrompt);
window.removeEventListener("appinstalled", onInstalled);
};
}, []);
const features = [
{ key: "resources", icon: "book", emoji: "📚" },
{ key: "community", icon: "globe", emoji: "👥" },
{ key: "tools", icon: "tool", emoji: "🛠️" },
] as const;
const installApp = async () => {
if (!installPrompt) {
setInstallState("manual");
return;
}
await installPrompt.prompt();
const result = await installPrompt.userChoice;
setInstallPrompt(null);
setInstallState(result.outcome === "accepted" ? "installed" : "manual");
};
return (
<div className="min-h-screen bg-slate-50 dark:bg-slate-950">
<section className="pt-24 pb-20 sm:pt-28 sm:pb-28">
<div className="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
<div className="grid items-center gap-10 lg:grid-cols-[1.05fr_0.95fr]">
<div>
<div className="dn-badge mb-5 inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-600 shadow-sm dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300">
<DigitalThemeIcon name="phone" emoji="📱" className="h-4 w-4" />
{t("badge")}
</div>
<h1 className="dn-section-title text-4xl font-bold tracking-tight text-slate-900 dark:text-slate-100 sm:text-5xl">
{t("title")}
</h1>
<p className="mt-4 max-w-2xl text-lg leading-8 text-slate-600 dark:text-slate-400">
{t("subtitle")}
</p>
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
<Link
href="/"
className="dn-button-primary inline-flex items-center justify-center gap-2 rounded-full bg-[var(--dn-primary)] px-6 py-3 font-semibold text-white transition hover:bg-[var(--dn-primary-strong)]"
>
<DigitalThemeIcon name="app" emoji="📱" className="h-5 w-5" />
{t("openWeb")}
</Link>
<button
type="button"
onClick={installApp}
className="dn-button-secondary inline-flex cursor-pointer items-center justify-center gap-2 rounded-full border border-slate-200 bg-white px-6 py-3 font-semibold text-slate-700 transition hover:border-[var(--dn-primary)] hover:text-[var(--dn-primary)] dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200"
>
<DigitalThemeIcon name="phone" emoji="📱" className="h-5 w-5" />
{installState === "available" ? t("installReady") : t("install")}
</button>
</div>
<p className="mt-4 text-sm text-slate-500 dark:text-slate-400">
{installState === "installed"
? t("installed")
: installState === "manual"
? t("manualInstall")
: t("installHint")}
</p>
</div>
<div className="mx-auto w-full max-w-sm">
<div
className="dn-card relative overflow-hidden rounded-[2rem] border border-slate-200 bg-white p-4 shadow-xl dark:border-slate-800 dark:bg-slate-900"
aria-label={t("previewLabel")}
>
<div className="rounded-[1.5rem] bg-slate-950 p-3">
<div className="overflow-hidden rounded-[1.15rem] bg-slate-50 dark:bg-slate-900">
<div className="border-b border-slate-200 bg-white px-4 py-3 dark:border-slate-800 dark:bg-slate-950">
<div className="flex items-center justify-between">
<span className="text-sm font-bold text-slate-900 dark:text-slate-100">{t("previewTitle")}</span>
<span className="h-2 w-2 rounded-full bg-[var(--dn-primary)]" />
</div>
</div>
<div className="space-y-3 p-4">
<div className="rounded-xl bg-[var(--dn-primary)] p-4 text-white">
<p className="text-xs font-medium text-white/75">{t("previewKicker")}</p>
<p className="mt-1 text-lg font-bold">{t("previewRoute")}</p>
</div>
{features.map(({ key, icon, emoji }) => (
<div
key={key}
className="flex items-start gap-3 rounded-xl border border-slate-200 bg-white p-3 dark:border-slate-800 dark:bg-slate-950"
>
<span className="dn-icon-box flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-slate-50 text-[var(--dn-primary)] dark:bg-slate-800">
<DigitalThemeIcon name={icon as IconName} emoji={emoji} className="h-5 w-5" />
</span>
<span className="min-w-0">
<span className="block text-sm font-bold text-slate-900 dark:text-slate-100">
{t(`features.${key}.title`)}
</span>
<span className="mt-0.5 block text-xs leading-5 text-slate-500 dark:text-slate-400">
{t(`features.${key}.desc`)}
</span>
</span>
</div>
))}
</div>
</div>
</div>
</div>
</div>
</div>
<div className="mt-12 grid gap-4 sm:grid-cols-3">
{features.map(({ key, icon, emoji }) => (
<div
key={key}
className="dn-card flex items-start gap-4 rounded-xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900"
>
<span className="dn-icon-box flex h-11 w-11 shrink-0 items-center justify-center rounded-lg bg-slate-50 text-[var(--dn-primary)] dark:bg-slate-800">
<DigitalThemeIcon name={icon as IconName} emoji={emoji} className="h-5 w-5" />
</span>
<span>
<span className="block font-bold text-slate-900 dark:text-slate-100">
{t(`features.${key}.title`)}
</span>
<span className="mt-1 block text-sm leading-6 text-slate-500 dark:text-slate-400">
{t(`features.${key}.desc`)}
</span>
</span>
</div>
))}
</div>
</div>
</section>
</div>
);
}