added tests by test and polished adaptivity
This commit is contained in:
parent
cba24b05c9
commit
6d9fa9061d
@ -1,6 +1,11 @@
|
|||||||
|
import type { CategoryType } from "../components/shared/types/TestTypes";
|
||||||
import axiosInstance from "./axiosInstance";
|
import axiosInstance from "./axiosInstance";
|
||||||
|
|
||||||
|
interface CategoriesResponse {
|
||||||
|
data: CategoryType[];
|
||||||
|
}
|
||||||
|
|
||||||
export const getCategories = async () => {
|
export const getCategories = async () => {
|
||||||
const res = await axiosInstance.get("/api/categories");
|
const res = await axiosInstance.get<CategoriesResponse>("/api/categories");
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import type {
|
import {
|
||||||
StartTestPayload,
|
type TestType,
|
||||||
SubmitAnswerPayload,
|
type PaginatedTests,
|
||||||
UserTestType,
|
type StartTestPayload,
|
||||||
|
type SubmitAnswerPayload,
|
||||||
|
type UserTestType,
|
||||||
} from "../components/shared/types/TestTypes";
|
} from "../components/shared/types/TestTypes";
|
||||||
import axiosInstance from "./axiosInstance";
|
import axiosInstance from "./axiosInstance";
|
||||||
|
|
||||||
@ -13,6 +15,14 @@ export const startTest = async (data: StartTestPayload) => {
|
|||||||
return res.data.data;
|
return res.data.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const startTestById = async (id: number) => {
|
||||||
|
const res = await axiosInstance.post<{ data: UserTestType }>(
|
||||||
|
"/api/user-tests/by-test",
|
||||||
|
{ test_id: id }
|
||||||
|
);
|
||||||
|
return res.data.data;
|
||||||
|
};
|
||||||
|
|
||||||
export const completeUserTest = async (userTestId: number) => {
|
export const completeUserTest = async (userTestId: number) => {
|
||||||
const res = await axiosInstance.post<{ message: string }>(
|
const res = await axiosInstance.post<{ message: string }>(
|
||||||
`/api/user-tests/${userTestId}/complete`
|
`/api/user-tests/${userTestId}/complete`
|
||||||
@ -28,7 +38,9 @@ export const getUserTestById = async (userTestId: number) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getUserTests = async () => {
|
export const getUserTests = async () => {
|
||||||
const res = await axiosInstance.get<{data: UserTestType[]}>("/api/user-tests/me");
|
const res = await axiosInstance.get<{ data: UserTestType[] }>(
|
||||||
|
"/api/user-tests/me"
|
||||||
|
);
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -39,3 +51,16 @@ export const submitAnswer = async (data: SubmitAnswerPayload) => {
|
|||||||
);
|
);
|
||||||
return res.data;
|
return res.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getTests = async (categoryId?: number, page = 1) => {
|
||||||
|
const params: Record<string, number> = { page };
|
||||||
|
if (categoryId !== undefined) params.category_id = categoryId;
|
||||||
|
|
||||||
|
const res = await axiosInstance.get<PaginatedTests>("/api/tests", { params });
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTestById = async (id: number) => {
|
||||||
|
const res = await axiosInstance.get<{ data: TestType }>(`/api/tests/${id}`);
|
||||||
|
return res.data.data;
|
||||||
|
};
|
||||||
|
|||||||
@ -109,6 +109,15 @@ function Header() {
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
handleCloseNavMenu();
|
||||||
|
navigate("/tests");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography textAlign="center">Tests</Typography>
|
||||||
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@ -160,6 +169,15 @@ function Header() {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<Button
|
||||||
|
sx={{ my: 2, color: "white", display: "block" }}
|
||||||
|
onClick={() => {
|
||||||
|
handleCloseNavMenu();
|
||||||
|
navigate("/tests");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography textAlign="center">Tests</Typography>
|
||||||
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import styled from "@emotion/styled";
|
import styled from "@emotion/styled";
|
||||||
|
import { Box } from "@mui/material";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
export const QuestionWrapper = styled(Link)({
|
export const QuestionWrapper = styled(Link)({
|
||||||
@ -6,24 +7,27 @@ export const QuestionWrapper = styled(Link)({
|
|||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
padding: "15px",
|
padding: "15px",
|
||||||
border: "3px solid #4B2981",
|
border: "1px solid #ccc",
|
||||||
borderRadius: "10px",
|
borderRadius: "10px",
|
||||||
marginBottom: "10px",
|
marginBottom: "10px",
|
||||||
textDecoration: "none",
|
textDecoration: "none",
|
||||||
color: "inherit"
|
color: "inherit",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const QuestionTitle = styled("div")({
|
export const QuestionTitle = styled(Box)({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
marginBottom: "10px"
|
marginBottom: "10px",
|
||||||
|
"@media (max-width:600px)": {
|
||||||
|
flexDirection: "column",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const QuestionMetadata = styled("div")({
|
export const QuestionMetadata = styled("div")({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
color: "#4c4c4c",
|
color: "#4c4c4c",
|
||||||
marginTop: "10px"
|
marginTop: "10px",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AuthorMeta = styled("div")({
|
export const AuthorMeta = styled("div")({
|
||||||
|
|||||||
@ -75,6 +75,7 @@ const StartTestForm = () => {
|
|||||||
<FormControl sx={{ minWidth: "300px" }} variant="filled">
|
<FormControl sx={{ minWidth: "300px" }} variant="filled">
|
||||||
<InputLabel id="category-select-label">Category</InputLabel>
|
<InputLabel id="category-select-label">Category</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
|
sx={{ maxWidth: "270px" }}
|
||||||
labelId="category-select-label"
|
labelId="category-select-label"
|
||||||
id="category-select"
|
id="category-select"
|
||||||
value={category}
|
value={category}
|
||||||
@ -93,6 +94,7 @@ const StartTestForm = () => {
|
|||||||
<FormControl sx={{ minWidth: "200px" }} variant="filled">
|
<FormControl sx={{ minWidth: "200px" }} variant="filled">
|
||||||
<InputLabel id="min-difficulty-select-label">Min Difficulty</InputLabel>
|
<InputLabel id="min-difficulty-select-label">Min Difficulty</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
|
sx={{ maxWidth: "270px" }}
|
||||||
labelId="min-difficulty-select-label"
|
labelId="min-difficulty-select-label"
|
||||||
id="min-difficulty-select"
|
id="min-difficulty-select"
|
||||||
value={minDifficulty}
|
value={minDifficulty}
|
||||||
@ -110,6 +112,7 @@ const StartTestForm = () => {
|
|||||||
<FormControl sx={{ minWidth: "200px" }} variant="filled">
|
<FormControl sx={{ minWidth: "200px" }} variant="filled">
|
||||||
<InputLabel id="max-difficulty-select-label">Max Difficulty</InputLabel>
|
<InputLabel id="max-difficulty-select-label">Max Difficulty</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
|
sx={{ maxWidth: "270px" }}
|
||||||
labelId="max-difficulty-select-label"
|
labelId="max-difficulty-select-label"
|
||||||
id="max-difficulty-select"
|
id="max-difficulty-select"
|
||||||
value={maxDifficulty}
|
value={maxDifficulty}
|
||||||
|
|||||||
@ -2,13 +2,19 @@ import { Box, Button, Chip, Typography } from "@mui/material";
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { formatDate } from "../../utils/functions";
|
import { formatDate } from "../../utils/functions";
|
||||||
import type { UserTestType } from "../shared/types/TestTypes";
|
import type { UserTestType } from "../shared/types/TestTypes";
|
||||||
|
import { useGetTestById } from "../../hooks/Tests/useGetTestById";
|
||||||
|
|
||||||
interface TestCardProps {
|
interface TestCardProps {
|
||||||
test: UserTestType;
|
test: UserTestType;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TestCard = ({ test }: TestCardProps) => {
|
const TestCard = ({ test }: TestCardProps) => {
|
||||||
const title = test.test?.title || "User Test";
|
let title: string | undefined = "User Test";
|
||||||
|
|
||||||
|
if (test.test_id) {
|
||||||
|
const { data } = useGetTestById(test.test_id);
|
||||||
|
title = data?.title;
|
||||||
|
}
|
||||||
|
|
||||||
let statusLabel = "";
|
let statusLabel = "";
|
||||||
let statusColor: "primary" | "success" | "error" = "primary";
|
let statusColor: "primary" | "success" | "error" = "primary";
|
||||||
|
|||||||
88
src/components/TestListCard/TestListCard.tsx
Normal file
88
src/components/TestListCard/TestListCard.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { Box, Button, Chip, Typography } from "@mui/material";
|
||||||
|
import CategoryIcon from "@mui/icons-material/Category";
|
||||||
|
import type { TestType } from "../shared/types/TestTypes";
|
||||||
|
import { useStartTestById } from "../../hooks/Tests/useStartTestById";
|
||||||
|
|
||||||
|
interface TestCardProps {
|
||||||
|
test: TestType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TestListCard = ({ test }: TestCardProps) => {
|
||||||
|
const { mutate: startTest } = useStartTestById();
|
||||||
|
const title = test.title;
|
||||||
|
|
||||||
|
let statusLabel = "";
|
||||||
|
let statusColor: "primary" | "success" | "error" = "primary";
|
||||||
|
|
||||||
|
if (!test.is_available) {
|
||||||
|
statusLabel = "Expired";
|
||||||
|
statusColor = "error";
|
||||||
|
} else {
|
||||||
|
statusLabel = "Active";
|
||||||
|
statusColor = "primary";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
border: "1px solid #ccc",
|
||||||
|
borderRadius: 2,
|
||||||
|
p: 2,
|
||||||
|
mb: 2,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
flexDirection: {
|
||||||
|
xs: "column",
|
||||||
|
sm: "column",
|
||||||
|
md: "row"
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Box sx={{display: "flex", gap: 1, flexDirection: {xs: "column", sm: "row"}}}>
|
||||||
|
<Typography variant="h6">{title}</Typography>
|
||||||
|
<Chip
|
||||||
|
icon={<CategoryIcon />}
|
||||||
|
label={test.category.name}
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{test.description && (
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
|
||||||
|
{test.description}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="text.secondary"
|
||||||
|
display="block"
|
||||||
|
mt={1}
|
||||||
|
>
|
||||||
|
Closes: {new Date(test.closed_at).toLocaleDateString()}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1, mt: 1 }}>
|
||||||
|
|
||||||
|
<Chip label={statusLabel} color={statusColor} />
|
||||||
|
{test.is_available && (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => startTest(test.id)}
|
||||||
|
>
|
||||||
|
Start Test
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TestListCard;
|
||||||
@ -33,7 +33,7 @@ export type TestType = {
|
|||||||
description: string | null;
|
description: string | null;
|
||||||
category_id: number;
|
category_id: number;
|
||||||
category: CategoryType;
|
category: CategoryType;
|
||||||
questions: QuestionType[];
|
questions?: QuestionType[];
|
||||||
is_available: boolean;
|
is_available: boolean;
|
||||||
author_id: number;
|
author_id: number;
|
||||||
author: User;
|
author: User;
|
||||||
@ -61,3 +61,28 @@ export type SubmitAnswerPayload = {
|
|||||||
answerId: number;
|
answerId: number;
|
||||||
answer: (string | number)[];
|
answer: (string | number)[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PaginatedTests = {
|
||||||
|
data: TestType[];
|
||||||
|
links: {
|
||||||
|
first: string | null;
|
||||||
|
last: string | null;
|
||||||
|
prev: string | null;
|
||||||
|
next: string | null;
|
||||||
|
};
|
||||||
|
meta: {
|
||||||
|
current_page: number;
|
||||||
|
from: number | null;
|
||||||
|
last_page: number;
|
||||||
|
links: {
|
||||||
|
url: string | null;
|
||||||
|
label: string;
|
||||||
|
page: number | null;
|
||||||
|
active: boolean;
|
||||||
|
}[];
|
||||||
|
path: string;
|
||||||
|
per_page: number;
|
||||||
|
to: number | null;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
10
src/hooks/Tests/useGetTestById.ts
Normal file
10
src/hooks/Tests/useGetTestById.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { getTestById } from "../../api/testApi";
|
||||||
|
import type { TestType } from "../../components/shared/types/TestTypes";
|
||||||
|
|
||||||
|
export const useGetTestById = (id: number) => {
|
||||||
|
return useQuery<TestType>({
|
||||||
|
queryKey: ["tests", id],
|
||||||
|
queryFn: () => getTestById(id),
|
||||||
|
});
|
||||||
|
};
|
||||||
15
src/hooks/Tests/useGetTests.ts
Normal file
15
src/hooks/Tests/useGetTests.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { getTests } from "../../api/testApi";
|
||||||
|
|
||||||
|
type UseTestsParams = {
|
||||||
|
page?: number;
|
||||||
|
categoryId?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetTests = ({ page = 1, categoryId }: UseTestsParams) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["tests", { page, categoryId }],
|
||||||
|
queryFn: () => getTests(categoryId, page),
|
||||||
|
staleTime: 1000 * 60,
|
||||||
|
});
|
||||||
|
};
|
||||||
20
src/hooks/Tests/useStartTestById.ts
Normal file
20
src/hooks/Tests/useStartTestById.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { startTestById } from "../../api/testApi";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
export const useStartTestById = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: number) => startTestById(id),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["user-tests"] });
|
||||||
|
toast.success("Test Started");
|
||||||
|
navigate(`/tests/${data.id}`);
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error.response?.data?.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -1,4 +1,12 @@
|
|||||||
import { Typography, Pagination } from "@mui/material";
|
import {
|
||||||
|
Typography,
|
||||||
|
Pagination,
|
||||||
|
type SelectChangeEvent,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
} from "@mui/material";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Container from "../../components/shared/Container";
|
import Container from "../../components/shared/Container";
|
||||||
import { WelcomeContainer } from "./IndexPage.styles";
|
import { WelcomeContainer } from "./IndexPage.styles";
|
||||||
@ -6,30 +14,71 @@ import StartTestForm from "../../components/StartTestForm/StartTestForm";
|
|||||||
import Question from "../../components/Question/Question";
|
import Question from "../../components/Question/Question";
|
||||||
import { useQuestions } from "../../hooks/Question/useQuestions";
|
import { useQuestions } from "../../hooks/Question/useQuestions";
|
||||||
import type { QuestionType } from "../../components/shared/types/QuestionTypes";
|
import type { QuestionType } from "../../components/shared/types/QuestionTypes";
|
||||||
|
import { useAuth } from "../../context/AuthContext";
|
||||||
|
import { useCategories } from "../../hooks/Question/useCategories";
|
||||||
|
|
||||||
const IndexPage = () => {
|
const IndexPage = () => {
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const questionsQuery = useQuestions({ page });
|
|
||||||
|
const { data: categories } = useCategories();
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState<number | "all">(
|
||||||
|
"all"
|
||||||
|
);
|
||||||
|
const questionsQuery = useQuestions({
|
||||||
|
page,
|
||||||
|
id: selectedCategory === "all" ? undefined : selectedCategory,
|
||||||
|
});
|
||||||
|
|
||||||
const questions = questionsQuery.data?.data ?? [];
|
const questions = questionsQuery.data?.data ?? [];
|
||||||
const loading = questionsQuery.isLoading;
|
const loading = questionsQuery.isLoading;
|
||||||
const error = questionsQuery.error?.message ?? null;
|
const error = questionsQuery.error?.message ?? null;
|
||||||
const meta = questionsQuery.data?.meta ?? null;
|
const meta = questionsQuery.data?.meta ?? null;
|
||||||
|
|
||||||
|
const handleCategoryChange = (event: SelectChangeEvent<number | "all">) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
setSelectedCategory(value === "all" ? "all" : Number(value));
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<WelcomeContainer>
|
<WelcomeContainer>
|
||||||
<Typography variant="h2">Welcome To HoshiAI!</Typography>
|
<Typography align="center" variant="h2">Welcome To HoshiAI!</Typography>
|
||||||
<Typography variant="subtitle1">The best place to learn!</Typography>
|
<Typography variant="subtitle1">The best place to learn!</Typography>
|
||||||
</WelcomeContainer>
|
</WelcomeContainer>
|
||||||
|
|
||||||
<StartTestForm />
|
{user && (
|
||||||
|
<>
|
||||||
|
<Typography align="center" sx={{ mb: "36px" }} variant="h3">
|
||||||
|
Start a Test
|
||||||
|
</Typography>
|
||||||
|
|
||||||
<Typography sx={{ mb: "36px" }} variant="h3">
|
<StartTestForm />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Typography align="center" sx={{ mb: "36px" }} variant="h3">
|
||||||
Browse Questions
|
Browse Questions
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
|
<FormControl sx={{ mb: 2, minWidth: 200 }}>
|
||||||
|
<InputLabel id="category-select-label">Category</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="category-select-label"
|
||||||
|
value={selectedCategory}
|
||||||
|
onChange={handleCategoryChange}
|
||||||
|
label="Category"
|
||||||
|
>
|
||||||
|
<MenuItem value="all">All</MenuItem>
|
||||||
|
{categories?.map((category) => (
|
||||||
|
<MenuItem key={category.id} value={category.id}>
|
||||||
|
{category.name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
{loading && <Typography>Loading questions...</Typography>}
|
{loading && <Typography>Loading questions...</Typography>}
|
||||||
{error && <Typography color="error">{error}</Typography>}
|
{error && <Typography color="error">{error}</Typography>}
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
|
|
||||||
import StartTestForm from "../../components/StartTestForm/StartTestForm";
|
|
||||||
|
|
||||||
const QuestionsPage = () => {
|
|
||||||
return(
|
|
||||||
<StartTestForm/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default QuestionsPage;
|
|
||||||
83
src/pages/TestsPage/TestsPage.tsx
Normal file
83
src/pages/TestsPage/TestsPage.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import Pagination from "@mui/material/Pagination";
|
||||||
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
|
import Alert from "@mui/material/Alert";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import { useGetTests } from "../../hooks/Tests/useGetTests";
|
||||||
|
import Container from "../../components/shared/Container";
|
||||||
|
import TestListCard from "../../components/TestListCard/TestListCard";
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
Typography,
|
||||||
|
type SelectChangeEvent,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { useCategories } from "../../hooks/Question/useCategories";
|
||||||
|
|
||||||
|
const TestsPage = () => {
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState<number | "all">(
|
||||||
|
"all"
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: categories } = useCategories();
|
||||||
|
const { data, isLoading, isError } = useGetTests({
|
||||||
|
categoryId: selectedCategory === "all" ? undefined : selectedCategory,
|
||||||
|
page: currentPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCategoryChange = (event: SelectChangeEvent<number | "all">) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
setSelectedCategory(value === "all" ? "all" : Number(value));
|
||||||
|
setCurrentPage(1);
|
||||||
|
};
|
||||||
|
if (isLoading) return <CircularProgress />;
|
||||||
|
if (isError) return <Alert severity="error">Failed to load tests</Alert>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Typography variant="h2">Curated tests</Typography>
|
||||||
|
<Box p={2}>
|
||||||
|
<FormControl sx={{ mb: 2, minWidth: 200 }}>
|
||||||
|
<InputLabel id="category-select-label">Category</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="category-select-label"
|
||||||
|
value={selectedCategory}
|
||||||
|
onChange={handleCategoryChange}
|
||||||
|
label="Category"
|
||||||
|
>
|
||||||
|
<MenuItem value="all">All</MenuItem>
|
||||||
|
{categories?.map((category) => (
|
||||||
|
<MenuItem key={category.id} value={category.id}>
|
||||||
|
{category.name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<div>Loading tests...</div>
|
||||||
|
) : isError ? (
|
||||||
|
<div>Failed to load tests</div>
|
||||||
|
) : (
|
||||||
|
data?.data.map((test) => <TestListCard key={test.id} test={test} />)
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data?.meta?.last_page && data.meta.last_page > 1 && (
|
||||||
|
<Pagination
|
||||||
|
color="primary"
|
||||||
|
shape="rounded"
|
||||||
|
count={data.meta.last_page}
|
||||||
|
page={currentPage}
|
||||||
|
onChange={(_, value) => setCurrentPage(value)}
|
||||||
|
sx={{ mt: 3, mb: 3, display: "flex", justifyContent: "center" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TestsPage;
|
||||||
@ -11,6 +11,7 @@ import NotFoundPage from "../pages/NotFoundPage/NotFoundPage";
|
|||||||
import ResetPasswordPage from "../pages/ResetPasswordPage/ResetPasswordPage";
|
import ResetPasswordPage from "../pages/ResetPasswordPage/ResetPasswordPage";
|
||||||
import SingleQuestionPage from "../pages/SingleQuestionPage/SingleQuestionPage";
|
import SingleQuestionPage from "../pages/SingleQuestionPage/SingleQuestionPage";
|
||||||
import { TestPage } from "../pages/TestPage/TestPage";
|
import { TestPage } from "../pages/TestPage/TestPage";
|
||||||
|
import TestsPage from "../pages/TestsPage/TestsPage";
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@ -57,6 +58,10 @@ const router = createBrowserRouter([
|
|||||||
path: "/tests/:id",
|
path: "/tests/:id",
|
||||||
element: <TestPage/>
|
element: <TestPage/>
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/tests",
|
||||||
|
element: <TestsPage/>
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "*",
|
path: "*",
|
||||||
element: <NotFoundPage />,
|
element: <NotFoundPage />,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user