-
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat[NOTIFICATIONS]: Added Notification functionality
Now users can get notifications based on other user activity
- Loading branch information
1 parent
b4675a9
commit d6b3385
Showing
17 changed files
with
694 additions
and
265 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { cn } from "@/lib/utils"; | ||
import type { NotificationType } from "@prisma/client"; | ||
import UserAvatar from "@zephyr-ui/Layouts/UserAvatar"; | ||
import type { NotificationData } from "@zephyr/db"; | ||
import { Heart, MessageCircle, User2 } from "lucide-react"; | ||
import Link from "next/link"; | ||
|
||
interface NotificationProps { | ||
notification: NotificationData; | ||
} | ||
|
||
export default function Notification({ notification }: NotificationProps) { | ||
const notificationTypeMap: Record< | ||
NotificationType, | ||
{ message: string; icon: JSX.Element; href: string } | ||
> = { | ||
FOLLOW: { | ||
message: `${notification.issuer.displayName} followed you`, | ||
icon: <User2 className="size-7 text-primary" />, | ||
href: `/users/${notification.issuer.username}` | ||
}, | ||
COMMENT: { | ||
message: `${notification.issuer.displayName} eddied on your post`, | ||
icon: <MessageCircle className="size-7 fill-primary text-primary" />, | ||
href: `/posts/${notification.postId}` | ||
}, | ||
AMPLIFY: { | ||
message: `${notification.issuer.displayName} amplified your post`, | ||
icon: <Heart className="size-7 fill-red-500 text-red-500" />, | ||
href: `/posts/${notification.postId}` | ||
} | ||
}; | ||
|
||
const { message, icon, href } = notificationTypeMap[notification.type]; | ||
|
||
return ( | ||
<Link href={href} className="block"> | ||
<article | ||
className={cn( | ||
"flex gap-3 rounded-2xl bg-card p-5 shadow-sm transition-colors hover:bg-card/70", | ||
!notification.read && "bg-primary/10" | ||
)} | ||
> | ||
<div className="my-1">{icon}</div> | ||
<div className="space-y-3"> | ||
<UserAvatar avatarUrl={notification.issuer.avatarUrl} size={36} /> | ||
<div> | ||
<span className="font-bold">{notification.issuer.displayName}</span>{" "} | ||
<span>{message}</span> | ||
</div> | ||
{notification.post && ( | ||
<div className="line-clamp-3 whitespace-pre-line text-muted-foreground"> | ||
{notification.post.content} | ||
</div> | ||
)} | ||
</div> | ||
</article> | ||
</Link> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
"use client"; | ||
|
||
import kyInstance from "@/lib/ky"; | ||
import { | ||
useInfiniteQuery, | ||
useMutation, | ||
useQueryClient | ||
} from "@tanstack/react-query"; | ||
import InfiniteScrollContainer from "@zephyr-ui/Layouts/InfiniteScrollContainer"; | ||
import PostsLoadingSkeleton from "@zephyr-ui/Posts/PostsLoadingSkeleton"; | ||
import type { NotificationsPage } from "@zephyr/db"; | ||
import { Loader2 } from "lucide-react"; | ||
import { useEffect } from "react"; | ||
import Notification from "./Notification"; | ||
|
||
export default function Notifications() { | ||
const { | ||
data, | ||
fetchNextPage, | ||
hasNextPage, | ||
isFetching, | ||
isFetchingNextPage, | ||
status | ||
} = useInfiniteQuery({ | ||
queryKey: ["notifications"], | ||
queryFn: ({ pageParam }) => | ||
kyInstance | ||
.get( | ||
"/api/notifications", | ||
pageParam ? { searchParams: { cursor: pageParam } } : {} | ||
) | ||
.json<NotificationsPage>(), | ||
initialPageParam: null as string | null, | ||
getNextPageParam: (lastPage) => lastPage.nextCursor | ||
}); | ||
|
||
const queryClient = useQueryClient(); | ||
|
||
const { mutate } = useMutation({ | ||
mutationFn: () => kyInstance.patch("/api/notifications/mark-as-read"), | ||
onSuccess: () => { | ||
queryClient.setQueryData(["unread-notification-count"], { | ||
unreadCount: 0 | ||
}); | ||
}, | ||
onError(error) { | ||
console.error("Failed to mark notifications as read", error); | ||
} | ||
}); | ||
|
||
useEffect(() => { | ||
mutate(); | ||
}, [mutate]); | ||
|
||
const notifications = data?.pages.flatMap((page) => page.notifications) || []; | ||
|
||
if (status === "pending") { | ||
return <PostsLoadingSkeleton />; | ||
} | ||
|
||
if (status === "success" && !notifications.length && !hasNextPage) { | ||
return ( | ||
<p className="text-center text-muted-foreground"> | ||
You don't have any rustles yet. | ||
</p> | ||
); | ||
} | ||
|
||
if (status === "error") { | ||
return ( | ||
<p className="text-center text-destructive"> | ||
An error occurred while loading rustles. | ||
</p> | ||
); | ||
} | ||
|
||
return ( | ||
<InfiniteScrollContainer | ||
className="space-y-5" | ||
onBottomReached={() => hasNextPage && !isFetching && fetchNextPage()} | ||
> | ||
{notifications.map((notification) => ( | ||
<Notification key={notification.id} notification={notification} /> | ||
))} | ||
{isFetchingNextPage && <Loader2 className="mx-auto my-3 animate-spin" />} | ||
</InfiniteScrollContainer> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import NavigationCard from "@/components/Home/sidebars/left/NavigationCard"; | ||
import SuggestedConnections from "@/components/Home/sidebars/right/SuggestedConnections"; | ||
import TrendingTopics from "@/components/Home/sidebars/right/TrendingTopics"; | ||
import StickyFooter from "@zephyr-ui/Layouts/StinkyFooter"; | ||
import type { Metadata } from "next"; | ||
import Notifications from "./Notifications"; | ||
|
||
export const metadata: Metadata = { | ||
title: "Rustles" | ||
}; | ||
|
||
export default async function Page() { | ||
return ( | ||
<main className="flex w-full min-w-0 gap-5"> | ||
<aside className="sticky top-[5rem] ml-1 h-[calc(100vh-5.25rem)] w-64 flex-shrink-0 overflow-y-auto "> | ||
<div className="mr-2"> | ||
<NavigationCard | ||
isCollapsed={false} | ||
className="h-[calc(100vh-4.5rem)]" | ||
stickyTop="5rem" | ||
/> | ||
</div> | ||
<div className="mt-2 mr-2"> | ||
<SuggestedConnections /> | ||
</div> | ||
</aside> | ||
<div className="mt-5 w-full min-w-0 space-y-5"> | ||
<Notifications /> | ||
</div> | ||
|
||
<div className="sticky mt-5 hidden h-fit w-80 flex-none lg:block"> | ||
<div className="space-y-5 rounded-2xl border border-border bg-card p-5 shadow-sm"> | ||
<h2 className="font-bold text-xl">Rustle Info</h2> | ||
<p className="text-muted-foreground"> | ||
Here you can view all your rustles aka notifications. You can also | ||
mark them as read. | ||
</p> | ||
</div> | ||
<div className="mt-2 mb-2"> | ||
<TrendingTopics /> | ||
</div> | ||
|
||
<div className="mt-4"> | ||
<StickyFooter /> | ||
</div> | ||
</div> | ||
</main> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { validateRequest } from "@zephyr/auth/auth"; | ||
import { prisma } from "@zephyr/db"; | ||
export async function PATCH() { | ||
try { | ||
const { user } = await validateRequest(); | ||
if (!user) { | ||
return Response.json({ error: "Unauthorized" }, { status: 401 }); | ||
} | ||
await prisma.notification.updateMany({ | ||
where: { | ||
recipientId: user.id, | ||
read: false | ||
}, | ||
data: { | ||
read: true | ||
} | ||
}); | ||
return new Response(); | ||
} catch (error) { | ||
console.error(error); | ||
return Response.json({ error: "Internal server error" }, { status: 500 }); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { validateRequest } from "@zephyr/auth/auth"; | ||
import { | ||
type NotificationsPage, | ||
notificationsInclude, | ||
prisma | ||
} from "@zephyr/db"; | ||
import type { NextRequest } from "next/server"; | ||
export async function GET(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: notificationsInclude, | ||
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 }); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { validateRequest } from "@zephyr/auth/auth"; | ||
import { prisma } from "@zephyr/db"; | ||
export async function PATCH() { | ||
try { | ||
const { user } = await validateRequest(); | ||
if (!user) { | ||
return Response.json({ error: "Unauthorized" }, { status: 401 }); | ||
} | ||
await prisma.notification.updateMany({ | ||
where: { | ||
recipientId: user.id, | ||
read: false | ||
}, | ||
data: { | ||
read: true | ||
} | ||
}); | ||
return new Response(); | ||
} catch (error) { | ||
console.error(error); | ||
return Response.json({ error: "Internal server error" }, { status: 500 }); | ||
} | ||
} |
Oops, something went wrong.