Files
gitlab-instance-0a899031_cn…/app/[locale]/settings/notifications/page.tsx

218 lines
9.5 KiB
TypeScript
Raw 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 { Link } from "@/i18n/navigation";
import Footer from "@/app/components/Footer";
import { apiFetch } from "@/app/lib/api-client";
interface PreferenceRecord {
channels: Record<string, boolean>;
categories: Record<string, boolean>;
quietHoursStart: string;
quietHoursEnd: string;
digest: string;
allowMarketing: boolean;
}
const DEFAULT_PREFERENCES: PreferenceRecord = {
channels: { in_app: true, push: false, email: false, wechat: false },
categories: {
system: true,
meetup: true,
gig: true,
match: true,
service: true,
community: true,
city: true,
},
quietHoursStart: "22:00",
quietHoursEnd: "08:00",
digest: "daily",
allowMarketing: false,
};
const CHANNELS = [
["in_app", "站内消息", "始终建议开启,所有业务通知都会进入消息中心。"],
["push", "手机推送 (ntfy)", "通过 ntfy 推送到手机;开启后请用下方订阅链接在 ntfy App 中订阅。"],
["email", "邮件摘要", "适合离线时收到每日/每周汇总。"],
["wechat", "微信提醒", "后续可接企业微信、公众号或机器人。"],
];
const CATEGORIES = [
["system", "系统公告"],
["meetup", "活动提醒"],
["gig", "赏金任务"],
["match", "社交匹配"],
["service", "服务咨询"],
["community", "社区讨论"],
["city", "城市更新"],
];
export default function NotificationSettingsPage() {
const [prefs, setPrefs] = useState<PreferenceRecord>(DEFAULT_PREFERENCES);
const [saved, setSaved] = useState("");
const [pushSubscribeUrl, setPushSubscribeUrl] = useState("");
useEffect(() => {
apiFetch<{ record: PreferenceRecord }>("/api/notifications/preferences")
.then((data) => setPrefs({ ...DEFAULT_PREFERENCES, ...(data.record || {}) }))
.catch(() => setPrefs(DEFAULT_PREFERENCES));
apiFetch<{ subscribeUrl?: string }>("/api/notifications/push-config")
.then((data) => setPushSubscribeUrl(data.subscribeUrl || ""))
.catch(() => setPushSubscribeUrl(""));
}, []);
const save = async () => {
await apiFetch("/api/notifications/preferences", {
method: "PUT",
body: JSON.stringify(prefs),
});
setSaved("通知偏好已保存");
};
return (
<div className="min-h-screen bg-[#f6f7f9] dark:bg-gray-950">
<main className="mx-auto max-w-5xl px-4 py-10 sm:px-6">
<Link href="/messages" className="text-sm font-medium text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100">
</Link>
<header className="mt-5 mb-8">
<p className="text-sm font-semibold text-[#ff4d4f]">Notification Settings</p>
<h1 className="mt-2 text-3xl font-bold text-gray-950 dark:text-gray-50"></h1>
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
PocketBase
</p>
</header>
{saved && (
<div className="mb-6 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800 dark:border-emerald-900/60 dark:bg-emerald-950/30 dark:text-emerald-200">
{saved}
</div>
)}
<div className="grid gap-6 lg:grid-cols-[1fr_320px]">
<section className="space-y-6">
<div className="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-gray-100 dark:bg-gray-900 dark:ring-gray-800">
<h2 className="font-semibold text-gray-950 dark:text-gray-50"></h2>
<div className="mt-4 space-y-3">
{CHANNELS.map(([key, title, desc]) => (
<label key={key} className="flex items-start justify-between gap-4 rounded-2xl border border-gray-100 p-4 dark:border-gray-800">
<span>
<span className="block font-medium text-gray-900 dark:text-gray-100">{title}</span>
<span className="mt-1 block text-sm text-gray-500 dark:text-gray-400">{desc}</span>
</span>
<input
type="checkbox"
checked={!!prefs.channels[key]}
onChange={(event) =>
setPrefs((prev) => ({
...prev,
channels: { ...prev.channels, [key]: event.target.checked },
}))
}
className="mt-1 h-5 w-5 accent-[#ff4d4f]"
/>
</label>
))}
</div>
{prefs.channels.push && pushSubscribeUrl ? (
<div className="mt-4 rounded-2xl border border-violet-200 bg-violet-50/80 p-4 dark:border-violet-900/50 dark:bg-violet-950/30">
<p className="text-sm font-semibold text-violet-900 dark:text-violet-200">ntfy </p>
<p className="mt-1 text-xs text-violet-800/80 dark:text-violet-300/80">
ntfy App
</p>
<a
href={pushSubscribeUrl}
target="_blank"
rel="noreferrer"
className="mt-2 block break-all text-sm font-medium text-violet-700 underline dark:text-violet-300"
>
{pushSubscribeUrl}
</a>
</div>
) : null}
</div>
<div className="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-gray-100 dark:bg-gray-900 dark:ring-gray-800">
<h2 className="font-semibold text-gray-950 dark:text-gray-50"></h2>
<div className="mt-4 grid gap-3 sm:grid-cols-2">
{CATEGORIES.map(([key, label]) => (
<label key={key} className="flex items-center justify-between rounded-xl bg-gray-50 px-4 py-3 dark:bg-gray-800">
<span className="text-sm font-medium text-gray-700 dark:text-gray-200">{label}</span>
<input
type="checkbox"
checked={!!prefs.categories[key]}
onChange={(event) =>
setPrefs((prev) => ({
...prev,
categories: { ...prev.categories, [key]: event.target.checked },
}))
}
className="h-4 w-4 accent-[#ff4d4f]"
/>
</label>
))}
</div>
</div>
</section>
<aside className="space-y-6">
<div className="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-gray-100 dark:bg-gray-900 dark:ring-gray-800">
<h2 className="font-semibold text-gray-950 dark:text-gray-50"></h2>
<div className="mt-4 grid grid-cols-2 gap-3">
<label>
<span className="text-xs text-gray-400"></span>
<input
value={prefs.quietHoursStart}
onChange={(event) => setPrefs((prev) => ({ ...prev, quietHoursStart: event.target.value }))}
className="mt-1 w-full rounded-xl border border-gray-200 px-3 py-2 text-sm dark:border-gray-800 dark:bg-gray-950 dark:text-gray-100"
/>
</label>
<label>
<span className="text-xs text-gray-400"></span>
<input
value={prefs.quietHoursEnd}
onChange={(event) => setPrefs((prev) => ({ ...prev, quietHoursEnd: event.target.value }))}
className="mt-1 w-full rounded-xl border border-gray-200 px-3 py-2 text-sm dark:border-gray-800 dark:bg-gray-950 dark:text-gray-100"
/>
</label>
</div>
</div>
<div className="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-gray-100 dark:bg-gray-900 dark:ring-gray-800">
<h2 className="font-semibold text-gray-950 dark:text-gray-50"></h2>
<select
value={prefs.digest}
onChange={(event) => setPrefs((prev) => ({ ...prev, digest: event.target.value }))}
className="mt-4 w-full rounded-xl border border-gray-200 px-3 py-2 text-sm dark:border-gray-800 dark:bg-gray-950 dark:text-gray-100"
>
<option value="instant"></option>
<option value="daily"></option>
<option value="weekly"></option>
</select>
<label className="mt-4 flex items-center justify-between rounded-xl bg-gray-50 px-4 py-3 dark:bg-gray-800">
<span className="text-sm text-gray-700 dark:text-gray-200"></span>
<input
type="checkbox"
checked={prefs.allowMarketing}
onChange={(event) => setPrefs((prev) => ({ ...prev, allowMarketing: event.target.checked }))}
className="h-4 w-4 accent-[#ff4d4f]"
/>
</label>
</div>
<button
type="button"
onClick={save}
className="w-full rounded-2xl bg-[#ff4d4f] px-5 py-3 font-semibold text-white shadow-sm hover:bg-[#ff3333]"
>
</button>
</aside>
</div>
</main>
<Footer />
</div>
);
}