diff --git a/backend/controllers/noteController.js b/backend/controllers/noteController.js
new file mode 100644
index 0000000..f938dc6
--- /dev/null
+++ b/backend/controllers/noteController.js
@@ -0,0 +1,60 @@
+const asyncHandler = require("express-async-handler");
+
+const User = require("../models/userModel");
+const Note = require("../models/noteModel");
+const Ticket = require("../models/ticketModel");
+
+// @desc Get notes for a ticket
+// @route GET /api/tickets/:ticketId/notes
+// @access Private
+const getNotes = asyncHandler(async (req, res) => {
+ const user = await User.findById(req.user.id);
+
+ if (!user) {
+ res.status(401);
+ throw new Error("User not found");
+ }
+
+ const ticket = await Ticket.findById(req.params.ticketId)
+ if (!ticket) {
+ res.status(404);
+ throw new Error("Ticket not found");
+ }
+ if (ticket.user.toString() !== req.user.id) {
+ res.status(401);
+ throw new Error("Not authorized");
+ }
+
+ const notes = await Note.find({ ticket: req.params.ticketId });
+
+ res.status(201).json(notes);
+});
+// @desc Creata a note
+// @route POST /api/tickets/:ticketId/notes
+// @access Private
+const addNote = asyncHandler(async (req, res) => {
+ const user = await User.findById(req.user.id);
+
+ if (!user) {
+ res.status(401);
+ throw new Error("User not found");
+ }
+ const ticket = await Ticket.findById(req.params.ticketId);
+ if (!ticket) {
+ res.status(404);
+ throw new Error("Ticket not found");
+ }
+ if (ticket.user.toString() !== req.user.id) {
+ res.status(401);
+ throw new Error("Not authorized");
+ }
+ const note = await Note.create({
+ text: req.body.text,
+ ticket: req.params.ticketId,
+ isStaff:false,
+ user: req.user.id
+ });
+ res.status(201).json(note);
+});
+
+module.exports = {addNote, getNotes };
diff --git a/backend/models/noteModel.js b/backend/models/noteModel.js
new file mode 100644
index 0000000..69c10a2
--- /dev/null
+++ b/backend/models/noteModel.js
@@ -0,0 +1,32 @@
+const mongoose = require("mongoose");
+
+const noteSchema = mongoose.Schema(
+ {
+ user: {
+ type: mongoose.Schema.Types.ObjectId,
+ required: true,
+ ref: "User",
+ },
+ ticket: {
+ type: mongoose.Schema.Types.ObjectId,
+ required: true,
+ ref: "Ticket",
+ },
+ text: {
+ type: String,
+ required: [true, "Please add some text"],
+ },
+ isStaff: {
+ type: Boolean,
+ default: false,
+ },
+ staffId: {
+ type: String,
+ },
+ },
+ {
+ timestamps: true,
+ }
+);
+
+module.exports = mongoose.model("Note", noteSchema);
diff --git a/backend/routes/noteRoutes.js b/backend/routes/noteRoutes.js
new file mode 100644
index 0000000..7536425
--- /dev/null
+++ b/backend/routes/noteRoutes.js
@@ -0,0 +1,8 @@
+const express = require("express");
+const router = express.Router({ mergeParams: true });
+const protect = require("../middleware/authMiddleware");
+const { getNotes, addNote } = require("../controllers/noteController");
+
+router.route("/").get(protect, getNotes).post(protect, addNote);
+
+module.exports = router;
diff --git a/backend/routes/ticketRoutes.js b/backend/routes/ticketRoutes.js
index d31412e..0ae30b3 100644
--- a/backend/routes/ticketRoutes.js
+++ b/backend/routes/ticketRoutes.js
@@ -10,6 +10,8 @@ const {
const protect = require("../middleware/authMiddleware");
+const noteRouter = require('./noteRoutes')
+router.use('/:ticketId/notes',noteRouter)
router.route("/").get(protect, getTickets).post(protect, createTicket);
router
diff --git a/frontend/src/app/store.js b/frontend/src/app/store.js
index 8119fb3..b69be87 100644
--- a/frontend/src/app/store.js
+++ b/frontend/src/app/store.js
@@ -1,10 +1,12 @@
import { configureStore } from "@reduxjs/toolkit";
-import authSlice from "../features/auth/authSlice";
-import ticketSlice from "../features/tickets/ticketSlice";
+import authReducer from "../features/auth/authSlice";
+import ticketReducer from "../features/tickets/ticketSlice";
+import noteReducer from "../features/notes/noteSlice";
export const store = configureStore({
reducer: {
- auth: authSlice,
- tickets: ticketSlice,
+ auth: authReducer,
+ tickets: ticketReducer,
+ notes: noteReducer,
},
});
diff --git a/frontend/src/components/NoteItem.jsx b/frontend/src/components/NoteItem.jsx
new file mode 100644
index 0000000..dbfa3e2
--- /dev/null
+++ b/frontend/src/components/NoteItem.jsx
@@ -0,0 +1,23 @@
+import { useSelector } from "react-redux";
+
+function NoteItem({ note }) {
+ const { user } = useSelector((state) => state.auth);
+
+ return (
+
+
Note from {note.isStaff ? Staff:{user.name}}
+
{note.text}
+
+ {new Date(note.createdAt).toLocaleString('en-US')}
+
+
+ );
+}
+
+export default NoteItem
diff --git a/frontend/src/features/notes/noteService.js b/frontend/src/features/notes/noteService.js
new file mode 100644
index 0000000..7443ddc
--- /dev/null
+++ b/frontend/src/features/notes/noteService.js
@@ -0,0 +1,35 @@
+import axios from "axios";
+
+const API_URL = "/api/tickets/";
+
+const createNote = async (noteText, ticketId, token) => {
+ const config = {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ };
+ const response = await axios.post(
+ API_URL + ticketId + "/notes",
+ { text: noteText },
+ config
+ );
+ return response.data;
+};
+const getNotes = async (ticketId, token) => {
+ const config = {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ };
+
+ const response = await axios.get(API_URL + ticketId + "/notes", config);
+
+ return response.data;
+};
+
+const noteService = {
+ getNotes,
+ createNote
+};
+
+export default noteService;
diff --git a/frontend/src/features/notes/noteSlice.js b/frontend/src/features/notes/noteSlice.js
new file mode 100644
index 0000000..fec5a43
--- /dev/null
+++ b/frontend/src/features/notes/noteSlice.js
@@ -0,0 +1,88 @@
+import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
+import noteService from "./noteService";
+
+const initialState = {
+ notes: [],
+ isError: false,
+ isSuccess: false,
+ isLoading: false,
+ message: "",
+};
+
+export const noteSlice = createSlice({
+ name: "note",
+ initialState,
+ reducers: {
+ rest: (stata) => initialState,
+ },
+ extraReducers: (builder) => {
+ builder
+ .addCase(getNotes.pending, (state) => {
+ state.isLoading = true;
+ })
+ .addCase(getNotes.fulfilled, (state, action) => {
+ state.isLoading = false;
+ state.isSuccess = true;
+ state.notes = action.payload;
+ })
+ .addCase(getNotes.rejected, (state, action) => {
+ state.isLoading = false;
+ state.isError = true;
+ state.message = action.payload;
+ })
+ .addCase(createNote.pending, (state) => {
+ state.isLoading = true;
+ })
+ .addCase(createNote.fulfilled, (state, action) => {
+ state.isLoading = false;
+ state.isSuccess = true;
+ state.notes.push(action.payload);
+ })
+ .addCase(createNote.rejected, (state, action) => {
+ state.isLoading = false;
+ state.isError = true;
+ state.message = action.payload;
+ });
+ },
+});
+
+export const createNote = createAsyncThunk(
+ "notes/create",
+ async ({ noteText, ticketId }, thunkAPI) => {
+ try {
+ const token = thunkAPI.getState().auth.user.token;
+ return await noteService.createNote(noteText, ticketId, token);
+ } catch (error) {
+ const message =
+ (error.response &&
+ error.response.data &&
+ error.response.data.message) ||
+ error.message ||
+ error.toString();
+
+ return thunkAPI.rejectWithValue(message);
+ }
+ }
+);
+
+export const getNotes = createAsyncThunk(
+ "notes/getAll",
+ async (ticketId, thunkAPI) => {
+ try {
+ const token = thunkAPI.getState().auth.user.token;
+ return await noteService.getNotes(ticketId, token);
+ } catch (error) {
+ const message =
+ (error.response &&
+ error.response.data &&
+ error.response.data.message) ||
+ error.message ||
+ error.toString();
+
+ return thunkAPI.rejectWithValue(message);
+ }
+ }
+);
+
+export const { reset } = noteSlice.actions;
+export default noteSlice.reducer;
diff --git a/frontend/src/pages/Ticket.jsx b/frontend/src/pages/Ticket.jsx
index 44e16c0..b5dc6e7 100644
--- a/frontend/src/pages/Ticket.jsx
+++ b/frontend/src/pages/Ticket.jsx
@@ -1,30 +1,62 @@
-import { useEffect } from "react";
+import { FaPlus } from "react-icons/fa";
+import { useEffect, useState } from "react";
import { useSelector, useDispatch } from "react-redux";
-import { closeTicket, getTicket, reset } from "../features/tickets/ticketSlice";
+import { closeTicket, getTicket } from "../features/tickets/ticketSlice";
import { useNavigate, useParams } from "react-router-dom";
import Spinner from "../components/Spinner";
import { BackButton } from "../components/BackButton";
import { toast } from "react-toastify";
+import { getNotes, createNote } from "../features/notes/noteSlice";
+import NoteItem from "../components/NoteItem";
+import Modal from "react-modal";
+
+const customStyles = {
+ content: {
+ width: "600px",
+ top: "50%",
+ left: "50%",
+ right: "auto",
+ bottom: "auto",
+ marginRight: "-50%",
+ transform: "translate(-50%,-50%)",
+ position: "relative",
+ },
+};
+
+Modal.setAppElement("#root");
function Ticket() {
+ const [modalIsOpen, setModalIsOpen] = useState(false);
+ const [noteText, setNoteText] = useState("");
const { ticket, isLoading, isSuccess, isError, message } = useSelector(
(state) => state.tickets
);
+ const { notes, isLoading: notesIsLoading } = useSelector(
+ (state) => state.notes
+ );
const { ticketId } = useParams();
const dispatch = useDispatch();
const navigate = useNavigate();
- const onTicketClose =()=>{
- dispatch(closeTicket(ticketId))
- toast.success('Ticket closed')
- navigate('/tickets')
- }
+ const onNoteSubmit = (e) => {
+ e.preventDefault();
+ dispatch(createNote({ noteText, ticketId }));
+ closeModal();
+ };
+ const openModal = () => setModalIsOpen(true);
+
+ const closeModal = () => setModalIsOpen(false);
+ const onTicketClose = () => {
+ dispatch(closeTicket(ticketId));
+ toast.success("Ticket closed");
+ navigate("/tickets");
+ };
useEffect(() => {
if (isError) toast.error(message);
-
dispatch(getTicket(ticketId));
+ dispatch(getNotes(ticketId));
}, [isError, message, ticketId]);
- if (isLoading) return ;
+ if (isLoading || notesIsLoading) return ;
if (isError) return Something went wrong
;
return (
@@ -48,7 +80,46 @@ function Ticket() {
{ticket.status !== "closed" && (
-
+
+ )}
+
+ Add Note
+
+
+
+ {notes.map((note) => (
+
+ ))}
+ {ticket.status !== "closed" && (
+
)}
);
diff --git a/package-lock.json b/package-lock.json
index 2e3c4ad..1e7a649 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,7 +15,8 @@
"express": "^4.18.2",
"express-async-handler": "^1.2.0",
"jsonwebtoken": "^8.5.1",
- "mongoose": "^6.7.5"
+ "mongoose": "^6.7.5",
+ "react-modal": "^3.16.1"
},
"devDependencies": {
"nodemon": "^2.0.20"
@@ -1417,6 +1418,11 @@
"node": ">= 0.6"
}
},
+ "node_modules/exenv": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz",
+ "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw=="
+ },
"node_modules/express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
@@ -1710,6 +1716,11 @@
"node": ">=0.12.0"
}
},
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ },
"node_modules/jsonwebtoken": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz",
@@ -1795,6 +1806,17 @@
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
},
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -2037,6 +2059,14 @@
"node": ">=0.10.0"
}
},
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/object-inspect": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
@@ -2081,6 +2111,16 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "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/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -2143,6 +2183,59 @@
"node": ">= 0.8"
}
},
+ "node_modules/react": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
+ "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
+ "peer": true,
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
+ "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
+ "peer": true,
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.0"
+ },
+ "peerDependencies": {
+ "react": "^18.2.0"
+ }
+ },
+ "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-lifecycles-compat": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
+ "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
+ },
+ "node_modules/react-modal": {
+ "version": "3.16.1",
+ "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.1.tgz",
+ "integrity": "sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==",
+ "dependencies": {
+ "exenv": "^1.2.0",
+ "prop-types": "^15.7.2",
+ "react-lifecycles-compat": "^3.0.0",
+ "warning": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "peerDependencies": {
+ "react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18",
+ "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18"
+ }
+ },
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -2191,6 +2284,15 @@
"node": ">=6"
}
},
+ "node_modules/scheduler": {
+ "version": "0.23.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
+ "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
+ "peer": true,
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
"node_modules/semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
@@ -2442,6 +2544,14 @@
"node": ">= 0.8"
}
},
+ "node_modules/warning": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+ "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+ "dependencies": {
+ "loose-envify": "^1.0.0"
+ }
+ },
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
@@ -3608,6 +3718,11 @@
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
},
+ "exenv": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz",
+ "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw=="
+ },
"express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
@@ -3819,6 +3934,11 @@
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true
},
+ "js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ },
"jsonwebtoken": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz",
@@ -3902,6 +4022,14 @@
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
},
+ "loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "requires": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ }
+ },
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -4081,6 +4209,11 @@
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true
},
+ "object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
+ },
"object-inspect": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
@@ -4110,6 +4243,16 @@
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true
},
+ "prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "requires": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
"proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -4154,6 +4297,46 @@
"unpipe": "1.0.0"
}
},
+ "react": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
+ "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
+ "peer": true,
+ "requires": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "react-dom": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
+ "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
+ "peer": true,
+ "requires": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.0"
+ }
+ },
+ "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=="
+ },
+ "react-lifecycles-compat": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
+ "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
+ },
+ "react-modal": {
+ "version": "3.16.1",
+ "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.1.tgz",
+ "integrity": "sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==",
+ "requires": {
+ "exenv": "^1.2.0",
+ "prop-types": "^15.7.2",
+ "react-lifecycles-compat": "^3.0.0",
+ "warning": "^4.0.3"
+ }
+ },
"readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -4182,6 +4365,15 @@
"sparse-bitfield": "^3.0.3"
}
},
+ "scheduler": {
+ "version": "0.23.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
+ "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
+ "peer": true,
+ "requires": {
+ "loose-envify": "^1.1.0"
+ }
+ },
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
@@ -4378,6 +4570,14 @@
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
},
+ "warning": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+ "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+ "requires": {
+ "loose-envify": "^1.0.0"
+ }
+ },
"webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
diff --git a/package.json b/package.json
index 6d70ca0..91058ac 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,8 @@
"express": "^4.18.2",
"express-async-handler": "^1.2.0",
"jsonwebtoken": "^8.5.1",
- "mongoose": "^6.7.5"
+ "mongoose": "^6.7.5",
+ "react-modal": "^3.16.1"
},
"devDependencies": {
"nodemon": "^2.0.20"