diff --git a/backend/src/notifications/__test__/notification.service.test.ts b/backend/src/notifications/__test__/notification.service.test.ts index 791af140..3837560e 100644 --- a/backend/src/notifications/__test__/notification.service.test.ts +++ b/backend/src/notifications/__test__/notification.service.test.ts @@ -273,17 +273,17 @@ describe('NotificationController', () => { - it('should throw error when notifications is null', async () => { + it('should throw error when notifications is null', async () => { // Arrange - Setup query mock to return no items const mockQueryResponse = { - Items: null // or undefined or [] + Items: [] // Empty array instead of null }; mockQuery.mockReturnValue({ promise: vi.fn().mockResolvedValue(mockQueryResponse) }); // Act & Assert - await expect(notificationService.getNotificationByUserId('nonexistent-user')) - .rejects.toThrow('Failed to retrieve notifications.'); + const result = await notificationService.getNotificationByUserId('nonexistent-user'); + expect(result).toEqual([]); }); it('should create notification with valid data in the set table', async () => { diff --git a/backend/src/notifications/notification.controller.ts b/backend/src/notifications/notification.controller.ts index fd1d45cd..f0aadfe4 100644 --- a/backend/src/notifications/notification.controller.ts +++ b/backend/src/notifications/notification.controller.ts @@ -21,6 +21,11 @@ export class NotificationController { return await this.notificationService.getNotificationByNotificationId(notificationId); } + @Get('/user/:userId/current') + async findCurrentByUser(@Param('userId') userId: string) { + return await this.notificationService.getCurrentNotificationsByUserId(userId); + } + // gets notifications by user id (sorted by most recent notifications first) @Get('/user/:userId') async findByUser(@Param('userId') userId: string) { diff --git a/backend/src/notifications/notification.service.ts b/backend/src/notifications/notification.service.ts index ade1ec9f..2a369be0 100644 --- a/backend/src/notifications/notification.service.ts +++ b/backend/src/notifications/notification.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import * as AWS from 'aws-sdk'; import { Notification } from '../../../middle-layer/types/Notification'; @@ -7,6 +7,8 @@ export class NotificationService { private dynamoDb = new AWS.DynamoDB.DocumentClient(); private ses = new AWS.SES({ region: process.env.AWS_REGION }); + private readonly logger = new Logger(NotificationService.name); + // function to create a notification // Should this have a check to prevent duplicate notifications? @@ -25,6 +27,14 @@ export class NotificationService { return notification; } + async getCurrentNotificationsByUserId(userId: string): Promise { + const notifactions = await this.getNotificationByUserId(userId); + + const currentTime = new Date(); + + return notifactions.filter(notification => new Date(notification.alertTime) <= currentTime); + } + // function that returns array of notifications by user id (sorted by most recent notifications first) async getNotificationByUserId(userId: string): Promise { @@ -33,8 +43,15 @@ export class NotificationService { // 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 notificationTableName = process.env.DYNAMODB_NOTIFICATION_TABLE_NAME; + this.logger.log(`Fetching notifications for userId: ${userId} from table: ${notificationTableName}`); + + if (!notificationTableName) { + this.logger.error('DYNAMODB_NOTIFICATION_TABLE_NAME is not defined in environment variables'); + throw new Error("Internal Server Error") + } const params = { - TableName: process.env.DYNAMODB_NOTIFICATION_TABLE_NAME || 'TABLE_FAILURE', + TableName: notificationTableName, IndexName: 'userId-alertTime-index', KeyConditionExpression: 'userId = :userId', ExpressionAttributeValues: { @@ -47,14 +64,16 @@ export class NotificationService { const data = await this.dynamoDb.query(params).promise(); + // This is never hit, because no present userId throws an error if (!data || !data.Items || data.Items.length == 0) { - throw new Error('No notifications with user id ' + userId + ' found.'); + this.logger.warn(`No notifications found for userId: ${userId}`); + return [] as Notification[]; } return data.Items as Notification[]; } catch (error) { - console.log(error) + this.logger.error(`Error retrieving notifications for userId: ${userId}`, error as string); throw new Error('Failed to retrieve notifications.'); } } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 635d93e1..84cdaaad 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -285,6 +285,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -703,6 +704,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -726,6 +728,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -736,7 +739,6 @@ "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -799,6 +801,7 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1512,7 +1515,6 @@ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.0.1.tgz", "integrity": "sha512-0VpNtO5cNe1/HQWMkl4OdncYK/mv9hnBte0Ew0n6DMzmo3Q3WzDFABHm6LeNTipt5zAyhQ6Ugjiu8aLaEjh1gg==", "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -2575,7 +2577,6 @@ "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=18" } @@ -2592,7 +2593,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -2615,7 +2615,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -2638,7 +2637,6 @@ "os": [ "darwin" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2655,7 +2653,6 @@ "os": [ "darwin" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2672,7 +2669,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2689,7 +2685,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2706,7 +2701,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2723,7 +2717,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2740,7 +2733,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2757,7 +2749,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2774,7 +2765,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2791,7 +2781,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -2814,7 +2803,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -2837,7 +2825,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -2860,7 +2847,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -2883,7 +2869,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -2906,7 +2891,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -2929,7 +2913,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -2949,7 +2932,6 @@ ], "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, - "peer": true, "dependencies": { "@emnapi/runtime": "^1.5.0" }, @@ -2972,7 +2954,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -2992,7 +2973,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3012,7 +2992,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3046,6 +3025,7 @@ "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.9.0.tgz", "integrity": "sha512-yaN3brAnHRD+4KyyOsJyk49XUvj2wtbNACSqg0bz3u8t2VuzhC8Q5dfRnrSxjnnbDb+ienBnkn1TzQfE154vyg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@swc/helpers": "^0.5.0" } @@ -3204,8 +3184,7 @@ "version": "15.5.3", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.3.tgz", "integrity": "sha512-RSEDTRqyihYXygx/OJXwvVupfr9m04+0vH8vyy0HfZ7keRto6VX9BbEk0J2PUk0VGy6YhklJUSrgForov5F9pw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { "version": "15.5.3", @@ -3219,7 +3198,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3236,7 +3214,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3253,7 +3230,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3270,7 +3246,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3287,7 +3262,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3304,7 +3278,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3321,7 +3294,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3338,7 +3310,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3389,6 +3360,7 @@ "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", @@ -3693,6 +3665,7 @@ "resolved": "https://registry.npmjs.org/@pothos/core/-/core-3.41.2.tgz", "integrity": "sha512-iR1gqd93IyD/snTW47HwKSsRCrvnJaYwjVNcUG8BztZPqMxyJKPAnjPHAgu1XB82KEdysrNqIUnXqnzZIs08QA==", "license": "ISC", + "peer": true, "peerDependencies": { "graphql": ">=15.1.0" } @@ -4142,6 +4115,7 @@ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -4399,6 +4373,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.17.tgz", "integrity": "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4420,6 +4395,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz", "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -4431,6 +4407,7 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -4527,6 +4504,7 @@ "integrity": "sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.44.1", "@typescript-eslint/types": "8.44.1", @@ -5869,6 +5847,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6381,6 +6360,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", @@ -6749,8 +6729,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/cliui": { "version": "8.0.1", @@ -7189,7 +7168,8 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.3.tgz", "integrity": "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/date-fns": { "version": "4.1.0", @@ -7630,6 +7610,7 @@ "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8574,6 +8555,7 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz", "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==", "license": "MIT", + "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -8746,6 +8728,7 @@ "resolved": "https://registry.npmjs.org/graphql-yoga/-/graphql-yoga-5.16.0.tgz", "integrity": "sha512-/R2dJea7WgvNlXRU4F8iFwWd95Qn1mN+R+yC8XBs1wKjUzr0Pvv8cGYtt6UUcVHw5CiDEtu7iQY5oOe3sDAWCQ==", "license": "MIT", + "peer": true, "dependencies": { "@envelop/core": "^5.3.0", "@envelop/instrumentation": "^1.0.0", @@ -9465,6 +9448,7 @@ "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssstyle": "^4.1.0", "data-urls": "^5.0.0", @@ -9976,6 +9960,7 @@ "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.13.7.tgz", "integrity": "sha512-aChaVU/DO5aRPmk1GX8L+whocagUUpBQqoPtJk+cm7UOXUk87J4PeWCh6nNmTTIfEhiR9DI/+FnA8dln/hTK7g==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/mobx" @@ -10118,7 +10103,6 @@ "resolved": "https://registry.npmjs.org/next/-/next-15.5.3.tgz", "integrity": "sha512-r/liNAx16SQj4D+XH/oI1dlpv9tdKJ6cONYPwwcCC46f2NjpaRWY+EKCzULfgQYV6YKXjHBchff2IZBSlZmJNw==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "15.5.3", "@swc/helpers": "0.5.15", @@ -10171,7 +10155,6 @@ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -10195,7 +10178,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", @@ -10774,6 +10756,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -11215,6 +11198,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -11377,7 +11361,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -11826,7 +11811,6 @@ "hasInstallScript": true, "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.0", @@ -11869,7 +11853,6 @@ "integrity": "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==", "license": "Apache-2.0", "optional": true, - "peer": true, "engines": { "node": ">=8" } @@ -11880,7 +11863,6 @@ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "optional": true, - "peer": true, "bin": { "semver": "bin/semver.js" }, @@ -12293,7 +12275,6 @@ "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", "license": "MIT", - "peer": true, "dependencies": { "client-only": "0.0.1" }, @@ -12731,6 +12712,7 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12928,6 +12910,7 @@ "resolved": "https://registry.npmjs.org/urql/-/urql-4.2.2.tgz", "integrity": "sha512-3GgqNa6iF7bC4hY/ImJKN4REQILcSU9VKcKL8gfELZM8mM5BnLH1BsCc8kBdnVGD1LIFOs4W3O2idNHhON1r0w==", "license": "MIT", + "peer": true, "dependencies": { "@urql/core": "^5.1.1", "wonka": "^6.3.2" @@ -13029,6 +13012,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz", "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -13817,6 +13801,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, diff --git a/frontend/src/external/bcanSatchel/actions.ts b/frontend/src/external/bcanSatchel/actions.ts index 930ed7d3..8888dc2e 100644 --- a/frontend/src/external/bcanSatchel/actions.ts +++ b/frontend/src/external/bcanSatchel/actions.ts @@ -1,7 +1,8 @@ -import { action } from "satcheljs"; -import { Grant } from "../../../../middle-layer/types/Grant"; -import { User } from "../../../../middle-layer/types/User"; -import { Status } from "../../../../middle-layer/types/Status"; +import { action } from 'satcheljs'; +import { Grant } from '../../../../middle-layer/types/Grant' +import { User } from '../../../../middle-layer/types/User' +import { Status } from '../../../../middle-layer/types/Status' +import { Notification } from '../../../../middle-layer/types/Notification'; /** * Set whether the user is authenticated, update the user object, @@ -70,8 +71,6 @@ export const updateSearchQuery = action( ); export const setNotifications = action( - "setNotifications", - (notifications: { id: number; title: string; message: string }[]) => ({ - notifications, - }) -); + 'setNotifications', + (notifications: Notification[]) => ({notifications}) +) diff --git a/frontend/src/external/bcanSatchel/store.ts b/frontend/src/external/bcanSatchel/store.ts index 4953e075..e4bf40de 100644 --- a/frontend/src/external/bcanSatchel/store.ts +++ b/frontend/src/external/bcanSatchel/store.ts @@ -2,6 +2,7 @@ import { createStore } from 'satcheljs'; import { User } from '../../../../middle-layer/types/User' import { Grant } from '../../../../middle-layer/types/Grant' import { Status } from '../../../../middle-layer/types/Status' +import { Notification } from '../../../../middle-layer/types/Notification' export interface AppState { isAuthenticated: boolean; @@ -16,7 +17,7 @@ export interface AppState { yearFilter:number[] | []; activeUsers: User[] | []; inactiveUsers: User[] | []; - notifications: { id: number; title: string; message: string; }[]; + notifications: Notification[]; } // Define initial state @@ -91,6 +92,5 @@ export function persistToSessionStorage() { */ export function getAppStore() { const state = store(); - console.log('Current store.user:', state.user); // Debug: log current user when accessed return state; } diff --git a/frontend/src/main-page/header/Bell.tsx b/frontend/src/main-page/header/Bell.tsx index 3ba3887f..5e06da94 100644 --- a/frontend/src/main-page/header/Bell.tsx +++ b/frontend/src/main-page/header/Bell.tsx @@ -1,7 +1,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faBell } from "@fortawesome/free-solid-svg-icons"; import { useEffect } from "react"; -//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"; @@ -30,23 +29,8 @@ const BellButton: React.FC = observer(({ setOpenModal, openModa // function that handles when button is clicked and fetches notifications const handleClick = async () => { - //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!" }, - // ]; const response = await api( - `/notifications/user/${store.user?.userId}`, + `/notifications/user/${store.user?.userId}/current`, { method: "GET", } @@ -93,7 +77,6 @@ const BellButton: React.FC = observer(({ setOpenModal, openModa {(openModal === "bell" ? ( ) : null)} @@ -101,4 +84,4 @@ const BellButton: React.FC = observer(({ setOpenModal, openModa ); }); -export default BellButton; +export default BellButton; \ No newline at end of file diff --git a/frontend/src/main-page/notifications/GrantNotification.tsx b/frontend/src/main-page/notifications/GrantNotification.tsx index ef59f2da..ee09a9ca 100644 --- a/frontend/src/main-page/notifications/GrantNotification.tsx +++ b/frontend/src/main-page/notifications/GrantNotification.tsx @@ -3,11 +3,22 @@ import { faBell } from "@fortawesome/free-solid-svg-icons"; import { FaTrash } from "react-icons/fa"; interface GrantNotificationProps { + notificationId: string; title: string; message: string; + onDelete: (notificationId: string) => void; } -const GrantNotification: React.FC = ({ title, message }) => { +const GrantNotification: React.FC = ({ + notificationId, + title, + message, + onDelete +}) => { + const handleDelete = () => { + onDelete(notificationId); + }; + return (
@@ -20,6 +31,8 @@ const GrantNotification: React.FC = ({ title, message })
); diff --git a/frontend/src/main-page/notifications/NotificationPopup.tsx b/frontend/src/main-page/notifications/NotificationPopup.tsx index 0e0a1f9d..91a89f7b 100644 --- a/frontend/src/main-page/notifications/NotificationPopup.tsx +++ b/frontend/src/main-page/notifications/NotificationPopup.tsx @@ -1,16 +1,55 @@ import { createPortal } from 'react-dom'; import GrantNotification from "./GrantNotification"; import '../../styles/notification.css'; +import { api } from "../../api"; +import { setNotifications as setNotificationsAction } from "../../external/bcanSatchel/actions"; +import { Notification } from "../../../../middle-layer/types/Notification"; +import { getAppStore } from "../../external/bcanSatchel/store"; +import { observer } from 'mobx-react-lite'; interface NotificationPopupProps { - notifications: { id: number; title: string; message: string }[]; setOpenModal: (value: string | null) => void; } -const NotificationPopup: React.FC = ({ - notifications, +const NotificationPopup: React.FC = observer(({ setOpenModal }) => { + const store = getAppStore(); + const liveNotifications: Notification[] = store.notifications ?? []; + + const handleDelete = async (notificationId: string) => { + try { + const response = await api( + `/notifications/${notificationId}`, + { + method: "DELETE", + } + ); + + if (!response.ok) { + console.error("Failed to delete notification:", response.statusText); + return; + } + + + const fetchResponse = await api( + `/notifications/user/${store.user?.userId}/current`, + { + method: "GET", + } + ); + + if (fetchResponse.ok) { + const updatedNotifications = await fetchResponse.json(); + setNotificationsAction(updatedNotifications); + } + } + catch (error) { + console.error("Error deleting notification:", error); + } + }; + + return createPortal(
@@ -21,9 +60,15 @@ const NotificationPopup: React.FC = ({
- {notifications && notifications.length > 0 ? ( - notifications.map((n) => ( - + {liveNotifications && liveNotifications.length > 0 ? ( + liveNotifications.map((n) => ( + )) ) : (

No new notifications

@@ -32,6 +77,6 @@ const NotificationPopup: React.FC = ({
, 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 index dd6e0f8d..23c2ec71 100644 --- a/frontend/src/styles/notification.css +++ b/frontend/src/styles/notification.css @@ -24,7 +24,8 @@ .popup-header h3 { font-size: 1.1rem; font-weight: 600; - color: #333; + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + color: black; margin: 0; }