summaryrefslogtreecommitdiff
path: root/apps/web/app/reader/_components/notification-panel.tsx
blob: 45746b4e41c763dd03f81c48cff61e11b89bd878 (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
130
131
"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) {
      const target = event.target as Node
      if (
        panelReference.current &&
        !panelReference.current.contains(target) &&
        !(target instanceof HTMLElement && target.closest("[data-notification-toggle]"))
      ) {
        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-[9998] w-80 max-w-[calc(100vw-1rem)] border border-border bg-background-secondary shadow-lg"
    >
      <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
}