Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions frontend/src/external/bcanSatchel/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
)
8 changes: 7 additions & 1 deletion frontend/src/external/bcanSatchel/mutators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
updateFilter,
updateStartDateFilter, updateEndDateFilter,
updateSearchQuery,
updateYearFilter
updateYearFilter,
setNotifications
} from './actions';
import { getAppStore } from './store';

Expand Down Expand Up @@ -80,3 +81,8 @@ mutator(updateYearFilter, (actionMessage) => {
const store = getAppStore();
store.yearFilter = actionMessage.yearFilter;
})

mutator(setNotifications, (actionMessage) => {
const store = getAppStore();
store.notifications = actionMessage.notifications;
})
6 changes: 4 additions & 2 deletions frontend/src/external/bcanSatchel/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -26,7 +27,8 @@ const initialState: AppState = {
startDateFilter: null,
endDateFilter: null,
searchQuery: '',
yearFilter: []
yearFilter: null,
notifications: []
};

const store = createStore<AppState>('appStore', initialState);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/main-page/dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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())
)
Expand Down
73 changes: 34 additions & 39 deletions frontend/src/main-page/header/Bell.tsx
Original file line number Diff line number Diff line change
@@ -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<any[]>([]);
const store = getAppStore();
const notifications = store.notifications ?? [];

// determines whether bell has been clicked
const [isClicked, setClicked] = useState(false);
Expand All @@ -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 (
<>
<div className="bell-container">
<button
className={`bell-button ${isClicked ? "hovered" : ""}`}
onClick={handleClick}
>
<FontAwesomeIcon icon={faBell} style={{ color: "black" }} />
</button>

{isClicked && (
<div className="notification-modal">
<div className="notification-modal-content">
<h4>
{currUserID ? `Notifications for ${currUserID}` : "Notifications"}
</h4>
{notifications.length > 0 ? (
<ul>
{notifications.map((notification, index) => (
<li key={index} className="notification-item">
{notification.message} <br />
<small>Alert Time: {notification.alertTime}</small>
</li>
))}
</ul>
) : (
<p>No new notifications</p>
)}
<button
onClick={() => setClicked(false)}
className="notification-close-button"
>
Close
</button>
</div>
</div>
<NotificationPopup
notifications={notifications}
onClose={handleClose}
/>
)}
</>
</div>
);
};

Expand Down
28 changes: 28 additions & 0 deletions frontend/src/main-page/notifications/GrantNotification.tsx
Original file line number Diff line number Diff line change
@@ -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<GrantNotificationProps> = ({ title, message }) => {
return (
<div className="grant-notification" role="listitem">
<div className="bell-notif">
<FontAwesomeIcon icon={faBell} style={{ color: "gray"}} />
</div>
<div className="notification-text">
<div className="notification-title">{title}</div>
<div className="notification-message">{message}</div>
</div>
<FaTrash
className="notification-trash-icon"
title="Delete notification"
/>
</div>
);
};

export default GrantNotification;
37 changes: 37 additions & 0 deletions frontend/src/main-page/notifications/NotificationPopup.tsx
Original file line number Diff line number Diff line change
@@ -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<NotificationPopupProps> = ({
notifications,
onClose
}) => {
return createPortal(
<div className="notification-popup" role="dialog" aria-label="Notifications">
<div className="popup-header">
<h3>Alerts</h3>
<button className="close-button" onClick={onClose} aria-label="Close notifications">
</button>
</div>

<div className="notification-list">
{notifications && notifications.length > 0 ? (
notifications.map((n) => (
<GrantNotification key={n.id} title={n.title} message={n.message} />
))
) : (
<p className="empty-text">No new notifications</p>
)}
</div>
</div>,
document.body
);
};

export default NotificationPopup;
110 changes: 110 additions & 0 deletions frontend/src/styles/notification.css
Original file line number Diff line number Diff line change
@@ -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
}