From d4d236bf8c7f021bfd04c09705d90dce2f4e3ba2 Mon Sep 17 00:00:00 2001
From: Guan Quan <76832850+guanquann@users.noreply.github.com>
Date: Sat, 21 Sep 2024 09:50:37 +0800
Subject: [PATCH] Refractor code
---
frontend/src/App.tsx | 2 +
frontend/src/pages/NewQuestion/index.tsx | 15 +-
frontend/src/pages/QuestionEdit/index.tsx | 193 ++++++++++++++++++++++
frontend/src/reducers/questionReducer.ts | 12 +-
frontend/src/utils/constants.ts | 14 ++
5 files changed, 213 insertions(+), 23 deletions(-)
create mode 100644 frontend/src/pages/QuestionEdit/index.tsx
create mode 100644 frontend/src/utils/constants.ts
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index a3f87b7364..1ffa7bc575 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -2,6 +2,7 @@ import { BrowserRouter, Routes, Route } from "react-router-dom";
import Layout from "./components/Layout";
import NewQuestion from "./pages/NewQuestion";
import QuestionDetail from "./pages/QuestionDetail";
+import QuestionEdit from "./pages/QuestionEdit";
import PageNotFound from "./pages/Error";
function App() {
@@ -12,6 +13,7 @@ function App() {
question page list>} />
} />
} />
+ } />
} />
diff --git a/frontend/src/pages/NewQuestion/index.tsx b/frontend/src/pages/NewQuestion/index.tsx
index 8ec973af95..eaf892e0c5 100644
--- a/frontend/src/pages/NewQuestion/index.tsx
+++ b/frontend/src/pages/NewQuestion/index.tsx
@@ -5,23 +5,11 @@ import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
+import { complexityList, categoryList } from "../../utils/constants";
import AppMargin from "../../components/AppMargin";
import QuestionMarkdown from "../../components/QuestionMarkdown";
import QuestionImageContainer from "../../components/QuestionImageContainer";
-// hardcode for now
-const complexityList: string[] = ["Easy", "Medium", "Hard"];
-const categoryList: string[] = [
- "Strings",
- "Algorithms",
- "Data Structures",
- "Bit Manipulation",
- "Recursion",
- "Databases",
- "Arrays",
- "Brainteaser",
-];
-
const NewQuestion = () => {
const navigate = useNavigate();
@@ -106,7 +94,6 @@ const NewQuestion = () => {
return;
}
- toast.success("Question successfully created");
navigate("/questions");
} catch (error) {
console.error(error);
diff --git a/frontend/src/pages/QuestionEdit/index.tsx b/frontend/src/pages/QuestionEdit/index.tsx
new file mode 100644
index 0000000000..b5d88067f7
--- /dev/null
+++ b/frontend/src/pages/QuestionEdit/index.tsx
@@ -0,0 +1,193 @@
+import { useEffect, useState, useReducer } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+import { Autocomplete, Button, IconButton, Stack, TextField } from "@mui/material";
+import ArrowBackIcon from "@mui/icons-material/ArrowBack";
+import { ToastContainer, toast } from "react-toastify";
+import "react-toastify/dist/ReactToastify.css";
+
+import { complexityList, categoryList } from "../../utils/constants";
+import reducer, { getQuestionById, initialState } from "../../reducers/questionReducer";
+import AppMargin from "../../components/AppMargin";
+import QuestionMarkdown from "../../components/QuestionMarkdown";
+import QuestionImageContainer from "../../components/QuestionImageContainer";
+
+const QuestionEdit = () => {
+ const navigate = useNavigate();
+
+ const { questionId } = useParams<{ questionId: string }>();
+ const [state, dispatch] = useReducer(reducer, initialState);
+
+ const [title, setTitle] = useState("");
+ const [markdownText, setMarkdownText] = useState("");
+ const [selectedComplexity, setselectedComplexity] = useState(null);
+ const [selectedCategories, setSelectedCategories] = useState([]);
+ const [uploadedImagesUrl, setUploadedImagesUrl] = useState([]);
+
+ useEffect(() => {
+ if (!questionId) {
+ return;
+ }
+ getQuestionById(questionId, dispatch);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useEffect(() => {
+ if (state.selectedQuestion) {
+ setTitle(state.selectedQuestion.title);
+ setMarkdownText(state.selectedQuestion.description);
+ setselectedComplexity(state.selectedQuestion.complexity);
+ setSelectedCategories(state.selectedQuestion.categories);
+ }
+ }, [state.selectedQuestion]);
+
+ const handleBack = () => {
+ if (!confirm("Are you sure you want to leave this page? All process will be lost.")) {
+ return;
+ }
+ navigate("/questions");
+ };
+
+ const handleImageUpload = async (event: React.ChangeEvent) => {
+ if (!event.target.files) {
+ return;
+ }
+
+ const formData = new FormData();
+ for (const file of event.target.files) {
+ if (!file.type.startsWith("image/")) {
+ toast.error(`${file.name} is not an image`);
+ continue;
+ }
+
+ if (file.size > 5 * 1024 * 1024) {
+ toast.error(`${file.name} is more than 5MB`);
+ continue;
+ }
+ formData.append("images[]", file);
+ }
+
+ try {
+ const response = await fetch("http://localhost:3000/api/questions/images", {
+ method: "POST",
+ body: formData,
+ });
+
+ if (!response.ok) {
+ throw new Error("Failed to upload image");
+ }
+
+ const data = await response.json();
+ for (const imageUrl of data.imageUrls) {
+ setUploadedImagesUrl((prev) => [...prev, imageUrl]);
+ }
+ toast.success("File uploaded successfully");
+ } catch (error) {
+ console.error(error);
+ toast.error("Error uploading file");
+ }
+ };
+
+ const handleUpdate = async () => {
+ if (!state.selectedQuestion) {
+ return;
+ }
+
+ if (
+ title === state.selectedQuestion.title &&
+ markdownText === state.selectedQuestion.description &&
+ selectedComplexity === state.selectedQuestion.complexity &&
+ selectedCategories === state.selectedQuestion.categories
+ ) {
+ toast.error("You have not made any changes to the question");
+ return;
+ }
+
+ try {
+ const response = await fetch(
+ `http://localhost:3000/api/questions/${state.selectedQuestion.questionId}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ title,
+ description: markdownText,
+ complexity: selectedComplexity,
+ category: selectedCategories,
+ }),
+ }
+ );
+
+ const data = await response.json();
+ if (response.status === 400) {
+ toast.error(data.message);
+ return;
+ }
+
+ navigate("/questions");
+ } catch (error) {
+ console.error(error);
+ toast.error("Failed to updated question");
+ }
+ };
+
+ return (
+
+
+
+
+
+ setTitle(value.target.value)}
+ />
+
+ {
+ setselectedComplexity(newcomplexitySelected);
+ }}
+ renderInput={(params) => }
+ />
+
+ {
+ setSelectedCategories(newCategoriesSelected);
+ }}
+ renderInput={(params) => }
+ />
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default QuestionEdit;
diff --git a/frontend/src/reducers/questionReducer.ts b/frontend/src/reducers/questionReducer.ts
index a8f5aa8a6b..ea58156ec9 100644
--- a/frontend/src/reducers/questionReducer.ts
+++ b/frontend/src/reducers/questionReducer.ts
@@ -36,10 +36,7 @@ export const initialState: QuestionsState = {
selectedQuestionError: null,
};
-export const getQuestionById = (
- questionId: string,
- dispatch: Dispatch
-) => {
+export const getQuestionById = (questionId: string, dispatch: Dispatch) => {
// questionClient
// .get(`/questions/${questionId}`)
// .then((res) =>
@@ -61,15 +58,12 @@ export const getQuestionById = (
title: "Test Question",
description: md,
complexity: "Easy",
- categories: ["Category1", "Category2"],
+ categories: ["Strings", "Databases"],
},
});
};
-const reducer = (
- state: QuestionsState,
- action: QuestionActions
-): QuestionsState => {
+const reducer = (state: QuestionsState, action: QuestionActions): QuestionsState => {
const { type } = action;
switch (type) {
diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts
new file mode 100644
index 0000000000..9364dd500c
--- /dev/null
+++ b/frontend/src/utils/constants.ts
@@ -0,0 +1,14 @@
+const complexityList: string[] = ["Easy", "Medium", "Hard"];
+
+const categoryList: string[] = [
+ "Strings",
+ "Algorithms",
+ "Data Structures",
+ "Bit Manipulation",
+ "Recursion",
+ "Databases",
+ "Arrays",
+ "Brainteaser",
+];
+
+export { complexityList, categoryList };