diff --git a/src/App.tsx b/src/App.tsx index 27736ba3..4f934fe4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,6 +6,9 @@ import Login from "./pages/Authentication/Login"; import Logout from "./pages/Authentication/Logout"; import InstitutionEditor, { loadInstitution } from "./pages/Institutions/InstitutionEditor"; import Institutions, { loadInstitutions } from "./pages/Institutions/Institutions"; +import NotificationEditor from "./pages/Notifications/NotificationEditor" +import Notifications from "./pages/Notifications/Notifications" +import ViewNotifications from "./pages/Notifications/ViewNotifications"; import RoleEditor, { loadAvailableRole } from "./pages/Roles/RoleEditor"; import Roles, { loadRoles } from "./pages/Roles/Roles"; import Assignment from "./pages/Assignments/Assignment"; @@ -142,6 +145,10 @@ function App() { path: "profile", element: } />, }, + { + path: "view-notifications", + element: } />, + }, { path: "assignments/edit/:assignmentId/participants", element: , @@ -226,6 +233,38 @@ function App() { }, ], }, + { + path: "notifications", + element: , + //loader: loadNotifications, + children: [ + { + path: "new", + element: , + }, + { + path: "edit/:id", + element: , + //loader: loadNotification, + }, + ], + }, + { + path: "institutions", + element: , + loader: loadInstitutions, + children: [ + { + path: "new", + element: , + }, + { + path: "edit/:id", + element: , + loader: loadInstitution, + }, + ], + }, { path: "administrator", element: ( @@ -250,22 +289,7 @@ function App() { }, ], }, - { - path: "institutions", - element: , - loader: loadInstitutions, - children: [ - { - path: "new", - element: , - }, - { - path: "edit/:id", - element: , - loader: loadInstitution, - }, - ], - }, + { path: ":user_type", element: , diff --git a/src/components/Form/FormInput.tsx b/src/components/Form/FormInput.tsx index cf74ed88..dde890e5 100644 --- a/src/components/Form/FormInput.tsx +++ b/src/components/Form/FormInput.tsx @@ -8,7 +8,7 @@ import { IFormikFieldProps, IFormProps } from "./interfaces"; * @author Ankur Mundra on May, 2023 */ -const FormInput: React.FC = (props) => { +const FormInput: React.FC = (props) => { // Make sure it's being used here const { name, label, @@ -21,43 +21,46 @@ const FormInput: React.FC = (props) => { inputGroupPrepend, inputGroupAppend, tooltipPlacement, + rows, // rows should be passed as part of props now } = props; const displayLabel = tooltip ? ( - <> - {label}  - - + <> + {label}  + + ) : ( - label + label ); return ( - - {({ field, form }: IFormikFieldProps) => { - const isValid = !form.errors[field.name]; - const isInvalid = form.touched[field.name] && !isValid; - return ( - - {label && {displayLabel}} - - {inputGroupPrepend} - - {inputGroupAppend} - - {form.errors[field.name]} - - - - ); - }} - + + {({ field, form }: IFormikFieldProps) => { + const isValid = !form.errors[field.name]; + const isInvalid = form.touched[field.name] && !isValid; + return ( + + {label && {displayLabel}} + + {inputGroupPrepend} + + {inputGroupAppend} + + {form.errors[field.name]} + + + + ); + }} + ); }; diff --git a/src/components/Form/interfaces.ts b/src/components/Form/interfaces.ts index 5300dad4..3b03e2d4 100644 --- a/src/components/Form/interfaces.ts +++ b/src/components/Form/interfaces.ts @@ -1,4 +1,4 @@ -import { ElementType, ReactNode } from "react"; +import React, { ElementType, ReactNode } from "react"; /** * @author Ankur Mundra on May, 2023 @@ -16,6 +16,7 @@ export interface IFormProps { tooltipPlacement?: "top" | "right" | "bottom" | "left"; inputGroupPrepend?: ReactNode; inputGroupAppend?: ReactNode; + rows?: number; // rows would be passed as part of props now } export interface IFormOption { diff --git a/src/layout/Header.tsx b/src/layout/Header.tsx index 5b278dd8..053565f8 100644 --- a/src/layout/Header.tsx +++ b/src/layout/Header.tsx @@ -1,11 +1,12 @@ import React, { Fragment, useState, useEffect } from "react"; -import { Button, Container, Nav, Navbar, NavDropdown } from "react-bootstrap"; +import { Button, Container, Nav, Navbar, NavDropdown, Badge } from "react-bootstrap"; import { useSelector } from "react-redux"; import { Link, useNavigate } from "react-router-dom"; import { RootState } from "../store/store"; import { ROLE } from "../utils/interfaces"; import { hasAllPrivilegesOf } from "../utils/util"; import detective from "../assets/detective.png"; +import { mockNotifications } from "../pages/Notifications/mock_data"; // Import the mock notifications /** * @author Ankur Mundra on May, 2023 @@ -19,53 +20,56 @@ const Header: React.FC = () => { const navigate = useNavigate(); const [visible, setVisible] = useState(true); + const [unreadCount, setUnreadCount] = useState(0); - const CustomBtn = () => { - return ( + // Calculate unread notifications on component load + useEffect(() => { + const countUnread = mockNotifications.filter( + (notification) => notification.isUnread && notification.isActive + ).length; + setUnreadCount(countUnread); + }, []); + + const CustomBtn = () => ( +
-
+
Anonymized View
+ -
+ x +
- ); - }; - - // useEffect(() => { - // console.log(visible, 'Changed'); - // }, [visible]); +
+ ); return ( @@ -100,9 +104,6 @@ const Header: React.FC = () => { Roles - - Institutions - Instructors @@ -119,45 +120,34 @@ const Header: React.FC = () => { )} {hasAllPrivilegesOf(auth.user.role, ROLE.TA) && ( - - Users - - - Courses - - - Assignments - - - Questionnaire - - - Edit Questionnaire - + Users + Courses + Notifications + Institutions + Assignments + Questionnaire + Edit Questionnaire - - Impersonate User - - - Anonymized View - + Impersonate User + Anonymized View )} - - Assignments - - - Profile - - - Student View - - - Grades View - - setVisible(!visible)}> - Anonymized View - + Assignments + Profile + Student View + Grades View + + {!hasAllPrivilegesOf(auth.user.role, ROLE.TA) && ( + + My Notifications{" "} + {unreadCount > 0 && ( + + {unreadCount} + + )} + + )} + setVisible(!visible)}>Anonymized View {visible ? ( @@ -177,9 +167,7 @@ const Header: React.FC = () => { )} - + )} diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index c696c49e..352b1094 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,11 +1,53 @@ -/** - * @author Ankur Mundra on May, 2023 - */ +import React, { useEffect, useState } from "react"; +import { Alert, Container } from "react-bootstrap"; + +// Mock data for notifications +const mockNotifications = [ + { + id: "1", + subject: "New Assignment Posted", + description: "Please check the new assignment in your course.", + isUnread: true, + isActive: true, + }, + { + id: "2", + subject: "Exam Scheduled", + description: "An exam has been scheduled for next week.", + isUnread: false, + isActive: true, + }, + { + id: "3", + subject: "Class Canceled", + description: "Class is canceled tomorrow.", + isUnread: true, + isActive: true, + }, +]; + const Home = () => { + // State to manage if there are unread notifications + const [hasUnreadNotifications, setHasUnreadNotifications] = useState(false); + + useEffect(() => { + // Check for any unread and active notifications + const unreadNotifications = mockNotifications.some( + (notification) => notification.isUnread && notification.isActive + ); + setHasUnreadNotifications(unreadNotifications); + }, []); + return ( -
+

Welcome Home!

-
+ {hasUnreadNotifications && ( + + You have new notifications. Please check your notifications page. + + )} + + ); }; diff --git a/src/pages/Institutions/InstitutionDelete.tsx b/src/pages/Institutions/InstitutionDelete.tsx index ef567279..9fae0ad8 100644 --- a/src/pages/Institutions/InstitutionDelete.tsx +++ b/src/pages/Institutions/InstitutionDelete.tsx @@ -6,10 +6,6 @@ import {HttpMethod} from "utils/httpMethods"; import useAPI from "../../hooks/useAPI"; import {IInstitution} from "../../utils/interfaces"; -/** - * @author Ankur Mundra on June, 2023 - */ - interface IDeleteInstitution { institutionData: IInstitution; onClose: () => void; diff --git a/src/pages/Institutions/InstitutionEditor.tsx b/src/pages/Institutions/InstitutionEditor.tsx index 2e2595cf..a9112458 100644 --- a/src/pages/Institutions/InstitutionEditor.tsx +++ b/src/pages/Institutions/InstitutionEditor.tsx @@ -11,9 +11,6 @@ import * as Yup from "yup"; import axiosClient from "../../utils/axios_client"; import { IEditor, IInstitution } from "../../utils/interfaces"; -/** - * @author Ankur Mundra on June, 2023 - */ const initialValues: IInstitution = { name: "", @@ -45,7 +42,7 @@ const InstitutionEditor: React.FC = ({ mode }) => { message: `Institution ${mode}d successfully!`, }) ); - navigate("/administrator/institutions"); + navigate("/institutions"); } }, [dispatch, mode, navigate, institutionResponse]); @@ -71,7 +68,7 @@ const InstitutionEditor: React.FC = ({ mode }) => { submitProps.setSubmitting(false); }; - const handleClose = () => navigate("/administrator/institutions"); + const handleClose = () => navigate("/institutions"); return ( diff --git a/src/pages/Institutions/Institutions.tsx b/src/pages/Institutions/Institutions.tsx index 7be8c79f..c432ac1f 100644 --- a/src/pages/Institutions/Institutions.tsx +++ b/src/pages/Institutions/Institutions.tsx @@ -7,16 +7,20 @@ import { institutionColumns as INSTITUTION_COLUMNS } from "./institutionColumns" import axiosClient from "../../utils/axios_client"; import InstitutionDelete from "./InstitutionDelete"; import { BsPlusSquareFill } from "react-icons/bs"; -import { IInstitution } from "../../utils/interfaces"; - -/** - * @author Ankur Mundra on June, 2023 - */ +import { IInstitution, ROLE } from "../../utils/interfaces"; +import { useSelector } from "react-redux"; +import { RootState } from "store/store"; +import { hasAllPrivilegesOf } from "utils/util"; const Institutions = () => { const navigate = useNavigate(); const institutions: any = useLoaderData(); + const auth = useSelector( + (state: RootState) => state.authentication, + (prev, next) => prev.isAuthenticated === next.isAuthenticated + ); + const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<{ visible: boolean; data?: IInstitution; @@ -55,28 +59,41 @@ const Institutions = () => {
- - - - - {showDeleteConfirmation.visible && ( - + + + + + {showDeleteConfirmation.visible && ( + + )} + + + + - )} - - -
- + + + + ) } + + {!hasAllPrivilegesOf(auth.user.role, ROLE.INSTRUCTOR) + && ( +

Institution changes not allowed

+ )} + diff --git a/src/pages/Institutions/institutionColumns.tsx b/src/pages/Institutions/institutionColumns.tsx index d15f9755..5d1e377a 100644 --- a/src/pages/Institutions/institutionColumns.tsx +++ b/src/pages/Institutions/institutionColumns.tsx @@ -4,10 +4,6 @@ import {BsPencilFill as Edit} from "react-icons/bs"; import {Button} from "react-bootstrap"; import {IInstitution} from "../../utils/interfaces"; -/** - * @author Ankur Mundra on June, 2023 - */ - type Fn = (row: Row) => void; const columnHelper = createColumnHelper(); export const institutionColumns = (handleEdit: Fn, handleDelete: Fn) => [ @@ -15,11 +11,13 @@ export const institutionColumns = (handleEdit: Fn, handleDelete: Fn) => [ header: "Id", enableSorting: false, enableColumnFilter: false, + minSize: 100, }), columnHelper.accessor("name", { header: "Name", enableSorting: true, + minSize: 300, }), columnHelper.display({ @@ -40,5 +38,6 @@ export const institutionColumns = (handleEdit: Fn, handleDelete: Fn) => [ ), + minSize: 300, }), ]; diff --git a/src/pages/Notifications/NotificationColumns.tsx b/src/pages/Notifications/NotificationColumns.tsx new file mode 100644 index 00000000..25de1c21 --- /dev/null +++ b/src/pages/Notifications/NotificationColumns.tsx @@ -0,0 +1,97 @@ +// notificationColumns.tsx +import { createColumnHelper, Row, ColumnDef } from "@tanstack/react-table"; +import { MdOutlineDeleteForever as Remove } from "react-icons/md"; +import { BsPencilFill as Edit } from "react-icons/bs"; +import { Button, Form } from "react-bootstrap"; +import { INotification } from "../../utils/interfaces"; +import React from "react"; + +/** + * @authors Vaibhavi Shetty, Soubarnica Suresh on October, 2024 + */ + +type Fn = (row: Row) => void; +type ToggleFn = (row: Row, isActive: boolean) => void; +const columnHelper = createColumnHelper(); + +export const notificationColumns = ( + handleEdit?: Fn, + handleDelete?: Fn, + showActions: boolean = true, + handleToggle?: ToggleFn // Added handleToggle parameter +): ColumnDef[] => { + const columns: ColumnDef[] = [ + columnHelper.accessor("id", { + header: "Id", + enableSorting: false, + enableColumnFilter: false, + minSize: 100, + }), + + columnHelper.accessor("course", { + header: "Course", + enableSorting: true, + minSize: 300, + }), + + columnHelper.accessor("subject", { + header: "Subject", + enableSorting: true, + minSize: 300, + }), + + columnHelper.accessor("description", { + header: "Description", + enableSorting: true, + minSize: 1000, + }), + + columnHelper.accessor("expirationDate", { + header: "Expiration Date", + enableSorting: true, + }), + + columnHelper.display({ + id: "isActive", + header: "Active", + cell: ({ row }) => handleToggle ? ( + handleToggle(row, !row.original.isActive)} + /> + ) : row.original.isActive ? "True" : "False", + }) + ]; + + if (showActions && handleEdit && handleDelete) { + columns.push( + columnHelper.display({ + id: "actions", + header: "Actions", + cell: ({ row }) => ( + <> + + + + ), + }) + ); + } + + return columns; +}; diff --git a/src/pages/Notifications/NotificationDelete.tsx b/src/pages/Notifications/NotificationDelete.tsx new file mode 100644 index 00000000..29724257 --- /dev/null +++ b/src/pages/Notifications/NotificationDelete.tsx @@ -0,0 +1,68 @@ +import React, { useEffect, useState } from "react"; +import { Button, Modal } from "react-bootstrap"; +import { useDispatch } from "react-redux"; +import { alertActions } from "store/slices/alertSlice"; +import { INotification } from "../../utils/interfaces"; +import { mockNotifications } from "./mock_data"; // Import the centralized mock data + +/** + * @authors Vaibhavi Shetty, Soubarnica Suresh on October, 2024 + */ + +interface IDeleteNotification { + notificationData: INotification; + onClose: () => void; +} + +const DeleteNotification: React.FC = ({ notificationData, onClose }) => { + const [show, setShow] = useState(true); + const dispatch = useDispatch(); + + const deleteHandler = () => { + // Simulate deletion by filtering out the selected notification + const updatedNotifications = mockNotifications.filter( + (notif) => notif.id !== notificationData.id + ); + + // Update the mockNotifications in the data file (replaceable by API call in production) + mockNotifications.splice(0, mockNotifications.length, ...updatedNotifications); + + // Show success message + setShow(false); + dispatch( + alertActions.showAlert({ + variant: "success", + message: `Notification ${notificationData.subject} deleted successfully!`, + }) + ); + onClose(); + }; + + const closeHandler = () => { + setShow(false); + onClose(); + }; + + return ( + + + Delete Notification + + +

+ Are you sure you want to delete the notification {notificationData.subject}? +

+
+ + + + +
+ ); +}; + +export default DeleteNotification; diff --git a/src/pages/Notifications/NotificationEditor.tsx b/src/pages/Notifications/NotificationEditor.tsx new file mode 100644 index 00000000..132b4990 --- /dev/null +++ b/src/pages/Notifications/NotificationEditor.tsx @@ -0,0 +1,126 @@ +import React, { useEffect, useState } from "react"; +import { Form, Formik, FormikHelpers } from "formik"; +import { Button, Modal, FormGroup, FormLabel, FormControl } from "react-bootstrap"; +import FormInput from "components/Form/FormInput"; +import { alertActions } from "store/slices/alertSlice"; +import { useDispatch } from "react-redux"; +import { useNavigate, useParams } from "react-router-dom"; +import * as Yup from "yup"; +import { INotification } from "../../utils/interfaces"; +import { mockNotifications, mockAssignedCourses } from "./mock_data"; // Import centralized mock data + +/** + * @authors Vaibhavi Shetty, Soubarnica Suresh on October, 2024 + */ + +const initialValues: INotification = { + id: "", + course: "", + subject: "", + description: "", + expirationDate: "", + isActive: false, + isUnread: true, +}; + +const validationSchema = Yup.object({ + subject: Yup.string().required("Required").min(3, "Subject must be at least 3 characters"), + description: Yup.string().required("Required").min(5, "Description must be at least 5 characters"), +}); + +const NotificationEditor: React.FC<{ mode: "create" | "update" }> = ({ mode }) => { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const { id } = useParams<{ id: string }>(); // Get the ID from URL parameters + const [notification, setNotification] = useState(initialValues); + + useEffect(() => { + if (mode === "update" && id) { + // Load data from centralized mockNotifications + const notificationToEdit = mockNotifications.find((notif) => notif.id === id); + if (notificationToEdit) { + setNotification(notificationToEdit); + } else { + dispatch(alertActions.showAlert({ + variant: "danger", + message: "Notification not found!", + })); + navigate("/notifications"); // Navigate back if notification doesn't exist + } + } + }, [id, mode, dispatch, navigate]); + + const onSubmit = (values: INotification, submitProps: FormikHelpers) => { + // Handle submission logic (no backend call for mock data) + const message = mode === "update" ? "Notification updated successfully!" : "Notification created successfully!"; + + // Update mock data directly for edit mode + if (mode === "update" && id) { + const index = mockNotifications.findIndex((notif) => notif.id === id); + if (index !== -1) { + mockNotifications[index] = values; + } + } else { + // For "create" mode, add a new notification + mockNotifications.push({ ...values, id: (mockNotifications.length + 1).toString() }); + } + + dispatch(alertActions.showAlert({ + variant: "success", + message: message, + })); + navigate("/notifications"); + submitProps.setSubmitting(false); + }; + + const handleClose = () => navigate("/notifications"); + + return ( + + + {mode === "update" ? "Update " : "Create "}Notification + + + + {(formik) => ( +
+ + Course + + + {mockAssignedCourses.map((course) => ( + + ))} + + + + + + + + + + + + + )} +
+
+
+ ); +}; + +export default NotificationEditor; diff --git a/src/pages/Notifications/Notifications.tsx b/src/pages/Notifications/Notifications.tsx new file mode 100644 index 00000000..941efbe4 --- /dev/null +++ b/src/pages/Notifications/Notifications.tsx @@ -0,0 +1,118 @@ +// Notifications.tsx +import { useCallback, useMemo, useState } from "react"; +import { Outlet, useNavigate } from "react-router-dom"; +import { Button, Col, Container, Row } from "react-bootstrap"; +import Table from "components/Table/Table"; +import { notificationColumns as NOTIFICATION_COLUMNS } from "./NotificationColumns"; +import { mockNotifications } from "./mock_data"; +import NotificationDelete from "./NotificationDelete"; +import { BsPlusSquareFill } from "react-icons/bs"; +import { INotification, ROLE } from "../../utils/interfaces"; +import { useSelector } from "react-redux"; +import { RootState } from "../../store/store"; +import { hasAllPrivilegesOf } from "utils/util"; +import React from "react"; +import { Row as TRow } from "@tanstack/react-table"; +/** + * @authors Vaibhavi Shetty, Soubarnica Suresh on October, 2024 + */ + +const mockAssignedCourses = ["CS101", "CS102", "CS103"]; + +const Notifications = () => { + const navigate = useNavigate(); + const auth = useSelector((state: RootState) => state.authentication); + + const [notifications, setNotifications] = useState(mockNotifications); + const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<{ + visible: boolean; + data?: INotification; + }>({ visible: false }); + + const filteredNotifications = useMemo( + () => notifications.filter((notification) => + mockAssignedCourses.includes(notification.course) + ), + [notifications] + ); + + const onDeleteNotificationHandler = useCallback( + () => setShowDeleteConfirmation({ visible: false }), + [] + ); + + const onEditHandle = useCallback( + (row: TRow) => navigate(`edit/${row.original.id}`), + [navigate] + ); + + const onDeleteHandle = useCallback( + (row: TRow) => setShowDeleteConfirmation({ visible: true, data: row.original }), + [] + ); + + // Toggle the isActive status of a notification + const handleToggle = (row: TRow, newIsActive: boolean) => { + setNotifications((prevNotifications) => + prevNotifications.map((notification) => + notification.id === row.original.id + ? { ...notification, isActive: newIsActive } + : notification + ) + ); + }; + + const tableColumns = useMemo( + () => NOTIFICATION_COLUMNS(onEditHandle, onDeleteHandle, true, handleToggle), + [onDeleteHandle, onEditHandle] + ); + + return ( + <> + +
+ + +
+

Manage Notifications

+ +
+ + {hasAllPrivilegesOf(auth.user.role, ROLE.TA) && ( + <> + + + + + {showDeleteConfirmation.visible && ( + + )} + + + +
+ + + + )} + {!hasAllPrivilegesOf(auth.user.role, ROLE.TA) && ( +

Notification changes not allowed

+ )} + + + + ); +}; + +export default Notifications; diff --git a/src/pages/Notifications/ViewNotifications.tsx b/src/pages/Notifications/ViewNotifications.tsx new file mode 100644 index 00000000..8f6fd0e9 --- /dev/null +++ b/src/pages/Notifications/ViewNotifications.tsx @@ -0,0 +1,47 @@ +// ViewNotifications.tsx +import React, { useMemo } from "react"; +import { Container, Row, Col } from "react-bootstrap"; +import Table from "components/Table/Table"; +import { notificationColumns as NOTIFICATION_COLUMNS } from "./NotificationColumns"; +import { mockNotifications } from "./mock_data"; +import { INotification } from "../../utils/interfaces"; + +/** + * @authors Vaibhavi Shetty, Soubarnica Suresh on October, 2024 + */ + +const enrolledCourses = ["CS101", "CS103"]; + +const ViewNotifications = () => { + // Filter notifications to show only active ones related to enrolled courses + const filteredNotifications = useMemo( + () => mockNotifications.filter( + (notification) => notification.isActive && enrolledCourses.includes(notification.course) + ), + [] + ); + + return ( + + + +

My Notifications

+ +
+ + + +
+ + + + ); +}; + +export default ViewNotifications; diff --git a/src/pages/Notifications/mock_data.tsx b/src/pages/Notifications/mock_data.tsx new file mode 100644 index 00000000..f8cdb6c0 --- /dev/null +++ b/src/pages/Notifications/mock_data.tsx @@ -0,0 +1,35 @@ +// mockData.ts +import { INotification } from "../../utils/interfaces"; + +export const mockNotifications: INotification[] = [ + { + id: "1", + course: "CS101", + subject: "New Homework", + description: "Homework 1 due next week", + expirationDate: "2024-10-31", + isActive: true, + isUnread: true, + }, + { + id: "2", + course: "CS102", + subject: "Class Canceled", + description: "No class tomorrow", + expirationDate: "2024-11-01", + isActive: false, + isUnread: false, + }, + { + id: "3", + course: "CS103", + subject: "Exam scheduled", + description: "Please prepare for the exam", + expirationDate: "2024-11-05", + isActive: true, + isUnread: true, + }, +]; + +// Define mock courses for assigned courses +export const mockAssignedCourses = ["CS101", "CS102", "CS103"]; diff --git a/src/utils/axios_client.ts b/src/utils/axios_client.ts index b5fe47d7..4911ff4d 100644 --- a/src/utils/axios_client.ts +++ b/src/utils/axios_client.ts @@ -7,7 +7,6 @@ import { getAuthToken } from "./auth"; const axiosClient = axios.create({ baseURL: "http://localhost:3002/api/v1", - timeout: 1000, headers: { "Content-Type": "application/json", Accept: "application/json", diff --git a/src/utils/interfaces.ts b/src/utils/interfaces.ts index 213909c9..93f26164 100644 --- a/src/utils/interfaces.ts +++ b/src/utils/interfaces.ts @@ -17,6 +17,16 @@ export interface IInstitution { name: string; } +export interface INotification { + id: string; + course: string; + subject: string; + description: string; + expirationDate: string; + isActive: boolean; + isUnread: boolean; +} + export interface IInstructor { id?: number; name: string;