diff --git a/frontend/src/external/bcanSatchel/actions.ts b/frontend/src/external/bcanSatchel/actions.ts index 253de1e..6b03e1a 100644 --- a/frontend/src/external/bcanSatchel/actions.ts +++ b/frontend/src/external/bcanSatchel/actions.ts @@ -66,3 +66,10 @@ export const updateSearchQuery = action( 'updateSearchQuery', (searchQuery: string) => ({searchQuery}) ) + +export const setNotifications = action( + 'setNotifications', + (notifications: {id: number; title: string; message: string }[]) => ({ + notifications, + }) +) diff --git a/frontend/src/external/bcanSatchel/mutators.ts b/frontend/src/external/bcanSatchel/mutators.ts index 2ec5483..fd98e50 100644 --- a/frontend/src/external/bcanSatchel/mutators.ts +++ b/frontend/src/external/bcanSatchel/mutators.ts @@ -7,7 +7,8 @@ import { updateFilter, updateStartDateFilter, updateEndDateFilter, updateSearchQuery, - updateYearFilter + updateYearFilter, + setNotifications } from './actions'; import { getAppStore } from './store'; @@ -80,3 +81,8 @@ mutator(updateYearFilter, (actionMessage) => { const store = getAppStore(); store.yearFilter = actionMessage.yearFilter; }) + +mutator(setNotifications, (actionMessage) => { + const store = getAppStore(); + store.notifications = actionMessage.notifications; +}) diff --git a/frontend/src/external/bcanSatchel/store.ts b/frontend/src/external/bcanSatchel/store.ts index 205ed5c..9b0ce34 100644 --- a/frontend/src/external/bcanSatchel/store.ts +++ b/frontend/src/external/bcanSatchel/store.ts @@ -13,7 +13,8 @@ export interface AppState { startDateFilter: Date | null; endDateFilter: Date | null; searchQuery: string; - yearFilter:number[] | []; + yearFilter:number[] | null; + notifications: { id: number; title: string; message: string; }[]; } // Define initial state @@ -26,7 +27,8 @@ const initialState: AppState = { startDateFilter: null, endDateFilter: null, searchQuery: '', - yearFilter: [] + yearFilter: null, + notifications: [] }; const store = createStore('appStore', initialState); diff --git a/frontend/src/main-page/dashboard/Dashboard.tsx b/frontend/src/main-page/dashboard/Dashboard.tsx index 86f5092..e754827 100644 --- a/frontend/src/main-page/dashboard/Dashboard.tsx +++ b/frontend/src/main-page/dashboard/Dashboard.tsx @@ -32,7 +32,7 @@ const Dashboard = observer(() => { const uniqueYears = Array.from( new Set( - yearFilter?.length > 0 + yearFilter && yearFilter?.length > 0 ? yearFilter : allGrants.map((g) => new Date(g.application_deadline).getFullYear()) ) diff --git a/frontend/src/main-page/header/Bell.tsx b/frontend/src/main-page/header/Bell.tsx index 8253c61..1aeb811 100644 --- a/frontend/src/main-page/header/Bell.tsx +++ b/frontend/src/main-page/header/Bell.tsx @@ -1,15 +1,20 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faBell } from "@fortawesome/free-solid-svg-icons"; import { useEffect, useState } from "react"; -import { api } from "../../api"; +//import { api } from "../../api"; //todo: swap out dummy data with real api fetch when backend is ready +import NotificationPopup from "../notifications/NotificationPopup"; +import { setNotifications as setNotificationsAction } from "../../external/bcanSatchel/actions"; +import { getAppStore } from "../../external/bcanSatchel/store"; + // get current user id // const currUserID = sessionStorage.getItem('userId'); -const currUserID = "bcanuser33"; +// const currUserID = "bcanuser33"; const BellButton = () => { // stores notifications for the current user - const [notifications, setNotifications] = useState([]); + const store = getAppStore(); + const notifications = store.notifications ?? []; // determines whether bell has been clicked const [isClicked, setClicked] = useState(false); @@ -21,55 +26,45 @@ const BellButton = () => { // function that handles when button is clicked and fetches notifications const handleClick = async () => { - const response = await api( - `/notifications/user/${currUserID}`, - { - method: "GET", - } - ); - console.log(response); - const currNotifications = await response.json(); - setNotifications(currNotifications); + //temporary dummy data for now + const dummyNotifications = [ + {id: 1, title: "Grant Deadline", message: "Grant A deadline approaching in 3 days"}, + {id: 2, title: "Grant Deadline", message: "Grant B deadline tomorrow!"}, + {id: 3, title: "Grant Deadline", message: "Grant C deadline passed yesterday!"}, + {id: 4, title: "Grant Deadline", message: "Grant D deadline tomorrow!"} + ]; + //previous api logic (for later) + //const response = await api( + //`/notifications/user/${currUserID}`, + //{ + //method: "GET", + //} + //); + //console.log(response); + //const currNotifications = await response.json(); + setNotificationsAction(dummyNotifications); setClicked(!isClicked); return notifications; }; + const handleClose = () => setClicked(false); + return ( - <> +
+ {isClicked && ( -
-
-

- {currUserID ? `Notifications for ${currUserID}` : "Notifications"} -

- {notifications.length > 0 ? ( -
    - {notifications.map((notification, index) => ( -
  • - {notification.message}
    - Alert Time: {notification.alertTime} -
  • - ))} -
- ) : ( -

No new notifications

- )} - -
-
+ )} - +
); }; diff --git a/frontend/src/main-page/notifications/GrantNotification.tsx b/frontend/src/main-page/notifications/GrantNotification.tsx new file mode 100644 index 0000000..ef59f2d --- /dev/null +++ b/frontend/src/main-page/notifications/GrantNotification.tsx @@ -0,0 +1,28 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faBell } from "@fortawesome/free-solid-svg-icons"; +import { FaTrash } from "react-icons/fa"; + +interface GrantNotificationProps { + title: string; + message: string; +} + +const GrantNotification: React.FC = ({ title, message }) => { + return ( +
+
+ +
+
+
{title}
+
{message}
+
+ +
+ ); +}; + +export default GrantNotification; diff --git a/frontend/src/main-page/notifications/NotificationPopup.tsx b/frontend/src/main-page/notifications/NotificationPopup.tsx new file mode 100644 index 0000000..26f2be5 --- /dev/null +++ b/frontend/src/main-page/notifications/NotificationPopup.tsx @@ -0,0 +1,37 @@ +import { createPortal } from 'react-dom'; +import GrantNotification from "./GrantNotification"; +import '../../styles/notification.css'; + +interface NotificationPopupProps { + notifications: { id: number; title: string; message: string }[]; + onClose: () => void; +} + +const NotificationPopup: React.FC = ({ + notifications, + onClose +}) => { + return createPortal( +
+
+

Alerts

+ +
+ +
+ {notifications && notifications.length > 0 ? ( + notifications.map((n) => ( + + )) + ) : ( +

No new notifications

+ )} +
+
, + document.body + ); +}; + +export default NotificationPopup; \ No newline at end of file diff --git a/frontend/src/styles/notification.css b/frontend/src/styles/notification.css new file mode 100644 index 0000000..dd6e0f8 --- /dev/null +++ b/frontend/src/styles/notification.css @@ -0,0 +1,110 @@ +.notification-popup { + position: absolute; + right: 7.5rem; + top: 75px; + width: min(340px, 70%); + background-color: white; + border: 1px solid #343131; + border-radius: 6px; + box-shadow: 0 4px 10px rgba(0,0,0,0.1); + padding: 0.5rem; + z-index: 1000; +} + + +.popup-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 0.5rem; + border-bottom: 1px solid #ddd; +} + + +.popup-header h3 { + font-size: 1.1rem; + font-weight: 600; + color: #333; + margin: 0; +} + + +.close-button { + background: none; + border: none; + font-size: 1rem; + cursor: pointer; + color: #777; +} +.close-button:hover { + color: #e74c3c; +} + + +.notification-list { + max-height: 200px; + overflow-y: auto; + margin-top: 10px; +} + + +.grant-notification { + position: relative; + border-bottom: 0.2px solid #dfd9d9; + border-radius: 0px; + padding: 8px 10px; + padding-right: 35px; + margin-bottom: 2px; + font-size: 0.9rem; + transition: background-color 0.2s ease; + display: flex; +} + +.bell-notif { + margin-right: 10px; + background-color: rgb(225, 225, 225); + padding: 10px; + border-radius: 100%; + scale: 73%; + flex-shrink: 0; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; +} + +.notification-text { + flex: 1; +} + +.notification-title { + font-weight: 600; + font-size: 13px; + color: #101010; + line-height: 1.15; +} + +.notification-message { + font-size: 12.5px; + color: #555; + margin-top: 4px; + line-height: 1.2; +} + +.grant-notification:hover { + background-color: #f1f1f1; +} + +.notification-trash-icon { + position: absolute; + bottom: 8px; + right: 10px; + color: #aaa; + cursor: pointer; + font-size: 0.9rem +} + +.notification-trash-icon:hover { + color: #e74c3c +}