From 76431e892f4c6d897cdc17aa9f7f7ed9eb6d5063 Mon Sep 17 00:00:00 2001 From: Pritam Kundu Date: Mon, 18 Nov 2024 02:04:45 +0530 Subject: [PATCH] feat : notification routes --- client/src/app/api/notifications/route.ts | 34 ++++++ .../src/app/api/posts/[postId]/likes/route.ts | 76 +++++++++--- .../app/api/users/[userId]/followers/route.ts | 58 ++++++--- client/src/app/playground/(post)/Post.tsx | 1 - client/src/components/comments/actions.ts | 34 ++++-- client/src/components/ui/card.tsx | 113 ++++++------------ client/src/lib/utils.ts | 10 +- client/src/types/index.ts | 34 +++++- 8 files changed, 230 insertions(+), 130 deletions(-) create mode 100644 client/src/app/api/notifications/route.ts diff --git a/client/src/app/api/notifications/route.ts b/client/src/app/api/notifications/route.ts new file mode 100644 index 0000000..d5f0969 --- /dev/null +++ b/client/src/app/api/notifications/route.ts @@ -0,0 +1,34 @@ +import { validateRequest } from '@/auth' +import { prisma } from '@/lib' +import { getNotificationDataInclude, NotificationsPage } from '@/types' +import { NextRequest } from 'next/server' + +export const GET = async (req: NextRequest) => { + try { + const cursor = req.nextUrl.searchParams.get('cursor') || undefined + const pageSize = 10 + const { user } = await validateRequest() + + if (!user) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }) + } + const notifications = await prisma.notification.findMany({ + where: { recipientId: user.id }, + include: getNotificationDataInclude(), + orderBy: { createdAt: 'desc' }, + take: pageSize + 1, + cursor: cursor ? { id: cursor } : undefined + }) + const nextCursor = + notifications.length > pageSize ? notifications[pageSize].id : null + const data: NotificationsPage = { + notifications: notifications.slice(0, pageSize), + nextCursor + } + + return Response.json(data) + } catch (error) { + console.error(error) + return Response.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/client/src/app/api/posts/[postId]/likes/route.ts b/client/src/app/api/posts/[postId]/likes/route.ts index fb3c5ef..09c69f2 100644 --- a/client/src/app/api/posts/[postId]/likes/route.ts +++ b/client/src/app/api/posts/[postId]/likes/route.ts @@ -1,6 +1,7 @@ import { validateRequest } from '@/auth' import { prisma } from '@/lib' import { LikeInfo } from '@/types' +import { NotificationType } from '@prisma/client' import { NextRequest } from 'next/server' export const GET = async (_: NextRequest, { params }: { params: Promise<{ postId: string }> }) => { @@ -54,19 +55,43 @@ export const POST = async (_: NextRequest, { params }: { params: Promise<{ postI return Response.json({ error: 'Unauthorized' }, { status: 401 }) } - await prisma.like.upsert({ - where: { - userId_postId: { + const post = await prisma.post.findUnique({ + where: { id: postId }, + select: { + authorId: true + } + }) + if (!post) { + return Response.json({ error: 'Post not found' }, { status: 404 }) + } + await prisma.$transaction([ + prisma.like.upsert({ + where: { + userId_postId: { + userId: currentUser.id, + postId + } + }, + create: { userId: currentUser.id, postId - } - }, - create: { - userId: currentUser.id, - postId - }, - update: {} - }) + }, + update: {} + }), + ...(post.authorId !== currentUser.id ? + [ + prisma.notification.create({ + data: { + isssuerId: currentUser.id, + recipientId: post.authorId, + postId, + type: NotificationType.LIKE + } + }) + ] + : []) + ]) + return new Response() } catch (error) { console.error(error) @@ -82,13 +107,32 @@ export const DELETE = async (_: NextRequest, { params }: { params: Promise<{ pos if (!currentUser) { return Response.json({ error: 'Unauthorized' }, { status: 401 }) } - - await prisma.like.deleteMany({ - where: { - userId: currentUser.id, - postId + const post = await prisma.post.findUnique({ + where: { id: postId }, + select: { + authorId: true } }) + if (!post) { + return Response.json({ error: 'Post not found' }, { status: 404 }) + } + await prisma.$transaction([ + prisma.like.deleteMany({ + where: { + userId: currentUser.id, + postId + } + }), + prisma.notification.deleteMany({ + where: { + isssuerId: currentUser.id, + recipientId: post.authorId, + postId, + type: NotificationType.LIKE + } + }) + ]) + return new Response() } catch (error) { console.error(error) diff --git a/client/src/app/api/users/[userId]/followers/route.ts b/client/src/app/api/users/[userId]/followers/route.ts index be2cbe3..99dc89d 100644 --- a/client/src/app/api/users/[userId]/followers/route.ts +++ b/client/src/app/api/users/[userId]/followers/route.ts @@ -1,9 +1,10 @@ import { validateRequest } from '@/auth' import { prisma } from '@/lib' import { FollowerInfo } from '@/types' +import { NotificationType } from '@prisma/client' import { NextRequest } from 'next/server' -export const GET = async (req: NextRequest, { params }: { params: Promise<{ userId: string }> }) => { +export const GET = async (_: NextRequest, { params }: { params: Promise<{ userId: string }> }) => { try { const { userId } = await params const { user: currentUser } = await validateRequest() @@ -51,20 +52,29 @@ export const POST = async (_: NextRequest, { params }: { params: Promise<{ userI if (!currentUser) { return Response.json({ error: 'Unauthorized' }, { status: 401 }) } - - await prisma.follow.upsert({ - where: { - followerId_followingId: { + await prisma.$transaction([ + prisma.follow.upsert({ + where: { + followerId_followingId: { + followerId: currentUser.id, + followingId: userId + } + }, + create: { followerId: currentUser.id, followingId: userId + }, + update: {} + }), + prisma.notification.create({ + data: { + isssuerId: currentUser.id, + recipientId: userId, + type: NotificationType.FOLLOW } - }, - create: { - followerId: currentUser.id, - followingId: userId - }, - update: {} - }) + }) + ]) + return new Response() } catch (error) { @@ -73,7 +83,7 @@ export const POST = async (_: NextRequest, { params }: { params: Promise<{ userI } } -export const DELETE = async (req: NextRequest, { params }: { params: Promise<{ userId: string }> }) => { +export const DELETE = async (_: NextRequest, { params }: { params: Promise<{ userId: string }> }) => { try { const { userId } = await params const { user: currentUser } = await validateRequest() @@ -81,12 +91,22 @@ export const DELETE = async (req: NextRequest, { params }: { params: Promise<{ u if (!currentUser) { return Response.json({ error: 'Unauthorized' }, { status: 401 }) } - await prisma.follow.deleteMany({ - where: { - followerId: currentUser.id, - followingId: userId - } - }) + + await prisma.$transaction([ + prisma.follow.deleteMany({ + where: { + followerId: currentUser.id, + followingId: userId + } + }), + prisma.notification.deleteMany({ + where: { + isssuerId: currentUser.id, + recipientId: userId, + type: NotificationType.FOLLOW + } + }) + ]) return new Response() } catch (error) { diff --git a/client/src/app/playground/(post)/Post.tsx b/client/src/app/playground/(post)/Post.tsx index 2358177..741c49e 100644 --- a/client/src/app/playground/(post)/Post.tsx +++ b/client/src/app/playground/(post)/Post.tsx @@ -10,7 +10,6 @@ import { Textarea } from '@/components/ui/textarea' import { useSession, useToast } from '@/hooks' import { Bookmark, Plus, X } from 'lucide-react' import { cn, formatRelativeDate } from '@/lib/utils' - import { UserTooltip } from '@/components/users' import { Avatar } from '@/components' import { LikeButton, ShareDialog, MoreButton } from '.' diff --git a/client/src/components/comments/actions.ts b/client/src/components/comments/actions.ts index 6490f0f..b4a988c 100644 --- a/client/src/components/comments/actions.ts +++ b/client/src/components/comments/actions.ts @@ -4,6 +4,7 @@ import { PostData, getCommentDataInclude } from '@/types' import { prisma } from '@/lib' import { validateRequest } from '@/auth' import { createCommentSchema } from '@/lib/validation' +import { NotificationType } from '@prisma/client' export const createComment = async ({ post, content }: { post: PostData; content: string }) => { const { user } = await validateRequest() @@ -12,15 +13,30 @@ export const createComment = async ({ post, content }: { post: PostData; content } const { content: validatedContent } = createCommentSchema.parse({ content }) - - return prisma.comment.create({ - data: { - content: validatedContent, - authorId: user.id, - postId: post.id - }, - include: getCommentDataInclude(user.id) - }) + const [comment] = await prisma.$transaction([ + prisma.comment.create({ + data: { + content: validatedContent, + authorId: user.id, + postId: post.id + }, + include: getCommentDataInclude(user.id) + }), + ...(post.authorId !== user.id ? + [ + prisma.notification.create({ + data: { + isssuerId: user.id, + recipientId: post.authorId, + postId: post.id, + type: NotificationType.COMMENT + } + }) + ] + : []) + ]) + + return comment } export const deleteComment = async (id: string) => { diff --git a/client/src/components/ui/card.tsx b/client/src/components/ui/card.tsx index f62edea..da88e04 100644 --- a/client/src/components/ui/card.tsx +++ b/client/src/components/ui/card.tsx @@ -1,79 +1,44 @@ -import * as React from "react" +import * as React from 'react' -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils' -const Card = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
+const Card = React.forwardRef>(({ className, ...props }, ref) => ( +
)) -Card.displayName = "Card" - -const CardHeader = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardHeader.displayName = "CardHeader" - -const CardTitle = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardTitle.displayName = "CardTitle" - -const CardDescription = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardDescription.displayName = "CardDescription" - -const CardContent = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardContent.displayName = "CardContent" - -const CardFooter = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardFooter.displayName = "CardFooter" +Card.displayName = 'Card' + +const CardHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardHeader.displayName = 'CardHeader' + +const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardTitle.displayName = 'CardTitle' + +const CardDescription = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardDescription.displayName = 'CardDescription' + +const CardContent = React.forwardRef>( + ({ className, ...props }, ref) =>
+) +CardContent.displayName = 'CardContent' + +const CardFooter = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardFooter.displayName = 'CardFooter' export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } + diff --git a/client/src/lib/utils.ts b/client/src/lib/utils.ts index e603462..93d598d 100644 --- a/client/src/lib/utils.ts +++ b/client/src/lib/utils.ts @@ -2,16 +2,13 @@ import { clsx, type ClassValue } from 'clsx' import { twMerge } from 'tailwind-merge' import { formatDate, formatDistanceToNowStrict } from 'date-fns' -export const cn = (...inputs: ClassValue[]) => { - return twMerge(clsx(inputs)) -} +export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs)) -export const formatNumber = (n: number): string => { - return Intl.NumberFormat('en-US', { +export const formatNumber = (n: number): string => + Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 1 }).format(n) -} export const formatRelativeDate = (from: Date) => { const currentDate = new Date() @@ -23,3 +20,4 @@ export const formatRelativeDate = (from: Date) => { } return formatDate(from, 'MMM d, yyyy') } + diff --git a/client/src/types/index.ts b/client/src/types/index.ts index f4682ca..bceba39 100644 --- a/client/src/types/index.ts +++ b/client/src/types/index.ts @@ -29,6 +29,14 @@ export interface PostsPage { posts: PostData[] nextCursor: string | null } +export interface CommentsPage { + comments: CommentData[] + previousCursor: string | null +} +export interface NotificationsPage { + notifications: NotificationData[] + nextCursor: string | null +} export const getCommentDataInclude = (userId: string) => { return { @@ -42,11 +50,6 @@ export type CommentData = Prisma.CommentGetPayload<{ include: ReturnType }> -export interface CommentsPage { - comments: CommentData[] - previousCursor: string | null -} - export interface FollowerInfo { followers: number isFollowedByUser: boolean @@ -57,6 +60,23 @@ export interface LikeInfo { isLikedByUser: boolean } +export const getNotificationDataInclude = () => { + return { + issuer: { + select: { + userName: true, + displayName: true, + avatarUrl: true + } + }, + post: { + select: { + content: true + } + } + } satisfies Prisma.NotificationInclude +} + export const getPostDataInclude = (userId: string) => { return { author: { @@ -92,6 +112,10 @@ export interface BookmarkInfo { isBookmarkedByUser: boolean } +export type NotificationData = Prisma.NotificationGetPayload<{ + include: ReturnType +}> + export type PostData = Prisma.PostGetPayload<{ include: ReturnType }> export type UserData = Prisma.UserGetPayload<{ select: ReturnType }>