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