diff --git a/package.json b/package.json
index f14e538fc..6ae4d9b6c 100755
--- a/package.json
+++ b/package.json
@@ -69,7 +69,7 @@
"webpack-dev-server": "^4.11.1"
},
"dependencies": {
- "@apollo/client": "^3.8.1",
+ "@apollo/client": "^3.11.8",
"@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4",
"@fortawesome/fontawesome-svg-core": "^6.4.2",
@@ -115,6 +115,8 @@
"patch-package": "^6.4.7",
"path": "^0.12.7",
"popper.js": "^1.16.1",
+ "pusher": "^5.2.0",
+ "pusher-js": "^8.4.0-rc2",
"react-datepicker": "^4.8.0",
"react-hook-form": "^7.45.4",
"react-hot-toast": "^2.4.1",
@@ -136,8 +138,10 @@
"redux-devtools-extension": "^2.13.9",
"redux-state-sync": "^3.1.4",
"redux-thunk": "^2.4.1",
+ "socket.io-client": "^4.8.0",
"stream": "^0.0.2",
"stream-http": "^3.2.0",
+ "subscriptions-transport-ws": "^0.11.0",
"ts-jest": "^29.0.3",
"with-click-outside": "^1.0.1",
"yup": "^1.2.0",
diff --git a/src/App.tsx b/src/App.tsx
index a4285556b..d0248855e 100755
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,8 +1,13 @@
-import React from 'react';
-import AppRoutes from './routes/index';
+import React from "react";
+import AppRoutes from "./routes/index";
+import { NotificationProvider } from "./utils/Notifications";
function App() {
- return ;
+ return (
+
+
+
+ );
}
export default App;
diff --git a/src/components/form/SignInForm.tsx b/src/components/form/SignInForm.tsx
index 6d461429a..592dd4f6d 100644
--- a/src/components/form/SignInForm.tsx
+++ b/src/components/form/SignInForm.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from "react";
+import React, { useState, useEffect, useContext } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faEye, faEyeSlash } from "@fortawesome/free-regular-svg-icons";
import { loginSchema } from "../validation/login";
@@ -14,6 +14,7 @@ import { loginAction } from "../../redux/actions/login";
import { Token } from '../../utils/utils';
import { getUserbyFilter } from "../../redux/actions/users";
import jwtDecode from "jwt-decode";
+import { useNotifications } from "../../utils/Notifications";
const googleIcn: string = require("../../assets/assets/googleIcon.jpg").default;
@@ -44,6 +45,7 @@ const LoginForm = () => {
const [isNormalLogin, setIsNormalLogin] = useState(false);
const navigate = useNavigate();
const location = useLocation();
+ const { setUserId } = useNotifications();
const {
register,
@@ -93,6 +95,7 @@ const LoginForm = () => {
localStorage.setItem("access_token", token);
if (userId) {
localStorage.setItem("userId", userId);
+ setUserId(userId);
}
await redirectAfterLogin();
} else {
@@ -253,4 +256,4 @@ const LoginForm = () => {
);
};
-export default LoginForm;
\ No newline at end of file
+export default LoginForm;
diff --git a/src/components/sidebar/navHeader.tsx b/src/components/sidebar/navHeader.tsx
index 3baacc6e9..578098cf4 100644
--- a/src/components/sidebar/navHeader.tsx
+++ b/src/components/sidebar/navHeader.tsx
@@ -1,5 +1,5 @@
import React, { useState } from "react";
-import { Link } from "react-router-dom";
+import { Link, useNavigate } from "react-router-dom";
import Sidebar from "./sidebar";
import { SunIcon } from "@heroicons/react/outline";
import { MoonIcon } from "@heroicons/react/solid";
@@ -8,48 +8,89 @@ import { IoClose } from "react-icons/io5";
import * as icon from "react-icons/hi2";
import { AiOutlineBell } from "react-icons/ai";
import { useTheme } from "../../hooks/darkmode";
+import jwtDecode from "jwt-decode";
+import { useNotifications } from "../../utils/Notifications";
+
const logo: string = require("../../assets/logo.svg").default;
const profile: string = require("../../assets/avatar.png").default;
const LogoWhite: string = require("../../assets/logoWhite.svg").default;
-import jwtDecode from "jwt-decode";
import {destination} from '../../utils/utils'
import SearchBar from "../../components/SearchBar";
const placeholderImage = profile;
const onImageError = (e) => {
- e.target.src = placeholderImage
-}
+ e.target.src = placeholderImage;
+};
+
+const ThemeToggle = ({ theme, handleToggleTheme }) => (
+
+ {theme ? (
+
+ ) : (
+
+ )}
+
+);
+
+const NotificationBell = ({ unreadCount, handleShowNotification }) => (
+
+
+ {unreadCount > 0 && (
+
+ {unreadCount}
+
+ )}
+
+);
+
+const ProfileSection = ({ user, onImageError, handleShowProfileDropdown }) => (
+
+
+
+);
function NavBar() {
const userDestination = destination();
const access_token = localStorage.getItem("access_token");
//@ts-ignore
const user = access_token ? jwtDecode(access_token).picture : profile;
- const [showNotification, setShowNotification] = useState(false);
- const [showProfileDropdown, setShowprofileDropdown] = useState(false);
- const { theme, setTheme } = useTheme();
- function handleToggleTheme() {
- setTheme(!theme);
- }
+
const [nav, setNav] = useState(false);
- const handleClick = () => setNav(!nav);
- const handleShowNotification = () => setShowNotification(!showNotification);
+ const [showProfileDropdown, setShowProfileDropdown] = useState(false);
+
+ const { theme, setTheme } = useTheme();
+ const handleToggleTheme = () => setTheme(!theme);
+
+ const navigate = useNavigate();
+ const handleShowNotification = () => navigate("/applicant/notifications");
const handleShowProfileDropdown = () =>
- setShowprofileDropdown(!showProfileDropdown);
+ setShowProfileDropdown(!showProfileDropdown);
+
+ const { unreadCount } = useNotifications();
+
+ const handleClick = () => setNav(!nav);
return (
-
-
+
{showProfileDropdown && (
)}
-
-
+
+
{theme ? (
-
+
) : (
)}
-
+
PULSE
@@ -92,38 +129,18 @@ function NavBar() {
{/* End of Search Bar */}
-
- {" "}
- {/* */}
-
-
-
- {theme ? (
-
- ) : (
-
- )}
-
-
-
-
+
+
+
- {/*
-
{}
*/}
);
}
diff --git a/src/components/sidebar/sidebarItems.tsx b/src/components/sidebar/sidebarItems.tsx
index c85018f6d..2a8737b0d 100644
--- a/src/components/sidebar/sidebarItems.tsx
+++ b/src/components/sidebar/sidebarItems.tsx
@@ -79,11 +79,11 @@ export const applicantSidebarItems = [
),
title: "Schedule Interview",
},
- {
- path: "notifications",
- icon:
,
- title: "Notifications",
- },
+ // {
+ // path: "notifications",
+ // icon:
,
+ // title: "Notifications",
+ // },
{
path: "calendar",
icon:
,
diff --git a/src/pages/ApplicantNotifications/AppNotification.tsx b/src/pages/ApplicantNotifications/AppNotification.tsx
new file mode 100644
index 000000000..60ed249c8
--- /dev/null
+++ b/src/pages/ApplicantNotifications/AppNotification.tsx
@@ -0,0 +1,200 @@
+import React, { useState } from "react";
+import { IoMdMailOpen, IoMdMailUnread } from "react-icons/io";
+import { useNotifications } from "../../utils/Notifications";
+
+interface Notification {
+ id: string;
+ message: string;
+ read: boolean;
+ createdAt: string;
+}
+
+function ApplicantNotifications() {
+ const [activeTab, setActiveTab] = useState("All");
+ const [sortOrder, setSortOrder] = useState<"new" | "old">("new");
+ const [searchQuery, setSearchQuery] = useState("");
+
+ const { notifications, markAsRead, markAsUnread, unreadCount } =
+ useNotifications();
+
+ const handleToggleRead = (notification: Notification) =>
+ notification.read
+ ? markAsUnread(notification.id)
+ : markAsRead(notification.id);
+
+ const handleSearchChange = (e: React.ChangeEvent
) =>
+ setSearchQuery(e.target.value);
+
+ const filteredNotifications = filterNotifications(
+ notifications,
+ activeTab,
+ searchQuery
+ );
+ const sortedNotifications = sortNotifications(
+ filteredNotifications,
+ sortOrder
+ );
+
+ return (
+
+
+
+
+ );
+}
+
+const filterNotifications = (
+ notifications: Notification[],
+ activeTab: string,
+ searchQuery: string
+) =>
+ notifications.filter(
+ (notification) =>
+ (activeTab === "Unread" ? !notification.read : true) &&
+ notification.message.toLowerCase().includes(searchQuery.toLowerCase())
+ );
+
+const sortNotifications = (
+ notifications: Notification[],
+ sortOrder: "new" | "old"
+) =>
+ [...notifications].sort((a, b) =>
+ sortOrder === "new"
+ ? new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
+ : new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
+ );
+
+const formatDate = (dateString: string) =>
+ new Date(dateString).toLocaleString("en-US", {
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ hour12: false,
+ });
+
+const Header = ({
+ activeTab,
+ unreadCount,
+ onTabChange,
+ sortOrder,
+ onSortOrderChange,
+ searchQuery,
+ onSearchChange,
+}: {
+ activeTab: string;
+ unreadCount: number;
+ onTabChange: (tab: string) => void;
+ sortOrder: "new" | "old";
+ onSortOrderChange: (order: "new" | "old") => void;
+ searchQuery: string;
+ onSearchChange: (e: React.ChangeEvent) => void;
+}) => (
+
+
+
+
+
+
+
+ Order By:
+
+
+
+
+
+
+
+);
+
+const NotificationList = ({
+ notifications,
+ onToggleRead,
+ formatDate,
+}: {
+ notifications: Notification[];
+ onToggleRead: (notification: Notification) => void;
+ formatDate: (dateString: string) => string;
+}) => (
+
+ {notifications.length > 0 ? (
+ notifications.map((notification) => (
+
+
+
+ {notification.message || "No message content available."}
+
+
+
+
+ {formatDate(notification.createdAt) || "Date not available"}
+
+
+
+
+ ))
+ ) : (
+
+ No notifications available.
+
+ )}
+
+);
+
+export default ApplicantNotifications;
diff --git a/src/routes/routes.tsx b/src/routes/routes.tsx
index bbf541989..0006edc91 100644
--- a/src/routes/routes.tsx
+++ b/src/routes/routes.tsx
@@ -49,6 +49,7 @@ import Dashboard from "../pages/Dashboard";
import ApplicantLayout from "../pages/Applicant/ApplicantLayout";
import AdminLayout from "../components/Layout/Admins/AdminLayout";
import GoogleSignup from "./../components/form/GoogleSignup";
+import ApplicantNotifications from "../pages/ApplicantNotifications/AppNotification";
import ApplicantDashboard from "../pages/Applicant/ApplicantDashboard";
import UpdateJobPost from "../pages/JobPost/updateJobPost";
import VerifyEmail from "../pages/verifyEmail";
@@ -68,10 +69,18 @@ function Navigation() {
} />
}/>
} />
- :
- roleName === 'Applicant' ? :
- } />
+
+ ) : roleName === 'Applicant' ? (
+
+ ) : (
+
+ )
+ }
+ />
{/* Admin Routes (Protected) */}
-
+
{/* Applicant Routes (Protected) */}
+
+
+
+ }
+ />
+ {/*
+
+
+ }
+ /> */}
}>
} />
-
+
}
/>
-
+
{/* Catch-All Route */}
+
}
@@ -374,4 +399,4 @@ function Navigation() {
);
}
-export default Navigation;
\ No newline at end of file
+export default Navigation;
diff --git a/src/utils/Notifications.tsx b/src/utils/Notifications.tsx
new file mode 100644
index 000000000..c511e61fa
--- /dev/null
+++ b/src/utils/Notifications.tsx
@@ -0,0 +1,98 @@
+import React, { createContext, useState, useContext } from "react";
+import { toast, ToastContainer } from "react-toastify";
+import "react-toastify/dist/ReactToastify.css";
+import { toastOptions } from "./toast";
+import { useFetchNotifications } from "./applicantNotifications/useFetchNotifications";
+import { usePusherNotifications } from "./applicantNotifications/usePusher";
+import { updateNotificationStatus } from "./applicantNotifications/NotificationService";
+import { Notification } from "./applicantNotifications/types";
+
+interface NotificationContextProps {
+ notifications: Notification[];
+ unreadCount: number;
+ markAsRead: (id: string) => void;
+ markAsUnread: (id: string) => void;
+ userId: string | null;
+ setUserId: (userId: string | null) => void;
+}
+
+const NotificationContext = createContext(
+ undefined
+);
+
+export const useNotifications = () => {
+ const context = useContext(NotificationContext);
+ if (!context) {
+ throw new Error(
+ "useNotifications must be used within a NotificationProvider"
+ );
+ }
+ return context;
+};
+
+export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({
+ children,
+}) => {
+ const { notifications, unreadCount, markAsRead, markAsUnread,userId, setUserId} =
+ useNotificationsState();
+
+ return (
+
+ {children}
+
+
+ );
+};
+
+const useNotificationsState = () => {
+ const [notifications, setNotifications] = useState([]);
+ const [unreadCount, setUnreadCount] = useState(0);
+ const [userId, setUserId] = useState(
+ localStorage.getItem("userId")
+ );
+
+
+ const addNotification = (notification: Notification) => {
+ setNotifications((prev) => [notification, ...prev]);
+ setUnreadCount((prev) => prev + 1);
+ toast.info(`New notification: ${notification.message}`, toastOptions);
+ };
+
+ useFetchNotifications(userId, setNotifications, setUnreadCount);
+ usePusherNotifications(userId, addNotification);
+
+ const updateNotificationReadStatus = (id: string, read: boolean) => {
+ setNotifications((prev) =>
+ prev.map((n) => (n.id === id ? { ...n, read } : n))
+ );
+ setUnreadCount((prev) => prev + (read ? -1 : 1));
+ };
+
+ const markAsRead = async (id: string) => {
+ await updateNotificationStatus(id, true);
+ updateNotificationReadStatus(id, true);
+ };
+
+ const markAsUnread = async (id: string) => {
+ await updateNotificationStatus(id, false);
+ updateNotificationReadStatus(id, false);
+ };
+
+ return {
+ notifications,
+ unreadCount,
+ markAsRead,
+ markAsUnread,
+ userId,
+ setUserId
+ };
+};
diff --git a/src/utils/applicantNotifications/NotificationService.ts b/src/utils/applicantNotifications/NotificationService.ts
new file mode 100644
index 000000000..2e3e57905
--- /dev/null
+++ b/src/utils/applicantNotifications/NotificationService.ts
@@ -0,0 +1,47 @@
+import { Notification } from "./types";
+
+export const fetchNotifications = async (
+ userId: string
+): Promise => {
+ const response = await fetch(`${process.env.BACKEND_URL}/`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ query: `query GetNotifications($userId: ID!) {
+ getNotifications(userId: $userId) {
+ id
+ message
+ read
+ createdAt
+ }
+ }`,
+ variables: { userId },
+ }),
+ });
+
+ const result = await response.json();
+ return result.data?.getNotifications || [];
+};
+
+export const updateNotificationStatus = async (id: string, read: boolean) => {
+ const response = await fetch(`${process.env.BACKEND_URL}/`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ query: `mutation MarkNotificationAsRead($id: ID!) {
+ markNotificationAsRead(id: $id) {
+ id
+ read
+ }
+ }`,
+ variables: { id },
+ }),
+ });
+
+ const result = await response.json();
+ return result.data?.markNotificationAsRead.read;
+};
diff --git a/src/utils/applicantNotifications/pusher.ts b/src/utils/applicantNotifications/pusher.ts
new file mode 100644
index 000000000..b5a7723e5
--- /dev/null
+++ b/src/utils/applicantNotifications/pusher.ts
@@ -0,0 +1,23 @@
+import Pusher, { Channel } from "pusher-js";
+import { Notification } from "./types";
+
+export const initializePusher = (
+ userId: string,
+ onNewNotification: (notification: Notification) => void
+): Channel => {
+ const pusher = new Pusher(process.env.PUSHER_KEY!, {
+ cluster: process.env.PUSHER_CLUSTER!,
+ });
+
+ const channel = pusher.subscribe(`notifications-${userId}`);
+ channel.bind("new-notification", onNewNotification);
+
+ return channel;
+};
+
+export const unsubscribePusher = (channel: Channel | null) => {
+ if (channel) {
+ channel.unbind_all();
+ channel.unsubscribe();
+ }
+};
diff --git a/src/utils/applicantNotifications/types.ts b/src/utils/applicantNotifications/types.ts
new file mode 100644
index 000000000..f9184864f
--- /dev/null
+++ b/src/utils/applicantNotifications/types.ts
@@ -0,0 +1,6 @@
+export interface Notification {
+ id: string;
+ message: string;
+ read: boolean;
+ createdAt: string;
+}
diff --git a/src/utils/applicantNotifications/useFetchNotifications.ts b/src/utils/applicantNotifications/useFetchNotifications.ts
new file mode 100644
index 000000000..91965adae
--- /dev/null
+++ b/src/utils/applicantNotifications/useFetchNotifications.ts
@@ -0,0 +1,21 @@
+import { useEffect } from "react";
+import { Notification } from "./types";
+import { fetchNotifications } from "./NotificationService";
+
+export const useFetchNotifications = (
+ userId: string | null,
+ setNotifications: (notifications: Notification[]) => void,
+ setUnreadCount: (count: number) => void
+) => {
+ useEffect(() => {
+ if (userId) {
+ fetchAndSetNotifications(userId);
+ }
+ }, [userId]);
+
+ const fetchAndSetNotifications = async (userId: string) => {
+ const fetchedNotifications = await fetchNotifications(userId);
+ setNotifications(fetchedNotifications);
+ setUnreadCount(fetchedNotifications.filter((n) => !n.read).length);
+ };
+};
diff --git a/src/utils/applicantNotifications/usePusher.ts b/src/utils/applicantNotifications/usePusher.ts
new file mode 100644
index 000000000..31b2edb62
--- /dev/null
+++ b/src/utils/applicantNotifications/usePusher.ts
@@ -0,0 +1,22 @@
+import { useEffect, useState } from "react";
+import { initializePusher, unsubscribePusher } from "./pusher";
+import { Notification } from "./types";
+import { Channel } from "pusher-js";
+
+export const usePusherNotifications = (
+ userId: string | null,
+ addNotification: (notification: Notification) => void
+) => {
+ const [channel, setChannel] = useState(null);
+
+ useEffect(() => {
+ if (userId) {
+ const pusherChannel = initializePusher(userId, addNotification);
+ setChannel(pusherChannel);
+ }
+
+ return () => unsubscribePusher(channel);
+ }, [userId]);
+
+ return channel;
+};
diff --git a/src/utils/toast.tsx b/src/utils/toast.tsx
index be34ea852..57db9e74a 100644
--- a/src/utils/toast.tsx
+++ b/src/utils/toast.tsx
@@ -1,4 +1,4 @@
-import { toast } from 'react-toastify';
+import { toast, ToastOptions } from 'react-toastify';
export const showSuccessToast = (message: string) => {
toast.success(message);
@@ -6,4 +6,24 @@ export const showSuccessToast = (message: string) => {
export const showErrorToast = (message: string) => {
toast.error(message);
-};
\ No newline at end of file
+};
+
+export const toastOptions: ToastOptions = {
+ position: "bottom-right",
+ autoClose: 5000,
+ hideProgressBar: true,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ className: "small-toast",
+ bodyClassName: "small-toast-body",
+ style: {
+ background: "#01acf0",
+ color: "#ffffff",
+ borderRadius: "10px",
+ padding: "10px",
+ fontSize: "0.875rem",
+ boxShadow: "0px 4px 10px rgba(0, 0, 0, 0.1)",
+ },
+};
diff --git a/src/utils/utils.tsx b/src/utils/utils.tsx
index f0d2a5031..c497d4b5b 100755
--- a/src/utils/utils.tsx
+++ b/src/utils/utils.tsx
@@ -31,6 +31,7 @@ export const Token = () => {
if (data) {
const roleName = data.checkUserRole?.roleName;
localStorage.setItem('roleName', roleName);
+ localStorage.setItem('userId', decoded.data.userId);
}
});
return decoded;