File size: 6,434 Bytes
766d85d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
import React, { useState, useEffect } from 'react'
import ReactDOM from 'react-dom'
import { useNavigate } from 'react-router-dom'
import { Bell, Trash2, CheckCheck } from 'lucide-react'
import { useTranslation } from '../../i18n'
import { useInAppNotificationStore } from '../../store/inAppNotificationStore.ts'
import { useSettingsStore } from '../../store/settingsStore'
import { useAuthStore } from '../../store/authStore'
import InAppNotificationItem from '../Notifications/InAppNotificationItem.tsx'

export default function InAppNotificationBell(): React.ReactElement {
  const { t } = useTranslation()
  const navigate = useNavigate()
  const { settings } = useSettingsStore()
  const darkMode = settings.dark_mode
  const dark = darkMode === true || darkMode === 'dark' || (darkMode === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches)

  const isAuthenticated = useAuthStore(s => s.isAuthenticated)
  const { notifications, unreadCount, isLoading, fetchNotifications, fetchUnreadCount, markAllRead, deleteAll } = useInAppNotificationStore()

  const [open, setOpen] = useState(false)

  useEffect(() => {
    if (isAuthenticated) {
      fetchUnreadCount()
    }
  }, [isAuthenticated])

  const handleOpen = () => {
    if (!open) {
      fetchNotifications(true)
    }
    setOpen(v => !v)
  }

  const handleShowAll = () => {
    setOpen(false)
    navigate('/notifications')
  }

  const displayCount = unreadCount > 99 ? '99+' : unreadCount

  return (
    <div className="relative flex-shrink-0">
      <button
        onClick={handleOpen}
        title={t('notifications.title')}
        className="relative p-2 rounded-lg transition-colors text-content-muted"
        onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'}
        onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
      >
        <Bell className="w-4 h-4" />
        {unreadCount > 0 && (
          <span
            className="absolute -top-0.5 -right-0.5 flex items-center justify-center rounded-full text-white font-bold"
            style={{
              background: '#ef4444',
              fontSize: 9,
              minWidth: 14,
              height: 14,
              padding: '0 3px',
              lineHeight: 1,
            }}
          >
            {displayCount}
          </span>
        )}
      </button>

      {open && ReactDOM.createPortal(
        <>
          <div style={{ position: 'fixed', inset: 0, zIndex: 9998 }} onClick={() => setOpen(false)} />
          <div
            className="rounded-xl shadow-xl border overflow-hidden bg-surface-card border-edge"
            style={{
              position: 'fixed',
              top: 'var(--nav-h)',
              right: 8,
              width: 360,
              maxWidth: 'calc(100vw - 16px)',
              maxHeight: 'min(480px, calc(100vh - var(--nav-h) - 16px))',
              zIndex: 9999,
              display: 'flex',
              flexDirection: 'column',
            }}
          >
            {/* Header */}
            <div
              className="flex items-center justify-between px-4 py-3 flex-shrink-0 border-b border-edge-secondary"
            >
              <span className="text-sm font-semibold text-content">
                {t('notifications.title')}
                {unreadCount > 0 && (
                  <span className="ml-2 px-1.5 py-0.5 rounded-full text-xs font-medium bg-content text-surface">
                    {unreadCount}
                  </span>
                )}
              </span>
              <div className="flex items-center gap-1">
                {unreadCount > 0 && (
                  <button
                    onClick={markAllRead}
                    title={t('notifications.markAllRead')}
                    className="p-1.5 rounded-lg transition-colors text-content-muted"
                    onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'}
                    onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
                  >
                    <CheckCheck className="w-3.5 h-3.5" />
                  </button>
                )}
                {notifications.length > 0 && (
                  <button
                    onClick={deleteAll}
                    title={t('notifications.deleteAll')}
                    className="p-1.5 rounded-lg transition-colors text-content-muted"
                    onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'}
                    onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
                  >
                    <Trash2 className="w-3.5 h-3.5" />
                  </button>
                )}
              </div>
            </div>

            {/* Notification list */}
            <div className="overflow-y-auto flex-1">
              {isLoading && notifications.length === 0 ? (
                <div className="flex items-center justify-center py-10">
                  <div className="w-5 h-5 border-2 rounded-full animate-spin border-edge border-t-content" />
                </div>
              ) : notifications.length === 0 ? (
                <div className="flex flex-col items-center justify-center py-10 px-4 text-center gap-2">
                  <Bell className="w-8 h-8 text-content-faint" />
                  <p className="text-sm font-medium text-content-muted">{t('notifications.empty')}</p>
                  <p className="text-xs text-content-faint">{t('notifications.emptyDescription')}</p>
                </div>
              ) : (
                notifications.slice(0, 10).map(n => (
                  <InAppNotificationItem key={n.id} notification={n} onClose={() => setOpen(false)} />
                ))
              )}
            </div>

            {/* Footer */}
            <button
              onClick={handleShowAll}
              className="w-full py-2.5 text-xs font-medium transition-colors flex-shrink-0 border-t border-edge-secondary text-content"
              style={{
                background: 'transparent',
              }}
              onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'}
              onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
            >
              {t('notifications.showAll')}
            </button>
          </div>
        </>,
        document.body
      )}
    </div>
  )
}