blob: 216741f68345f837219c1d9823a17dcb6c7e9d4a (
plain) (
blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
|
"use client"
import { useEffect, useRef } from "react"
import { formatDistanceToNow } from "date-fns"
import { toast } from "sonner"
import {
useNotificationStore,
type StoredNotification,
} from "@/lib/stores/notification-store"
export function NotificationPanel({ onClose }: { onClose: () => void }) {
const panelReference = useRef<HTMLDivElement>(null)
const notifications = useNotificationStore((state) => state.notifications)
const dismissNotification = useNotificationStore(
(state) => state.dismissNotification
)
const clearAllNotifications = useNotificationStore(
(state) => state.clearAllNotifications
)
const markAllAsViewed = useNotificationStore(
(state) => state.markAllAsViewed
)
useEffect(() => {
markAllAsViewed()
}, [markAllAsViewed])
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (
panelReference.current &&
!panelReference.current.contains(event.target as Node)
) {
onClose()
}
}
document.addEventListener("mousedown", handleClickOutside)
return () => document.removeEventListener("mousedown", handleClickOutside)
}, [onClose])
function handleNotificationClick(notification: StoredNotification) {
if (notification.actionUrl) {
navigator.clipboard.writeText(notification.actionUrl)
toast("link copied to clipboard")
}
}
return (
<div
ref={panelReference}
className="fixed bottom-16 left-2 z-50 w-80 max-w-[calc(100vw-1rem)] border border-border bg-background-secondary shadow-lg md:absolute md:bottom-full md:left-0 md:mb-1"
>
<div className="flex items-center justify-between border-b border-border px-3 py-2">
<span className="text-text-primary">notifications</span>
{notifications.length > 0 && (
<button
type="button"
onClick={() => {
clearAllNotifications()
onClose()
}}
className="text-text-dim transition-colors hover:text-text-secondary"
>
clear all
</button>
)}
</div>
<div className="max-h-64 overflow-auto">
{notifications.length === 0 ? (
<p className="px-3 py-4 text-center text-text-dim">
no notifications
</p>
) : (
notifications.map((notification: StoredNotification) => (
<div
key={notification.identifier}
className={`flex items-start gap-2 border-b border-border px-3 py-2 last:border-b-0 ${
notification.actionUrl
? "cursor-pointer transition-colors hover:bg-background-tertiary"
: ""
}`}
onClick={
notification.actionUrl
? () => handleNotificationClick(notification)
: undefined
}
>
<div className="min-w-0 flex-1">
<p className="text-text-secondary">{notification.message}</p>
{notification.actionUrl && (
<p className="mt-0.5 text-text-dim">
tap to copy link
</p>
)}
<p className="mt-0.5 text-text-dim">
{formatDistanceToNow(new Date(notification.timestamp), {
addSuffix: true,
})}
</p>
</div>
<button
type="button"
onClick={(event) => {
event.stopPropagation()
dismissNotification(notification.identifier)
}}
className="shrink-0 px-1 text-text-dim transition-colors hover:text-text-secondary"
>
×
</button>
</div>
))
)}
</div>
</div>
)
}
export function useUnviewedNotificationCount(): number {
const notifications = useNotificationStore((state) => state.notifications)
const lastViewedAt = useNotificationStore((state) => state.lastViewedAt)
if (!lastViewedAt) return notifications.length
return notifications.filter(
(notification) => notification.timestamp > lastViewedAt
).length
}
|