120 lines
5.0 KiB
TypeScript
120 lines
5.0 KiB
TypeScript
'use client';
|
|
|
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
|
import Link from 'next/link';
|
|
import { useRouter } from 'next/navigation';
|
|
import { Icons } from '@/components/icons';
|
|
import { Button } from '@/components/ui/button';
|
|
import { NotificationCard, type NotificationAction } from '@/components/ui/notification-card';
|
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import {
|
|
markAllNotificationsReadMutation,
|
|
markNotificationReadMutation
|
|
} from '../api/mutations';
|
|
import { notificationsQueryOptions } from '../api/queries';
|
|
|
|
const MAX_VISIBLE = 5;
|
|
|
|
function buildNotificationActions(linkUrl: string | null): NotificationAction[] {
|
|
if (!linkUrl) {
|
|
return [];
|
|
}
|
|
|
|
return [{ id: `open:${linkUrl}`, label: 'Open', type: 'redirect', style: 'primary' }];
|
|
}
|
|
|
|
export function NotificationCenter() {
|
|
const router = useRouter();
|
|
const notificationsQuery = useQuery(notificationsQueryOptions({ page: 1, limit: MAX_VISIBLE }));
|
|
const markRead = useMutation(markNotificationReadMutation);
|
|
const markAllRead = useMutation(markAllNotificationsReadMutation);
|
|
|
|
const notifications = notificationsQuery.data?.items ?? [];
|
|
const unreadCount = notificationsQuery.data?.unreadCount ?? 0;
|
|
|
|
return (
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<Button variant='ghost' size='icon' className='relative h-8 w-8'>
|
|
<Icons.notification className='h-4 w-4' />
|
|
{unreadCount > 0 && (
|
|
<span className='bg-destructive text-destructive-foreground absolute -top-0.5 -right-0.5 flex h-4 min-w-4 items-center justify-center rounded-full px-1 text-[10px] font-medium'>
|
|
{unreadCount > 9 ? '9+' : unreadCount}
|
|
</span>
|
|
)}
|
|
<span className='sr-only'>Notifications</span>
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent align='end' className='w-[calc(100vw-2rem)] p-0 sm:w-[380px]' sideOffset={8}>
|
|
<div className='flex items-center justify-between px-4 py-3'>
|
|
<Link href='/dashboard/notifications' className='group flex items-center gap-1'>
|
|
<h4 className='text-sm font-semibold group-hover:underline'>Notifications</h4>
|
|
<Icons.chevronRight className='text-muted-foreground h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5' />
|
|
</Link>
|
|
<div className='flex items-center gap-2'>
|
|
{unreadCount > 0 && (
|
|
<span className='bg-muted text-muted-foreground rounded-full px-2 py-0.5 text-xs'>
|
|
{unreadCount} new
|
|
</span>
|
|
)}
|
|
{unreadCount > 0 && (
|
|
<Button
|
|
variant='ghost'
|
|
size='sm'
|
|
className='text-muted-foreground h-auto px-2 py-1 text-xs'
|
|
disabled={markAllRead.isPending}
|
|
onClick={() => markAllRead.mutate()}
|
|
>
|
|
Mark all as read
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<Separator />
|
|
<ScrollArea className='h-[400px]'>
|
|
{notificationsQuery.isError ? (
|
|
<div className='flex flex-col items-center justify-center py-12 text-center'>
|
|
<Icons.alertCircle className='text-muted-foreground/40 mb-2 h-8 w-8' />
|
|
<p className='text-muted-foreground text-sm'>Unable to load notifications</p>
|
|
</div>
|
|
) : notificationsQuery.isLoading ? (
|
|
<div className='flex flex-col gap-2 p-2'>
|
|
{Array.from({ length: 3 }).map((_, index) => (
|
|
<div key={index} className='bg-muted h-24 animate-pulse rounded-2xl' />
|
|
))}
|
|
</div>
|
|
) : notifications.length === 0 ? (
|
|
<div className='flex flex-col items-center justify-center py-12'>
|
|
<Icons.notification className='text-muted-foreground/40 mb-2 h-8 w-8' />
|
|
<p className='text-muted-foreground text-sm'>No notifications yet</p>
|
|
</div>
|
|
) : (
|
|
<div className='flex flex-col gap-1 p-2'>
|
|
{notifications.map((notification) => (
|
|
<NotificationCard
|
|
key={notification.id}
|
|
id={notification.id}
|
|
title={notification.title}
|
|
body={notification.body}
|
|
status={notification.status}
|
|
createdAt={notification.createdAt}
|
|
actions={buildNotificationActions(notification.linkUrl)}
|
|
onMarkAsRead={(id) => markRead.mutate(id)}
|
|
onAction={async (notificationId, actionId) => {
|
|
if (actionId.startsWith('open:')) {
|
|
await markRead.mutateAsync(notificationId);
|
|
router.push(actionId.replace('open:', ''));
|
|
}
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</ScrollArea>
|
|
</PopoverContent>
|
|
</Popover>
|
|
);
|
|
}
|