diff --git a/backend/dist/main.js b/backend/dist/main.js index 75b977a..f9de4e8 100644 --- a/backend/dist/main.js +++ b/backend/dist/main.js @@ -30,6 +30,7 @@ const core_1 = require("@nestjs/core"); const app_module_1 = require("./app.module"); const dotenv = __importStar(require("dotenv")); const aws_sdk_1 = __importDefault(require("aws-sdk")); +aws_sdk_1.default.config.update({ region: 'us-east-2' }); /* ! */ async function bootstrap() { aws_sdk_1.default.config.update({ diff --git a/backend/dist/user/user.service.js b/backend/dist/user/user.service.js index 69e6362..7473221 100644 --- a/backend/dist/user/user.service.js +++ b/backend/dist/user/user.service.js @@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.UserService = void 0; const common_1 = require("@nestjs/common"); const aws_sdk_1 = __importDefault(require("aws-sdk")); +aws_sdk_1.default.config.update({ region: 'us-east-2' }); // need to explicitly mention the region here otherwise an error is thrown for some reason const dynamodb = new aws_sdk_1.default.DynamoDB.DocumentClient(); let UserService = class UserService { async getAllUsers() { @@ -23,6 +24,7 @@ let UserService = class UserService { return data.Items; } catch (error) { + console.log(error); throw new Error('Could not retrieve users.'); } } @@ -38,6 +40,7 @@ let UserService = class UserService { return data.Item; } catch (error) { + console.log(error); throw new Error('Could not retrieve user.'); } } diff --git a/backend/src/main.ts b/backend/src/main.ts index b8ddad5..a0b87a7 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -3,6 +3,8 @@ import { AppModule } from './app.module'; import * as dotenv from 'dotenv' import AWS from 'aws-sdk'; +AWS.config.update({ region: 'us-east-2' }); + /* ! */ async function bootstrap() { AWS.config.update({ diff --git a/backend/src/notifications/notifcation.service.ts b/backend/src/notifications/notifcation.service.ts index be72104..5f03bc4 100644 --- a/backend/src/notifications/notifcation.service.ts +++ b/backend/src/notifications/notifcation.service.ts @@ -1,12 +1,11 @@ // src/notifications/notifications.service.ts import { Injectable } from '@nestjs/common'; import * as AWS from 'aws-sdk'; -import { Notification } from './notification.model'; // Adjust the path as needed +import { Notification } from './notification.model'; - -AWS.config.update({ region: 'us-east-1' }); +AWS.config.update({ region: 'us-east-1' }); const dynamodb = new AWS.DynamoDB.DocumentClient(); @@ -14,10 +13,10 @@ const dynamodb = new AWS.DynamoDB.DocumentClient(); export class NotificationService { - // Function to create a notification + // function to create a notification async createNotification(notification: Notification): Promise { - const alertTime = new Date(notification.alertTime); // Ensures a Date can be created from the given alertTime + const alertTime = new Date(notification.alertTime); // ensures a Date can be created from the given alertTime const params = { @@ -32,31 +31,73 @@ export class NotificationService { } - // function to find notifications by notification id - async getNotificationByUserId(userId: string): Promise { + // function that returns array of notifications by user id (sorted by most recent notifications first) + async getNotificationByUserId(userId: string): Promise { + console.log("USER ID", userId) - const params = { - TableName: process.env.DYNAMODB_NOTIFICATION_TABLE_NAME || 'TABLE_FAILURE', - Key: { - userId : userId - }, - }; + // KeyConditionExpression specifies the query condition + // ExpressionAttributeValues specifies the actual value of the key + // IndexName specifies our Global Secondary Index, which was created in the BCANNotifs table to + // allow for querying by userId, as it is not a primary/partition key + const params = { + TableName: process.env.DYNAMODB_NOTIFICATION_TABLE_NAME || 'TABLE_FAILURE', + IndexName: 'userId-alertTime-index', + KeyConditionExpression: 'userId = :userId', + ExpressionAttributeValues: { + ':userId': userId, + }, + ScanIndexForward: false // sort in descending order + }; try { - const data = await dynamodb.get(params).promise(); + const data = await dynamodb.query(params).promise(); - if (!data.Item) { - throw new Error('No notification with user id ' + userId + ' found.'); + if (!data.Items) { + throw new Error('No notifications with user id ' + userId + ' found.'); } - return data.Item as Notification; + return data.Items as Notification[]; } catch (error) { console.log(error) - throw new Error('Failed to retrieve notification.'); + throw new Error('Failed to retrieve notifications.'); } } + + + // function that returns array of notifications by notification id + async getNotificationByNotificationId(notificationId: string): Promise { + + console.log("NOTIF ID", notificationId) + + // key condition expression specifies the query condition + // expression attribute values specifies the actual value of the key + const params = { + TableName: process.env.DYNAMODB_NOTIFICATION_TABLE_NAME || 'TABLE_FAILURE', + KeyConditionExpression: 'notificationId = :notificationId', + ExpressionAttributeValues: { + ':notificationId': notificationId, + }, + }; + + + try { + const data = await dynamodb.query(params).promise(); + + + if (!data.Items) { + throw new Error('No notifications with notification id ' + notificationId + ' found.'); + } + + + return data.Items as Notification[]; + } catch (error) { + console.log(error) + throw new Error('Failed to retrieve notification.'); + } + } + } diff --git a/backend/src/notifications/notification.controller.ts b/backend/src/notifications/notification.controller.ts index 9f12ad3..8bdd926 100644 --- a/backend/src/notifications/notification.controller.ts +++ b/backend/src/notifications/notification.controller.ts @@ -1,5 +1,5 @@ // src/notifications/notifications.controller.ts -import { Controller, Post, Body, Get, Query } from '@nestjs/common'; +import { Controller, Post, Body, Get, Query, Param } from '@nestjs/common'; import { NotificationService } from './notifcation.service'; import { Notification } from './notification.model'; @@ -11,15 +11,22 @@ export class NotificationController { constructor(private readonly notificationService: NotificationService) { } + // allows to create a new notification @Post() async create(@Body() notification: Partial): Promise { - // Call the service's createNotification method and return the result + // call the service's createNotification method and return the result return await this.notificationService.createNotification(notification as Notification); } + // gets notifications based on the noticationId + @Get(':notificationId') + async findByNotification(@Param('notificationId') notificationId: string) { + return await this.notificationService.getNotificationByNotificationId(notificationId); + } - @Get(':userId') - async findByUser(@Query('userId') userId: string) { + // gets notifications by user id (sorted by most recent notifications first) + @Get('/user/:userId') + async findByUser(@Param('userId') userId: string) { return await this.notificationService.getNotificationByUserId(userId); } diff --git a/backend/src/user/user.service.ts b/backend/src/user/user.service.ts index 1a65b9f..2bbc60e 100644 --- a/backend/src/user/user.service.ts +++ b/backend/src/user/user.service.ts @@ -1,6 +1,8 @@ import { Injectable } from '@nestjs/common'; import AWS from 'aws-sdk'; +AWS.config.update({ region: 'us-east-2' }); // need to explicitly mention the region here otherwise an error is thrown for some reason + const dynamodb = new AWS.DynamoDB.DocumentClient(); @Injectable() @@ -14,6 +16,7 @@ export class UserService { const data = await dynamodb.scan(params).promise(); return data.Items; } catch (error) { + console.log(error) throw new Error('Could not retrieve users.'); } } @@ -30,6 +33,7 @@ export class UserService { const data = await dynamodb.get(params).promise(); return data.Item; } catch (error) { + console.log(error) throw new Error('Could not retrieve user.'); } } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2599be2..b7478c6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,9 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.7.1", + "@fortawesome/free-solid-svg-icons": "^6.7.1", + "@fortawesome/react-fontawesome": "^0.2.2", "@types/react-router-dom": "^5.3.3", "core-js": "^3.38.1", "mobx": "^6.13.2", @@ -19,8 +22,8 @@ }, "devDependencies": { "@eslint/js": "^9.9.0", - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", + "@types/react": "^18.3.13", + "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.1", "eslint": "^9.9.0", "eslint-plugin-react-hooks": "^5.1.0-rc.0", @@ -872,6 +875,48 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.1.tgz", + "integrity": "sha512-gbDz3TwRrIPT3i0cDfujhshnXO9z03IT1UKRIVi/VEjpNHtSBIP2o5XSm+e816FzzCFEzAxPw09Z13n20PaQJQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.1.tgz", + "integrity": "sha512-8dBIHbfsKlCk2jHQ9PoRBg2Z+4TwyE3vZICSnoDlnsHA6SiMlTwfmW6yX0lHsRmWJugkeb92sA0hZdkXJhuz+g==", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.7.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.1.tgz", + "integrity": "sha512-BTKc0b0mgjWZ2UDKVgmwaE0qt0cZs6ITcDgjrti5f/ki7aF5zs+N91V6hitGo3TItCFtnKg6cUVGdTmBFICFRg==", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.7.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/react-fontawesome": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz", + "integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~1 || ~6", + "react": ">=16.3" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -1289,21 +1334,19 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.8", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.8.tgz", - "integrity": "sha512-syBUrW3/XpnW4WJ41Pft+I+aPoDVbrBVQGEnbD7NijDGlVC+8gV/XKRY+7vMDlfPpbwYt0l1vd/Sj8bJGMbs9Q==", - "license": "MIT", + "version": "18.3.13", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.13.tgz", + "integrity": "sha512-ii/gswMmOievxAJed4PAHT949bpYjPKXvXo1v6cRB/kqc2ZR4n+SgyCyvyc5Fec5ez8VnUumI1Vk7j6fRyRogg==", "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", - "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/react": "*" } @@ -2917,6 +2960,16 @@ "asap": "~2.0.6" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2999,6 +3052,11 @@ "react": "^18.3.1" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 2db6415..248d0dc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,9 @@ "preview": "vite preview" }, "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.7.1", + "@fortawesome/free-solid-svg-icons": "^6.7.1", + "@fortawesome/react-fontawesome": "^0.2.2", "@types/react-router-dom": "^5.3.3", "core-js": "^3.38.1", "mobx": "^6.13.2", @@ -21,8 +24,8 @@ }, "devDependencies": { "@eslint/js": "^9.9.0", - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", + "@types/react": "^18.3.13", + "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.1", "eslint": "^9.9.0", "eslint-plugin-react-hooks": "^5.1.0-rc.0", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8572c85..6a5d319 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,23 +1,25 @@ // src/App.tsx -import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-dom'; +import { BrowserRouter as Router, Route} from 'react-router-dom'; +import { Navigate } from '../node_modules/react-router-dom/dist/index.js'; +import { Routes } from '../node_modules/react-router-dom/dist/index.js'; import { observer } from 'mobx-react-lite'; -import Login from './Login'; -import Register from './Register'; -import Dashboard from './Dashboard'; +import Login from './Login.js'; +import Register from './Register.js'; +import Dashboard from './Dashboard.js'; import './App.css'; // Register store and mutators import './external/bcanSatchel/mutators'; -import { getStore } from './external/bcanSatchel/store'; -import GrantPage from "./grant-info/components/GrantPage.tsx"; +import { getStore } from './external/bcanSatchel/store.js'; +import GrantPage from "./grant-info/components/GrantPage.js"; const App = observer(() => { const store = getStore(); return ( - < div className="app-container"> +
{ + + + // stores notifications for the current user + const [notifications, setNotifications] = useState([]) + + // determines whether bell has been clicked + const [isClicked, setClicked] = useState(false) + + // logs the notifications for the current user whenever they are fetched + useEffect(() => { + console.log(notifications) + }, [notifications]); + + + // function that handles when button is clicked and fetches notifications + const handleClick = async () => { + const response = await fetch(`http://localhost:3001/notifications/user/${currUserID}`, { + method: 'GET' + }); + const currNotifications = await response.json() + setNotifications(currNotifications) + setClicked(!isClicked) + return notifications + } + + return ( + <> + + {isClicked && +
+
+

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

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

No new notifications

+ )} + +
+
} + + ); +}; + +export default BellButton; diff --git a/frontend/src/Dashboard.tsx b/frontend/src/Dashboard.tsx index 5555fcc..fe2da00 100644 --- a/frontend/src/Dashboard.tsx +++ b/frontend/src/Dashboard.tsx @@ -1,14 +1,15 @@ // src/Dashboard.tsx import { observer } from 'mobx-react-lite'; -import { getStore } from './external/bcanSatchel/store'; -import { logout } from './external/bcanSatchel/actions'; -import Profile from './Profile'; +import { getStore } from './external/bcanSatchel/store.js'; +import { logout } from './external/bcanSatchel/actions.js'; +import Profile from './Profile.js'; const Dashboard = observer(() => { const store = getStore(); const handleLogout = () => { + sessionStorage.clear() // clear session storage while logging out to remove user data logout(); }; diff --git a/frontend/src/Login.tsx b/frontend/src/Login.tsx index ebd65df..b11aa75 100644 --- a/frontend/src/Login.tsx +++ b/frontend/src/Login.tsx @@ -1,10 +1,12 @@ // src/Login.tsx import React, { useState } from 'react'; -import { setAuthentication } from './external/bcanSatchel/actions'; // Corrected import path +import { setAuthentication } from './external/bcanSatchel/actions.js'; // Corrected import path import { observer } from 'mobx-react-lite'; import { useNavigate } from 'react-router-dom'; +console.log("user id before logging in: " , sessionStorage.getItem('userId')) + const Login = observer(() => { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); @@ -30,6 +32,13 @@ const Login = observer(() => { console.log('Login response data:', data); if (data.access_token) { + // store the username in session storage for use by other functions + sessionStorage.setItem('userId', data.user.userId); + + // console.log(data.user) + + console.log('Username stored:', sessionStorage.getItem('userId')); + setAuthentication(true, data.user, data.access_token); navigate('/dashboard'); alert(data.message); // Alert wit message from backend indicating success diff --git a/frontend/src/Profile.tsx b/frontend/src/Profile.tsx index 83ff348..94a2f5c 100644 --- a/frontend/src/Profile.tsx +++ b/frontend/src/Profile.tsx @@ -2,8 +2,8 @@ import React, { useState } from 'react'; import { observer } from 'mobx-react-lite'; -import { getStore } from './external/bcanSatchel/store'; -import { updateUserProfile } from './external/bcanSatchel/actions'; +import { getStore } from './external/bcanSatchel/store.js'; +import { updateUserProfile } from './external/bcanSatchel/actions.js'; const Profile = observer(() => { const store = getStore(); diff --git a/frontend/src/Register.tsx b/frontend/src/Register.tsx index de3b9d7..9780313 100644 --- a/frontend/src/Register.tsx +++ b/frontend/src/Register.tsx @@ -1,9 +1,7 @@ -// src/Register.tsx - -import React, { useState } from 'react'; -import { setAuthentication } from './external/bcanSatchel/actions'; +import { useState } from 'react'; +import { setAuthentication } from './external/bcanSatchel/actions.js'; import { observer } from 'mobx-react-lite'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate } from '../node_modules/react-router-dom/dist/index.js'; const Register = observer(() => { const [username, setUsername] = useState(''); @@ -45,12 +43,12 @@ const Register = observer(() => { return (

Register

diff --git a/frontend/src/grant-info/components/Footer.tsx b/frontend/src/grant-info/components/Footer.tsx index 8b85b5b..20b23d8 100644 --- a/frontend/src/grant-info/components/Footer.tsx +++ b/frontend/src/grant-info/components/Footer.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import Pagination from "./Pagination"; +import Pagination from "./Pagination.js"; import './styles/Footer.css' const Footer: React.FC = () => { diff --git a/frontend/src/grant-info/components/GrantList.tsx b/frontend/src/grant-info/components/GrantList.tsx index a1a5bed..ab158e3 100644 --- a/frontend/src/grant-info/components/GrantList.tsx +++ b/frontend/src/grant-info/components/GrantList.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import GrantItem from './GrantItem'; +import GrantItem from './GrantItem.js'; import './styles/GrantList.css' const GrantList: React.FC = () => { diff --git a/frontend/src/grant-info/components/GrantPage.tsx b/frontend/src/grant-info/components/GrantPage.tsx index 1c630bc..310f75f 100644 --- a/frontend/src/grant-info/components/GrantPage.tsx +++ b/frontend/src/grant-info/components/GrantPage.tsx @@ -1,7 +1,10 @@ import './styles/GrantPage.css' -import Header from './Header'; -import GrantList from './GrantList'; -import Footer from './Footer'; +import Header from './Header.js'; +import GrantList from './GrantList.js'; +import Footer from './Footer.js'; +import BellButton from '../../Bell.js'; +import '../../Bell.css' + function GrantPage() { @@ -9,6 +12,10 @@ function GrantPage() {
+ +
+
+
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index d97fb91..7383b05 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -3,8 +3,8 @@ import 'react-app-polyfill/stable'; // For stable polyfills for older browsers import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; -import App from './App'; -import { AuthProvider } from './context/auth/authContext'; +import App from './App.js'; +import { AuthProvider } from './context/auth/authContext.js'; ReactDOM.render( diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index f0a2350..21f3a60 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -5,10 +5,7 @@ "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, + "moduleResolution": "node16", "isolatedModules": true, "moduleDetection": "force", "noEmit": true,