feat: add notifications support
This commit is contained in:
parent
1a9a299c9c
commit
511acbca62
12 changed files with 483 additions and 33 deletions
|
@ -45,6 +45,12 @@ enum notification_type {
|
||||||
CALENDAR_SYNC_ERROR
|
CALENDAR_SYNC_ERROR
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum entity_type {
|
||||||
|
USER
|
||||||
|
MEETING
|
||||||
|
GROUP
|
||||||
|
}
|
||||||
|
|
||||||
enum group_member_role {
|
enum group_member_role {
|
||||||
ADMIN
|
ADMIN
|
||||||
MEMBER
|
MEMBER
|
||||||
|
@ -253,8 +259,8 @@ model Notification {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
user_id String
|
user_id String
|
||||||
type notification_type
|
type notification_type
|
||||||
related_entity_type String?
|
related_entity_type entity_type
|
||||||
related_entity_id String?
|
related_entity_id String
|
||||||
message String
|
message String
|
||||||
is_read Boolean @default(false)
|
is_read Boolean @default(false)
|
||||||
created_at DateTime @default(now())
|
created_at DateTime @default(now())
|
||||||
|
|
|
@ -129,6 +129,17 @@ export const DELETE = auth(async (req, { params }) => {
|
||||||
{ participants: { some: { user_id: dbUser.id } } },
|
{ participants: { some: { user_id: dbUser.id } } },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
include: {
|
||||||
|
participants: {
|
||||||
|
select: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!event)
|
if (!event)
|
||||||
|
@ -151,6 +162,18 @@ export const DELETE = auth(async (req, { params }) => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const participant of event.participants) {
|
||||||
|
await prisma.notification.create({
|
||||||
|
data: {
|
||||||
|
user_id: participant.user.id,
|
||||||
|
type: 'MEETING_CANCEL',
|
||||||
|
related_entity_id: eventID,
|
||||||
|
related_entity_type: 'MEETING',
|
||||||
|
message: `The event "${event.title}" has been cancelled by the organizer.`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return returnZodTypeCheckedResponse(
|
return returnZodTypeCheckedResponse(
|
||||||
SuccessResponseSchema,
|
SuccessResponseSchema,
|
||||||
{ success: true, message: 'Event deleted successfully' },
|
{ success: true, message: 'Event deleted successfully' },
|
||||||
|
@ -190,6 +213,17 @@ export const PATCH = auth(async (req, { params }) => {
|
||||||
{ participants: { some: { user_id: dbUser.id } } },
|
{ participants: { some: { user_id: dbUser.id } } },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
include: {
|
||||||
|
participants: {
|
||||||
|
select: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!event)
|
if (!event)
|
||||||
|
@ -244,6 +278,31 @@ export const PATCH = auth(async (req, { params }) => {
|
||||||
},
|
},
|
||||||
update: {},
|
update: {},
|
||||||
});
|
});
|
||||||
|
if (event.participants.some((p) => p.user.id === participant)) {
|
||||||
|
await prisma.notification.create({
|
||||||
|
data: {
|
||||||
|
user_id: participant,
|
||||||
|
type: 'MEETING_UPDATE',
|
||||||
|
related_entity_id: eventID,
|
||||||
|
related_entity_type: 'MEETING',
|
||||||
|
message: `The event "${event.title}" has been updated.`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const participant of event.participants) {
|
||||||
|
if (participants && !participants.includes(participant.user.id)) {
|
||||||
|
await prisma.notification.create({
|
||||||
|
data: {
|
||||||
|
user_id: participant.user.id,
|
||||||
|
type: 'MEETING_CANCEL',
|
||||||
|
related_entity_id: eventID,
|
||||||
|
related_entity_type: 'MEETING',
|
||||||
|
message: `You have been removed from the event "${event.title}".`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedEvent = await prisma.meeting.update({
|
const updatedEvent = await prisma.meeting.update({
|
||||||
|
|
|
@ -167,6 +167,18 @@ export const POST = auth(async (req) => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const participant of newEvent.participants) {
|
||||||
|
await prisma.notification.create({
|
||||||
|
data: {
|
||||||
|
user_id: participant.user.id,
|
||||||
|
type: 'MEETING_INVITE',
|
||||||
|
related_entity_type: 'MEETING',
|
||||||
|
related_entity_id: newEvent.id,
|
||||||
|
message: `You have been invited to the meeting "${newEvent.title}" by ${newEvent.organizer.first_name} ${newEvent.organizer.last_name}.`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return returnZodTypeCheckedResponse(
|
return returnZodTypeCheckedResponse(
|
||||||
EventResponseSchema,
|
EventResponseSchema,
|
||||||
{
|
{
|
||||||
|
|
77
src/app/api/notifications/[notification]/route.ts
Normal file
77
src/app/api/notifications/[notification]/route.ts
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import { auth } from '@/auth';
|
||||||
|
import { prisma } from '@/prisma';
|
||||||
|
import {
|
||||||
|
getNotificationResponseSchema,
|
||||||
|
updateNotificationRequestSchema,
|
||||||
|
} from '../validation';
|
||||||
|
import {
|
||||||
|
returnZodTypeCheckedResponse,
|
||||||
|
userAuthenticated,
|
||||||
|
} from '@/lib/apiHelpers';
|
||||||
|
import {
|
||||||
|
ErrorResponseSchema,
|
||||||
|
ZodErrorResponseSchema,
|
||||||
|
} from '@/app/api/validation';
|
||||||
|
|
||||||
|
export const PATCH = auth(async function GET(req, { params }: { params: Promise<{ notification: string }> }) {
|
||||||
|
const authCheck = userAuthenticated(req);
|
||||||
|
if (!authCheck.continue)
|
||||||
|
return returnZodTypeCheckedResponse(
|
||||||
|
ErrorResponseSchema,
|
||||||
|
authCheck.response,
|
||||||
|
authCheck.metadata,
|
||||||
|
);
|
||||||
|
|
||||||
|
const userId = authCheck.user.id;
|
||||||
|
|
||||||
|
const notificationId = (await params).notification;
|
||||||
|
|
||||||
|
const dataRaw = await req.json();
|
||||||
|
const data = await updateNotificationRequestSchema.safeParseAsync(dataRaw);
|
||||||
|
if (!data.success)
|
||||||
|
return returnZodTypeCheckedResponse(
|
||||||
|
ZodErrorResponseSchema,
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
message: 'Invalid request data',
|
||||||
|
errors: data.error.issues,
|
||||||
|
},
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
const { is_read } = data.data;
|
||||||
|
|
||||||
|
const notification = await prisma.notification.update({
|
||||||
|
where: {
|
||||||
|
id: notificationId,
|
||||||
|
user_id: userId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
is_read,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
type: true,
|
||||||
|
related_entity_id: true,
|
||||||
|
related_entity_type: true,
|
||||||
|
message: true,
|
||||||
|
is_read: true,
|
||||||
|
created_at: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!notification)
|
||||||
|
return returnZodTypeCheckedResponse(
|
||||||
|
ErrorResponseSchema,
|
||||||
|
{ success: false, message: 'Notification not found or you do not have permission to update it' },
|
||||||
|
{ status: 404 },
|
||||||
|
);
|
||||||
|
|
||||||
|
return returnZodTypeCheckedResponse(
|
||||||
|
getNotificationResponseSchema,
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
notification,
|
||||||
|
},
|
||||||
|
{ status: 200 },
|
||||||
|
);
|
||||||
|
});
|
44
src/app/api/notifications/[notification]/swagger.ts
Normal file
44
src/app/api/notifications/[notification]/swagger.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi';
|
||||||
|
import {
|
||||||
|
invalidRequestDataResponse,
|
||||||
|
notAuthenticatedResponse,
|
||||||
|
serverReturnedDataValidationErrorResponse,
|
||||||
|
userNotFoundResponse,
|
||||||
|
} from '@/lib/defaultApiResponses';
|
||||||
|
import { getNotificationResponseSchema, updateNotificationRequestSchema } from '../validation';
|
||||||
|
import zod from 'zod/v4';
|
||||||
|
import { NotificationIdSchema } from '../../validation';
|
||||||
|
|
||||||
|
export default function registerSwaggerPaths(registry: OpenAPIRegistry) {
|
||||||
|
registry.registerPath({
|
||||||
|
method: 'patch',
|
||||||
|
path: '/api/notifications/{notification}',
|
||||||
|
request: {
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: updateNotificationRequestSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params: zod.object({
|
||||||
|
notification: NotificationIdSchema,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'Notification updated successfully',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: getNotificationResponseSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...invalidRequestDataResponse,
|
||||||
|
...notAuthenticatedResponse,
|
||||||
|
...userNotFoundResponse,
|
||||||
|
...serverReturnedDataValidationErrorResponse,
|
||||||
|
},
|
||||||
|
tags: ['Notifications'],
|
||||||
|
});
|
||||||
|
}
|
80
src/app/api/notifications/route.ts
Normal file
80
src/app/api/notifications/route.ts
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import { auth } from '@/auth';
|
||||||
|
import { prisma } from '@/prisma';
|
||||||
|
import {
|
||||||
|
getNotificationsRequestSchema,
|
||||||
|
getNotificationsResponseSchema,
|
||||||
|
} from './validation';
|
||||||
|
import {
|
||||||
|
returnZodTypeCheckedResponse,
|
||||||
|
userAuthenticated,
|
||||||
|
} from '@/lib/apiHelpers';
|
||||||
|
import {
|
||||||
|
ErrorResponseSchema,
|
||||||
|
ZodErrorResponseSchema,
|
||||||
|
} from '@/app/api/validation';
|
||||||
|
|
||||||
|
export const GET = auth(async function GET(req) {
|
||||||
|
const authCheck = userAuthenticated(req);
|
||||||
|
if (!authCheck.continue)
|
||||||
|
return returnZodTypeCheckedResponse(
|
||||||
|
ErrorResponseSchema,
|
||||||
|
authCheck.response,
|
||||||
|
authCheck.metadata,
|
||||||
|
);
|
||||||
|
|
||||||
|
const userId = authCheck.user.id;
|
||||||
|
|
||||||
|
const dataRaw = Object.fromEntries(new URL(req.url).searchParams);
|
||||||
|
const data = await getNotificationsRequestSchema.safeParseAsync(dataRaw);
|
||||||
|
if (!data.success)
|
||||||
|
return returnZodTypeCheckedResponse(
|
||||||
|
ZodErrorResponseSchema,
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
message: 'Invalid request data',
|
||||||
|
errors: data.error.issues,
|
||||||
|
},
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
const { skip, all } = data.data;
|
||||||
|
|
||||||
|
const notifications = await prisma.notification.findMany({
|
||||||
|
where: {
|
||||||
|
user_id: userId,
|
||||||
|
created_at: all
|
||||||
|
? undefined
|
||||||
|
: { gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) },
|
||||||
|
},
|
||||||
|
orderBy: { created_at: 'desc' },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
type: true,
|
||||||
|
related_entity_id: true,
|
||||||
|
related_entity_type: true,
|
||||||
|
message: true,
|
||||||
|
is_read: true,
|
||||||
|
created_at: true,
|
||||||
|
},
|
||||||
|
take: 50,
|
||||||
|
skip,
|
||||||
|
});
|
||||||
|
|
||||||
|
const total_count = await prisma.notification.count({
|
||||||
|
where: { user_id: userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
const unread_count = await prisma.notification.count({
|
||||||
|
where: { user_id: userId, is_read: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
return returnZodTypeCheckedResponse(
|
||||||
|
getNotificationsResponseSchema,
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
notifications,
|
||||||
|
total_count,
|
||||||
|
unread_count,
|
||||||
|
},
|
||||||
|
{ status: 200 },
|
||||||
|
);
|
||||||
|
});
|
36
src/app/api/notifications/swagger.ts
Normal file
36
src/app/api/notifications/swagger.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi';
|
||||||
|
import {
|
||||||
|
getNotificationsRequestSchema,
|
||||||
|
getNotificationsResponseSchema,
|
||||||
|
} from './validation';
|
||||||
|
import {
|
||||||
|
invalidRequestDataResponse,
|
||||||
|
notAuthenticatedResponse,
|
||||||
|
serverReturnedDataValidationErrorResponse,
|
||||||
|
userNotFoundResponse,
|
||||||
|
} from '@/lib/defaultApiResponses';
|
||||||
|
|
||||||
|
export default function registerSwaggerPaths(registry: OpenAPIRegistry) {
|
||||||
|
registry.registerPath({
|
||||||
|
method: 'get',
|
||||||
|
path: '/api/notifications',
|
||||||
|
request: {
|
||||||
|
query: getNotificationsRequestSchema,
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'List of notifications for the authenticated user',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: getNotificationsResponseSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...invalidRequestDataResponse,
|
||||||
|
...notAuthenticatedResponse,
|
||||||
|
...userNotFoundResponse,
|
||||||
|
...serverReturnedDataValidationErrorResponse,
|
||||||
|
},
|
||||||
|
tags: ['Notifications'],
|
||||||
|
});
|
||||||
|
}
|
44
src/app/api/notifications/validation.ts
Normal file
44
src/app/api/notifications/validation.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import zod from 'zod/v4';
|
||||||
|
|
||||||
|
export const notificationsSchema = zod.object({
|
||||||
|
id: zod.string(),
|
||||||
|
type: zod.enum([
|
||||||
|
'FRIEND_REQUEST',
|
||||||
|
'FRIEND_ACCEPT',
|
||||||
|
'MEETING_INVITE',
|
||||||
|
'MEETING_UPDATE',
|
||||||
|
'MEETING_CANCEL',
|
||||||
|
'MEETING_REMINDER',
|
||||||
|
'GROUP_MEMBER_ADDED',
|
||||||
|
'CALENDAR_SYNC_ERROR',
|
||||||
|
]),
|
||||||
|
related_entity_id: zod.string().nullable(),
|
||||||
|
related_entity_type: zod.enum(['USER', 'MEETING', 'GROUP']),
|
||||||
|
message: zod.string().max(500),
|
||||||
|
is_read: zod.boolean(),
|
||||||
|
created_at: zod.date(),
|
||||||
|
}).openapi('Notification');
|
||||||
|
|
||||||
|
export const getNotificationsResponseSchema = zod.object({
|
||||||
|
success: zod.boolean(),
|
||||||
|
notifications: zod.array(notificationsSchema),
|
||||||
|
total_count: zod.number(),
|
||||||
|
unread_count: zod.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getNotificationResponseSchema = zod.object({
|
||||||
|
success: zod.boolean(),
|
||||||
|
notification: notificationsSchema,
|
||||||
|
}).openapi('GetNotificationResponse');
|
||||||
|
|
||||||
|
export const getNotificationsRequestSchema = zod.object({
|
||||||
|
skip: zod.string().transform((val) => {
|
||||||
|
const num = parseInt(val, 10);
|
||||||
|
return isNaN(num) ? 0 : num;
|
||||||
|
}).default(0),
|
||||||
|
all: zod.boolean().default(false),
|
||||||
|
}).openapi('GetNotificationsRequest');
|
||||||
|
|
||||||
|
export const updateNotificationRequestSchema = zod.object({
|
||||||
|
is_read: zod.boolean(),
|
||||||
|
}).openapi('UpdateNotificationRequest');
|
|
@ -85,3 +85,14 @@ export const EventIdParamSchema = registry.registerParameter(
|
||||||
example: '67890',
|
example: '67890',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const NotificationIdSchema = registry.registerParameter(
|
||||||
|
'NotificationIdParam',
|
||||||
|
zod.string().openapi({
|
||||||
|
param: {
|
||||||
|
name: 'notification',
|
||||||
|
in: 'path',
|
||||||
|
},
|
||||||
|
example: '12345',
|
||||||
|
}),
|
||||||
|
);
|
|
@ -8,24 +8,28 @@ import { NDot, NotificationDot } from '@/components/misc/notification-dot';
|
||||||
|
|
||||||
export function NotificationButton({
|
export function NotificationButton({
|
||||||
dotVariant,
|
dotVariant,
|
||||||
|
icon,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
dotVariant?: NDot;
|
dotVariant?: NDot;
|
||||||
children: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
|
children?: React.ReactNode;
|
||||||
} & React.ComponentProps<typeof Button>) {
|
} & React.ComponentProps<typeof Button>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button type='button' variant='outline_primary' {...props}>
|
<Button type='button' variant='outline_primary' {...props}>
|
||||||
{children}
|
{icon}
|
||||||
<NotificationDot
|
<NotificationDot
|
||||||
dotVariant={dotVariant}
|
dotVariant={dotVariant}
|
||||||
className='absolute ml-[30px] mt-[30px]'
|
className='absolute ml-[30px] mt-[30px]'
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align='end'></DropdownMenuContent>
|
<DropdownMenuContent align='end'>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,34 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
import { SidebarTrigger } from '@/components/custom-ui/sidebar';
|
import { SidebarTrigger } from '@/components/custom-ui/sidebar';
|
||||||
import { ThemePicker } from '@/components/misc/theme-picker';
|
import { ThemePicker } from '@/components/misc/theme-picker';
|
||||||
import { NotificationButton } from '@/components/buttons/notification-button';
|
import { NotificationButton } from '@/components/buttons/notification-button';
|
||||||
|
|
||||||
import { BellRing, Inbox } from 'lucide-react';
|
import { BellRing, Inbox } from 'lucide-react';
|
||||||
import UserDropdown from '@/components/misc/user-dropdown';
|
import UserDropdown from '@/components/misc/user-dropdown';
|
||||||
|
import {
|
||||||
const items = [
|
useGetApiNotifications,
|
||||||
{
|
usePatchApiNotificationsNotification,
|
||||||
title: 'Calendar',
|
} from '@/generated/api/notifications/notifications';
|
||||||
url: '#',
|
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
||||||
icon: Inbox,
|
import { cn } from '@/lib/utils';
|
||||||
},
|
import { Button } from '@/components/ui/button';
|
||||||
{
|
import { useRouter } from 'next/navigation';
|
||||||
title: 'Friends',
|
|
||||||
url: '#',
|
|
||||||
icon: BellRing,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function Header({
|
export default function Header({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
|
const router = useRouter();
|
||||||
|
const {
|
||||||
|
data: notifications,
|
||||||
|
isLoading: notificationsLoading,
|
||||||
|
refetch,
|
||||||
|
} = useGetApiNotifications();
|
||||||
|
|
||||||
|
const markNotificationAsRead = usePatchApiNotificationsNotification();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full grid grid-rows-[50px_1fr] h-screen'>
|
<div className='w-full grid grid-rows-[50px_1fr] h-screen'>
|
||||||
<header className='border-b-1 grid-cols-[1fr_3fr_1fr] grid items-center px-2 shadow-md'>
|
<header className='border-b-1 grid-cols-[1fr_3fr_1fr] grid items-center px-2 shadow-md'>
|
||||||
|
@ -32,16 +38,85 @@ export default function Header({
|
||||||
<span className='flex justify-center'>Search</span>
|
<span className='flex justify-center'>Search</span>
|
||||||
<span className='flex gap-1 justify-end'>
|
<span className='flex gap-1 justify-end'>
|
||||||
<ThemePicker />
|
<ThemePicker />
|
||||||
{items.map((item) => (
|
|
||||||
<NotificationButton
|
<NotificationButton
|
||||||
key={item.title}
|
variant='outline_primary'
|
||||||
|
dotVariant={
|
||||||
|
!notificationsLoading && notifications?.data.unread_count
|
||||||
|
? 'active'
|
||||||
|
: notifications?.data.notifications.length
|
||||||
|
? 'neutral'
|
||||||
|
: 'hidden'
|
||||||
|
}
|
||||||
|
size='icon'
|
||||||
|
icon={<Inbox />}
|
||||||
|
>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
{notificationsLoading
|
||||||
|
? 'Loading...'
|
||||||
|
: `${notifications?.data.unread_count} unread Notifications`}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{notifications?.data.notifications.map((notification) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={notification.id}
|
||||||
|
className={cn('grid grid-rows-[auto-auto-auto] gap-2', {
|
||||||
|
'font-bold': !notification.is_read,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<p className='text-sm'>{notification.message}</p>
|
||||||
|
<p className='text-xs text-gray-500'>
|
||||||
|
{new Date(notification.created_at).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
<div className='grid auto-cols-fr grid-flow-col gap-2'>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
markNotificationAsRead.mutate(
|
||||||
|
{
|
||||||
|
notification: notification.id,
|
||||||
|
data: {
|
||||||
|
is_read: !notification.is_read,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
refetch();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
variant={
|
||||||
|
notification.is_read ? 'outline_primary' : 'primary'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{notification.is_read ? 'Read' : 'Mark as Read'}
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" onClick={() => {
|
||||||
|
switch (notification.related_entity_type) {
|
||||||
|
case "MEETING":
|
||||||
|
router.push(`/events/${notification.related_entity_id}`);
|
||||||
|
break;
|
||||||
|
case "USER":
|
||||||
|
router.push(`/users/${notification.related_entity_id}`);
|
||||||
|
break;
|
||||||
|
case "GROUP":
|
||||||
|
router.push(`/groups/${notification.related_entity_id}`);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn('Unknown notification type:', notification.related_entity_type);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
View Details
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</NotificationButton>
|
||||||
|
<NotificationButton
|
||||||
variant='outline_primary'
|
variant='outline_primary'
|
||||||
dotVariant='hidden'
|
dotVariant='hidden'
|
||||||
size='icon'
|
size='icon'
|
||||||
>
|
icon={<BellRing />}
|
||||||
<item.icon />
|
></NotificationButton>
|
||||||
</NotificationButton>
|
|
||||||
))}
|
|
||||||
<UserDropdown />
|
<UserDropdown />
|
||||||
</span>
|
</span>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
@ -41,11 +41,13 @@ export default function UserDropdown() {
|
||||||
<UserCard />
|
<UserCard />
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
<Link href='/settings'>
|
||||||
<DropdownMenuItem>Settings</DropdownMenuItem>
|
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem>
|
</Link>
|
||||||
<Link href='/logout'>Logout</Link>
|
<Link href='/logout'>
|
||||||
</DropdownMenuItem>
|
<DropdownMenuItem>Logout</DropdownMenuItem>
|
||||||
|
</Link>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue