'd'
This commit is contained in:
196
app/[locale]/settings/notifications/page.tsx
Normal file
196
app/[locale]/settings/notifications/page.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user