232 lines
8.6 KiB
TypeScript
232 lines
8.6 KiB
TypeScript
"use client"
|
||
|
||
import { useState, useEffect } from "react"
|
||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Switch } from "@/components/ui/switch"
|
||
import { Label } from "@/components/ui/label"
|
||
import { Bell, BellOff, X, Check } from "lucide-react"
|
||
import { useDialog } from "@/components/ui/alert-dialog-custom"
|
||
import { requestNotificationPermission, checkUpcomingAnniversaries } from "@/lib/notifications"
|
||
import { useFamily } from "@/context/family-context"
|
||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||
import { MemberNameWithStatus } from "@/components/member-name-with-status"
|
||
|
||
export function NotificationSettings() {
|
||
const { showAlert } = useDialog()
|
||
const { treeData } = useFamily()
|
||
const [notificationEnabled, setNotificationEnabled] = useState(false)
|
||
const [permission, setPermission] = useState<NotificationPermission>("default")
|
||
const [isClient, setIsClient] = useState(false)
|
||
const [upcomingEvents, setUpcomingEvents] = useState<{
|
||
birthdays: Array<{ member: any; daysUntil: number }>
|
||
deathdays: Array<{ member: any; daysUntil: number }>
|
||
}>({ birthdays: [], deathdays: [] })
|
||
|
||
useEffect(() => {
|
||
// 标记为客户端
|
||
setIsClient(true)
|
||
|
||
// 检查通知权限
|
||
if (typeof window !== "undefined" && "Notification" in window) {
|
||
setPermission(Notification.permission)
|
||
setNotificationEnabled(Notification.permission === "granted")
|
||
}
|
||
|
||
// 检查即将到来的纪念日
|
||
const members = Object.values(treeData.members)
|
||
const upcoming = checkUpcomingAnniversaries(members, 7)
|
||
setUpcomingEvents(upcoming)
|
||
}, [treeData])
|
||
|
||
const handleEnableNotifications = async () => {
|
||
const granted = await requestNotificationPermission()
|
||
if (granted) {
|
||
setNotificationEnabled(true)
|
||
setPermission("granted")
|
||
|
||
// 发送测试通知
|
||
new Notification("通知已启用", {
|
||
body: "您将收到家族纪念日提醒",
|
||
icon: "/icon.svg",
|
||
})
|
||
} else {
|
||
setPermission(Notification.permission)
|
||
}
|
||
}
|
||
|
||
const handleDisableNotifications = async () => {
|
||
setNotificationEnabled(false)
|
||
await showAlert("请在浏览器设置中禁用此网站的通知权限", "提示")
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<Bell className="h-5 w-5" />
|
||
纪念日通知
|
||
</CardTitle>
|
||
<CardDescription>
|
||
接收家族成员生日和忌日的浏览器通知
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
{!isClient ? (
|
||
<div className="text-center py-4 text-muted-foreground">
|
||
加载中...
|
||
</div>
|
||
) : !(typeof window !== "undefined" && "Notification" in window) ? (
|
||
<Alert>
|
||
<AlertDescription>
|
||
您的浏览器不支持通知功能
|
||
</AlertDescription>
|
||
</Alert>
|
||
) : (
|
||
<>
|
||
<div className="flex items-center justify-between">
|
||
<div className="space-y-0.5">
|
||
<Label htmlFor="notifications">启用通知</Label>
|
||
<p className="text-sm text-muted-foreground">
|
||
每天自动检查并提醒当日纪念日
|
||
</p>
|
||
</div>
|
||
<Switch
|
||
id="notifications"
|
||
checked={notificationEnabled}
|
||
onCheckedChange={(checked) => {
|
||
if (checked) {
|
||
handleEnableNotifications()
|
||
} else {
|
||
handleDisableNotifications()
|
||
}
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
{permission === "denied" && (
|
||
<Alert variant="destructive">
|
||
<X className="h-4 w-4" />
|
||
<AlertDescription>
|
||
通知权限已被拒绝。请在浏览器设置中允许此网站发送通知。
|
||
</AlertDescription>
|
||
</Alert>
|
||
)}
|
||
|
||
{permission === "granted" && (
|
||
<Alert>
|
||
<Check className="h-4 w-4" />
|
||
<AlertDescription>
|
||
通知已启用。您将在每天首次访问时收到当日纪念日提醒。
|
||
</AlertDescription>
|
||
</Alert>
|
||
)}
|
||
|
||
{notificationEnabled && (
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => {
|
||
new Notification("测试通知", {
|
||
body: "这是一条测试通知",
|
||
icon: "/icon.svg",
|
||
})
|
||
}}
|
||
>
|
||
发送测试通知
|
||
</Button>
|
||
)}
|
||
</>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 即将到来的纪念日 */}
|
||
{(upcomingEvents.birthdays.length > 0 || upcomingEvents.deathdays.length > 0) && (
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>即将到来的纪念日(7天内)</CardTitle>
|
||
<CardDescription>提前准备,不错过重要日子</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="space-y-4">
|
||
{upcomingEvents.birthdays.length > 0 && (
|
||
<div>
|
||
<h4 className="font-medium mb-2 flex items-center gap-2">
|
||
🎂 生日
|
||
</h4>
|
||
<div className="space-y-2">
|
||
{upcomingEvents.birthdays.map(({ member, daysUntil }) => (
|
||
<div
|
||
key={member.id}
|
||
className="flex items-center justify-between p-3 bg-muted/50 rounded-lg"
|
||
>
|
||
<div>
|
||
<p className="font-medium">
|
||
<MemberNameWithStatus
|
||
name={member.fullName}
|
||
isDead={!!member.deathDate}
|
||
/>
|
||
</p>
|
||
<p className="text-sm text-muted-foreground">
|
||
{new Date(member.birthDate).toLocaleDateString('zh-CN', {
|
||
month: 'long',
|
||
day: 'numeric'
|
||
})}
|
||
</p>
|
||
</div>
|
||
<div className="text-right">
|
||
<p className="text-sm font-medium text-primary">
|
||
{daysUntil === 1 ? '明天' : `${daysUntil}天后`}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{upcomingEvents.deathdays.length > 0 && (
|
||
<div>
|
||
<h4 className="font-medium mb-2 flex items-center gap-2">
|
||
🕯️ 忌日
|
||
</h4>
|
||
<div className="space-y-2">
|
||
{upcomingEvents.deathdays.map(({ member, daysUntil }) => (
|
||
<div
|
||
key={member.id}
|
||
className="flex items-center justify-between p-3 bg-muted/50 rounded-lg"
|
||
>
|
||
<div>
|
||
<p className="font-medium">
|
||
<MemberNameWithStatus
|
||
name={member.fullName}
|
||
isDead={!!member.deathDate}
|
||
/>
|
||
</p>
|
||
<p className="text-sm text-muted-foreground">
|
||
{new Date(member.deathDate!).toLocaleDateString('zh-CN', {
|
||
month: 'long',
|
||
day: 'numeric'
|
||
})}
|
||
</p>
|
||
</div>
|
||
<div className="text-right">
|
||
<p className="text-sm font-medium text-muted-foreground">
|
||
{daysUntil === 1 ? '明天' : `${daysUntil}天后`}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|