summaryrefslogtreecommitdiff
path: root/apps/web/app/reader/_components/notification-panel.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/app/reader/_components/notification-panel.tsx')
-rw-r--r--apps/web/app/reader/_components/notification-panel.tsx129
1 files changed, 129 insertions, 0 deletions
diff --git a/apps/web/app/reader/_components/notification-panel.tsx b/apps/web/app/reader/_components/notification-panel.tsx
new file mode 100644
index 0000000..216741f
--- /dev/null
+++ b/apps/web/app/reader/_components/notification-panel.tsx
@@ -0,0 +1,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"
+ >
+ &times;
+ </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
+}