User-side feedback

This commit is contained in:
Wind-Explorer
2024-08-11 15:22:32 +08:00
parent b35c74ac68
commit 7966f8710b
7 changed files with 270 additions and 3 deletions

View File

@@ -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={<ResetPasswordPage />}
path="reset-password/:token"
/>
<Route element={<FeedbackPage />} path="feedback" />
</Route>
</Route>

View File

@@ -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 (
<div className="bg-black text-white p-8">
<div className="flex flex-col text-center *:mx-auto gap-16">
<div className="flex flex-col gap-4 *:mx-auto">
<p className="text-2xl font-bold">Have a question?</p>
<Button color="primary" variant="solid" className="px-24" size="lg">
<Button
color="primary"
variant="solid"
className="px-24"
size="lg"
onPress={() => {
navigate("/feedback");
}}
>
Get in touch with us
</Button>
</div>

View File

@@ -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() {
</div>
</div>
</div>
<Toaster />
</div>
);
}

View File

@@ -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<any>();
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 && (
<div className="">
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-4">
<p className="text-3xl font-bold">Feedback</p>
<p>
Use the form below to send us your comments. We read all
feedback carefully, but we are unable to respond to each
submission individually.
</p>
</div>
<div>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
{({ isValid, dirty }) => (
<Form>
<div className="flex flex-col gap-4">
<div className="flex flex-row gap-4">
<NextUIFormikInput
label="Subject"
name="subject"
type="text"
placeholder=""
labelPlacement="inside"
/>
<div className="w-96">
<NextUIFormikSelect
label="Feedback category"
name="feedbackCategory"
placeholder=""
labelPlacement="inside"
options={[
{ key: "0", label: "Feature request" },
{ key: "1", label: "Bug report" },
{ key: "2", label: "Get in contact" },
]}
/>
</div>
</div>
<NextUIFormikTextarea
label="Comments"
name="comment"
placeholder=""
labelPlacement="inside"
/>
<div>
<Field
name="allowContact"
type="checkbox"
as={Checkbox}
aria-label="Allow the team to contact you"
>
<p>
I permit the ecoconnect administrators to contact me
via{" "}
<span className="font-bold underline">
{userInformation.email}
</span>{" "}
to better understand the comments I submitted.
</p>
</Field>
<ErrorMessage
name="allowContact"
component="div"
className="text-red-500"
/>
</div>
<Button
type="submit"
color="primary"
isDisabled={!isValid || !dirty}
>
Submit Feedback
</Button>
</div>
</Form>
)}
</Formik>
</div>
</div>
</div>
)}
</>
);
}

View File

@@ -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(() => {

41
server/models/Feedback.js Normal file
View File

@@ -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;
};

59
server/routes/feedback.js Normal file
View File

@@ -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;