diff --git a/package-lock.json b/package-lock.json index cb9ccbd..d56d81b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.4.3", "axios": "^1.4.0", + "formik": "^2.4.3", "isbn-validate": "^1.0.3", "lodash": "^4.17.21", "material-ui-popup-state": "^5.0.9", @@ -31,7 +32,8 @@ "react-scripts": "5.0.1", "react-simple-star-rating": "^5.1.7", "styled-components": "^5.3.11", - "web-vitals": "^3.3.2" + "web-vitals": "^3.3.2", + "yup": "^1.2.0" }, "devDependencies": { "eslint": "^8.45.0", @@ -8891,6 +8893,37 @@ "node": ">= 6" } }, + "node_modules/formik": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/formik/-/formik-2.4.3.tgz", + "integrity": "sha512-2Dy79Szw3zlXmZiokUdKsn+n1ow4G8hRrC/n92cOWHNTWXCRpQXlyvz6HcjW7aSQZrldytvDOavYjhfmDnUq8Q==", + "funding": [ + { + "type": "individual", + "url": "https://opencollective.com/formik" + } + ], + "dependencies": { + "deepmerge": "^2.1.1", + "hoist-non-react-statics": "^3.3.0", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "react-fast-compare": "^2.0.1", + "tiny-warning": "^1.0.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/formik/node_modules/deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -12322,6 +12355,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -14624,6 +14662,11 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/property-expr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz", + "integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -14899,6 +14942,11 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "node_modules/react-fast-compare": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", + "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" + }, "node_modules/react-hot-toast": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz", @@ -16727,6 +16775,16 @@ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "node_modules/titleize": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", @@ -16771,6 +16829,11 @@ "node": ">=0.6" } }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, "node_modules/tough-cookie": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", @@ -18073,6 +18136,28 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yup": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.2.0.tgz", + "integrity": "sha512-PPqYKSAXjpRCgLgLKVGPA33v5c/WgEx3wi6NFjIiegz90zSwyMpvTFp/uGcVnnbx6to28pgnzp/q8ih3QRjLMQ==", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, + "node_modules/yup/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 28f47f4..25691fd 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.4.3", "axios": "^1.4.0", + "formik": "^2.4.3", "isbn-validate": "^1.0.3", "lodash": "^4.17.21", "material-ui-popup-state": "^5.0.9", @@ -25,7 +26,8 @@ "react-scripts": "5.0.1", "react-simple-star-rating": "^5.1.7", "styled-components": "^5.3.11", - "web-vitals": "^3.3.2" + "web-vitals": "^3.3.2", + "yup": "^1.2.0" }, "scripts": { "start": "react-scripts start", diff --git a/src/App.js b/src/App.js index fc9af7e..fb83893 100644 --- a/src/App.js +++ b/src/App.js @@ -8,14 +8,15 @@ import Register from './components/Register/Register'; import UserAuthenticated from './ProtectedRoute'; import ForgotPassword from './pages/ForgotPassword/ForgotPassword'; import ResetPassword from './pages/ResetPassword/ResetPassword'; +import Support from './pages/Support/Support'; import PrivacyPolicy from './pages/PrivacyPolicy/PrivacyPolicy'; - export default function App() { return ( }> } /> + } /> } /> } /> diff --git a/src/components/Dropdown/Dropdown.js b/src/components/Dropdown/Dropdown.js index 5d5eea4..5a8344e 100644 --- a/src/components/Dropdown/Dropdown.js +++ b/src/components/Dropdown/Dropdown.js @@ -118,7 +118,7 @@ export default function Dropdown() { transformOrigin={{ horizontal: 'right', vertical: 'top' }} anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }} > - + navigate('/support')}>   Help diff --git a/src/components/FormikContainer/SupportForm/SupportForm.js b/src/components/FormikContainer/SupportForm/SupportForm.js new file mode 100644 index 0000000..a4a9c2b --- /dev/null +++ b/src/components/FormikContainer/SupportForm/SupportForm.js @@ -0,0 +1,186 @@ +import { Box, Button, Grid, IconButton, Paper, TextField } from '@mui/material'; +import axios from 'axios'; +import { Formik, Field, Form, ErrorMessage } from 'formik'; +import * as Yup from 'yup'; +import CircularProgress from '@mui/material/CircularProgress'; +import Snackbar from '@mui/material/Snackbar'; +import CloseIcon from '@mui/icons-material/Close'; +import React, { useState } from 'react'; +const SupportForm = () => { + const [loading, setLoading] = useState(false); + const [showSnackBar, setShowSnackBar] = useState(false); + const [showError, setShowError] = useState(false); + const [supportError, setSupportError] = useState(''); + const initialValue = { + name: '', + email: '', + description: '', + }; + const validationSchema = Yup.object({ + name: Yup.string().required('Required'), + email: Yup.string().email('E-mail is not valid').required('Required'), + description: Yup.string().required('Required!'), + }); + const handleSubmit = async (values, onSubmitProps) => { + const { name, email, description } = values; + try { + setLoading(true); + const response = await axios.post( + 'http://localhost:3001/api/support/newsupport', + { + name, + email, + description, + } + ); + if (response.status === 200) { + setShowSnackBar(true); + onSubmitProps.resetForm(); + } + } catch (err) { + const errorMsg = err.response.data.message; + setSupportError(errorMsg); + setShowError(true); + } + setLoading(false); + }; + const closeSnackBar = () => { + setShowSnackBar(false); + setShowError(false); + }; + const action = ( + + + + + + ); + + return ( + <> + + + + + + + + + {({ + values, + handleChange, + handleBlur, + touched, + errors, + }) => { + const { name, email } = values; + return ( +
+ + } + error={ + errors.name && touched.name + } + required + /> + + } + error={ + errors.email && + touched.email + } + required + /> + + } + error={ + errors.description && + touched.description + } + /> + + + ); + }} +
+
+
+
+ +
+ + ); +}; + +export default SupportForm; diff --git a/src/pages/Support/Support.css b/src/pages/Support/Support.css new file mode 100644 index 0000000..1891f38 --- /dev/null +++ b/src/pages/Support/Support.css @@ -0,0 +1,72 @@ +.support-wrapper{ + position: relative; + max-width: 100vw; + width: 100%; + display: flex; + flex-direction: column; + +} + +.global-header{ + width: 100%; + background-color: #1976d2; + z-index: 1; + position: fixed; +} + +.help-navigation{ + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 10px 0 10px; + height: 60px; +} + + +.support-search-form{ + display: flex; + height: 40px; + width: 100%; + position: relative; +} + +.search{ + height: 100%; + flex: 1; + border: none; + outline: none; + padding: 10px; +} + + +.support-form{ + width: 100%; + display: grid; + place-items: center; +} + +.support-input-wrapper { + display: flex; + flex-direction: column; + gap: 1rem; + width: 35%; +} + +.form-group{ + display: flex; + flex-direction: column; +} + + + + +@media screen and (max-width: 991px){ + .help-navigation{ + gap: 5px; + } + + .login-wrapper-input{ + width: 80%; + } + +} \ No newline at end of file diff --git a/src/pages/Support/Support.jsx b/src/pages/Support/Support.jsx new file mode 100644 index 0000000..7b5ea41 --- /dev/null +++ b/src/pages/Support/Support.jsx @@ -0,0 +1,328 @@ +import React from 'react'; +import './Support.css'; +import HouseIcon from '@mui/icons-material/House'; +import LogoutIcon from '@mui/icons-material/Logout'; +import SearchIcon from '@mui/icons-material/Search'; +import MenuIcon from '@mui/icons-material/Menu'; +import PersonIcon from '@mui/icons-material/Person'; +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; +import CloseIcon from '@mui/icons-material/Close'; +import { + Avatar, + Box, + Button, + Divider, + IconButton, + ListItemIcon, + Menu, + MenuItem, + Tooltip, + Typography, + useMediaQuery, +} from '@mui/material'; +import { useState } from 'react'; +import { useNavigate } from 'react-router'; +import SupportForm from '../../components/FormikContainer/SupportForm/SupportForm'; + +const Support = () => { + const [anchorEl, setAnchorEl] = useState(null); + const [closeNotice, setCloseNotce] = useState(true); + const userName = JSON.parse(localStorage.getItem('user')).username; + const navigate = useNavigate(); + const open = Boolean(anchorEl); + const handleLogOut = () => { + localStorage.setItem('user', null); + localStorage.setItem('token', null); + navigate('/login'); + }; + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + const matches = useMediaQuery('(max-width:700px)'); + const mobileDisplay = useMediaQuery('(max-width:991px)'); + return ( +
+
+ + + + + Due to the ongoing development phase of Codebooker, + our search functionality is currently unavailable. + Kindly submit your queries via the form below. + + + + setCloseNotce(false)} + sx={{ + color: '#fff', + cursor: 'pointer', + }} + /> + + + + + +
+ + + + + + Hi {userName}, Welcome to Codebooker Support. + + +
+ + + + +
+
+
+
+
+
+ + + Please provide details of your support inquiry in the form + below. + + + +
+ ); +}; +export default Support; diff --git a/src/server/controller/supportController.js b/src/server/controller/supportController.js new file mode 100644 index 0000000..9d61207 --- /dev/null +++ b/src/server/controller/supportController.js @@ -0,0 +1,63 @@ +const User = require('../model/userModel'); +const Support = require('../model/supportModel'); +const nodemailer = require('nodemailer'); + +module.exports.newSupport = async (req, res) => { + const { name, email, description } = req.body; + try { + // find user + const submittingUser = await User.findOne({ email }); + if (!submittingUser) { + return res.status(404).json({ + message: + 'Please use your Codebooker email address or verify your email address (NOT FOUND)', + }); + } + + // save to db + const newSupport = new Support({ + userId: submittingUser._id, + description, + status : 'In Progress', + createdAt: Date.now(), + }); + + await newSupport.save(); + // send message to support mail address + const message = ` +

New Support Request

+
  • +

    User name:${name}

    +

    User email:${email}

    +
  • +

    ${description}

    + `; + const transporter = nodemailer.createTransport({ + service: 'gmail', + auth: { + user: process.env.MAIL_USERNAME, + pass: process.env.MAIL_PASSWORD, + }, + }); + + const mailOptions = { + from: email, + to: process.env.MAIL_USERNAME, + subject: 'Assistance Needed', + 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); + } + }); + res.status(200); + } catch (err) { + console.error(err); + res.status(500).json({ message: 'Internal server error' }); + } +}; diff --git a/src/server/index.js b/src/server/index.js index 3ac6f2b..a014276 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -11,6 +11,7 @@ const session = require('express-session'); const userRoutes = require('./routes/userRoutes'); const booksRoutes = require('./routes/booksRoutes'); const reviewRoutes = require('./routes/reviewRoutes'); +const supportRoutes = require('./routes/supportRoutes'); const sessionConfig = { secret: 'keyboard cat', @@ -53,6 +54,9 @@ app.use('/api/books', booksRoutes); // routes for review app.use('/api/review', reviewRoutes); +// routes for support/help +app.use('/api/support', supportRoutes); + app.use('/api/*', (req, res) => { res.send('404 No routes found'); }); diff --git a/src/server/model/supportModel.js b/src/server/model/supportModel.js new file mode 100644 index 0000000..f556fdd --- /dev/null +++ b/src/server/model/supportModel.js @@ -0,0 +1,25 @@ +const mongoose = require('mongoose'); + +const supportSchema = new mongoose.Schema({ + userId:{ + type: mongoose.Schema.Types.ObjectId, + required: true, + ref: 'User', + }, + description: { + type: String, + unique: true, + required: true, + }, + status: { + type: String, + enum: ['Open', 'In Progress', 'Resolved'], + default: 'Open', + }, + createdAt: { + type: Date, + default: Date.now, + }, +}); +const Support = mongoose.model('Support', supportSchema); +module.exports = Support; \ No newline at end of file diff --git a/src/server/routes/supportRoutes.js b/src/server/routes/supportRoutes.js new file mode 100644 index 0000000..967c6b8 --- /dev/null +++ b/src/server/routes/supportRoutes.js @@ -0,0 +1,6 @@ +const express = require('express'); +const router = express.Router(); + +const {newSupport} = require('../controller/supportController'); +router.post('/newsupport', newSupport); +module.exports = router; \ No newline at end of file