diff --git a/src/api/testApi.ts b/src/api/testApi.ts index 75f6c3d..f770b88 100644 --- a/src/api/testApi.ts +++ b/src/api/testApi.ts @@ -7,6 +7,30 @@ import { } from "../components/shared/types/TestTypes"; import axiosInstance from "./axiosInstance"; +export type UserTestsResponse = { + data: UserTestType[]; + links: { + first: string; + last: string; + prev: string | null; + next: string | null; + }; + meta: { + current_page: number; + from: number | null; + last_page: number; + path: string; + per_page: number; + to: number | null; + total: number; + links: { + url: string | null; + label: string; + active: boolean; + }[]; + }; +}; + export const startTest = async (data: StartTestPayload) => { const res = await axiosInstance.post<{ data: UserTestType }>( "/api/user-tests", @@ -64,3 +88,19 @@ export const getTestById = async (id: number) => { const res = await axiosInstance.get<{ data: TestType }>(`/api/tests/${id}`); return res.data.data; }; + +export const getAllUserTests = async ( + page = 1, + test_id?: number, + question_id?: number +) => { + const res = await axiosInstance.get("/api/user-tests", { + params: { page, test_id, question_id }, + }); + return res.data; +}; + +export const deleteUserTest = async (id: number) => { + const res = await axiosInstance.delete(`/api/user-tests/${id}`); + return res.data; +}; diff --git a/src/components/Answers/Answers.tsx b/src/components/Answers/Answers.tsx index 2c978f5..5b5fed1 100644 --- a/src/components/Answers/Answers.tsx +++ b/src/components/Answers/Answers.tsx @@ -75,9 +75,9 @@ const LearningAnswers = ({ variant="contained" color="primary" sx={{ mt: 2 }} - onClick={() => setShowAnswer(true)} + onClick={() => setShowAnswer(!showAnswer)} > - Reveal Answer + {showAnswer ? "Hide Answer" : "Reveal Answer"} )} diff --git a/src/components/TestCard/TestCard.tsx b/src/components/TestCard/TestCard.tsx index 36ac1e0..4dfa9bc 100644 --- a/src/components/TestCard/TestCard.tsx +++ b/src/components/TestCard/TestCard.tsx @@ -10,6 +10,7 @@ interface TestCardProps { const TestCard = ({ test }: TestCardProps) => { let title: string | undefined = "User Test"; + if (test.test_id) { const { data } = useGetTestById(test.test_id); diff --git a/src/components/TestListCard/TestListCard.tsx b/src/components/TestListCard/TestListCard.tsx index 90ac55d..f57b850 100644 --- a/src/components/TestListCard/TestListCard.tsx +++ b/src/components/TestListCard/TestListCard.tsx @@ -1,7 +1,8 @@ -import { Box, Button, Chip, Typography } from "@mui/material"; +import { Box, Button, Chip, Tooltip, Typography } from "@mui/material"; import CategoryIcon from "@mui/icons-material/Category"; import type { TestType } from "../shared/types/TestTypes"; import { useStartTestById } from "../../hooks/Tests/useStartTestById"; +import { useNavigate } from "react-router-dom"; interface TestCardProps { test: TestType; @@ -9,6 +10,8 @@ interface TestCardProps { const TestListCard = ({ test }: TestCardProps) => { const { mutate: startTest } = useStartTestById(); + const navigate = useNavigate(); + const title = test.title; let statusLabel = ""; @@ -33,21 +36,36 @@ const TestListCard = ({ test }: TestCardProps) => { justifyContent: "space-between", alignItems: "center", flexDirection: { - xs: "column", - sm: "column", - md: "row" + xs: "column", + sm: "column", + md: "row", }, }} > - - {title} - } - label={test.category.name} - color="primary" - variant="outlined" - /> + + + navigate(`/tests/view/${test.id}`)} + > + {title} + + + } + label={test.category.name} + color="primary" + variant="outlined" + /> {test.description && ( @@ -67,20 +85,17 @@ const TestListCard = ({ test }: TestCardProps) => { - - {test.is_available && ( - - )} + {test.is_available && ( + + )} - - ); }; diff --git a/src/components/UserTestRow/UserTestRow.tsx b/src/components/UserTestRow/UserTestRow.tsx new file mode 100644 index 0000000..a21af0d --- /dev/null +++ b/src/components/UserTestRow/UserTestRow.tsx @@ -0,0 +1,69 @@ +import { TableRow, TableCell, Button } from "@mui/material"; +import { useNavigate } from "react-router-dom"; +import type { UserTestType } from "../../components/shared/types/TestTypes"; +import { useGetTestById } from "../../hooks/Tests/useGetTestById"; +import { formatDate } from "../../utils/functions"; + +type Props = { + userTest: UserTestType; + onDelete: (id: number) => void; +}; + +const UserTestRow = ({ userTest, onDelete }: Props) => { + const navigate = useNavigate(); + + const testId = userTest.test_id; + let title = "User Test"; + let author = "—"; + + if (testId) { + const { data } = useGetTestById(testId); + title = data?.title ?? "User Test"; + author = data?.author.username ?? "-"; + } + + return ( + + {userTest.id} + + {title} + + {userTest.user?.username ?? "Unknown"} + + {author} + + + {userTest.score !== undefined ? userTest.score : "—"} + + + {userTest.is_completed ? "Yes" : "No"} + + + {formatDate(userTest.created_at)} + + {formatDate(userTest.closed_at)} + + + + + + + + ); +}; + +export default UserTestRow; diff --git a/src/hooks/Tests/useDeteleUserTest.ts b/src/hooks/Tests/useDeteleUserTest.ts new file mode 100644 index 0000000..9fa3295 --- /dev/null +++ b/src/hooks/Tests/useDeteleUserTest.ts @@ -0,0 +1,15 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { deleteUserTest } from "../../api/testApi"; +import { toast } from "react-toastify"; + +export const useDeleteUserTest = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (id: number) => deleteUserTest(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["all-user-tests"] }); + toast.success("User Test deleted") + }, + }); +}; diff --git a/src/hooks/Tests/useGetAllUserTests.ts b/src/hooks/Tests/useGetAllUserTests.ts new file mode 100644 index 0000000..6369715 --- /dev/null +++ b/src/hooks/Tests/useGetAllUserTests.ts @@ -0,0 +1,12 @@ +import { useQuery } from "@tanstack/react-query"; +import { getAllUserTests } from "../../api/testApi"; + +export const useGetAllUserTests = ( + page: number, + test_id?: number, + question_id?: number +) => + useQuery({ + queryKey: ["all-user-tests", page], + queryFn: () => getAllUserTests(page, test_id, question_id), + }); diff --git a/src/pages/AdminUserTestsPage/AdminUserTestsPage.tsx b/src/pages/AdminUserTestsPage/AdminUserTestsPage.tsx new file mode 100644 index 0000000..5172f34 --- /dev/null +++ b/src/pages/AdminUserTestsPage/AdminUserTestsPage.tsx @@ -0,0 +1,114 @@ +import { useState } from "react"; +import { + Box, + Button, + CircularProgress, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, + Pagination, + Dialog, + DialogTitle, + DialogContent, + DialogActions, +} from "@mui/material"; + +import Container from "../../components/shared/Container"; +import { useGetAllUserTests } from "../../hooks/Tests/useGetAllUserTests"; +import { useDeleteUserTest } from "../../hooks/Tests/useDeteleUserTest"; + +import UserTestRow from "../../components/UserTestRow/UserTestRow"; + +const UserTestsPage = () => { + const [currentPage, setCurrentPage] = useState(1); + const [deleteId, setDeleteId] = useState(null); + + const { data, isLoading, isError } = useGetAllUserTests(currentPage); + const deleteMutation = useDeleteUserTest(); + + const handleDelete = () => { + if (deleteId !== null) { + deleteMutation.mutate(deleteId, { + onSuccess: () => setDeleteId(null), + }); + } + }; + + if (isLoading) return ; + if (isError) + return Error loading user tests.; + + return ( + + + User Tests + + + + + + + ID + Test + Test Taker + Test Author + Score + Completed + Created At + Closed At + Actions + + + + + {data?.data.map((ut) => ( + setDeleteId(id)} + /> + ))} + +
+
+ + setCurrentPage(value)} + sx={{ + mt: 3, + mb: 3, + display: "flex", + justifyContent: "center", + }} + /> + + setDeleteId(null)}> + Confirm Delete + + Are you sure you want to delete this user test? + + + + + + +
+ ); +}; + +export default UserTestsPage; diff --git a/src/pages/SingleTestPage/SingleTestPage.tsx b/src/pages/SingleTestPage/SingleTestPage.tsx new file mode 100644 index 0000000..86422c2 --- /dev/null +++ b/src/pages/SingleTestPage/SingleTestPage.tsx @@ -0,0 +1,147 @@ +import { useParams } from "react-router-dom"; +import { useGetTestById } from "../../hooks/Tests/useGetTestById"; +import Container from "../../components/shared/Container"; +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Pagination, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, +} from "@mui/material"; +import LearningAnswers from "../../components/Answers/Answers"; +import NotFoundPage from "../NotFoundPage/NotFoundPage"; +import { useAuth } from "../../context/AuthContext"; +import { useState } from "react"; +import { useGetAllUserTests } from "../../hooks/Tests/useGetAllUserTests"; +import UserTestRow from "../../components/UserTestRow/UserTestRow"; +import { useDeleteUserTest } from "../../hooks/Tests/useDeteleUserTest"; + +export const SingleTestPage = () => { + const { id } = useParams(); + const { user } = useAuth(); + + const [currentPage, setCurrentPage] = useState(1); + const [deleteId, setDeleteId] = useState(null); + const deleteMutation = useDeleteUserTest(); + const { data: tests } = useGetAllUserTests(currentPage, Number(id)); + + const handleDelete = () => { + if (deleteId !== null) { + deleteMutation.mutate(deleteId, { + onSuccess: () => setDeleteId(null), + }); + } + }; + + const { data: test } = useGetTestById(Number(id)); + if (!test) { + return ; + } + return ( + + + {test?.title} + + + {test?.questions?.map((q) => ( + + {q.title} + + {q.description} + + + + + ))} + {(user?.type == "admin" || user?.type == "creator") && ( + + + {test.title} results + + + + + + + ID + Test + Test Taker + Test Author + Score + Completed + Created At + Closed At + Actions + + + + + {tests?.data && tests.data.length > 0 ? ( + tests.data.map((ut) => ( + setDeleteId(id)} + /> + )) + ) : ( + + + + No results found for this test. + + + + )} + +
+
+ + setCurrentPage(value)} + sx={{ + mt: 3, + mb: 3, + display: "flex", + justifyContent: "center", + }} + /> + setDeleteId(null)}> + Confirm Delete + + Are you sure you want to delete this user test? + + + + + + +
+ )} +
+ ); +}; diff --git a/src/pages/TestPage/TestPage.tsx b/src/pages/TestPage/TestPage.tsx index 060bc4a..b6f3246 100644 --- a/src/pages/TestPage/TestPage.tsx +++ b/src/pages/TestPage/TestPage.tsx @@ -8,9 +8,12 @@ import { useSubmitAnswer } from "../../hooks/Tests/useSubmitAnswer"; import { useCompleteTest } from "../../hooks/Tests/useCompleteTest"; import LearningAnswers from "../../components/Answers/Answers"; import { formatDate } from "../../utils/functions"; +import NotFoundPage from "../NotFoundPage/NotFoundPage"; +import { useAuth } from "../../context/AuthContext"; export const TestPage = () => { const { id } = useParams(); + const { user } = useAuth(); const { data: test, isLoading, error } = useUserTestById(Number(id)); const [currentQuestion, setCurrentQuestion] = useState(1); const submitAnswerMutation = useSubmitAnswer(); @@ -30,25 +33,11 @@ export const TestPage = () => { ); - if (error) - return ( - - - Oops! An error occurred: {error.message} - - - ); + if (error) return ; - if (!test) - return ( - - - No test found. - - - ); + if (!test) return ; - if (!test.is_available && !test.is_completed) + if (!test.is_available && !test.is_completed && user?.type == 'user') return ( diff --git a/src/pages/UserForm/UserForm.tsx b/src/pages/UserForm/UserForm.tsx index 0e44f73..83b67b9 100644 --- a/src/pages/UserForm/UserForm.tsx +++ b/src/pages/UserForm/UserForm.tsx @@ -12,7 +12,6 @@ import { useUserById } from "../../hooks/users/useUserById"; import { useCreateUser } from "../../hooks/users/useCreateUser"; import { useUpdateUser } from "../../hooks/users/useUpdateUser"; - const UserForm = () => { const { id } = useParams<{ id: string }>(); const isUpdate = Boolean(id); @@ -46,8 +45,6 @@ const UserForm = () => { const payload = { username, email, type, ...(password && { password }) }; if (isUpdate && id) { - console.log(payload); - console.log(id); updateUserMutation.mutate( { id: Number(id), payload }, { onSuccess: () => navigate("/dashboard/users") } @@ -103,6 +100,7 @@ const UserForm = () => { User Admin Creator + Banned