197 lines
8.2 KiB
TypeScript
197 lines
8.2 KiB
TypeScript
"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, 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", "站内消息", "始终建议开启,所有业务通知都会进入消息中心。"],
|
|
["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("");
|
|
|
|
useEffect(() => {
|
|
apiFetch<{ record: PreferenceRecord }>("/api/notifications/preferences")
|
|
.then((data) => setPrefs({ ...DEFAULT_PREFERENCES, ...(data.record || {}) }))
|
|
.catch(() => setPrefs(DEFAULT_PREFERENCES));
|
|
}, []);
|
|
|
|
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>
|
|
</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>
|
|
);
|
|
}
|