From 022dc695067049874a4ae1f8fcdd57a21a266220 Mon Sep 17 00:00:00 2001 From: El Hadji Malick Seck Date: Tue, 8 Aug 2023 13:34:06 +0000 Subject: [PATCH] feat: add a user notifications page (#1478) --- .../NotificationsCard/notification-card.tsx | 13 +- .../molecules/AuthSection/auth-section.tsx | 70 +----- lib/utils/get-notification-url.ts | 16 ++ pages/user/notifications.tsx | 215 ++++++++++++++++++ tests/lib/utils/get-notifications-url.test.ts | 24 ++ 5 files changed, 266 insertions(+), 72 deletions(-) create mode 100644 lib/utils/get-notification-url.ts create mode 100644 pages/user/notifications.tsx create mode 100644 tests/lib/utils/get-notifications-url.test.ts diff --git a/components/atoms/NotificationsCard/notification-card.tsx b/components/atoms/NotificationsCard/notification-card.tsx index 4324dcfde0..3272248b96 100644 --- a/components/atoms/NotificationsCard/notification-card.tsx +++ b/components/atoms/NotificationsCard/notification-card.tsx @@ -2,21 +2,14 @@ import Link from "next/link"; import React from "react"; import { FaRegSmile, FaUserCircle } from "react-icons/fa"; +import { getNotificationURL } from "lib/utils/get-notification-url"; + interface NotificationCard { type: "highlight_reaction" | "follow" | "collaboration"; message: string; id: string; } -const getSourceURL = (type: string, id: string) => { - switch (type) { - case "highlight_reaction": - return `/feed/${id}`; - case "follow": - return `/user/${id}`; - } -}; - const NotificationCard = ({ type, message, id }: NotificationCard) => { const Icons = { highlight_reaction: FaRegSmile, @@ -25,7 +18,7 @@ const NotificationCard = ({ type, message, id }: NotificationCard) => { }; const Icon = Icons[type]; - const linkURL = getSourceURL(type, id); + const linkURL = getNotificationURL(type, id); return (
diff --git a/components/molecules/AuthSection/auth-section.tsx b/components/molecules/AuthSection/auth-section.tsx index f5f594eff8..23565f33fa 100644 --- a/components/molecules/AuthSection/auth-section.tsx +++ b/components/molecules/AuthSection/auth-section.tsx @@ -17,9 +17,6 @@ import Button from "components/atoms/Button/button"; import Text from "components/atoms/Typography/text"; import GitHubIcon from "img/icons/github-icon.svg"; import Icon from "components/atoms/Icon/icon"; -import NotificationCard from "components/atoms/NotificationsCard/notification-card"; -import { Spinner } from "components/atoms/SpinLoader/spin-loader"; -import { Popover, PopoverContent, PopoverTrigger } from "../Popover/popover"; import DropdownList from "../DropdownList/dropdown-list"; import OnboardingButton from "../OnboardingButton/onboarding-button"; import userAvatar from "../../../img/ellipse-1.png"; @@ -30,38 +27,17 @@ const AuthSection: React.FC = ({}) => { const router = useRouter(); const currentPath = router.asPath; - const { signIn, signOut, user, sessionToken } = useSupabaseAuth(); + const { signIn, signOut, user } = useSupabaseAuth(); const { onboarded, session } = useSession(true); - const [notifications, setNotifications] = useState([]); - const [loading, setLoading] = useState(false); const [userInfo, setUserInfo] = useState(undefined); const [host, setHost] = useState(""); + useEffect(() => { if (typeof window !== "undefined") { setHost(window.location.origin as string); } }, []); - // Fetch user notifications - const fetchNotifications = async () => { - if (userInfo && userInfo.notification_count > 0) { - setLoading(true); - const req = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/user/notifications`, { - headers: { - accept: "application/json", - Authorization: `Bearer ${sessionToken}`, - }, - }); - setLoading(false); - if (req.ok) { - const notifications = await req.json(); - setNotifications(notifications.data as DbUserNotification[]); - } - } else { - return; - } - }; - useEffect(() => { if (session && !userInfo) { setUserInfo(session); @@ -128,42 +104,12 @@ const AuthSection: React.FC = ({}) => { ) : ( "" )} - { - // reset the notification state to empty when the popover is closed - if (!loading && !state) setUserInfo(undefined); - }} - > - await fetchNotifications()} asChild> - - - - {loading ? ( -
- -
- ) : ( - <> - {notifications.length > 0 ? ( -
- {notifications.map(({ type, message, id, meta_id }) => ( - - ))} -
- ) : ( -
- You don't have any unread notifications -
- )} - - )} -
-
+
diff --git a/lib/utils/get-notification-url.ts b/lib/utils/get-notification-url.ts new file mode 100644 index 0000000000..4f5fc4d82b --- /dev/null +++ b/lib/utils/get-notification-url.ts @@ -0,0 +1,16 @@ +/** + * Returns the URL for a notification based on its type and ID or username. + * @param type - The type of the notification. + * @param id - The ID or username associated with the notification. + * @returns The URL for the notification. + */ +export const getNotificationURL = (type: string, id: string | number) => { + switch (type) { + case "highlight_reaction": + return `/feed/${id}`; + case "follow": + return `/user/${id}`; + default: + return "/"; + } +}; diff --git a/pages/user/notifications.tsx b/pages/user/notifications.tsx new file mode 100644 index 0000000000..9ccc78f920 --- /dev/null +++ b/pages/user/notifications.tsx @@ -0,0 +1,215 @@ +import { useEffect, useMemo, useRef, useState } from "react"; +import { useRouter } from "next/router"; +import clsx from "clsx"; +import Link from "next/link"; + +import formatDistanceToNowStrict from "date-fns/formatDistanceToNowStrict"; +import useSupabaseAuth from "lib/hooks/useSupabaseAuth"; +import Title from "components/atoms/Typography/title"; +import Button from "components/atoms/Button/button"; +import SkeletonWrapper from "components/atoms/SkeletonLoader/skeleton-wrapper"; +import PaginationResults from "components/molecules/PaginationResults/pagination-result"; +import Pagination from "components/molecules/Pagination/pagination"; +import DashContainer from "components/atoms/DashedContainer/DashContainer"; +import Avatar from "components/atoms/Avatar/avatar"; +import { getAvatarByUsername } from "lib/utils/github"; +import { getNotificationURL } from "lib/utils/get-notification-url"; +import changeCapitalization from "lib/utils/change-capitalization"; +import ProfileLayout from "layouts/profile"; +import { WithPageLayout } from "interfaces/with-page-layout"; + +interface NotificationResponse { + data: DbUserNotification[]; + meta: Meta; +} + +const Notifications: WithPageLayout = () => { + const [page, setPage] = useState(1); + const [loading, setLoading] = useState(false); + const [notificationsResponse, setNotificationsResponse] = useState(); + const [filter, setFilter] = useState<"all" | "follow" | "highlight_reaction">("all"); + + const topRef = useRef(null); + const { sessionToken, user } = useSupabaseAuth(true); + + const router = useRouter(); + const username = user?.user_metadata.user_name as string; + + const fetchNotifications = async (page = 1) => { + if (!sessionToken) return; + setLoading(true); + const req = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/user/notifications?limit=10&page=${page}`, { + headers: { + accept: "application/json", + Authorization: `Bearer ${sessionToken}`, + }, + }); + const notifications = await req.json(); + setNotificationsResponse(notifications); + setLoading(false); + }; + + const notifications = useMemo(() => { + if (!notificationsResponse?.data) return []; + if (filter === "all") + return notificationsResponse?.data?.sort( + (a, b) => new Date(b.notified_at).getTime() - new Date(a.notified_at).getTime() + ); + return notificationsResponse?.data + .filter((notification) => notification.type === filter) + ?.sort((a, b) => new Date(b.notified_at).getTime() - new Date(a.notified_at).getTime()); + }, [notificationsResponse?.data, filter]); + + useEffect(() => { + fetchNotifications(); + + if (sessionToken) { + router.push(`/user/notifications?filter=all`); + } + }, [sessionToken]); + + const handlePageChange = (page: number) => { + setPage(page); + fetchNotifications(page); + if (topRef.current) { + topRef.current.scrollIntoView({ + behavior: "smooth", + }); + } + }; + + return ( +
+
+
+ + Notifications + + +
+
+ {loading ? ( +
+ {Array.from({ length: 8 }).map((_, index) => ( +
+ +
+ + +
+
+ ))} +
+ ) : notifications?.length <= 0 && !loading ? ( + +
+

+ You don't have any{" "} + {changeCapitalization(filter === "highlight_reaction" ? "reaction" : filter, true)} notifications yet!{" "} +
+

+
+
+ ) : ( +
+ {notifications?.map((notification) => ( +
+ +
+

+ + {notification.meta_id} + + {notification.message.replace(notification.meta_id, " ")} +

+ + {formatDistanceToNowStrict(new Date(notification.notified_at), { addSuffix: true })} + +
+
+ ))} +
+ )} + {notifications?.length > 0 && (notificationsResponse?.meta?.pageCount ?? 0) > 1 && ( +
+
+ +
+ handlePageChange(pageNumber)} + /> +
+ )} +
+
+
+ ); +}; + +Notifications.PageLayout = ProfileLayout; +Notifications.isPrivateRoute = true; +export default Notifications; diff --git a/tests/lib/utils/get-notifications-url.test.ts b/tests/lib/utils/get-notifications-url.test.ts new file mode 100644 index 0000000000..19d3c5e0df --- /dev/null +++ b/tests/lib/utils/get-notifications-url.test.ts @@ -0,0 +1,24 @@ +import { getNotificationURL } from "lib/utils/get-notification-url"; + +describe("[lib] getNotificationURL()", () => { + it("should return the correct URL for a highlight reaction notification", () => { + const type = "highlight_reaction"; + const idOrUsername = 30; + const result = getNotificationURL(type, idOrUsername); + expect(result).toBe("/feed/30"); + }); + + it("should return the correct URL for a follow notification", () => { + const type = "follow"; + const idOrUsername = "takanome-dev"; + const result = getNotificationURL(type, idOrUsername); + expect(result).toBe("/user/takanome-dev"); + }); + + it("should return the correct URL for an unknown notification", () => { + const type = "unknown"; + const idOrUsername = "unknown"; + const result = getNotificationURL(type, idOrUsername); + expect(result).toBe("/"); + }); +});