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"