1+ import type React from 'react' ;
2+ import { useState , useCallback } from 'react' ;
3+ import { BellIcon , SettingsIcon , CheckIcon , MailIcon , FileTextIcon , MessageSquareIcon } from 'lucide-react' ;
4+ import { Button } from "../ui/button" ;
5+ import { Badge } from "../ui/badge" ;
6+ import {
7+ DropdownMenu ,
8+ DropdownMenuContent ,
9+ DropdownMenuItem ,
10+ DropdownMenuLabel ,
11+ DropdownMenuSeparator ,
12+ DropdownMenuTrigger
13+ } from "../ui/dropdown-menu" ;
14+ import { cn } from '@/lib/utils' ;
15+
16+ // Types
17+ export interface NotificationItem {
18+ id : string ;
19+ title : string ;
20+ message : string ;
21+ timestamp : Date ;
22+ isRead : boolean ;
23+ type : 'document' | 'comment' | 'share' | 'system' ;
24+ actionUrl ?: string ;
25+ }
26+
27+ interface NotificationsProps {
28+ unreadCount ?: number ;
29+ notifications ?: NotificationItem [ ] ;
30+ onOpenSettings ?: ( ) => void ;
31+ onMarkAllAsRead ?: ( ) => void ;
32+ onNotificationClick ?: ( notification : NotificationItem ) => void ;
33+ }
34+
35+ const Notifications : React . FC < NotificationsProps > = ( {
36+ unreadCount = 0 ,
37+ notifications = [ ] ,
38+ onOpenSettings,
39+ onMarkAllAsRead,
40+ onNotificationClick
41+ } ) => {
42+ const [ isOpen , setIsOpen ] = useState ( false ) ;
43+
44+ // Format the timestamp to a relative time string
45+ const formatTime = ( date : Date ) : string => {
46+ const now = new Date ( ) ;
47+ const diffInSeconds = Math . floor ( ( now . getTime ( ) - date . getTime ( ) ) / 1000 ) ;
48+
49+ if ( diffInSeconds < 60 ) return 'Just now' ;
50+ if ( diffInSeconds < 3600 ) return `${ Math . floor ( diffInSeconds / 60 ) } m ago` ;
51+ if ( diffInSeconds < 86400 ) return `${ Math . floor ( diffInSeconds / 3600 ) } h ago` ;
52+ if ( diffInSeconds < 604800 ) return `${ Math . floor ( diffInSeconds / 86400 ) } d ago` ;
53+
54+ return date . toLocaleDateString ( ) ;
55+ } ;
56+
57+ // Get icon based on notification type
58+ const getNotificationIcon = ( type : NotificationItem [ 'type' ] ) : React . ReactNode => {
59+ switch ( type ) {
60+ case 'document' :
61+ return < FileTextIcon size = { 14 } className = "text-blue-500" /> ;
62+ case 'comment' :
63+ return < MessageSquareIcon size = { 14 } className = "text-green-500" /> ;
64+ case 'share' :
65+ return < MailIcon size = { 14 } className = "text-purple-500" /> ;
66+ default :
67+ return < BellIcon size = { 14 } className = "text-gray-500" /> ;
68+ }
69+ } ;
70+
71+ // Handle notification click
72+ const handleNotificationClick = useCallback ( ( notification : NotificationItem ) => {
73+ setIsOpen ( false ) ;
74+ onNotificationClick ?.( notification ) ;
75+ } , [ onNotificationClick ] ) ;
76+
77+ // Handle mark all as read
78+ const handleMarkAllAsRead = useCallback ( ( ) => {
79+ setIsOpen ( false ) ;
80+ onMarkAllAsRead ?.( ) ;
81+ } , [ onMarkAllAsRead ] ) ;
82+
83+ // Handle settings click
84+ const handleSettingsClick = useCallback ( ( ) => {
85+ setIsOpen ( false ) ;
86+ onOpenSettings ?.( ) ;
87+ } , [ onOpenSettings ] ) ;
88+
89+ return (
90+ < DropdownMenu open = { isOpen } onOpenChange = { setIsOpen } >
91+ < DropdownMenuTrigger asChild >
92+ < Button
93+ variant = "ghost"
94+ size = "icon"
95+ className = "p-1 h-8 w-8 rounded-full transition-all duration-200 hover:bg-primary/10 hover:text-primary relative"
96+ title = "Notifications"
97+ >
98+ < BellIcon size = { 16 } />
99+ { unreadCount > 0 && (
100+ < Badge
101+ variant = "destructive"
102+ className = "absolute -top-1 -right-1 flex items-center justify-center h-4 min-w-4 text-[10px] px-[5px] py-0 rounded-full"
103+ >
104+ { unreadCount > 99 ? '99+' : unreadCount }
105+ </ Badge >
106+ ) }
107+ </ Button >
108+ </ DropdownMenuTrigger >
109+
110+ < DropdownMenuContent align = "end" className = "w-80" >
111+ < DropdownMenuLabel className = "flex items-center justify-between" >
112+ < span > Notifications</ span >
113+ { unreadCount > 0 && (
114+ < Button
115+ variant = "ghost"
116+ size = "sm"
117+ className = "h-7 text-xs flex items-center gap-1 hover:text-primary"
118+ onClick = { handleMarkAllAsRead }
119+ >
120+ < CheckIcon size = { 12 } />
121+ Mark all as read
122+ </ Button >
123+ ) }
124+ </ DropdownMenuLabel >
125+
126+ < DropdownMenuSeparator />
127+
128+ { notifications . length === 0 ? (
129+ < div className = "py-8 px-2 flex flex-col items-center justify-center text-center" >
130+ < BellIcon size = { 32 } className = "text-muted-foreground mb-2 opacity-20" />
131+ < p className = "text-sm text-muted-foreground" > No notifications yet</ p >
132+ < p className = "text-xs text-muted-foreground/70" > We'll notify you when something important happens</ p >
133+ </ div >
134+ ) : (
135+ < div className = "max-h-[350px] overflow-y-auto py-1" >
136+ { notifications . map ( ( notification ) => (
137+ < DropdownMenuItem
138+ key = { notification . id }
139+ className = { cn (
140+ "flex flex-col items-start p-3 cursor-pointer relative transition-all duration-200" ,
141+ notification . isRead ? "opacity-80" : "bg-primary/5"
142+ ) }
143+ onClick = { ( ) => handleNotificationClick ( notification ) }
144+ >
145+ < div className = "flex w-full justify-between items-start" >
146+ < div className = "flex items-center gap-2" >
147+ < div className = { cn (
148+ "p-1 rounded-full" ,
149+ notification . isRead ? "bg-muted/70" : "bg-muted"
150+ ) } >
151+ { getNotificationIcon ( notification . type ) }
152+ </ div >
153+ < span className = { cn (
154+ "text-sm" ,
155+ notification . isRead ? "font-normal" : "font-medium"
156+ ) } >
157+ { notification . title }
158+ </ span >
159+ </ div >
160+ < span className = { cn (
161+ "text-xs text-muted-foreground ml-2 transition-all duration-200" ,
162+ notification . isRead ? "mr-0" : "mr-5"
163+ ) } >
164+ { formatTime ( notification . timestamp ) }
165+ </ span >
166+ </ div >
167+ < p className = "text-xs text-muted-foreground pl-7 pt-1" >
168+ { notification . message }
169+ </ p >
170+ { ! notification . isRead && (
171+ < div className = "w-2 h-2 rounded-full bg-primary absolute top-3.5 right-2.5" />
172+ ) }
173+ </ DropdownMenuItem >
174+ ) ) }
175+ </ div >
176+ ) }
177+
178+ < DropdownMenuSeparator />
179+
180+ < DropdownMenuItem
181+ className = "p-2 cursor-pointer"
182+ onClick = { handleSettingsClick }
183+ >
184+ < div className = "flex items-center gap-2 text-muted-foreground hover:text-foreground w-full justify-center" >
185+ < SettingsIcon size = { 14 } />
186+ < span > Notification Settings</ span >
187+ </ div >
188+ </ DropdownMenuItem >
189+ </ DropdownMenuContent >
190+ </ DropdownMenu >
191+ ) ;
192+ } ;
193+
194+ export default Notifications ;
0 commit comments