This commit is contained in:
phaichayon
2026-06-26 15:27:05 +07:00
parent 2ad57f0ad9
commit 2ace5dbb83
26 changed files with 7463 additions and 240 deletions

View File

@@ -1,39 +1,47 @@
'use client';
import { Icons } from '@/components/icons';
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 { NotificationCard } from '@/components/ui/notification-card';
import { useNotificationStore } from '../utils/store';
import { useRouter } from 'next/navigation';
import {
markAllNotificationsReadMutation,
markNotificationReadMutation
} from '../api/mutations';
import { notificationsQueryOptions } from '../api/queries';
const MAX_VISIBLE = 5;
const actionRoutes: Record<string, string> = {
view: '/dashboard/workspaces',
'view-product': '/dashboard/product',
billing: '/dashboard/billing',
open: '/dashboard/kanban',
'open-chat': '/dashboard/chat'
};
function buildNotificationActions(linkUrl: string | null): NotificationAction[] {
if (!linkUrl) {
return [];
}
return [{ id: `open:${linkUrl}`, label: 'Open', type: 'redirect', style: 'primary' }];
}
export function NotificationCenter() {
const { notifications, markAsRead, markAllAsRead, unreadCount } = useNotificationStore();
const router = useRouter();
const count = unreadCount();
const visibleNotifications = notifications.slice(0, MAX_VISIBLE);
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' />
{count > 0 && (
{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'>
{count > 9 ? '9+' : count}
{unreadCount > 9 ? '9+' : unreadCount}
</span>
)}
<span className='sr-only'>Notifications</span>
@@ -46,17 +54,18 @@ export function NotificationCenter() {
<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'>
{count > 0 && (
{unreadCount > 0 && (
<span className='bg-muted text-muted-foreground rounded-full px-2 py-0.5 text-xs'>
{count} new
{unreadCount} new
</span>
)}
{count > 0 && (
{unreadCount > 0 && (
<Button
variant='ghost'
size='sm'
className='text-muted-foreground h-auto px-2 py-1 text-xs'
onClick={markAllAsRead}
disabled={markAllRead.isPending}
onClick={() => markAllRead.mutate()}
>
Mark all as read
</Button>
@@ -65,14 +74,25 @@ export function NotificationCenter() {
</div>
<Separator />
<ScrollArea className='h-[400px]'>
{notifications.length === 0 ? (
{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'>
{visibleNotifications.map((notification) => (
{notifications.map((notification) => (
<NotificationCard
key={notification.id}
id={notification.id}
@@ -80,13 +100,12 @@ export function NotificationCenter() {
body={notification.body}
status={notification.status}
createdAt={notification.createdAt}
actions={notification.actions}
onMarkAsRead={markAsRead}
onAction={(notifId, actionId) => {
const route = actionRoutes[actionId];
if (route) {
markAsRead(notifId);
router.push(route);
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:', ''));
}
}}
/>