diff --git a/src/api/categoryApi.ts b/src/api/categoryApi.ts new file mode 100644 index 0000000..1948693 --- /dev/null +++ b/src/api/categoryApi.ts @@ -0,0 +1,6 @@ +import axiosInstance from "./axiosInstance"; + +export const getCategories = async () => { + const res = await axiosInstance.get("/api/categories"); + return res.data.data; +}; diff --git a/src/api/questionsApi.ts b/src/api/questionsApi.ts new file mode 100644 index 0000000..b9649b1 --- /dev/null +++ b/src/api/questionsApi.ts @@ -0,0 +1,25 @@ +import axiosInstance from "./axiosInstance"; + + + +type GetQuestionsParams = { + page?: number; + id?: number; +}; + +export const getQuestions = async ({ page = 1, id }: GetQuestionsParams) => { + const params: Record = { page }; + if (id) params.category_id = id; + + const { data } = await axiosInstance.get( + "/api/questions", + { params } + ); + + return data; +}; + +export const getQuestionById = async (id: number) => { + const response = await axiosInstance.get(`/api/questions/${id}`); + return response.data.data; +}; diff --git a/src/api/testApi.ts b/src/api/testApi.ts new file mode 100644 index 0000000..974696e --- /dev/null +++ b/src/api/testApi.ts @@ -0,0 +1,41 @@ +import type { + StartTestPayload, + SubmitAnswerPayload, + UserTestType, +} from "../components/shared/types/TestTypes"; +import axiosInstance from "./axiosInstance"; + +export const startTest = async (data: StartTestPayload) => { + const res = await axiosInstance.post<{ data: UserTestType }>( + "/api/user-tests", + data + ); + return res.data.data; +}; + +export const completeUserTest = async (userTestId: number) => { + const res = await axiosInstance.post<{ message: string }>( + `/api/user-tests/${userTestId}/complete` + ); + return res.data; +}; + +export const getUserTestById = async (userTestId: number) => { + const res = await axiosInstance.get<{ data: UserTestType }>( + `/api/user-tests/${userTestId}` + ); + return res.data.data; +}; + +export const getUserTests = async () => { + const res = await axiosInstance.get("/api/user-tests/me"); + return res.data; +}; + +export const submitAnswer = async (data: SubmitAnswerPayload) => { + const res = await axiosInstance.post<{ message: string }>( + `/api/user-test-answers/${data.answerId}/submit`, + { answer: data.answer } + ); + return res.data; +}; diff --git a/src/assets/bg.jpg b/src/assets/bg.jpg deleted file mode 100644 index 18c8ae2..0000000 Binary files a/src/assets/bg.jpg and /dev/null differ diff --git a/src/assets/bg.png b/src/assets/bg.png deleted file mode 100644 index 0f209b8..0000000 Binary files a/src/assets/bg.png and /dev/null differ diff --git a/src/assets/hoshi.jpg b/src/assets/hoshi.jpg deleted file mode 100644 index 31ecc56..0000000 Binary files a/src/assets/hoshi.jpg and /dev/null differ diff --git a/src/components/Answers/Answers.tsx b/src/components/Answers/Answers.tsx new file mode 100644 index 0000000..63f64d8 --- /dev/null +++ b/src/components/Answers/Answers.tsx @@ -0,0 +1,81 @@ +import { useState } from "react"; +import { Box, Button, Typography } from "@mui/material"; + +interface Variant { + id: number; + text: string; +} + +interface LearningAnswersProps { + variants?: Variant[]; + correctAnswers: number[] | string[]; + type: "single" | "text"; +} + +const LearningAnswers = ({ + variants = [], + correctAnswers = [], + type, +}: LearningAnswersProps) => { + const [showAnswer, setShowAnswer] = useState(false); + + if (type === "text") { + return ( + + {showAnswer && ( + + Correct answer:{" "} + {Array.isArray(correctAnswers) + ? correctAnswers.join(", ") + : correctAnswers} + + )} + + + ); + } + + return ( + + {variants.map((variant) => ( + + {variant.text} + + ))} + + + + ); +}; + +export default LearningAnswers; diff --git a/src/components/Question/Question.styles.ts b/src/components/Question/Question.styles.ts index 65ec319..7cb4580 100644 --- a/src/components/Question/Question.styles.ts +++ b/src/components/Question/Question.styles.ts @@ -1,12 +1,16 @@ import styled from "@emotion/styled"; +import { Link } from "react-router-dom"; -export const QuestionWrapper = styled("div")({ +export const QuestionWrapper = styled(Link)({ display: "flex", flexDirection: "column", justifyContent: "center", padding: "15px", border: "3px solid #4B2981", borderRadius: "10px", + marginBottom: "10px", + textDecoration: "none", + color: "inherit" }); export const QuestionTitle = styled("div")({ diff --git a/src/components/Question/Question.tsx b/src/components/Question/Question.tsx index ec13965..8203c3b 100644 --- a/src/components/Question/Question.tsx +++ b/src/components/Question/Question.tsx @@ -7,22 +7,15 @@ import { } from "./Question.styles"; import CategoryIcon from "@mui/icons-material/Category"; import PersonIcon from "@mui/icons-material/Person"; +import type { QuestionType } from "../shared/types/QuestionTypes"; -type Question = { - title: string; - category: string; - description: string; - difficulty: number; - author: string; -}; - -type QuestionProps = { - question: Question; -}; +interface QuestionProps { + question: QuestionType; +} const Question = ({ question }: QuestionProps) => { return ( - + {question.title} @@ -31,17 +24,20 @@ const Question = ({ question }: QuestionProps) => { variant="outlined" icon={} color="primary" - label={`${question.category}`} + label={question.category.name} /> + {question.description} + Difficulty: {question.difficulty} + - {question.author} + {question.author.username} diff --git a/src/components/StartTestForm/StartTestForm.tsx b/src/components/StartTestForm/StartTestForm.tsx index b7dd3be..0b8b13a 100644 --- a/src/components/StartTestForm/StartTestForm.tsx +++ b/src/components/StartTestForm/StartTestForm.tsx @@ -9,30 +9,67 @@ import { } from "@mui/material"; import { StartTestWrapper } from "./StartTestForm.styles"; import { useState } from "react"; -import { notifyError, notifySuccess } from "../shared/toastify"; +import { useCategories } from "../../hooks/Question/useCategories"; +import type { Category } from "../shared/types/QuestionTypes"; +import { useStartTest } from "../../hooks/Tests/useStartTest"; +import { toast } from "react-toastify"; const StartTestForm = () => { const [category, setCategory] = useState(""); const [minDifficulty, setMinDifficulty] = useState(1); const [maxDifficulty, setMaxDifficulty] = useState(10); + const { data: categories, isLoading, isError } = useCategories(); + const startTestMutation = useStartTest(); + + const handleStartTest = () => { + if (!category) { + toast.error("Please select a category!"); + return; + } + + startTestMutation.mutate({ + category_id: Number(category), + min_difficulty: minDifficulty, + max_difficulty: maxDifficulty, + }); + }; + + const handleFeelingLucky = () => { + if (!categories || categories.length === 0) { + toast.error("No categories available"); + return; + } + + const randomCategory = + categories[Math.floor(Math.random() * categories.length)]; + + const minDifficulty = 1; + const maxDifficulty = 10; + + startTestMutation.mutate({ + category_id: randomCategory.id, + min_difficulty: minDifficulty, + max_difficulty: maxDifficulty, + }); + }; const handleChangeMinDifficulty = (e: SelectChangeEvent) => { const value = Number(e.target.value); - if(value <= maxDifficulty){ + if (value <= maxDifficulty) { setMinDifficulty(value); - } else{ - notifyError("Min difficulty should be smaller than max difficulty!") + } else { + toast.error("Min difficulty should be smaller than max difficulty!"); } - } + }; - const handleChangeMaxDifficulty = (e: SelectChangeEvent) => { + const handleChangeMaxDifficulty = (e: SelectChangeEvent) => { const value = Number(e.target.value); - if(value >= minDifficulty){ + if (value >= minDifficulty) { setMaxDifficulty(value); - } else{ - notifyError("Max difficulty should be bigger than min difficulty!") + } else { + toast.error("Max difficulty should be bigger than min difficulty!"); } - } + }; return ( @@ -42,11 +79,15 @@ const StartTestForm = () => { id="category-select" value={category} label="Category" - onChange={(e) => setCategory(e.target.value as string)} + onChange={(e) => setCategory(e.target.value)} > - Physics - Microcontrollers - English + {isLoading && Loading...} + {isError && Error loading categories} + {categories?.map((cat: Category) => ( + + {cat.name} + + ))} @@ -83,10 +124,16 @@ const StartTestForm = () => { - + - diff --git a/src/components/TestQuestion/TestQuestion.tsx b/src/components/TestQuestion/TestQuestion.tsx new file mode 100644 index 0000000..908293b --- /dev/null +++ b/src/components/TestQuestion/TestQuestion.tsx @@ -0,0 +1,98 @@ +import { useState, useEffect } from "react"; +import { + Typography, + Box, + Checkbox, + FormControlLabel, + Button, + TextField, +} from "@mui/material"; +import type { UserTestAnswer } from "../shared/types/TestTypes"; +import { useQueryClient } from "@tanstack/react-query"; + +interface TestQuestionProps { + qa: UserTestAnswer; + onSubmit: (answer: (string | number)[]) => void; +} + +const TestQuestion = ({ qa, onSubmit }: TestQuestionProps) => { + const queryClient = useQueryClient(); + const { question, answer: initialAnswer } = qa; + const [selectedAnswers, setSelectedAnswers] = useState<(string | number)[]>( + initialAnswer ?? [] + ); + + useEffect(() => { + setSelectedAnswers(initialAnswer ?? []); + queryClient.invalidateQueries({ queryKey: ["current-test"] }); + }, [initialAnswer, qa.question.id]); + + const handleCheckboxChange = (variantId: number) => { + if (question.type === "single") { + setSelectedAnswers([variantId]); + } else { + setSelectedAnswers((prev) => + prev.includes(variantId) + ? prev.filter((x) => x !== variantId) + : [...prev, variantId] + ); + } + }; + + const handleTextChange = (value: string) => { + setSelectedAnswers([value]); + }; + + const handleSubmit = () => { + onSubmit(selectedAnswers); + }; + + return ( + + {question.title} + {question.description && ( + + {question.description} + + )} + + {question.type === "text" && ( + handleTextChange(e.target.value)} + variant="outlined" + /> + )} + + {(question.type === "single" || question.type === "multiple") && + question.variants.map((variant) => ( + handleCheckboxChange(variant.id)} + /> + } + label={variant.text} + /> + ))} + + + + ); +}; + +export default TestQuestion; diff --git a/src/components/shared/toastify.ts b/src/components/shared/toastify.ts deleted file mode 100644 index 09b0713..0000000 --- a/src/components/shared/toastify.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { toast } from "react-toastify"; - -export const notifySuccess = (msg: string) => toast.success(msg); -export const notifyError = (msg: string) => toast.error(msg); \ No newline at end of file diff --git a/src/components/shared/types/QuestionTypes.ts b/src/components/shared/types/QuestionTypes.ts new file mode 100644 index 0000000..b2b7812 --- /dev/null +++ b/src/components/shared/types/QuestionTypes.ts @@ -0,0 +1,26 @@ +export type Variant = { + id: number; + text: string; +}; + +export type Category = { + id: number; + name: string; +}; + +export type Author = { + id: number; + username: string; +}; + +export type QuestionType = { + id: number; + title: string; + description: string; + type: "single" | "multiple" | "text"; + difficulty: number; + variants: Variant[]; + correct_answers: number[] | string[]; + category: Category; + author: Author; +}; \ No newline at end of file diff --git a/src/components/shared/types/TestTypes.ts b/src/components/shared/types/TestTypes.ts new file mode 100644 index 0000000..f63a7f8 --- /dev/null +++ b/src/components/shared/types/TestTypes.ts @@ -0,0 +1,63 @@ +import type { User } from "./AuthTypes"; +import type { QuestionType } from "./QuestionTypes"; + +export type CategoryType = { + id: number; + name: string; + created_at?: string; + questions_count?: number; + user_tests_count?: number; +}; + +export interface StartTestPayload { + category_id: number; + min_difficulty: number; + max_difficulty: number; +} + +export type UserTestAnswer = { + id: number; + user_test_id: number; + question_id: number; + question: QuestionType; + answer: (string | number)[] | null; + user_id: number; + is_correct: boolean; + created_at: string; + updated_at: string; +}; + +export type TestType = { + id: number; + title: string; + description: string | null; + category_id: number; + category: CategoryType; + questions: QuestionType[]; + is_available: boolean; + author_id: number; + author: User; + closed_at: string; + created_at: string; + updated_at: string; +}; + +export type UserTestType = { + id: number; + test_id: number | null; + test?: TestType; + user_id: number; + user?: User; + closed_at: string | null; + is_completed?: boolean; + score?: number; + is_available: boolean; + answers?: UserTestAnswer[]; + created_at: string; + updated_at: string; +}; + +export type SubmitAnswerPayload = { + answerId: number; + answer: (string | number)[]; +}; diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index 7ca3984..ce79d52 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -1,7 +1,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { toast } from "react-toastify"; -import { useCurrentUser } from "../hooks/useCurrentUser"; -import { useLogin } from "../hooks/useLogin"; +import { useCurrentUser } from "../hooks/auth/useCurrentUser"; +import { useLogin } from "../hooks/auth/useLogin"; import type { LoginPayload, LoginResponse, diff --git a/src/hooks/Question/useCategories.ts b/src/hooks/Question/useCategories.ts new file mode 100644 index 0000000..32d86cc --- /dev/null +++ b/src/hooks/Question/useCategories.ts @@ -0,0 +1,8 @@ +import { useQuery } from "@tanstack/react-query"; +import { getCategories } from "../../api/categoryApi"; + +export const useCategories = () => + useQuery({ + queryKey: ["categories"], + queryFn: getCategories, + }); diff --git a/src/hooks/Question/useQuestionById.ts b/src/hooks/Question/useQuestionById.ts new file mode 100644 index 0000000..bac39ff --- /dev/null +++ b/src/hooks/Question/useQuestionById.ts @@ -0,0 +1,9 @@ +import { useQuery } from "@tanstack/react-query"; +import { getQuestionById } from "../../api/questionsApi"; + +export const useQuestionById = (id?: number) => { + return useQuery({ + queryKey: ["single-question", id], + queryFn: () => getQuestionById(Number(id)), + }); +}; diff --git a/src/hooks/Question/useQuestions.ts b/src/hooks/Question/useQuestions.ts new file mode 100644 index 0000000..c7552b9 --- /dev/null +++ b/src/hooks/Question/useQuestions.ts @@ -0,0 +1,15 @@ +import { useQuery } from "@tanstack/react-query"; +import { getQuestions } from "../../api/questionsApi"; + +type UseQuestionsParams = { + page?: number; + id?: number; +}; + +export const useQuestions = ({ page = 1, id }: UseQuestionsParams) => { + return useQuery({ + queryKey: ["questions", { page, id }], + queryFn: () => getQuestions({ page, id }), + staleTime: 1000 * 60, + }); +}; diff --git a/src/hooks/Tests/useCompleteTest.ts b/src/hooks/Tests/useCompleteTest.ts new file mode 100644 index 0000000..940dc19 --- /dev/null +++ b/src/hooks/Tests/useCompleteTest.ts @@ -0,0 +1,15 @@ +import { useMutation } from "@tanstack/react-query"; +import { completeUserTest } from "../../api/testApi"; +import { toast } from "react-toastify"; + +export const useCompleteTest = () => { + return useMutation({ + mutationFn: completeUserTest, + onSuccess: (data) => { + toast.success(data.message); + }, + onError: (error: any) => { + toast.error(error.response?.data?.message); + }, + }); +}; diff --git a/src/hooks/Tests/useGetUserTests.ts b/src/hooks/Tests/useGetUserTests.ts new file mode 100644 index 0000000..7371ce3 --- /dev/null +++ b/src/hooks/Tests/useGetUserTests.ts @@ -0,0 +1,9 @@ +import { useQuery } from "@tanstack/react-query"; +import { getUserTests } from "../../api/testApi"; + +export const useGetUserTests = () => { + return useQuery({ + queryKey: ["user-tests"], + queryFn: () => getUserTests, + }); +}; diff --git a/src/hooks/Tests/useStartTest.ts b/src/hooks/Tests/useStartTest.ts new file mode 100644 index 0000000..a534e89 --- /dev/null +++ b/src/hooks/Tests/useStartTest.ts @@ -0,0 +1,18 @@ +import { useMutation } from "@tanstack/react-query"; +import { startTest } from "../../api/testApi"; +import { toast } from "react-toastify"; +import { useNavigate } from "react-router-dom"; + +export const useStartTest = () => { + const navigate = useNavigate(); + return useMutation({ + mutationFn: startTest, + onSuccess: (data) => { + toast.success("Test Started"); + navigate(`/tests/${data.id}`); + }, + onError: (error: any) => { + toast.error(error.response?.data?.message); + }, + }); +}; diff --git a/src/hooks/Tests/useSubmitAnswer.ts b/src/hooks/Tests/useSubmitAnswer.ts new file mode 100644 index 0000000..1410811 --- /dev/null +++ b/src/hooks/Tests/useSubmitAnswer.ts @@ -0,0 +1,16 @@ +import { useMutation } from "@tanstack/react-query"; +import { submitAnswer } from "../../api/testApi"; +import { toast } from "react-toastify"; + +export const useSubmitAnswer = () => { + return useMutation({ + mutationFn: submitAnswer, + onSuccess: (data) => { + toast.success(data.message); + console.log(data); + }, + onError: (error: any) => { + toast.error(error.response?.data?.message || "Something went wrong"); + }, + }); +}; diff --git a/src/hooks/Tests/useUserTestById.ts b/src/hooks/Tests/useUserTestById.ts new file mode 100644 index 0000000..c2ffc9c --- /dev/null +++ b/src/hooks/Tests/useUserTestById.ts @@ -0,0 +1,9 @@ +import { useQuery } from "@tanstack/react-query"; +import { getUserTestById } from "../../api/testApi"; + +export const useUserTestById = (id: number) => { + return useQuery({ + queryKey: ["current-test"], + queryFn: () => getUserTestById(id), + }); +}; diff --git a/src/hooks/useActivateAccount.ts b/src/hooks/auth/useActivateAccount.ts similarity index 80% rename from src/hooks/useActivateAccount.ts rename to src/hooks/auth/useActivateAccount.ts index b27af3d..9b77278 100644 --- a/src/hooks/useActivateAccount.ts +++ b/src/hooks/auth/useActivateAccount.ts @@ -1,12 +1,12 @@ import { useMutation } from "@tanstack/react-query"; -import { activateAccount } from "../api/authApi"; +import { activateAccount } from "../../api/authApi"; import { toast } from "react-toastify"; import { useNavigate } from "react-router-dom"; export const useActivateAccount = () => { const navigate = useNavigate(); return useMutation({ - mutationFn: (token: string) => activateAccount(token), + mutationFn: activateAccount, onSuccess: (data) => { toast.success(data.message); navigate("/login"); diff --git a/src/hooks/useCurrentUser.ts b/src/hooks/auth/useCurrentUser.ts similarity index 84% rename from src/hooks/useCurrentUser.ts rename to src/hooks/auth/useCurrentUser.ts index d1ad99f..c9250a6 100644 --- a/src/hooks/useCurrentUser.ts +++ b/src/hooks/auth/useCurrentUser.ts @@ -1,5 +1,5 @@ import { useQuery } from "@tanstack/react-query"; -import { fetchMe } from "../api/authApi"; +import { fetchMe } from "../../api/authApi"; export const useCurrentUser = () => useQuery({ diff --git a/src/hooks/useLogin.ts b/src/hooks/auth/useLogin.ts similarity index 73% rename from src/hooks/useLogin.ts rename to src/hooks/auth/useLogin.ts index 95fbc4d..b03ff9a 100644 --- a/src/hooks/useLogin.ts +++ b/src/hooks/auth/useLogin.ts @@ -1,13 +1,12 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { loginRequest } from "../api/authApi"; +import { loginRequest } from "../../api/authApi"; import { toast } from "react-toastify"; -import type { LoginPayload } from "../components/shared/types/AuthTypes"; export const useLogin = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: (payload: LoginPayload) => loginRequest(payload), + mutationFn: loginRequest, onSuccess: (data) => { localStorage.setItem("access_token", data.access_token); diff --git a/src/hooks/useRegistration.ts b/src/hooks/auth/useRegistration.ts similarity index 57% rename from src/hooks/useRegistration.ts rename to src/hooks/auth/useRegistration.ts index a699ac5..ea1fa79 100644 --- a/src/hooks/useRegistration.ts +++ b/src/hooks/auth/useRegistration.ts @@ -1,19 +1,12 @@ import { useMutation } from "@tanstack/react-query"; -import { registrationRequest } from "../api/authApi"; -import type { RegistrationPayload } from "../components/shared/types/AuthTypes"; +import { registrationRequest } from "../../api/authApi"; import { toast } from "react-toastify"; import { useNavigate } from "react-router-dom"; export const useRegistration = () => { const navigate = useNavigate(); return useMutation({ - mutationFn: ({ - username, - email, - password, - password_confirmation, - }: RegistrationPayload) => - registrationRequest({ username, email, password, password_confirmation }), + mutationFn: registrationRequest, onSuccess: (data) => { toast.success(data.message); navigate("/login"); diff --git a/src/hooks/useRequestPasswordReset.ts b/src/hooks/auth/useRequestPasswordReset.ts similarity index 73% rename from src/hooks/useRequestPasswordReset.ts rename to src/hooks/auth/useRequestPasswordReset.ts index 4225679..6003db2 100644 --- a/src/hooks/useRequestPasswordReset.ts +++ b/src/hooks/auth/useRequestPasswordReset.ts @@ -1,10 +1,10 @@ import { useMutation } from "@tanstack/react-query"; -import { requestPasswordReset } from "../api/authApi"; +import { requestPasswordReset } from "../../api/authApi"; import { toast } from "react-toastify"; export const useRequestPasswordReset = () => { return useMutation({ - mutationFn: (email: string) => requestPasswordReset(email), + mutationFn: requestPasswordReset, onSuccess: (data) => { toast.success(data.message); }, diff --git a/src/hooks/useResetPassword.ts b/src/hooks/auth/useResetPassword.ts similarity index 61% rename from src/hooks/useResetPassword.ts rename to src/hooks/auth/useResetPassword.ts index 35ad449..2a4acde 100644 --- a/src/hooks/useResetPassword.ts +++ b/src/hooks/auth/useResetPassword.ts @@ -1,11 +1,10 @@ import { useMutation } from "@tanstack/react-query"; -import type { ResetPasswordPayload } from "../components/shared/types/AuthTypes"; -import { resetPassword } from "../api/authApi"; +import { resetPassword } from "../../api/authApi"; import { toast } from "react-toastify"; export const useResetPassword = () => { return useMutation({ - mutationFn: (data: ResetPasswordPayload) => resetPassword(data), + mutationFn: resetPassword, onSuccess: (data) => { toast.success(data.message); }, diff --git a/src/pages/ActivateAccountPage/ActivateAccountPage.tsx b/src/pages/ActivateAccountPage/ActivateAccountPage.tsx index 9a44614..15b7bf2 100644 --- a/src/pages/ActivateAccountPage/ActivateAccountPage.tsx +++ b/src/pages/ActivateAccountPage/ActivateAccountPage.tsx @@ -1,6 +1,6 @@ import { useEffect } from "react"; import { useParams } from "react-router-dom"; -import { useActivateAccount } from "../../hooks/useActivateAccount"; +import { useActivateAccount } from "../../hooks/auth/useActivateAccount"; import { CircularProgress, Box, Typography } from "@mui/material"; const ActivateAccountPage = () => { diff --git a/src/pages/IndexPage/IndexPage.styles.ts b/src/pages/IndexPage/IndexPage.styles.ts index 0711049..7869932 100644 --- a/src/pages/IndexPage/IndexPage.styles.ts +++ b/src/pages/IndexPage/IndexPage.styles.ts @@ -7,11 +7,15 @@ export const WelcomeContainer = styled("div")({ justifyContent: "center", alignItems: "center", padding: "10px", - marginBottom: "36px" + marginBottom: "36px", }); export const ButtonGroup = styled("div")({ display: "flex", + marginTop: 20, + alignItems: "center", + justifyContent: "center", + gap: 10 }); export const IndexWrapper = styled("div")({ diff --git a/src/pages/IndexPage/IndexPage.tsx b/src/pages/IndexPage/IndexPage.tsx index 86796b4..8bc075b 100644 --- a/src/pages/IndexPage/IndexPage.tsx +++ b/src/pages/IndexPage/IndexPage.tsx @@ -1,18 +1,20 @@ -import { Typography } from "@mui/material"; +import { Typography, Pagination } from "@mui/material"; +import { useState } from "react"; import Container from "../../components/shared/Container"; -import { WelcomeContainer } from "./IndexPage.styles"; +import { WelcomeContainer } from "./IndexPage.styles"; import StartTestForm from "../../components/StartTestForm/StartTestForm"; import Question from "../../components/Question/Question"; +import { useQuestions } from "../../hooks/Question/useQuestions"; +import type { QuestionType } from "../../components/shared/types/QuestionTypes"; const IndexPage = () => { + const [page, setPage] = useState(1); + const questionsQuery = useQuestions({ page }); -const myQuestion = { - title: "Example Question", - category: "Math", - description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed ultricies, urna eu aliquet tincidunt, augue turpis gravida risus, sit amet posuere neque tellus sit amet justo. Curabitur non facilisis orci, vitae eleifend est. Cras fermentum, velit at scelerisque varius, mauris justo cursus lacus, ac porttitor ante metus sit amet nibh. Vivamus imperdiet, diam vel efficitur euismod, urna odio pharetra ipsum, vitae sollicitudin neque enim ut tortor. Donec gravida orci.", - difficulty: 3, - author: "David" -}; + const questions = questionsQuery.data?.data ?? []; + const loading = questionsQuery.isLoading; + const error = questionsQuery.error?.message ?? null; + const meta = questionsQuery.data?.meta ?? null; return ( @@ -21,9 +23,30 @@ const myQuestion = { Welcome To HoshiAI! The best place to learn! + - Browse Questions - + + + Browse Questions + + + {loading && Loading questions...} + {error && {error}} + + {questions.map((q: QuestionType) => ( + + ))} + + {meta && meta.last_page > 1 && ( + setPage(value)} + color="primary" + shape="rounded" + sx={{ mt: 3, mb: 3, display: "flex", justifyContent: "center" }} + /> + )} ); }; diff --git a/src/pages/RegistrationPage/RegistrationPage.tsx b/src/pages/RegistrationPage/RegistrationPage.tsx index 0cea742..bd37493 100644 --- a/src/pages/RegistrationPage/RegistrationPage.tsx +++ b/src/pages/RegistrationPage/RegistrationPage.tsx @@ -3,7 +3,7 @@ import { useAuth } from "../../context/AuthContext"; import { useNavigate } from "react-router-dom"; import Container from "../../components/shared/Container"; import { Box, Button, TextField, Typography } from "@mui/material"; -import { useRegistration } from "../../hooks/useRegistration"; +import { useRegistration } from "../../hooks/auth/useRegistration"; import { toast } from "react-toastify"; const RegistrationPage = () => { diff --git a/src/pages/RequestResetPage/RequestResetPage.tsx b/src/pages/RequestResetPage/RequestResetPage.tsx index edaea9b..5811cab 100644 --- a/src/pages/RequestResetPage/RequestResetPage.tsx +++ b/src/pages/RequestResetPage/RequestResetPage.tsx @@ -3,7 +3,7 @@ import Container from "../../components/shared/Container"; import { useNavigate } from "react-router-dom"; import { useAuth } from "../../context/AuthContext"; import { useEffect, useState } from "react"; -import { useRequestPasswordReset } from "../../hooks/useRequestPasswordReset"; +import { useRequestPasswordReset } from "../../hooks/auth/useRequestPasswordReset"; const RequestResetPage = () => { const { user } = useAuth(); diff --git a/src/pages/ResetPasswordPage/ResetPasswordPage.tsx b/src/pages/ResetPasswordPage/ResetPasswordPage.tsx index 415e6bf..1280136 100644 --- a/src/pages/ResetPasswordPage/ResetPasswordPage.tsx +++ b/src/pages/ResetPasswordPage/ResetPasswordPage.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { useAuth } from "../../context/AuthContext"; import { toast } from "react-toastify"; -import { useResetPassword } from "../../hooks/useResetPassword"; +import { useResetPassword } from "../../hooks/auth/useResetPassword"; import Container from "../../components/shared/Container"; import { Box, Button, TextField, Typography } from "@mui/material"; diff --git a/src/pages/SingleQuestionPage/SingleQuestionPage.tsx b/src/pages/SingleQuestionPage/SingleQuestionPage.tsx new file mode 100644 index 0000000..47a716f --- /dev/null +++ b/src/pages/SingleQuestionPage/SingleQuestionPage.tsx @@ -0,0 +1,28 @@ +import { Container } from "@mui/material"; +import { useParams } from "react-router-dom"; +import { useQuestionById } from "../../hooks/Question/useQuestionById"; +import Question from "../../components/Question/Question"; +import LearningAnswers from "../../components/Answers/Answers"; + +const SingleQuestionPage = () => { + const { id } = useParams(); + const { data: question, isLoading, error } = useQuestionById(Number(id)); + + if (isLoading) return Loading...; + if (error) return Error: {error.message}; + if (!question) return No question found; + + return ( + + + + + + ); +}; + +export default SingleQuestionPage; diff --git a/src/pages/TestPage/TestPage.tsx b/src/pages/TestPage/TestPage.tsx new file mode 100644 index 0000000..cc32106 --- /dev/null +++ b/src/pages/TestPage/TestPage.tsx @@ -0,0 +1,136 @@ +import { useParams } from "react-router-dom"; +import Container from "../../components/shared/Container"; +import { useUserTestById } from "../../hooks/Tests/useUserTestById"; +import { useState } from "react"; +import { Box, Button, Pagination, Typography } from "@mui/material"; +import TestQuestion from "../../components/TestQuestion/TestQuestion"; +import { useSubmitAnswer } from "../../hooks/Tests/useSubmitAnswer"; +import { useCompleteTest } from "../../hooks/Tests/useCompleteTest"; + +export const TestPage = () => { + const { id } = useParams(); + const { data: test, isLoading, error } = useUserTestById(Number(id)); + const [currentQuestion, setCurrentQuestion] = useState(1); + const submitAnswerMutation = useSubmitAnswer(); + const completeTestMutation = useCompleteTest(); + + const handleCompleteTest = () => { + if (test) completeTestMutation.mutate(test.id); + }; + + if (isLoading) + return ( + + + Loading your test, please wait... + + + ); + + if (error) + return ( + + + Oops! An error occurred: {error.message} + + + ); + + if (!test) + return ( + + + No test found. + + + ); + + if (!test.is_available && !test.is_completed) + return ( + + + This test is no longer available. + + + ); + + if (test.is_completed) + return ( + + + You have already completed this test. + + + ); + + const questionsAndAnswers = test.answers; + const currentQA = questionsAndAnswers?.[currentQuestion - 1]; + + return ( + + + {test.test?.title ?? "User Test"} + + Expires at:{" "} + {test.closed_at + ? new Date(test.closed_at).toLocaleString("en-GB", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + hour12: false, + }) + : "N/A"} + + + {currentQA ? ( + + submitAnswerMutation.mutate({ + answerId: questionsAndAnswers[currentQuestion - 1].id, + answer: ans, + }) + } + /> + ) : ( + No question found. + )} + + setCurrentQuestion(page)} + /> + {currentQuestion == questionsAndAnswers?.length && ( + + )} + + + ); +}; diff --git a/src/router/router.tsx b/src/router/router.tsx index 121d10d..ff800c0 100644 --- a/src/router/router.tsx +++ b/src/router/router.tsx @@ -9,6 +9,8 @@ import ActivateAccountPage from "../pages/ActivateAccountPage/ActivateAccountPag import RequestResetPage from "../pages/RequestResetPage/RequestResetPage"; import NotFoundPage from "../pages/NotFoundPage/NotFoundPage"; import ResetPasswordPage from "../pages/ResetPasswordPage/ResetPasswordPage"; +import SingleQuestionPage from "../pages/SingleQuestionPage/SingleQuestionPage"; +import { TestPage } from "../pages/TestPage/TestPage"; const router = createBrowserRouter([ { @@ -47,6 +49,14 @@ const router = createBrowserRouter([ path: "/auth/reset-password/:token", element: , }, + { + path: "/questions/:id", + element: + }, + { + path: "/tests/:id", + element: + }, { path: "*", element: ,