notifications

This commit is contained in:
alma 2025-05-04 12:06:35 +02:00
parent decc6cf778
commit 5c9f68b080
2 changed files with 3 additions and 199 deletions

View File

@ -1,186 +0,0 @@
"use client";
import { useEffect, useState } from 'react';
import { useNotifications } from '@/hooks/use-notifications';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Notification } from '@/lib/types/notification';
import { Badge } from '@/components/ui/badge';
import { formatDistanceToNow } from 'date-fns';
import { Bell, Check, CheckCheck, ExternalLink, Trash2 } from 'lucide-react';
import Link from 'next/link';
// Source icon mapping
const SourceIcons: Record<string, React.ReactNode> = {
leantime: <div className="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center text-blue-600">L</div>,
nextcloud: <div className="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center text-blue-600">N</div>,
gitea: <div className="w-8 h-8 rounded-full bg-green-100 flex items-center justify-center text-green-600">G</div>,
dolibarr: <div className="w-8 h-8 rounded-full bg-purple-100 flex items-center justify-center text-purple-600">D</div>,
moodle: <div className="w-8 h-8 rounded-full bg-orange-100 flex items-center justify-center text-orange-600">M</div>,
};
export default function NotificationsPage() {
const { notifications, notificationCount, loading, error, fetchNotifications, markAsRead, markAllAsRead } = useNotifications();
const [activeTab, setActiveTab] = useState('all');
// Filter notifications based on active tab
const filteredNotifications = activeTab === 'all'
? notifications
: notifications.filter(notification => notification.source === activeTab);
// Group notifications by source for counts
const sourceCounts = Object.entries(notificationCount.sources).map(([source, count]) => ({
source,
count: count.total,
unread: count.unread
}));
const handleMarkAsRead = async (notification: Notification) => {
if (!notification.isRead) {
await markAsRead(notification.id);
}
};
const handleMarkAllAsRead = async () => {
await markAllAsRead();
};
return (
<div className="container mx-auto py-6">
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-2xl font-bold tracking-tight">Notifications</h1>
<p className="text-muted-foreground">
Manage your notifications from all connected services
</p>
</div>
<Button variant="outline" onClick={handleMarkAllAsRead} disabled={notificationCount.unread === 0}>
<CheckCheck className="mr-2 h-4 w-4" />
Mark all as read
</Button>
</div>
<Tabs defaultValue="all" value={activeTab} onValueChange={setActiveTab}>
<div className="flex justify-between items-center mb-4">
<TabsList>
<TabsTrigger value="all" className="relative">
All
{notificationCount.unread > 0 && (
<Badge variant="secondary" className="ml-2">
{notificationCount.unread}
</Badge>
)}
</TabsTrigger>
{sourceCounts.map(({ source, unread }) => (
<TabsTrigger key={source} value={source} className="relative capitalize">
{source}
{unread > 0 && (
<Badge variant="secondary" className="ml-2">
{unread}
</Badge>
)}
</TabsTrigger>
))}
</TabsList>
</div>
<TabsContent value={activeTab} className="space-y-4">
{loading ? (
<Card>
<CardContent className="py-10 text-center">
<div className="flex flex-col items-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mb-2"></div>
<p>Loading notifications...</p>
</div>
</CardContent>
</Card>
) : error ? (
<Card>
<CardContent className="py-10 text-center">
<div className="flex flex-col items-center">
<p className="text-red-500">{error}</p>
<Button
variant="outline"
onClick={() => fetchNotifications()}
className="mt-4"
>
Retry
</Button>
</div>
</CardContent>
</Card>
) : filteredNotifications.length === 0 ? (
<Card>
<CardContent className="py-10 text-center">
<div className="flex flex-col items-center">
<Bell className="h-12 w-12 text-gray-400 mb-4" />
<h3 className="font-medium text-lg">No notifications</h3>
<p className="text-muted-foreground">
You don't have any {activeTab !== 'all' ? `${activeTab} ` : ''}notifications right now.
</p>
</div>
</CardContent>
</Card>
) : (
filteredNotifications.map((notification) => (
<Card
key={notification.id}
className={notification.isRead ? 'bg-white' : 'bg-blue-50 border-blue-200'}
>
<CardContent className="p-4">
<div className="flex">
<div className="mr-4 mt-1">
{SourceIcons[notification.source] || <Bell className="h-8 w-8" />}
</div>
<div className="flex-1">
<div className="flex items-start justify-between">
<div>
<h3 className="font-medium">
{notification.title}
{!notification.isRead && (
<Badge className="ml-2 bg-blue-500">New</Badge>
)}
</h3>
<p className="text-sm text-muted-foreground capitalize">
From {notification.source} {formatDistanceToNow(new Date(notification.timestamp), { addSuffix: true })}
</p>
</div>
<div className="flex space-x-2 ml-4">
{!notification.isRead && (
<Button
size="sm"
variant="ghost"
onClick={() => handleMarkAsRead(notification)}
title="Mark as read"
>
<Check className="h-4 w-4" />
</Button>
)}
{notification.link && (
<Link href={notification.link} target="_blank">
<Button
size="sm"
variant="ghost"
title="Open link"
>
<ExternalLink className="h-4 w-4" />
</Button>
</Link>
)}
</div>
</div>
<p className="mt-2">{notification.message}</p>
</div>
</div>
</CardContent>
</Card>
))
)}
</TabsContent>
</Tabs>
</div>
);
}

View File

@ -34,8 +34,8 @@ export const NotificationBadge = memo(function NotificationBadge({ className }:
setIsOpen(false);
};
// Take the latest 5 notifications for the dropdown
const recentNotifications = notifications.slice(0, 5);
// Take the latest 10 notifications for the dropdown
const recentNotifications = notifications.slice(0, 10);
return (
<div className={`relative ${className || ''}`}>
@ -55,7 +55,7 @@ export const NotificationBadge = memo(function NotificationBadge({ className }:
<span className="sr-only">Notifications</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-80">
<DropdownMenuContent align="end" className="w-80 max-h-[80vh] overflow-y-auto">
<div className="flex items-center justify-between p-4">
<h3 className="font-medium">Notifications</h3>
{notificationCount.unread > 0 && (
@ -116,16 +116,6 @@ export const NotificationBadge = memo(function NotificationBadge({ className }:
))}
</>
)}
<DropdownMenuSeparator />
<div className="p-2 text-center">
<Link
href="/notifications"
className="text-xs text-muted-foreground hover:text-foreground"
>
View all notifications
</Link>
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>