187 lines
7.6 KiB
TypeScript
187 lines
7.6 KiB
TypeScript
"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>
|
|
);
|
|
}
|
|
|