22import React , { useState } from 'react' ;
33import { Link , useNavigate , useLocation } from 'react-router-dom' ;
44import { useAuth } from '../../context/AuthContext' ;
5+ import { useNotifications } from '../../context/NotificationContext' ;
6+ import { formatDistanceToNow , parseISO } from 'date-fns' ;
57
68const DashboardIcon = ( ) => (
79 < svg className = "w-5 h-5" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
@@ -77,12 +79,12 @@ const Sidebar: React.FC<{ isOpen: boolean; onClose: () => void }> = ({ isOpen, o
7779 < >
7880 { /* Mobile overlay */ }
7981 { isOpen && (
80- < div
82+ < div
8183 className = "fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden"
8284 onClick = { onClose }
8385 />
8486 ) }
85-
87+
8688 { /* Sidebar */ }
8789 < div className = { `
8890 fixed lg:static inset-y-0 left-0 z-50 w-64
@@ -94,13 +96,13 @@ const Sidebar: React.FC<{ isOpen: boolean; onClose: () => void }> = ({ isOpen, o
9496 < div className = "flex flex-col h-full" >
9597 { /* Logo */ }
9698 < div className = "flex-shrink-0 p-6 border-b border-slate-700/50" >
97- < Link
98- to = "/dashboard"
99+ < Link
100+ to = "/dashboard"
99101 className = "flex items-center space-x-3 group"
100102 onClick = { ( ) => window . innerWidth < 1024 && onClose ( ) }
101103 >
102- < div className = "w-10 h-10 bg-linear-to-r from-blue-500 to-purple-600 rounded-xl flex items-center justify-center transform group-hover:scale-110 transition-transform duration-200 " >
103- < span className = "text-white font-bold text-lg" > CE </ span >
104+ < div className = "w-8 h-8 sm:w-10 sm:h-10 rounded-lg flex items-center justify-center" >
105+ < img src = "/open-container-engine-logo.png" alt = "Open Container Engine" className = "w-full h-full object-contain rounded-md" / >
104106 </ div >
105107 < div >
106108 < div className = "text-xl font-bold text-white group-hover:text-blue-300 transition-colors" >
@@ -116,16 +118,16 @@ const Sidebar: React.FC<{ isOpen: boolean; onClose: () => void }> = ({ isOpen, o
116118 { navItems . map ( ( item ) => {
117119 const Icon = item . icon ;
118120 const active = isActive ( item . path ) ;
119-
121+
120122 return (
121123 < Link
122124 key = { item . path }
123125 to = { item . path }
124126 onClick = { ( ) => window . innerWidth < 1024 && onClose ( ) }
125127 className = { `
126128 flex items-center space-x-3 px-4 py-3 rounded-xl transition-all duration-200
127- ${ active
128- ? 'bg-linear-to-r from-blue-500 to-purple-600 text-white shadow-lg transform scale-[1.02]'
129+ ${ active
130+ ? 'bg-linear-to-r from-blue-500 to-purple-600 text-white shadow-lg transform scale-[1.02]'
129131 : 'text-slate-300 hover:text-white hover:bg-slate-700/50'
130132 }
131133 group
@@ -168,8 +170,24 @@ const Sidebar: React.FC<{ isOpen: boolean; onClose: () => void }> = ({ isOpen, o
168170
169171const Header : React . FC < { onMenuClick : ( ) => void } > = ( { onMenuClick } ) => {
170172 const { user } = useAuth ( ) ;
173+ const { notifications, unreadCount, markNotificationAsRead } = useNotifications ( ) ;
171174 const [ showNotifications , setShowNotifications ] = useState ( false ) ;
172175 const [ showProfile , setShowProfile ] = useState ( false ) ;
176+ const navigate = useNavigate ( ) ;
177+
178+ const handleNotificationClick = ( notification : any ) => {
179+ // Mark as read
180+ markNotificationAsRead ( notification . id ) ;
181+
182+ // Close notification dropdown
183+ setShowNotifications ( false ) ;
184+
185+ // Navigate to deployment detail if deployment_id exists
186+ const deploymentId = notification . data . data ?. deployment_id ;
187+ if ( deploymentId ) {
188+ navigate ( `/deployments/${ deploymentId } ` ) ;
189+ }
190+ } ;
173191
174192 return (
175193 < header className = "bg-white/80 backdrop-blur-md shadow-sm border-b border-slate-200 sticky top-0 z-30" >
@@ -183,17 +201,17 @@ const Header: React.FC<{ onMenuClick: () => void }> = ({ onMenuClick }) => {
183201 >
184202 < MenuIcon />
185203 </ button >
186-
204+
187205 < div className = "min-w-0" >
188206 < h1 className = "text-base sm:text-lg lg:text-xl font-bold text-slate-800 truncate" >
189207 Welcome back, { user ?. username || 'User' } ! 👋
190208 </ h1 >
191209 < p className = "text-xs sm:text-sm text-slate-500 hidden sm:block" >
192- { new Date ( ) . toLocaleDateString ( 'vi-VN' , {
193- weekday : 'long' ,
194- year : 'numeric' ,
195- month : 'long' ,
196- day : 'numeric'
210+ { new Date ( ) . toLocaleDateString ( 'vi-VN' , {
211+ weekday : 'long' ,
212+ year : 'numeric' ,
213+ month : 'long' ,
214+ day : 'numeric'
197215 } ) }
198216 </ p >
199217 </ div >
@@ -220,23 +238,65 @@ const Header: React.FC<{ onMenuClick: () => void }> = ({ onMenuClick }) => {
220238 className = "relative p-2 rounded-lg hover:bg-slate-100 transition-colors"
221239 >
222240 < BellIcon />
223- < span className = "absolute -top-1 -right-1 w-4 h-4 sm:w-5 sm:h-5 bg-red-500 text-white text-xs rounded-full flex items-center justify-center" >
224- 3
225- </ span >
241+ { unreadCount > 0 && (
242+ < span className = "absolute -top-1 -right-1 w-4 h-4 sm:w-5 sm:h-5 bg-red-500 text-white text-xs rounded-full flex items-center justify-center" >
243+ { unreadCount > 99 ? '99+' : unreadCount }
244+ </ span >
245+ ) }
226246 </ button >
227-
247+
228248 { showNotifications && (
229249 < div className = "absolute right-0 mt-2 w-72 sm:w-80 bg-white rounded-xl shadow-lg border border-slate-200 py-2 z-50" >
230250 < div className = "px-4 py-2 border-b border-slate-100" >
231251 < h3 className = "font-semibold text-slate-800" > Notifications</ h3 >
252+ { unreadCount > 0 && (
253+ < button
254+ onClick = { ( ) => {
255+ notifications . forEach ( n => markNotificationAsRead ( n . id ) ) ;
256+ } }
257+ className = "text-xs text-blue-600 hover:text-blue-800"
258+ >
259+ Mark all as read
260+ </ button >
261+ ) }
232262 </ div >
233263 < div className = "max-h-64 overflow-y-auto" >
234- { [ 1 , 2 , 3 ] . map ( ( i ) => (
235- < div key = { i } className = "px-4 py-3 hover:bg-slate-50 cursor-pointer border-b border-slate-50 last:border-0" >
236- < p className = "text-sm font-medium text-slate-800" > New deployment completed</ p >
237- < p className = "text-xs text-slate-500 mt-1" > 2 minutes ago</ p >
264+ { notifications . length > 0 ? (
265+ notifications . slice ( 0 , 10 ) . map ( ( notification ) => (
266+ < div
267+ key = { notification . id }
268+ className = "px-4 py-3 hover:bg-slate-50 cursor-pointer border-b border-slate-50 last:border-0"
269+ onClick = { ( ) => handleNotificationClick ( notification ) }
270+ >
271+ < p className = "text-sm font-medium text-slate-800" >
272+ { notification . type === 'deployment_status_changed' &&
273+ `${ notification . data . data ?. app_name || 'Deployment' } status changed to ${ notification . data . data ?. status } ` }
274+ { notification . type === 'deployment_created' &&
275+ `New deployment created: ${ notification . data . data ?. app_name || 'Unknown' } ` }
276+ { notification . type === 'deployment_deleted' &&
277+ `Deployment deleted: ${ notification . data . data ?. app_name || 'Unknown' } ` }
278+ { notification . type === 'deployment_scaled' &&
279+ `${ notification . data . data ?. app_name || 'Deployment' } scaled from ${ notification . data . data ?. old_replicas } to ${ notification . data . data ?. new_replicas } replicas` }
280+ </ p >
281+ < p className = "text-xs text-slate-500 mt-1" >
282+ { formatDistanceToNow ( parseISO ( notification . timestamp ) , { addSuffix : true } ) }
283+ </ p >
284+ { notification . data . data ?. message && (
285+ < p className = "text-xs text-slate-600 mt-1" > { notification . data . data . message } </ p >
286+ ) }
287+ { notification . data . data ?. error_message && (
288+ < p className = "text-xs text-red-600 mt-1" > { notification . data . data . error_message } </ p >
289+ ) }
290+ </ div >
291+ ) )
292+ ) : (
293+ < div className = "px-4 py-6 text-center text-slate-500" >
294+ < svg className = "h-8 w-8 mx-auto mb-2 text-slate-300" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
295+ < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
296+ </ svg >
297+ < p className = "text-sm" > No notifications yet</ p >
238298 </ div >
239- ) ) }
299+ ) }
240300 </ div >
241301 </ div >
242302 ) }
@@ -286,10 +346,10 @@ const DashboardLayout: React.FC<{ children: React.ReactNode }> = ({ children })
286346 return (
287347 < div className = "flex min-h-screen bg-linear-to-br from-slate-50 to-slate-100" >
288348 < Sidebar isOpen = { sidebarOpen } onClose = { ( ) => setSidebarOpen ( false ) } />
289-
349+
290350 < div className = "flex-1 flex flex-col lg:ml-0" >
291351 < Header onMenuClick = { ( ) => setSidebarOpen ( ! sidebarOpen ) } />
292-
352+
293353 < main className = "flex-1 overflow-auto" >
294354 < div className = "p-4 sm:p-6" >
295355 { children }
0 commit comments