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 }) => (
+
+ )}
+
+
+
+
+ )}
+ >
+ );
+}
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