diff --git a/package-lock.json b/package-lock.json
index d56d81b..a687f95 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -27,6 +27,7 @@
"react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1",
"react-jwt": "^1.2.0",
+ "react-modal": "^3.16.1",
"react-router": "^6.13.0",
"react-router-dom": "^6.13.0",
"react-scripts": "5.0.1",
@@ -8446,6 +8447,11 @@
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
+ "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/exit": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
@@ -14981,6 +14987,29 @@
"react": "^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
+ "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/react-refresh": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
@@ -17250,6 +17279,14 @@
"makeerror": "1.0.12"
}
},
+ "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/watchpack": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
diff --git a/package.json b/package.json
index 25691fd..54a3d2a 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1",
"react-jwt": "^1.2.0",
+ "react-modal": "^3.16.1",
"react-router": "^6.13.0",
"react-router-dom": "^6.13.0",
"react-scripts": "5.0.1",
diff --git a/src/components/Dropdown/Dropdown.js b/src/components/Dropdown/Dropdown.js
index 5a8344e..575dc17 100644
--- a/src/components/Dropdown/Dropdown.js
+++ b/src/components/Dropdown/Dropdown.js
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useState, useEffect } from 'react';
import toast, { Toaster } from 'react-hot-toast';
import { useNavigate } from 'react-router-dom';
import Box from '@mui/material/Box';
@@ -16,11 +16,12 @@ import HelpIcon from '@mui/icons-material/Help';
import MessageIcon from '@mui/icons-material/Message';
import PolicyIcon from '@mui/icons-material/Policy';
import ManageAccountsIcon from '@mui/icons-material/ManageAccounts';
-import { useEffect } from 'react';
+import { CaptureFeedback } from '../Feedback/CaptureFeedback'; //
export default function Dropdown() {
const [anchorEl, setAnchorEl] = useState(null);
const [loggedIn, setLoggedIn] = useState(false); // Change the state name to 'loggedIn'
+ const [feedbackVisible, setFeedbackVisible] = useState(false); // Toggle visibility of feedback modal
const open = Boolean(anchorEl);
const navigate = useNavigate();
@@ -62,6 +63,12 @@ export default function Dropdown() {
navigate('/privacy-policy');
}
+ // Toggle CaptureFeedback modal component visibility
+ function toggleFeedbackModal() {
+ setFeedbackVisible((prevFeedbackVisible) => !prevFeedbackVisible);
+ handleClose();
+ }
+
return (
Help
-
);
}
diff --git a/src/components/Feedback/CaptureFeedback.jsx b/src/components/Feedback/CaptureFeedback.jsx
new file mode 100644
index 0000000..334de18
--- /dev/null
+++ b/src/components/Feedback/CaptureFeedback.jsx
@@ -0,0 +1,102 @@
+import React, { useState } from 'react';
+import Modal from 'react-modal';
+import PropTypes from 'prop-types';
+import { Toaster } from 'react-hot-toast';
+import DirectFeedback from './DirectFeedback';
+import EmailFeedback from './EmailFeedback';
+import Typography from '@mui/material/Typography';
+
+Modal.setAppElement('#root');
+
+export function CaptureFeedback({ isActive, onClose }) {
+ const [formToShow, setFormToShow] = useState('initialPrompt');
+ const [rating, setRating] = useState(0);
+
+ const handleFormSwitch = (formType) => {
+ setRating(0);
+ setFormToShow(formType);
+ };
+
+ const onPointerMove = (value) => {
+ if (rating === 0) {
+ setRating(value);
+ }
+ };
+
+ return (
+
+
+
+
+
+ We appreciate your feedback.
+ Please let us know how we can improve.
+
+ {formToShow === 'initialPrompt' && (
+
+
+
+
+ )}
+ {formToShow === 'directFeedback' && (
+
+ )}
+ {formToShow === 'emailFeedback' && (
+
+ )}
+
+
+
+
+ );
+}
+
+CaptureFeedback.propTypes = {
+ isActive: PropTypes.bool.isRequired,
+ onClose: PropTypes.func.isRequired,
+};
diff --git a/src/components/Feedback/DirectFeedback.jsx b/src/components/Feedback/DirectFeedback.jsx
new file mode 100644
index 0000000..4d4a000
--- /dev/null
+++ b/src/components/Feedback/DirectFeedback.jsx
@@ -0,0 +1,132 @@
+import React from 'react';
+import { Formik, Form, Field } from 'formik';
+import { Rating } from 'react-simple-star-rating';
+import * as Yup from 'yup';
+import axios from 'axios';
+import PropTypes from 'prop-types';
+import toast, { Toaster } from 'react-hot-toast';
+import './Feedback.css';
+
+const feedbackValidationSchema = Yup.object().shape({
+ feedback: Yup.string()
+ .trim()
+ .required('Feedback is required')
+ .max(2000, 'Feedback should not exceed 2000 characters'),
+ rating: Yup.number()
+ .required('Rating is required')
+ .min(0, 'Rating should be at least 0')
+ .max(5, 'Rating should not exceed 5'),
+});
+
+export default function DirectFeedback({
+ handleFormSwitch,
+ rating,
+ onPointerMove,
+ setRating,
+ onClose,
+}) {
+ return (
+
+ {
+ //Get token and user info
+ const token = localStorage.getItem('token');
+ const user = JSON.parse(localStorage.getItem('user'));
+
+ if (token && user) {
+ // User logged in and user details are available
+ const userId = user._id;
+ axios
+ .post(
+ 'http://localhost:3001/api/user/feedback/new',
+ {
+ feedback: values.feedback,
+ rating: values.rating,
+ author: userId,
+ },
+ {
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${token}`,
+ },
+ }
+ )
+ .then((response) => {})
+ .then(() => {
+ setSubmitting(false);
+ onClose();
+ toast.success(
+ 'Thank you, your feedback has been submitted!'
+ );
+ })
+ .catch((error) => {
+ console.error(
+ 'There was an error submitting the feedback',
+ error
+ );
+ });
+ } else {
+ // User is not logged in or user details are not available
+ console.error(
+ 'User is not authenticated or user details are missing.'
+ );
+ }
+ }}
+ >
+ {({ isSubmitting, setFieldValue }) => (
+
+ )}
+
+
+
+ );
+}
+
+DirectFeedback.propTypes = {
+ handleFormSwitch: PropTypes.func.isRequired,
+ rating: PropTypes.number.isRequired,
+ onPointerMove: PropTypes.func.isRequired,
+ setRating: PropTypes.func.isRequired,
+ onClose: PropTypes.func.isRequired,
+};
diff --git a/src/components/Feedback/EmailFeedback.jsx b/src/components/Feedback/EmailFeedback.jsx
new file mode 100644
index 0000000..cd95308
--- /dev/null
+++ b/src/components/Feedback/EmailFeedback.jsx
@@ -0,0 +1,142 @@
+import React from 'react';
+import { Formik, Form, Field } from 'formik';
+import { Rating } from 'react-simple-star-rating';
+import * as Yup from 'yup';
+import axios from 'axios';
+import PropTypes from 'prop-types';
+import toast, { Toaster } from 'react-hot-toast';
+import './Feedback.css';
+
+const feedbackValidationSchema = Yup.object().shape({
+ feedback: Yup.string()
+ .trim()
+ .required('Feedback is required')
+ .max(2000, 'Feedback should not exceed 2000 characters'),
+ rating: Yup.number()
+ .required('Rating is required')
+ .min(1, 'Rating should be at least 1')
+ .max(5, 'Rating should not exceed 5'),
+});
+
+export default function EmailFeedback({
+ handleFormSwitch,
+ rating,
+ onPointerMove,
+ setRating,
+ onClose,
+}) {
+ return (
+
+ {
+ const token = localStorage.getItem('token');
+ const user = JSON.parse(localStorage.getItem('user'));
+ const userId = user._id;
+ const userEmail = user.email;
+ const username = user.username;
+
+ if (token && user) {
+ // User logged in and user details are available
+ axios
+ .post(
+ 'http://localhost:3001/api/user/feedback/mail',
+ {
+ feedback: values.feedback,
+ rating: values.rating,
+ userId: userId,
+ userEmail: userEmail,
+ username: username,
+ },
+ {
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${token}`,
+ },
+ }
+ )
+ .then((response) => {})
+ .then(() => {
+ setSubmitting(false);
+ onClose();
+ toast.success(
+ 'Thank you, your feedback has been submitted!'
+ );
+ })
+ .catch((error) => {
+ console.error(
+ 'There was an error submitting the feedback',
+ error
+ );
+ });
+ } else {
+ // User is not logged in or user details are not available
+ console.error(
+ 'User is not authenticated or user details are missing.'
+ );
+ }
+ }}
+ >
+ {({ isSubmitting, setFieldValue }) => (
+
+ )}
+
+
+
+ );
+}
+
+EmailFeedback.propTypes = {
+ handleFormSwitch: PropTypes.func.isRequired,
+ rating: PropTypes.number.isRequired,
+ onPointerMove: PropTypes.func.isRequired,
+ setRating: PropTypes.func.isRequired,
+ onClose: PropTypes.func.isRequired,
+};
diff --git a/src/components/Feedback/Feedback.css b/src/components/Feedback/Feedback.css
new file mode 100644
index 0000000..a427dc4
--- /dev/null
+++ b/src/components/Feedback/Feedback.css
@@ -0,0 +1,89 @@
+/* MODAL Styling */
+.overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.7);
+}
+
+.feedback-modal {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 55%;
+ height: 60%;
+ transform: translate(-50%, -50%);
+ background: white;
+ padding: 20px;
+ border-radius: 10px;
+}
+
+.close-button {
+ position: relative;
+ z-index: 9999;
+ border: none;
+ font-weight: bold;
+ font-size: 1.6vw;
+ cursor: pointer;
+ background: none;
+}
+
+/* FORM Styling */
+.modal-form {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-around;
+ height: 80%;
+}
+
+.feedback-header {
+ text-align: center;
+ font-weight: bold;
+ font-size: 2vw;
+}
+
+.feedback-textarea {
+ width: 100%;
+ height: 120px;
+ margin-bottom: 10px;
+ padding: 10px;
+ border-radius: 5px;
+ border: 1px solid #ccc;
+ resize: none;
+}
+
+.button-group {
+ display: flex;
+ justify-content: space-around;
+ margin-top: auto;
+}
+
+.button-group button {
+ border-radius: 3px;
+ color: white;
+ background-color: rgb(46, 125, 50);
+ font-weight: bold;
+ font-size: 1em;
+ padding: 0.5em;
+ cursor: pointer;
+ transition: box-shadow 0.3s ease;
+}
+
+.button-group button:hover {
+ background-color: rgb(36, 110, 40);
+}
+
+.feedback-textarea {
+ resize: none;
+}
+
+.rating-container {
+ display: flex;
+ justify-content: center;
+ margin-bottom: 20px;
+}
+
+
+
diff --git a/src/server/.variable.env b/src/server/.variable.env
index 431e35b..f4e7468 100644
--- a/src/server/.variable.env
+++ b/src/server/.variable.env
@@ -1,7 +1,6 @@
-<<<<<<< HEAD
MONGO_DB=
PORT=
-=======
-MONGO_DB=mongodb://username:password@localhost:27017/codebooker
-PORT=3001
->>>>>>> origin/master
+
+//Credentials for sending/receiving feedback from server
+MAIL_USERNAME="senderAddressEmail"
+MAIL_PASSWORD="senderAddressPassword"
\ No newline at end of file
diff --git a/src/server/controller/feedBackController.js b/src/server/controller/feedBackController.js
index aa50d54..27f8863 100644
--- a/src/server/controller/feedBackController.js
+++ b/src/server/controller/feedBackController.js
@@ -1,3 +1,4 @@
+const nodemailer = require('nodemailer');
const FeedBack = require('../model/feedBackModel');
module.exports.addfeedback = async (req, res) => {
@@ -12,6 +13,52 @@ module.exports.addfeedback = async (req, res) => {
}
};
+// TO DO:
+// - feedback module reopens once server successfully sends the email?
+
+module.exports.emailFeedback = async (req, res) => {
+ try {
+ const { username, userId, userEmail, feedback, rating } = req.body;
+
+ // message to send
+ const message = `
+
${username} has submitted some feedback.
+ userId: ${userId}
+ User email: ${userEmail}
+ Feedback: ${feedback}
+ Rating: ${rating}
+ `;
+
+ const transporter = nodemailer.createTransport({
+ service: 'gmail',
+ auth: {
+ user: process.env.MAIL_USERNAME,
+ pass: process.env.MAIL_PASSWORD,
+ },
+ });
+
+ const mailOptions = {
+ from: process.env.MAIL_USERNAME,
+ to: process.env.MAIL_USERNAME,
+ subject: 'CodeBooker: New Feedback Received',
+ html: message,
+ };
+
+ transporter.sendMail(mailOptions, (error, info) => {
+ if (error) {
+ console.log(error);
+ } else {
+ res.status(200).json('Email sent!');
+ console.log('Email sent: ' + info.response);
+ }
+ });
+ } catch (err) {
+ // Add catch block to handle exceptions
+ console.log(err.message);
+ res.status(500).json('Something went wrong...');
+ }
+};
+
module.exports.allfeedback = async (req, res) => {
try {
const newfeedback = await FeedBack.find();
diff --git a/src/server/controller/userController.js b/src/server/controller/userController.js
index aa2b03e..ea499a6 100644
--- a/src/server/controller/userController.js
+++ b/src/server/controller/userController.js
@@ -84,6 +84,7 @@ module.exports.login = async (req, res, next) => {
if (err) {
return next(err);
}
+ req.session.userId = user._id; //Save userId to session for feedback submission
if (req.body.rememberMe) {
token = jwt.sign(
{ email: user.email, id: user._id },
diff --git a/src/server/package.json b/src/server/package.json
index 4dc47ef..e838387 100644
--- a/src/server/package.json
+++ b/src/server/package.json
@@ -6,6 +6,7 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
+ "proxy": "http://localhost:3001",
"repository": {
"type": "git",
"url": "git+https://github.com/gbowne1/codebooker.git"
diff --git a/src/server/routes/userRoutes.js b/src/server/routes/userRoutes.js
index 26808ed..573bd1c 100644
--- a/src/server/routes/userRoutes.js
+++ b/src/server/routes/userRoutes.js
@@ -7,7 +7,11 @@ const {
forgotPassword,
resetPassword,
} = require('../controller/userController');
-const { allfeedback, addfeedback } = require('../controller/feedBackController');
+const {
+ allfeedback,
+ addfeedback,
+ emailFeedback,
+} = require('../controller/feedBackController');
//auth
router.post('/login', login);
@@ -21,5 +25,6 @@ router.put('/reset-password/:token', resetPassword);
//feedback routes
router.get('/feedback/all', allfeedback);
router.post('/feedback/new', addfeedback);
+router.post('/feedback/mail', emailFeedback);
module.exports = router;