From 7966f8710b38123b5279358391207bfa4d34fdba Mon Sep 17 00:00:00 2001 From: Wind-Explorer <66894537+Wind-Explorer@users.noreply.github.com> Date: Sun, 11 Aug 2024 15:22:32 +0800 Subject: [PATCH] User-side feedback --- client/src/App.tsx | 3 +- client/src/components/SiteFooter.tsx | 12 ++- client/src/layouts/restricted.tsx | 2 + client/src/pages/FeedbackPage.tsx | 151 +++++++++++++++++++++++++++ server/index.js | 5 +- server/models/Feedback.js | 41 ++++++++ server/routes/feedback.js | 59 +++++++++++ 7 files changed, 270 insertions(+), 3 deletions(-) create mode 100644 client/src/pages/FeedbackPage.tsx create mode 100644 server/models/Feedback.js create mode 100644 server/routes/feedback.js diff --git a/client/src/App.tsx b/client/src/App.tsx index cebb6ed..03e0e21 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -33,7 +33,7 @@ import CommunityPostManagement from "./pages/CommunityPostManagement"; import ManageVoucherPage from "./pages/ManageVoucherPage"; import CreateVoucherPage from "./pages/CreateVoucherPage"; import EditVoucherPage from "./pages/EditVoucherPage"; - +import FeedbackPage from "./pages/FeedbackPage"; function App() { return ( @@ -83,6 +83,7 @@ function App() { element={} path="reset-password/:token" /> + } path="feedback" /> diff --git a/client/src/components/SiteFooter.tsx b/client/src/components/SiteFooter.tsx index 28b8d2d..1abe143 100644 --- a/client/src/components/SiteFooter.tsx +++ b/client/src/components/SiteFooter.tsx @@ -1,12 +1,22 @@ import { Button, Link } from "@nextui-org/react"; +import { useNavigate } from "react-router-dom"; export default function SiteFooter() { + const navigate = useNavigate(); return (

Have a question?

-
diff --git a/client/src/layouts/restricted.tsx b/client/src/layouts/restricted.tsx index 99eac89..5170020 100644 --- a/client/src/layouts/restricted.tsx +++ b/client/src/layouts/restricted.tsx @@ -1,6 +1,7 @@ import { Outlet, useNavigate } from "react-router-dom"; import EcoconnectFullLogo from "../components/EcoconnectFullLogo"; import { Button, Card } from "@nextui-org/react"; +import { Toaster } from "react-hot-toast"; export default function RestrictedLayout() { const navigate = useNavigate(); @@ -29,6 +30,7 @@ export default function RestrictedLayout() {
+ ); } diff --git a/client/src/pages/FeedbackPage.tsx b/client/src/pages/FeedbackPage.tsx new file mode 100644 index 0000000..7f15ddb --- /dev/null +++ b/client/src/pages/FeedbackPage.tsx @@ -0,0 +1,151 @@ +import { useEffect, useState } from "react"; +import { retrieveUserInformation } from "../security/users"; +import { useNavigate } from "react-router-dom"; +import { ErrorMessage, Field, Formik, Form } from "formik"; +import * as Yup from "yup"; +import NextUIFormikInput from "../components/NextUIFormikInput"; +import { Button, Checkbox } from "@nextui-org/react"; +import NextUIFormikTextarea from "../components/NextUIFormikTextarea"; +import NextUIFormikSelect from "../components/NextUIFormikSelect"; +import instance from "../security/http"; +import config from "../config"; +import { popErrorToast, popToast } from "../utilities"; + +export default function FeedbackPage() { + const [userInformation, setUserInformation] = useState(); + const navigate = useNavigate(); + useEffect(() => { + retrieveUserInformation() + .then((response) => { + setUserInformation(response); + }) + .catch(() => { + navigate("/signin"); + }); + }, []); + + const validationSchema = Yup.object({ + feedbackCategory: Yup.string().trim().required("Select feedback type."), + subject: Yup.string().trim().min(1).max(100).required("Enter a subject."), + comment: Yup.string() + .trim() + .min(1) + .max(1024) + .required("Enter your comments."), + allowContact: Yup.boolean().oneOf([true, false], "please decide"), + }); + + const initialValues = { + feedbackCategory: "", + subject: "", + comment: "", + allowContact: false, + }; + + const handleSubmit = async (values: any) => { + try { + console.log(values.feedbackCategory); + instance + .post(config.serverAddress + "/feedback", { + ...values, + userId: userInformation.id, + feedbackCategory: parseInt(values.feedbackCategory), + }) + .then(() => { + popToast("Your feedback has been submitted!", 1); + navigate("/springboard"); + }); + } catch (error) { + popErrorToast(error); + } + }; + return ( + <> + {userInformation && ( +
+
+
+

Feedback

+

+ Use the form below to send us your comments. We read all + feedback carefully, but we are unable to respond to each + submission individually. +

+
+
+ + {({ isValid, dirty }) => ( +
+
+
+ +
+ +
+
+ +
+ +

+ I permit the ecoconnect administrators to contact me + via{" "} + + {userInformation.email} + {" "} + to better understand the comments I submitted. +

+
+ +
+ +
+
+ )} +
+
+
+
+ )} + + ); +} diff --git a/server/index.js b/server/index.js index de49c3f..e8960b3 100644 --- a/server/index.js +++ b/server/index.js @@ -40,9 +40,12 @@ app.use("/hbcform", HBCformRoute); const connections = require("./routes/connections"); app.use("/connections", connections); -const vouchers = require("./routes/vouchers.js"); +const vouchers = require("./routes/vouchers"); app.use("/vouchers", vouchers); +const feedback = require("./routes/feedback.js"); +app.use("/feedback", feedback) + db.sequelize .sync({ alter: true }) .then(() => { diff --git a/server/models/Feedback.js b/server/models/Feedback.js new file mode 100644 index 0000000..56f71af --- /dev/null +++ b/server/models/Feedback.js @@ -0,0 +1,41 @@ +const { DataTypes } = require("sequelize"); + +module.exports = (sequelize) => { + const Feedback = sequelize.define( + "Feedback", + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + allowNull: false, + primaryKey: true, + }, + userId: { + type: DataTypes.UUID, + allowNull: false, + }, + feedbackCategory: { + type: DataTypes.TINYINT(2), + allowNull: false, + }, + allowContact: { + type: DataTypes.BOOLEAN, + allowNull: false, + }, + subject: { + type: DataTypes.STRING(100), + allowNull: false, + }, + comment: { + type: DataTypes.STRING(1024), + allowNull: false, + }, + }, + { + tableName: "feedbacks", + timestamps: true, + } + ); + + return Feedback; +}; diff --git a/server/routes/feedback.js b/server/routes/feedback.js new file mode 100644 index 0000000..feb74c4 --- /dev/null +++ b/server/routes/feedback.js @@ -0,0 +1,59 @@ +const express = require("express"); +const router = express.Router(); +const yup = require("yup"); +const { validateToken } = require("../middlewares/auth"); +const {Feedback} = require("../models"); + +let validationSchema = yup.object({ + userId: yup.string().trim().min(36).max(36).required(), + feedbackCategory: yup.number().min(0).max(2).required(), + allowContact: yup.boolean().required(), + subject: yup.string().trim().min(1).max(100).required(), + comment: yup.string().trim().min(1).max(100).required(), +}); + +router.get("/all", validateToken, async (req, res) => { + let condition = {}; + let search = req.query.search; + if (search) { + condition[Op.or] = [ + { type: { [Op.like]: `%${search}%` } }, + { comment: { [Op.like]: `%${search}%` } }, + ]; + } + + let list = await Feedback.findAll({ + where: condition, + order: [["createdAt", "DESC"]], + }); + res.json(list); +}); + +router.get("/:id", validateToken, async (req, res) => { + let id = req.params.id; + let feedback = await Feedback.findByPk(id); + + if (!feedback) { + res.sendStatus(404); + return; + } + + res.json(feedback); +}); + +router.post("/", validateToken, async (req, res) => { + let data = req.body; + + try { + console.log("Validating schema..."); + data = await validationSchema.validate(data, { abortEarly: false }); + + let result = await Feedback.create(data); + res.json(result); + } catch (err) { + console.log("Error caught! Info: " + err); + res.status(400).json({ errors: err.errors }); + } +}); + +module.exports = router; \ No newline at end of file