Spaces:
Paused
Paused
| import { useState, useEffect } from 'react'; | |
| import { | |
| Bell, BellOff, Check, CheckCheck, Trash2, Volume2, VolumeX, | |
| Monitor, AlertTriangle, AlertCircle, Info, Settings, ShieldCheck, ShieldX | |
| } from 'lucide-react'; | |
| import { Button } from '@/components/ui/button'; | |
| import { Switch } from '@/components/ui/switch'; | |
| import { | |
| Popover, | |
| PopoverContent, | |
| PopoverTrigger, | |
| } from '@/components/ui/popover'; | |
| import { | |
| Tabs, | |
| TabsContent, | |
| TabsList, | |
| TabsTrigger, | |
| } from '@/components/ui/tabs'; | |
| import { ScrollArea } from '@/components/ui/scroll-area'; | |
| import { useNotifications, NotificationSeverity, requestNotificationPermission, getNotificationPermission } from '@/contexts/NotificationContext'; | |
| import { cn } from '@/lib/utils'; | |
| const severityConfig: Record<NotificationSeverity, { icon: any; color: string }> = { | |
| critical: { icon: AlertTriangle, color: 'text-destructive' }, | |
| warning: { icon: AlertCircle, color: 'text-orange-400' }, | |
| info: { icon: Info, color: 'text-primary' }, | |
| }; | |
| const NotificationPermissionStatus = () => { | |
| const [permission, setPermission] = useState<NotificationPermission | 'unsupported'>('default'); | |
| useEffect(() => { | |
| setPermission(getNotificationPermission()); | |
| }, []); | |
| if (permission === 'unsupported') { | |
| return ( | |
| <div className="flex items-center gap-2 text-xs text-muted-foreground"> | |
| <ShieldX className="w-3 h-3" /> | |
| <span>Browser understøtter ikke notifikationer</span> | |
| </div> | |
| ); | |
| } | |
| if (permission === 'denied') { | |
| return ( | |
| <div className="flex items-center gap-2 text-xs text-destructive"> | |
| <ShieldX className="w-3 h-3" /> | |
| <span>Notifikationer blokeret - tillad i browser-indstillinger</span> | |
| </div> | |
| ); | |
| } | |
| if (permission === 'granted') { | |
| return ( | |
| <div className="flex items-center gap-2 text-xs text-green-500"> | |
| <ShieldCheck className="w-3 h-3" /> | |
| <span>Notifikationer aktiveret</span> | |
| </div> | |
| ); | |
| } | |
| return null; | |
| }; | |
| const NotificationCenter = () => { | |
| const { | |
| notifications, | |
| unreadCount, | |
| settings, | |
| markAsRead, | |
| markAllAsRead, | |
| clearAll, | |
| updateSettings, | |
| addNotification | |
| } = useNotifications(); | |
| const [activeTab, setActiveTab] = useState('notifications'); | |
| const sendTestNotification = (severity: NotificationSeverity) => { | |
| const testAlerts: Record<NotificationSeverity, { title: string; message: string }> = { | |
| critical: { | |
| title: 'KRITISK: Ransomware Detekteret', | |
| message: 'Mistænkelig krypteringsaktivitet opdaget på endpoint WKS-2847. Øjeblikkelig handling påkrævet.', | |
| }, | |
| warning: { | |
| title: 'Advarsel: Brute Force Forsøg', | |
| message: 'Flere mislykkede login-forsøg fra IP 192.168.1.105 på admin-konto.', | |
| }, | |
| info: { | |
| title: 'System Opdatering', | |
| message: 'Sikkerhedspatch KB5034441 er tilgængelig til installation.', | |
| }, | |
| }; | |
| const alert = testAlerts[severity]; | |
| addNotification({ | |
| title: alert.title, | |
| message: alert.message, | |
| severity, | |
| source: 'Test', | |
| }); | |
| }; | |
| const formatTime = (timestamp: number) => { | |
| const diff = Date.now() - timestamp; | |
| const minutes = Math.floor(diff / 60000); | |
| const hours = Math.floor(diff / 3600000); | |
| const days = Math.floor(diff / 86400000); | |
| if (minutes < 1) return 'Lige nu'; | |
| if (minutes < 60) return `${minutes}m siden`; | |
| if (hours < 24) return `${hours}t siden`; | |
| return `${days}d siden`; | |
| }; | |
| return ( | |
| <Popover> | |
| <PopoverTrigger asChild> | |
| <Button variant="ghost" size="sm" className="relative"> | |
| <Bell className="w-5 h-5" /> | |
| {unreadCount > 0 && ( | |
| <span className="absolute -top-1 -right-1 w-5 h-5 bg-destructive text-destructive-foreground text-xs rounded-full flex items-center justify-center animate-pulse"> | |
| {unreadCount > 9 ? '9+' : unreadCount} | |
| </span> | |
| )} | |
| </Button> | |
| </PopoverTrigger> | |
| <PopoverContent align="end" className="w-80 p-0 bg-card border-border"> | |
| <Tabs value={activeTab} onValueChange={setActiveTab}> | |
| <div className="flex items-center justify-between px-4 py-2 border-b border-border"> | |
| <TabsList className="grid grid-cols-2 h-8 bg-secondary/30"> | |
| <TabsTrigger value="notifications" className="text-xs"> | |
| Notifikationer | |
| </TabsTrigger> | |
| <TabsTrigger value="settings" className="text-xs"> | |
| Indstillinger | |
| </TabsTrigger> | |
| </TabsList> | |
| </div> | |
| <TabsContent value="notifications" className="m-0"> | |
| {notifications.length > 0 && ( | |
| <div className="flex items-center justify-between px-4 py-2 border-b border-border/50"> | |
| <Button variant="ghost" size="sm" onClick={markAllAsRead} className="h-7 text-xs"> | |
| <CheckCheck className="w-3 h-3 mr-1" /> | |
| Marker alle læst | |
| </Button> | |
| <Button variant="ghost" size="sm" onClick={clearAll} className="h-7 text-xs text-destructive hover:text-destructive"> | |
| <Trash2 className="w-3 h-3 mr-1" /> | |
| Ryd | |
| </Button> | |
| </div> | |
| )} | |
| <ScrollArea className="h-[300px]"> | |
| {notifications.length === 0 ? ( | |
| <div className="flex flex-col items-center justify-center h-[200px] text-muted-foreground"> | |
| <BellOff className="w-10 h-10 mb-2 opacity-50" /> | |
| <p className="text-sm">Ingen notifikationer</p> | |
| </div> | |
| ) : ( | |
| <div className="divide-y divide-border/50"> | |
| {notifications.map((notification) => { | |
| const config = severityConfig[notification.severity]; | |
| const Icon = config.icon; | |
| return ( | |
| <div | |
| key={notification.id} | |
| className={cn( | |
| "px-4 py-3 hover:bg-secondary/30 transition-colors cursor-pointer", | |
| !notification.read && "bg-primary/5" | |
| )} | |
| onClick={() => markAsRead(notification.id)} | |
| > | |
| <div className="flex items-start gap-3"> | |
| <Icon className={cn("w-4 h-4 mt-0.5 shrink-0", config.color)} /> | |
| <div className="flex-1 min-w-0"> | |
| <div className="flex items-center justify-between gap-2"> | |
| <p className="text-sm font-medium truncate">{notification.title}</p> | |
| {!notification.read && ( | |
| <span className="w-2 h-2 rounded-full bg-primary shrink-0" /> | |
| )} | |
| </div> | |
| <p className="text-xs text-muted-foreground line-clamp-2 mt-0.5"> | |
| {notification.message} | |
| </p> | |
| <p className="text-xs text-muted-foreground/60 mt-1"> | |
| {formatTime(notification.timestamp)} | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| )} | |
| </ScrollArea> | |
| </TabsContent> | |
| <TabsContent value="settings" className="m-0 p-4 space-y-4"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-2"> | |
| {settings.soundEnabled ? <Volume2 className="w-4 h-4" /> : <VolumeX className="w-4 h-4" />} | |
| <span className="text-sm">Lyd</span> | |
| </div> | |
| <Switch | |
| checked={settings.soundEnabled} | |
| onCheckedChange={(v) => updateSettings({ soundEnabled: v })} | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-2"> | |
| <Monitor className="w-4 h-4" /> | |
| <span className="text-sm">Desktop notifikationer</span> | |
| </div> | |
| <Switch | |
| checked={settings.desktopEnabled} | |
| onCheckedChange={async (v) => { | |
| if (v) { | |
| const permission = await requestNotificationPermission(); | |
| if (permission === 'granted') { | |
| updateSettings({ desktopEnabled: true }); | |
| } | |
| } else { | |
| updateSettings({ desktopEnabled: false }); | |
| } | |
| }} | |
| /> | |
| </div> | |
| <NotificationPermissionStatus /> | |
| </div> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-2"> | |
| <AlertTriangle className="w-4 h-4" /> | |
| <span className="text-sm">Kun kritiske</span> | |
| </div> | |
| <Switch | |
| checked={settings.criticalOnly} | |
| onCheckedChange={(v) => updateSettings({ criticalOnly: v })} | |
| /> | |
| </div> | |
| <div className="pt-3 border-t border-border/50 space-y-2"> | |
| <p className="text-xs text-muted-foreground mb-2">Test notifikationer</p> | |
| <div className="flex gap-2"> | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| className="flex-1 text-xs h-8 border-destructive/50 text-destructive hover:bg-destructive hover:text-destructive-foreground" | |
| onClick={() => sendTestNotification('critical')} | |
| > | |
| Kritisk | |
| </Button> | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| className="flex-1 text-xs h-8 border-orange-400/50 text-orange-400 hover:bg-orange-400 hover:text-white" | |
| onClick={() => sendTestNotification('warning')} | |
| > | |
| Advarsel | |
| </Button> | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| className="flex-1 text-xs h-8 border-primary/50 text-primary hover:bg-primary hover:text-primary-foreground" | |
| onClick={() => sendTestNotification('info')} | |
| > | |
| Info | |
| </Button> | |
| </div> | |
| </div> | |
| </TabsContent> | |
| </Tabs> | |
| </PopoverContent> | |
| </Popover> | |
| ); | |
| }; | |
| export default NotificationCenter; | |