admin tests and questions
This commit is contained in:
parent
b7313488a4
commit
69382f2f4c
@ -1,3 +1,4 @@
|
||||
import type { Variant } from "../components/shared/types/QuestionTypes";
|
||||
import axiosInstance from "./axiosInstance";
|
||||
|
||||
type GetQuestionsParams = {
|
||||
@ -5,14 +6,33 @@ type GetQuestionsParams = {
|
||||
id?: number;
|
||||
};
|
||||
|
||||
export type QuestionPayload = {
|
||||
title: string;
|
||||
description?: string;
|
||||
type: "single" | "multiple" | "text";
|
||||
difficulty: number;
|
||||
variants: Variant[];
|
||||
correct_answers: number[] | string[];
|
||||
category_id: number;
|
||||
};
|
||||
|
||||
export type OpenaiPayload = {
|
||||
type: "single" | "multiple" | "text";
|
||||
category_id: number;
|
||||
language: string;
|
||||
difficulty: number;
|
||||
promt: string;
|
||||
};
|
||||
|
||||
type OpenaiGenerateResponse = {
|
||||
question: QuestionPayload;
|
||||
};
|
||||
|
||||
export const getQuestions = async ({ page = 1, id }: GetQuestionsParams) => {
|
||||
const params: Record<string, any> = { page };
|
||||
if (id) params.category_id = id;
|
||||
|
||||
const { data } = await axiosInstance.get(
|
||||
"/api/questions",
|
||||
{ params }
|
||||
);
|
||||
const { data } = await axiosInstance.get("/api/questions", { params });
|
||||
|
||||
return data;
|
||||
};
|
||||
@ -21,3 +41,29 @@ export const getQuestionById = async (id: number) => {
|
||||
const response = await axiosInstance.get(`/api/questions/${id}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createQuestion = async (payload: QuestionPayload) => {
|
||||
const res = await axiosInstance.post("/api/questions", payload);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const updateQuestion = async (id: number, payload: QuestionPayload) => {
|
||||
const res = await axiosInstance.put(`/api/questions/${id}`, payload);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const deleteQuestion = async (id: number) => {
|
||||
const res = await axiosInstance.delete(`/api/questions/${id}`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const openaiGenerateQuestion = async (
|
||||
payload: OpenaiPayload
|
||||
): Promise<QuestionPayload> => {
|
||||
const res = await axiosInstance.post<OpenaiGenerateResponse>(
|
||||
'/api/questions/openai-generate',
|
||||
payload
|
||||
);
|
||||
|
||||
return res.data.question;
|
||||
};
|
||||
|
||||
@ -31,6 +31,14 @@ export type UserTestsResponse = {
|
||||
};
|
||||
};
|
||||
|
||||
export type TestPayload = {
|
||||
title: string;
|
||||
description: string;
|
||||
closed_at: string | null;
|
||||
category_id: number;
|
||||
questions: number[];
|
||||
};
|
||||
|
||||
export const startTest = async (data: StartTestPayload) => {
|
||||
const res = await axiosInstance.post<{ data: UserTestType }>(
|
||||
"/api/user-tests",
|
||||
@ -104,3 +112,18 @@ export const deleteUserTest = async (id: number) => {
|
||||
const res = await axiosInstance.delete(`/api/user-tests/${id}`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const createTest = async (payload: TestPayload) => {
|
||||
const res = await axiosInstance.post("/api/tests", payload);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const updateTest = async (id: number, payload: TestPayload) => {
|
||||
const res = await axiosInstance.put(`/api/tests/${id}`, payload);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const deleteTest = async (id: number) => {
|
||||
const res = await axiosInstance.delete(`/api/tests/${id}`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
134
src/components/GenerateQuestionModal/GenerateQuestionModal.tsx
Normal file
134
src/components/GenerateQuestionModal/GenerateQuestionModal.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
TextField,
|
||||
MenuItem,
|
||||
CircularProgress,
|
||||
} from "@mui/material";
|
||||
|
||||
import { useOpenaiGenerateQuestion } from "../../hooks/questions/useOpenaiGenerateQuestion";
|
||||
import type { QuestionPayload } from "../../api/questionsApi";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onGenerated: (question: QuestionPayload) => void;
|
||||
|
||||
};
|
||||
|
||||
const GenerateQuestionModal = ({ open, onClose, onGenerated }: Props) => {
|
||||
const generateMutation = useOpenaiGenerateQuestion();
|
||||
|
||||
const [type, setType] = useState<"single" | "multiple" | "text">("single");
|
||||
const [categoryId, setCategoryId] = useState<number | "">("");
|
||||
const [language, setLanguage] = useState<"en" | "sr" | "hu" | "ru">("en");
|
||||
const [difficulty, setDifficulty] = useState(5);
|
||||
const [prompt, setPrompt] = useState("");
|
||||
|
||||
const handleGenerate = () => {
|
||||
if (!categoryId || !prompt.trim()) return;
|
||||
|
||||
generateMutation.mutate(
|
||||
{
|
||||
type,
|
||||
category_id: Number(categoryId),
|
||||
language,
|
||||
difficulty,
|
||||
promt: prompt,
|
||||
},
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
onGenerated(data);
|
||||
onClose();
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Generate Question (AI)</DialogTitle>
|
||||
|
||||
<DialogContent dividers>
|
||||
<TextField
|
||||
select
|
||||
label="Type"
|
||||
value={type}
|
||||
onChange={(e) =>
|
||||
setType(e.target.value as "single" | "multiple" | "text")
|
||||
}
|
||||
fullWidth
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
<MenuItem value="single">Single</MenuItem>
|
||||
<MenuItem value="multiple">Multiple</MenuItem>
|
||||
<MenuItem value="text">Text</MenuItem>
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
label="Category ID"
|
||||
type="number"
|
||||
value={categoryId}
|
||||
onChange={(e) => setCategoryId(Number(e.target.value))}
|
||||
fullWidth
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
select
|
||||
label="Language"
|
||||
value={language}
|
||||
onChange={(e) =>
|
||||
setLanguage(e.target.value as "en" | "sr" | "hu" | "ru")
|
||||
}
|
||||
fullWidth
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
<MenuItem value="en">English</MenuItem>
|
||||
<MenuItem value="sr">Serbian</MenuItem>
|
||||
<MenuItem value="hu">Hungarian</MenuItem>
|
||||
<MenuItem value="ru">Russian</MenuItem>
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
label="Difficulty (1–10)"
|
||||
type="number"
|
||||
value={difficulty}
|
||||
onChange={(e) => setDifficulty(Number(e.target.value))}
|
||||
fullWidth
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Prompt"
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
/>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleGenerate}
|
||||
disabled={generateMutation.isPending}
|
||||
>
|
||||
{generateMutation.isPending ? (
|
||||
<CircularProgress size={20} />
|
||||
) : (
|
||||
"Generate"
|
||||
)}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenerateQuestionModal;
|
||||
@ -11,7 +11,7 @@ import { StartTestWrapper } from "./StartTestForm.styles";
|
||||
import { useState } from "react";
|
||||
import { useCategories } from "../../hooks/categories/useCategories";
|
||||
import type { Category } from "../shared/types/QuestionTypes";
|
||||
import { useStartTest } from "../../hooks/Tests/useStartTest";
|
||||
import { useStartTest } from "../../hooks/tests/useStartTest";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
const StartTestForm = () => {
|
||||
|
||||
@ -2,7 +2,7 @@ import { Box, Button, Chip, Typography } from "@mui/material";
|
||||
import { Link } from "react-router-dom";
|
||||
import { formatDate } from "../../utils/functions";
|
||||
import type { UserTestType } from "../shared/types/TestTypes";
|
||||
import { useGetTestById } from "../../hooks/Tests/useGetTestById";
|
||||
import { useGetTestById } from "../../hooks/tests/useGetTestById";
|
||||
|
||||
interface TestCardProps {
|
||||
test: UserTestType;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
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 { useStartTestById } from "../../hooks/tests/useStartTestById";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
interface TestCardProps {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
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 { useGetTestById } from "../../hooks/tests/useGetTestById";
|
||||
import { formatDate } from "../../utils/functions";
|
||||
|
||||
type Props = {
|
||||
|
||||
18
src/hooks/questions/useCreateQuestion.ts
Normal file
18
src/hooks/questions/useCreateQuestion.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { createQuestion, type QuestionPayload } from "../../api/questionsApi";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const useCreateQuestion = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (payload: QuestionPayload) => createQuestion(payload),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["questions"] });
|
||||
toast.success("Question Created");
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
};
|
||||
18
src/hooks/questions/useDeleteQuestion.ts
Normal file
18
src/hooks/questions/useDeleteQuestion.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { deleteQuestion } from "../../api/questionsApi";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const useDeleteQuestion = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => deleteQuestion(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["questions"] });
|
||||
toast.success("Question Deleted");
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
};
|
||||
10
src/hooks/questions/useOpenaiGenerateQuestion.ts
Normal file
10
src/hooks/questions/useOpenaiGenerateQuestion.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { openaiGenerateQuestion } from "../../api/questionsApi";
|
||||
import type { OpenaiPayload } from "../../api/questionsApi";
|
||||
import type { QuestionPayload } from "../../api/questionsApi";
|
||||
|
||||
export const useOpenaiGenerateQuestion = () => {
|
||||
return useMutation<QuestionPayload, Error, OpenaiPayload>({
|
||||
mutationFn: openaiGenerateQuestion,
|
||||
});
|
||||
};
|
||||
@ -4,6 +4,7 @@ import { getQuestionById } from "../../api/questionsApi";
|
||||
export const useQuestionById = (id?: number) => {
|
||||
return useQuery({
|
||||
queryKey: ["single-question", id],
|
||||
queryFn: () => getQuestionById(Number(id)),
|
||||
queryFn: () => getQuestionById(Number(id!)),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
19
src/hooks/questions/useUpdateQuestion.ts
Normal file
19
src/hooks/questions/useUpdateQuestion.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { updateQuestion, type QuestionPayload } from "../../api/questionsApi";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const useUpdateQuestion = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, payload }: { id: number; payload: QuestionPayload }) =>
|
||||
updateQuestion(id, payload),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["questions"] });
|
||||
toast.success("Question Updated");
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
};
|
||||
18
src/hooks/tests/useCreateTest.ts
Normal file
18
src/hooks/tests/useCreateTest.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { createTest } from "../../api/testApi";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const useCreateTest = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: createTest,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["tests"] });
|
||||
toast.success("Test Created");
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
};
|
||||
18
src/hooks/tests/useDeleteTest.ts
Normal file
18
src/hooks/tests/useDeleteTest.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { deleteTest } from "../../api/testApi";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const useDeleteTest = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => deleteTest(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["tests"] });
|
||||
toast.success("Test deleted");
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message ?? "Failed to delete test");
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -2,9 +2,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) => {
|
||||
export const useGetTestById = (id?: number) => {
|
||||
return useQuery<TestType>({
|
||||
queryKey: ["tests", id],
|
||||
queryFn: () => getTestById(id),
|
||||
queryFn: () => getTestById(id!),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
23
src/hooks/tests/useUpdateTest.ts
Normal file
23
src/hooks/tests/useUpdateTest.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { updateTest, type TestPayload } from "../../api/testApi";
|
||||
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const useUpdateTest = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, payload }: { id: number; payload: TestPayload }) =>
|
||||
updateTest(id, payload),
|
||||
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["tests"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["tests", variables.id] });
|
||||
toast.success("Test updated");
|
||||
},
|
||||
|
||||
onError: (error: any) => {
|
||||
toast.error(error.message ?? "Failed to update test");
|
||||
},
|
||||
});
|
||||
};
|
||||
159
src/pages/AdminTestsPage/AdminTestsPage.tsx
Normal file
159
src/pages/AdminTestsPage/AdminTestsPage.tsx
Normal file
@ -0,0 +1,159 @@
|
||||
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 { useNavigate } from "react-router-dom";
|
||||
|
||||
import Container from "../../components/shared/Container";
|
||||
|
||||
import type { TestType } from "../../components/shared/types/TestTypes";
|
||||
import { useGetTests } from "../../hooks/tests/useGetTests";
|
||||
import { useDeleteTest } from "../../hooks/tests/useDeleteTest";
|
||||
import { formatDate } from "../../utils/functions";
|
||||
|
||||
const AdminTestsPage = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [deleteTestId, setDeleteTestId] = useState<number | null>(null);
|
||||
|
||||
const { data, isLoading, isError } = useGetTests({ page: currentPage });
|
||||
const deleteMutation = useDeleteTest();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleViewTest = (id: number) => {
|
||||
navigate(`/tests/view/${id}`);
|
||||
};
|
||||
|
||||
const handleUpdateTest = (id: number) => {
|
||||
navigate(`/dashboard/tests/${id}/update`);
|
||||
};
|
||||
|
||||
const handleDeleteTest = () => {
|
||||
if (deleteTestId !== null) {
|
||||
deleteMutation.mutate(deleteTestId, {
|
||||
onSuccess: () => setDeleteTestId(null),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) return <CircularProgress />;
|
||||
if (isError)
|
||||
return <Typography color="error">Error loading tests.</Typography>;
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", mb: 3 }}>
|
||||
<Typography variant="h4">Tests</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => navigate("/dashboard/tests/create")}
|
||||
>
|
||||
Create Test
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>ID</TableCell>
|
||||
<TableCell>Title</TableCell>
|
||||
<TableCell>Category</TableCell>
|
||||
<TableCell>Author</TableCell>
|
||||
<TableCell>Available</TableCell>
|
||||
<TableCell>Created At</TableCell>
|
||||
<TableCell>Closed At</TableCell>
|
||||
<TableCell align="right">Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
{data?.data.map((test: TestType) => (
|
||||
<TableRow key={test.id}>
|
||||
<TableCell>{test.id}</TableCell>
|
||||
<TableCell>{test.title}</TableCell>
|
||||
<TableCell>{test.category?.name}</TableCell>
|
||||
<TableCell>{test.author?.username}</TableCell>
|
||||
<TableCell>{test.is_available ? "Yes" : "No"}</TableCell>
|
||||
<TableCell>{formatDate(test.created_at)}</TableCell>
|
||||
<TableCell>{formatDate(test.closed_at)}</TableCell>
|
||||
<TableCell align="right">
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={() => handleViewTest(test.id)}
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => handleUpdateTest(test.id)}
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={() => setDeleteTestId(test.id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<Pagination
|
||||
color="primary"
|
||||
shape="rounded"
|
||||
count={data?.meta.last_page}
|
||||
page={currentPage}
|
||||
onChange={(_, value) => setCurrentPage(value)}
|
||||
sx={{ mt: 3, display: "flex", justifyContent: "center" }}
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
open={deleteTestId !== null}
|
||||
onClose={() => setDeleteTestId(null)}
|
||||
>
|
||||
<DialogTitle>Confirm Delete</DialogTitle>
|
||||
<DialogContent>
|
||||
Are you sure you want to delete this test?
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDeleteTestId(null)}>Cancel</Button>
|
||||
<Button
|
||||
color="error"
|
||||
onClick={handleDeleteTest}
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminTestsPage;
|
||||
@ -19,8 +19,8 @@ import {
|
||||
} from "@mui/material";
|
||||
|
||||
import Container from "../../components/shared/Container";
|
||||
import { useGetAllUserTests } from "../../hooks/Tests/useGetAllUserTests";
|
||||
import { useDeleteUserTest } from "../../hooks/Tests/useDeteleUserTest";
|
||||
import { useGetAllUserTests } from "../../hooks/tests/useGetAllUserTests";
|
||||
import { useDeleteUserTest } from "../../hooks/tests/useDeteleUserTest";
|
||||
|
||||
import UserTestRow from "../../components/UserTestRow/UserTestRow";
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ import Container from "../../components/shared/Container";
|
||||
import { WelcomeContainer } from "./IndexPage.styles";
|
||||
import StartTestForm from "../../components/StartTestForm/StartTestForm";
|
||||
import Question from "../../components/Question/Question";
|
||||
import { useQuestions } from "../../hooks/Question/useQuestions";
|
||||
import { useQuestions } from "../../hooks/questions/useQuestions";
|
||||
import type { QuestionType } from "../../components/shared/types/QuestionTypes";
|
||||
import { useAuth } from "../../context/AuthContext";
|
||||
import { useCategories } from "../../hooks/categories/useCategories";
|
||||
|
||||
@ -2,7 +2,7 @@ import { Box, Button, Tab, Tabs, Typography } from "@mui/material";
|
||||
import { useAuth } from "../../context/AuthContext";
|
||||
import Container from "../../components/shared/Container";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useGetUserTests } from "../../hooks/Tests/useGetUserTests";
|
||||
import { useGetUserTests } from "../../hooks/tests/useGetUserTests";
|
||||
import TestCard from "../../components/TestCard/TestCard";
|
||||
import { useState } from "react";
|
||||
|
||||
|
||||
285
src/pages/QuestionForm/QuestionForm.tsx
Normal file
285
src/pages/QuestionForm/QuestionForm.tsx
Normal file
@ -0,0 +1,285 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
MenuItem,
|
||||
TextField,
|
||||
Typography,
|
||||
IconButton,
|
||||
FormControlLabel,
|
||||
Checkbox,
|
||||
Paper,
|
||||
} from "@mui/material";
|
||||
import { Add, Remove } from "@mui/icons-material";
|
||||
|
||||
import { useCategories } from "../../hooks/categories/useCategories";
|
||||
import { useCreateQuestion } from "../../hooks/questions/useCreateQuestion";
|
||||
import { useUpdateQuestion } from "../../hooks/questions/useUpdateQuestion";
|
||||
|
||||
import type { QuestionPayload } from "../../api/questionsApi";
|
||||
import type { Variant } from "../../components/shared/types/QuestionTypes";
|
||||
import Container from "../../components/shared/Container";
|
||||
import { useQuestionById } from "../../hooks/questions/useQuestionById";
|
||||
import GenerateQuestionModal from "../../components/GenerateQuestionModal/GenerateQuestionModal";
|
||||
|
||||
const QuestionForm = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const isUpdate = Boolean(id);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [title, setTitle] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [type, setType] = useState<"single" | "multiple" | "text">("single");
|
||||
const [difficulty, setDifficulty] = useState(1);
|
||||
const [categoryId, setCategoryId] = useState<number | "">("");
|
||||
const [variants, setVariants] = useState<Variant[]>([{ id: 1, text: "" }]);
|
||||
const [correctAnswers, setCorrectAnswers] = useState<number[]>([]);
|
||||
const [textAnswer, setTextAnswer] = useState("");
|
||||
const [openAiModal, setOpenAiModal] = useState(false);
|
||||
const [generatedQuestion, setGeneratedQuestion] =
|
||||
useState<QuestionPayload | null>(null);
|
||||
|
||||
const { data: categories } = useCategories();
|
||||
const { data: question, isLoading: isLoadingQuestion } = useQuestionById(
|
||||
isUpdate ? Number(id) : undefined
|
||||
);
|
||||
|
||||
const createMutation = useCreateQuestion();
|
||||
const updateMutation = useUpdateQuestion();
|
||||
|
||||
useEffect(() => {
|
||||
if (question || generatedQuestion) {
|
||||
const q = question ?? generatedQuestion;
|
||||
console.log(q);
|
||||
setTitle(q.title);
|
||||
setDescription(q.description ?? "");
|
||||
setType(q.type);
|
||||
setDifficulty(q.difficulty);
|
||||
setCategoryId(q.category_id);
|
||||
if (q.type === "text") {
|
||||
setTextAnswer(q.correct_answers[0] as string);
|
||||
} else {
|
||||
setVariants(q.variants.length ? q.variants : [{ id: 1, text: "" }]);
|
||||
|
||||
setCorrectAnswers(
|
||||
q.correct_answers.map((i: number) =>
|
||||
typeof i === "number" ? i - 1 : 0
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [question, generatedQuestion]);
|
||||
|
||||
if (isUpdate && isLoadingQuestion) return <CircularProgress />;
|
||||
|
||||
const handleVariantChange = (index: number, value: string) => {
|
||||
const newVariants = [...variants];
|
||||
newVariants[index].text = value;
|
||||
setVariants(newVariants);
|
||||
};
|
||||
|
||||
const addVariant = () => {
|
||||
setVariants([...variants, { id: variants.length + 1, text: "" }]);
|
||||
};
|
||||
|
||||
const removeVariant = (index: number) => {
|
||||
const newVariants = variants.filter((_, i) => i !== index);
|
||||
setVariants(newVariants);
|
||||
setCorrectAnswers((prev) => prev.filter((i) => i !== index));
|
||||
};
|
||||
|
||||
const toggleCorrectAnswer = (index: number) => {
|
||||
if (type === "single") {
|
||||
setCorrectAnswers([index]);
|
||||
} else {
|
||||
setCorrectAnswers((prev) =>
|
||||
prev.includes(index)
|
||||
? prev.filter((i) => i !== index)
|
||||
: [...prev, index]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const validate = () => {
|
||||
if (!title || !categoryId) return false;
|
||||
if (type !== "text") {
|
||||
if (variants.length < 2) return false;
|
||||
if (correctAnswers.length === 0) return false;
|
||||
if (type === "single" && correctAnswers.length !== 1) return false;
|
||||
} else {
|
||||
if (!textAnswer.trim()) return false;
|
||||
}
|
||||
if (difficulty < 1 || difficulty > 10) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!validate()) return;
|
||||
|
||||
const payload: QuestionPayload = {
|
||||
title,
|
||||
description,
|
||||
type,
|
||||
difficulty,
|
||||
category_id: Number(categoryId),
|
||||
variants: type === "text" ? [] : variants,
|
||||
correct_answers:
|
||||
type === "text" ? [textAnswer] : correctAnswers.map((i) => i + 1),
|
||||
};
|
||||
|
||||
if (isUpdate && id) {
|
||||
updateMutation.mutate(
|
||||
{ id: Number(id), payload },
|
||||
{ onSuccess: () => navigate("/dashboard/questions") }
|
||||
);
|
||||
} else {
|
||||
createMutation.mutate(payload, {
|
||||
onSuccess: () => navigate("/dashboard/questions"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Typography variant="h5" mb={3}>
|
||||
{isUpdate ? "Update Question" : "Create Question"}
|
||||
</Typography>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<TextField
|
||||
label="Title"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
fullWidth
|
||||
required
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
fullWidth
|
||||
multiline
|
||||
rows={2}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
select
|
||||
label="Type"
|
||||
value={type}
|
||||
onChange={(e) =>
|
||||
setType(e.target.value as "single" | "multiple" | "text")
|
||||
}
|
||||
fullWidth
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
<MenuItem value="single">Single Choice</MenuItem>
|
||||
<MenuItem value="multiple">Multiple Choice</MenuItem>
|
||||
<MenuItem value="text">Text Answer</MenuItem>
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
label="Difficulty (1-10)"
|
||||
type="number"
|
||||
value={difficulty}
|
||||
onChange={(e) => setDifficulty(Number(e.target.value))}
|
||||
fullWidth
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
select
|
||||
label="Category"
|
||||
value={categoryId}
|
||||
onChange={(e) => setCategoryId(Number(e.target.value))}
|
||||
fullWidth
|
||||
required
|
||||
sx={{ mb: 3 }}
|
||||
>
|
||||
{categories?.map((cat) => (
|
||||
<MenuItem key={cat.id} value={cat.id}>
|
||||
{cat.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
{type === "text" ? (
|
||||
<TextField
|
||||
label="Correct Answer"
|
||||
value={textAnswer}
|
||||
onChange={(e) => setTextAnswer(e.target.value)}
|
||||
fullWidth
|
||||
required
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
) : (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="subtitle1">
|
||||
Variants & Correct Answer
|
||||
</Typography>
|
||||
{variants.map((v, i) => (
|
||||
<Paper
|
||||
key={i}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
mb: 1,
|
||||
p: 1,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
value={v.text}
|
||||
onChange={(e) => handleVariantChange(i, e.target.value)}
|
||||
fullWidth
|
||||
required
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={correctAnswers.includes(i)}
|
||||
onChange={() => toggleCorrectAnswer(i)}
|
||||
/>
|
||||
}
|
||||
label="Correct"
|
||||
sx={{ ml: 1 }}
|
||||
/>
|
||||
<IconButton onClick={() => removeVariant(i)}>
|
||||
<Remove />
|
||||
</IconButton>
|
||||
</Paper>
|
||||
))}
|
||||
<Button variant="outlined" startIcon={<Add />} onClick={addVariant}>
|
||||
Add Variant
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Button variant="contained" type="submit">
|
||||
{isUpdate ? "Update Question" : "Create Question"}
|
||||
</Button>
|
||||
{!isUpdate && (
|
||||
<Button
|
||||
|
||||
variant="outlined"
|
||||
sx={{ ml: 2 }}
|
||||
onClick={() => setOpenAiModal(true)}
|
||||
>
|
||||
Generate with AI
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
<GenerateQuestionModal
|
||||
open={openAiModal}
|
||||
onClose={() => setOpenAiModal(false)}
|
||||
onGenerated={(q) => setGeneratedQuestion(q)}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuestionForm;
|
||||
161
src/pages/QuestionsPage/QuestionsPage.tsx
Normal file
161
src/pages/QuestionsPage/QuestionsPage.tsx
Normal file
@ -0,0 +1,161 @@
|
||||
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 { useQuestions } from "../../hooks/questions/useQuestions";
|
||||
import { useDeleteQuestion } from "../../hooks/questions/useDeleteQuestion";
|
||||
import Container from "../../components/shared/Container";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import type { QuestionType } from "../../components/shared/types/QuestionTypes";
|
||||
|
||||
const QuestionsPage = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [deleteQuestionId, setDeleteQuestionId] = useState<number | null>(null);
|
||||
const { data, isLoading, isError } = useQuestions({ page: currentPage });
|
||||
const deleteMutation = useDeleteQuestion();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleCreateQuestion = () => {
|
||||
navigate("/dashboard/questions/create");
|
||||
};
|
||||
|
||||
const handleUpdateQuestion = (id: number) => {
|
||||
navigate(`/dashboard/questions/${id}/update`);
|
||||
};
|
||||
|
||||
const handleViewQuestion = (id: number) => {
|
||||
navigate(`/questions/${id}`);
|
||||
};
|
||||
|
||||
const handleDeleteQuestion = () => {
|
||||
if (deleteQuestionId !== null) {
|
||||
deleteMutation.mutate(deleteQuestionId, {
|
||||
onSuccess: () => setDeleteQuestionId(null),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) return <CircularProgress />;
|
||||
if (isError)
|
||||
return <Typography color="error">Error loading questions.</Typography>;
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", mb: 3 }}>
|
||||
<Typography variant="h4">Questions</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleCreateQuestion}
|
||||
>
|
||||
Create Question
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>ID</TableCell>
|
||||
<TableCell>Title</TableCell>
|
||||
<TableCell>Type</TableCell>
|
||||
<TableCell>Difficulty</TableCell>
|
||||
<TableCell>Category</TableCell>
|
||||
<TableCell>Author</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
{data?.data.map((question: QuestionType) => (
|
||||
<TableRow key={question.id}>
|
||||
<TableCell>{question.id}</TableCell>
|
||||
<TableCell>{question.title}</TableCell>
|
||||
<TableCell>{question.type}</TableCell>
|
||||
<TableCell>{question.difficulty}</TableCell>
|
||||
<TableCell>{question.category?.name ?? "—"}</TableCell>
|
||||
<TableCell>{question.author.username}</TableCell>
|
||||
|
||||
<TableCell sx={{ display: "flex" }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="info"
|
||||
size="small"
|
||||
onClick={() => handleViewQuestion(question.id)}
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
size="small"
|
||||
onClick={() => handleUpdateQuestion(question.id)}
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
size="small"
|
||||
onClick={() => setDeleteQuestionId(question.id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<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" }}
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
open={deleteQuestionId !== null}
|
||||
onClose={() => setDeleteQuestionId(null)}
|
||||
>
|
||||
<DialogTitle>Confirm Delete</DialogTitle>
|
||||
<DialogContent>
|
||||
Are you sure you want to delete this question?
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDeleteQuestionId(null)}>Cancel</Button>
|
||||
<Button
|
||||
color="error"
|
||||
onClick={handleDeleteQuestion}
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuestionsPage;
|
||||
@ -1,6 +1,6 @@
|
||||
import { Container } from "@mui/material";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useQuestionById } from "../../hooks/Question/useQuestionById";
|
||||
import { useQuestionById } from "../../hooks/questions/useQuestionById";
|
||||
import Question from "../../components/Question/Question";
|
||||
import LearningAnswers from "../../components/Answers/Answers";
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useGetTestById } from "../../hooks/Tests/useGetTestById";
|
||||
import { useGetTestById } from "../../hooks/tests/useGetTestById";
|
||||
import Container from "../../components/shared/Container";
|
||||
import {
|
||||
Box,
|
||||
@ -22,9 +22,9 @@ 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 { useGetAllUserTests } from "../../hooks/tests/useGetAllUserTests";
|
||||
import UserTestRow from "../../components/UserTestRow/UserTestRow";
|
||||
import { useDeleteUserTest } from "../../hooks/Tests/useDeteleUserTest";
|
||||
import { useDeleteUserTest } from "../../hooks/tests/useDeteleUserTest";
|
||||
|
||||
export const SingleTestPage = () => {
|
||||
const { id } = useParams();
|
||||
|
||||
193
src/pages/TestForm/TestForm.tsx
Normal file
193
src/pages/TestForm/TestForm.tsx
Normal file
@ -0,0 +1,193 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
FormControlLabel,
|
||||
Checkbox,
|
||||
MenuItem,
|
||||
TextField,
|
||||
Typography,
|
||||
Pagination,
|
||||
} from "@mui/material";
|
||||
|
||||
import { useCategories } from "../../hooks/categories/useCategories";
|
||||
import { useGetTestById } from "../../hooks/tests/useGetTestById";
|
||||
import { useQuestions } from "../../hooks/questions/useQuestions";
|
||||
import { useCreateTest } from "../../hooks/tests/useCreateTest";
|
||||
import { useUpdateTest } from "../../hooks/tests/useUpdateTest";
|
||||
import type { QuestionType } from "../../components/shared/types/QuestionTypes";
|
||||
import Container from "../../components/shared/Container";
|
||||
|
||||
const TestForm = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const isUpdate = Boolean(id);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [title, setTitle] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [closedAt, setClosedAt] = useState("");
|
||||
const [categoryId, setCategoryId] = useState<number | "">("");
|
||||
const [selectedQuestions, setSelectedQuestions] = useState<number[]>([]);
|
||||
const [questionsPage, setQuestionsPage] = useState(1);
|
||||
|
||||
const { data: test, isLoading: isLoadingTest } = useGetTestById(
|
||||
isUpdate ? Number(id) : undefined
|
||||
);
|
||||
|
||||
const { data: categories } = useCategories();
|
||||
const { data: questions, isLoading: isLoadingQuestions } = useQuestions({
|
||||
page: questionsPage,
|
||||
id: categoryId || undefined,
|
||||
});
|
||||
|
||||
const createMutation = useCreateTest();
|
||||
const updateMutation = useUpdateTest();
|
||||
|
||||
useEffect(() => {
|
||||
if (test) {
|
||||
setTitle(test.title);
|
||||
setDescription(test.description ?? "");
|
||||
setClosedAt(test.closed_at);
|
||||
setCategoryId(test.category_id);
|
||||
setSelectedQuestions(test.questions?.map((q) => q.id) ?? []);
|
||||
}
|
||||
}, [test]);
|
||||
|
||||
if (isUpdate && isLoadingTest) return <CircularProgress />;
|
||||
|
||||
const toggleQuestion = (id: number) => {
|
||||
setSelectedQuestions((prev) =>
|
||||
prev.includes(id) ? prev.filter((q) => q !== id) : [...prev, id]
|
||||
);
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const payload = {
|
||||
title,
|
||||
description,
|
||||
closed_at: closedAt || null,
|
||||
category_id: Number(categoryId),
|
||||
questions: selectedQuestions,
|
||||
};
|
||||
|
||||
if (isUpdate && id) {
|
||||
updateMutation.mutate(
|
||||
{ id: Number(id), payload },
|
||||
{ onSuccess: () => navigate("/dashboard/tests") }
|
||||
);
|
||||
} else {
|
||||
createMutation.mutate(payload, {
|
||||
onSuccess: () => navigate("/dashboard/tests"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Typography variant="h5" mb={3}>
|
||||
{isUpdate ? "Update Test" : "Create Test"}
|
||||
</Typography>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<TextField
|
||||
label="Title"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
fullWidth
|
||||
required
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
type="datetime-local"
|
||||
label="Closed At"
|
||||
value={closedAt}
|
||||
onChange={(e) => setClosedAt(e.target.value)}
|
||||
fullWidth
|
||||
InputLabelProps={{ shrink: true }}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
select
|
||||
label="Category"
|
||||
value={categoryId}
|
||||
onChange={(e) => {
|
||||
setCategoryId(Number(e.target.value));
|
||||
setQuestionsPage(1);
|
||||
setSelectedQuestions([]);
|
||||
}}
|
||||
fullWidth
|
||||
required
|
||||
sx={{ mb: 3 }}
|
||||
>
|
||||
{categories?.map((cat) => (
|
||||
<MenuItem key={cat.id} value={cat.id}>
|
||||
{cat.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
{categoryId && (
|
||||
<>
|
||||
<Typography variant="h6" mb={1}>
|
||||
Questions
|
||||
</Typography>
|
||||
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
||||
{isLoadingQuestions ? (
|
||||
<CircularProgress />
|
||||
) : (
|
||||
questions?.data.map((q: QuestionType) => (
|
||||
<FormControlLabel
|
||||
key={q.id}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={selectedQuestions.includes(q.id)}
|
||||
onChange={() => toggleQuestion(q.id)}
|
||||
/>
|
||||
}
|
||||
label={q.title}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{!categoryId && (
|
||||
<Typography variant="h6" mb={1}>
|
||||
Please select category to be able to select questions
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Pagination
|
||||
color="primary"
|
||||
shape="rounded"
|
||||
page={questionsPage}
|
||||
count={questions?.meta.last_page}
|
||||
onChange={(_, page) => setQuestionsPage(page)}
|
||||
sx={{ mt: 3, mb: 3, display: "flex", justifyContent: "start" }}
|
||||
/>
|
||||
|
||||
<Button variant="contained" type="submit">
|
||||
{isUpdate ? "Update Test" : "Create Test"}
|
||||
</Button>
|
||||
</form>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestForm;
|
||||
@ -1,11 +1,11 @@
|
||||
import { useParams } from "react-router-dom";
|
||||
import Container from "../../components/shared/Container";
|
||||
import { useUserTestById } from "../../hooks/Tests/useUserTestById";
|
||||
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";
|
||||
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";
|
||||
|
||||
@ -3,7 +3,7 @@ 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 { useGetTests } from "../../hooks/tests/useGetTests";
|
||||
import Container from "../../components/shared/Container";
|
||||
import TestListCard from "../../components/TestListCard/TestListCard";
|
||||
import {
|
||||
|
||||
@ -13,7 +13,6 @@ import SingleQuestionPage from "../pages/SingleQuestionPage/SingleQuestionPage";
|
||||
import { TestPage } from "../pages/TestPage/TestPage";
|
||||
import TestsPage from "../pages/TestsPage/TestsPage";
|
||||
import AdminLayout from "../layouts/AdminLayout/AdminLayout";
|
||||
import Container from "../components/shared/Container";
|
||||
import UsersPage from "../pages/UsersPage/UsersPage";
|
||||
import UserForm from "../pages/UserForm/UserForm";
|
||||
import CategoriesPage from "../pages/CategoriesPage/CategoriesPage";
|
||||
@ -22,6 +21,10 @@ import LogsPage from "../pages/LogsPage/LogsPage";
|
||||
import HitCountsPage from "../pages/HitcountsPage/HitcountsPage";
|
||||
import UserTestsPage from "../pages/AdminUserTestsPage/AdminUserTestsPage";
|
||||
import { SingleTestPage } from "../pages/SingleTestPage/SingleTestPage";
|
||||
import AdminTestsPage from "../pages/AdminTestsPage/AdminTestsPage";
|
||||
import TestForm from "../pages/TestForm/TestForm";
|
||||
import QuestionsPage from "../pages/QuestionsPage/QuestionsPage";
|
||||
import QuestionForm from "../pages/QuestionForm/QuestionForm";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@ -72,9 +75,7 @@ const router = createBrowserRouter([
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{path: "/tests/view/:id",
|
||||
element: <SingleTestPage/>
|
||||
},
|
||||
{ path: "/tests/view/:id", element: <SingleTestPage /> },
|
||||
|
||||
{
|
||||
path: "/tests",
|
||||
@ -99,8 +100,22 @@ const router = createBrowserRouter([
|
||||
{ path: ":id/update", element: <UserForm /> },
|
||||
],
|
||||
},
|
||||
{ path: "questions", element: <Container>questions</Container> },
|
||||
{ path: "tests", element: <Container>tests</Container> },
|
||||
{
|
||||
path: "questions",
|
||||
children: [
|
||||
{ index: true, element: <QuestionsPage /> },
|
||||
{ path: "create", element: <QuestionForm /> },
|
||||
{ path: ":id/update", element: <QuestionForm /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "tests",
|
||||
children: [
|
||||
{ index: true, element: <AdminTestsPage /> },
|
||||
{ path: "create", element: <TestForm /> },
|
||||
{ path: ":id/update", element: <TestForm /> },
|
||||
],
|
||||
},
|
||||
{ path: "user-tests", element: <UserTestsPage /> },
|
||||
{
|
||||
path: "categories",
|
||||
@ -111,7 +126,7 @@ const router = createBrowserRouter([
|
||||
],
|
||||
},
|
||||
{ path: "logs", element: <LogsPage /> },
|
||||
{path: "hitcounts", element: <HitCountsPage/>}
|
||||
{ path: "hitcounts", element: <HitCountsPage /> },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user