diff --git a/src/components/navbar/Navbar.tsx b/src/components/navbar/Navbar.tsx index 6918eec..1a1f7b8 100644 --- a/src/components/navbar/Navbar.tsx +++ b/src/components/navbar/Navbar.tsx @@ -1,11 +1,17 @@ -import React, { useState, useEffect } from 'react'; -import { NavLink, useNavigate } from 'react-router-dom'; +import React, { useRef, useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; import { DesktopNav, PopularCategory } from '../../containers/nav/NavbarComponents'; import WishNav from './wishNav/WishNav'; import CartNav from './cartNav/CartNav'; +import { LuBell } from 'react-icons/lu'; +import Notifications from './notifications/Notifications'; +import { cn } from '../../utils'; const Navbar: React.FC = () => { - const [cartOpen, setCartOpen] = useState(false); + const [notificationOpen, setNotificationOpen] = useState(false); + const navbarRef = useRef(null); + const [navbarHeight, setNavbarHeight] = useState(20); + const [cartOpen, SetCartOpen] = useState(false); const [wish, setWish] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const navigate = useNavigate(); @@ -34,7 +40,6 @@ const Navbar: React.FC = () => { container?.classList.remove('-translate-x-full'); hideScrollbar(); }); - overlay?.addEventListener('click', e => { if (e.target !== e.currentTarget) { return; @@ -45,21 +50,34 @@ const Navbar: React.FC = () => { }); }, []); - const handleSearch = (e: React.FormEvent) => { + useEffect(() => { + const updateNavbarHeight = () => { + setNavbarHeight(navbarRef.current?.offsetHeight as number); + }; + updateNavbarHeight(); + // setTimeout(updateNavbarHeight, 0); + window.addEventListener('resize', updateNavbarHeight); + return () => { + window.removeEventListener('resize', updateNavbarHeight); + }; + }, []); + + const handleSearch = (e: any) => { e.preventDefault(); navigate(`/search?searchQuery=${searchQuery}`); }; return ( <> - {(wish || cartOpen) && ( + {(wish || cartOpen || notificationOpen) && (
{ if (e.target !== e.currentTarget) { return; } setWish(false); - setCartOpen(false); + SetCartOpen(false); + setNotificationOpen(false); }} className='absolute bg-[#00000000] top-0 w-full border h-full' style={{ zIndex: 10 }} @@ -70,7 +88,10 @@ const Navbar: React.FC = () => { wish || cartOpen ? 'sticky' : '' } z-10`} > -
+
{
)}
- setNotificationOpen(!notificationOpen)} > - { strokeLinejoin='round' d='M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z' /> - + */} + - +
setCartOpen(state => !state)} + onClick={() => SetCartOpen(state => !state)} className='rounded-full transition-all ease-in-out delay-100 hover:bg-grayColor active:bg-greenColor active:text-blackColor hover:text-blackColor p-1 select-none' >
@@ -178,7 +201,7 @@ const Navbar: React.FC = () => {
{ if (e.target !== e.currentTarget) { - setCartOpen(false); + SetCartOpen(false); return; } }} @@ -253,6 +276,15 @@ const Navbar: React.FC = () => {
+
+ +
); diff --git a/src/components/navbar/notifications/NotificationComponent.tsx b/src/components/navbar/notifications/NotificationComponent.tsx new file mode 100644 index 0000000..8e7a7e4 --- /dev/null +++ b/src/components/navbar/notifications/NotificationComponent.tsx @@ -0,0 +1,122 @@ +import { BiDotsVertical } from 'react-icons/bi'; +import { cn } from '../../../utils'; +import { useEffect, useRef, useState } from 'react'; +import { + useMarkNotificationAsReadMutation, + useDeleteSingleNotificationsMutation, +} from '../../../services/notificationsAPI'; +import { FaSpinner } from 'react-icons/fa6'; + +interface NotificationProps { + id: string; + message: string; + time: string; + date: string; + isRead: boolean; + onDelete: (id: string) => void; + onReadChange: (isRead: boolean) => void; +} +const NotificationComponent = ({ + message, + time, + date, + isRead: initialIsRead, + id, + onDelete, + onReadChange, +}: NotificationProps) => { + const [isMenuClicked, setIsMenuClicked] = useState(false); + const [isRead, setIsRead] = useState(initialIsRead); + const menubarRef = useRef(null); + const [markNotificationAsRead, { isLoading }] = useMarkNotificationAsReadMutation(); + const [deleteNotification, { isLoading: isDeleting }] = useDeleteSingleNotificationsMutation(); + const markAsRead = async () => { + const { data } = await markNotificationAsRead({ isRead: !isRead, id }); + setIsRead(data.data.isRead); + onReadChange(data.data.isRead); + }; + const notificationDelete = () => { + deleteNotification(id); + onDelete(id); + }; + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (menubarRef.current && !menubarRef.current.contains(event.target as Node)) { + setIsMenuClicked(false); + } + }; + + if (isMenuClicked) { + document.addEventListener('mousedown', handleClickOutside); + } else { + document.removeEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isMenuClicked]); + + return ( +
+ + +

{message}

+

+ Unread +

+
+
+ +

{date}

+

{time}

+
+
+ + {isMenuClicked && ( +
+ + +
+ )} +
+ ); +}; + +export default NotificationComponent; diff --git a/src/components/navbar/notifications/Notifications.tsx b/src/components/navbar/notifications/Notifications.tsx new file mode 100644 index 0000000..4dec9ce --- /dev/null +++ b/src/components/navbar/notifications/Notifications.tsx @@ -0,0 +1,172 @@ +import { BiDotsVertical } from 'react-icons/bi'; +import NotificationComponent from './NotificationComponent'; +import { + useGetNotificationsQuery, + useDeleteAllNotificationsMutation, + useMarkAllNotificationsAsReadMutation, +} from '../../../services/notificationsAPI'; +import { useSelector } from 'react-redux'; +import { cn } from '../../../utils'; +import { NotificationProps } from '../../../types/Types'; +import { useEffect, useRef, useState } from 'react'; +import { FaSpinner } from 'react-icons/fa6'; + +export const Notifications = () => { + const userId = useSelector((state: any) => state.userId) || localStorage.getItem('userId'); + // const userId = '06e0d866-2544-4cfa-83b0-2c3cede7a2f0'; + const [mainMenuClicked, setMainMenuClicked] = useState(false); + + const mainMenuRef = useRef(null); + const [notifications, setNotifications] = useState([]); + const [unRead, setUnRead] = useState(0); + + const { data: fetchedNotifications, isLoading, isError, error } = useGetNotificationsQuery(userId); + + const [deleteAll, { isLoading: areAllDeleting, isSuccess: areDeleted }] = useDeleteAllNotificationsMutation(); + const [markAll, { isLoading: areAllUpdating, isSuccess: areAllUpdated }] = useMarkAllNotificationsAsReadMutation(); + useEffect(() => { + if (fetchedNotifications) { + setNotifications(fetchedNotifications.data); + } + }, [fetchedNotifications]); + + useEffect(() => { + setUnRead(notifications.filter((notification: NotificationProps) => !notification.isRead).length); + }, [notifications]); + useEffect(() => { + const handleOutClick = (e: MouseEvent) => { + if (mainMenuRef && !mainMenuRef.current?.contains(e.target as Node)) { + setMainMenuClicked(false); + } + }; + if (mainMenuClicked) { + document.addEventListener('mousedown', handleOutClick); + } else { + document.removeEventListener('mousedown', handleOutClick); + } + + return () => { + document.removeEventListener('mousedown', handleOutClick); + }; + }, [mainMenuClicked]); + useEffect(() => { + if (areAllUpdated) { + setNotifications(prevNotifications => + prevNotifications.map(notification => ({ + ...notification, + isRead: true, + })) + ); + + setUnRead(0); + } + }, [areAllUpdated]); + useEffect(() => { + if (areDeleted) { + setNotifications([]); + console.log(notifications); + setUnRead(0); + } + }, [areDeleted]); + if (isLoading) + return ( +
+
+
+
+ ); + + const handleDelete = (id: string) => { + setNotifications(prevNotifications => prevNotifications.filter(notification => notification.id !== id)); + }; + const handleReadChange = (isRead: boolean) => { + if (isRead) setUnRead(unRead - 1); + else setUnRead(unRead + 1); + return; + }; + const handleDeleteAll = () => { + deleteAll(userId); + }; + const handleMarkAll = () => { + markAll({ userId, isRead: true }); + }; + return ( +
+
+ + +

Notifications

+

+ {unRead} +

+
+ + + {mainMenuClicked && ( +
+ + +
+ )} +
+
+ {isError ? ( + !userId ? ( +

You need to sign in first

+ ) : ( +

Notification could not be fetched. Please try again

+ ) + ) : ( +
+ {notifications.map((notification: NotificationProps) => { + const mainDate = new Date(notification.createdAt); + const date = `${mainDate.getDate()}/${mainDate.getMonth() + 1}/${mainDate.getFullYear()}`; + const time = `${mainDate.getHours()}:${mainDate.getMinutes()}`; + return ( + + ); + })} +
+ )} +
+
+ ); +}; + +export default Notifications; diff --git a/src/services/index.ts b/src/services/index.ts index 30ab56b..777788a 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -5,5 +5,6 @@ export const mavericksApi = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://e-commerce-mavericcks-bn-staging-istf.onrender.com/api/', }), + tagTypes: ['Notifications'], endpoints: () => ({}), }); diff --git a/src/services/notificationsAPI.ts b/src/services/notificationsAPI.ts new file mode 100644 index 0000000..0a14d93 --- /dev/null +++ b/src/services/notificationsAPI.ts @@ -0,0 +1,47 @@ +import { mavericksApi } from '.'; + +const notificationsApi = mavericksApi.injectEndpoints({ + endpoints: builder => ({ + getNotifications: builder.query({ + query: userId => `notifications/${userId}`, + providesTags: ['Notifications'], + }), + markNotificationAsRead: builder.mutation({ + query: ({ isRead, id }) => ({ + url: `notifications/${id}`, + method: 'PATCH', + body: { isRead }, + }), + }), + deleteSingleNotifications: builder.mutation({ + query: notificationID => ({ + url: `notifications/${notificationID}`, + method: 'DELETE', + }), + }), + deleteAllNotifications: builder.mutation({ + query: userId => ({ + url: `notifications/delete/${userId}`, + method: 'DELETE', + }), + invalidatesTags: ['Notifications'], + }), + markAllNotificationsAsRead: builder.mutation({ + query: ({ userId, isRead }) => ({ + url: `notifications/update/${userId}`, + method: 'PATCH', + body: { isRead }, + }), + invalidatesTags: ['Notifications'], + }), + }), + overrideExisting: true, +}); + +export const { + useGetNotificationsQuery, + useDeleteAllNotificationsMutation, + useDeleteSingleNotificationsMutation, + useMarkNotificationAsReadMutation, + useMarkAllNotificationsAsReadMutation, +} = notificationsApi; diff --git a/src/types/Types.ts b/src/types/Types.ts index 5f412df..25aeaa0 100644 --- a/src/types/Types.ts +++ b/src/types/Types.ts @@ -46,4 +46,11 @@ export type CategoryResponse = { ok: boolean; message: string; data: Category[]; -}; \ No newline at end of file +}; +export interface NotificationProps { + id: string; + message: string; + createdAt: string; + updatedAt: string; + isRead: boolean; +}