fin
This commit is contained in:
parent
69382f2f4c
commit
bf919daaf8
20
package-lock.json
generated
20
package-lock.json
generated
@ -16,6 +16,7 @@
|
||||
"axios": "^1.13.2",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-qr-code": "^2.0.18",
|
||||
"react-router-dom": "^7.9.5",
|
||||
"react-toastify": "^11.0.5"
|
||||
},
|
||||
@ -3797,6 +3798,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qr.js": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz",
|
||||
"integrity": "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
@ -3845,6 +3852,19 @@
|
||||
"integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-qr-code": {
|
||||
"version": "2.0.18",
|
||||
"resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.18.tgz",
|
||||
"integrity": "sha512-v1Jqz7urLMhkO6jkgJuBYhnqvXagzceg3qJUWayuCK/c6LTIonpWbwxR1f1APGd4xrW/QcQEovNrAojbUz65Tg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.8.1",
|
||||
"qr.js": "0.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
"axios": "^1.13.2",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-qr-code": "^2.0.18",
|
||||
"react-router-dom": "^7.9.5",
|
||||
"react-toastify": "^11.0.5"
|
||||
},
|
||||
|
||||
@ -38,7 +38,13 @@ export const deleteUser = async (id: number) => {
|
||||
};
|
||||
|
||||
export const createUser = async (payload: UserPayload) => {
|
||||
const res = await axiosInstance.post<User>("/api/users", payload);
|
||||
const res = await axiosInstance.post<User>("/api/users",{
|
||||
username: payload.username,
|
||||
email: payload.email,
|
||||
email_verified_at: "2025-11-12T10:13:48.000000Z",
|
||||
password: payload.password,
|
||||
type: payload.type,
|
||||
});
|
||||
return res.data;
|
||||
};
|
||||
|
||||
|
||||
@ -12,50 +12,45 @@ import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
|
||||
import QuizIcon from "@mui/icons-material/Quiz";
|
||||
import CategoryIcon from "@mui/icons-material/Category";
|
||||
import ArticleIcon from "@mui/icons-material/Article";
|
||||
import AssignmentIndIcon from '@mui/icons-material/AssignmentInd';
|
||||
import FmdBadIcon from '@mui/icons-material/FmdBad';
|
||||
import AssignmentIndIcon from "@mui/icons-material/AssignmentInd";
|
||||
import FmdBadIcon from "@mui/icons-material/FmdBad";
|
||||
import { useAuth } from "../../context/AuthContext";
|
||||
type MenuItem = {
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
to: string;
|
||||
};
|
||||
|
||||
const drawerWidth = 240;
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
label: "Users",
|
||||
icon: <PeopleIcon />,
|
||||
to: "/dashboard/users",
|
||||
},
|
||||
{
|
||||
label: "Questions",
|
||||
icon: <HelpOutlineIcon />,
|
||||
to: "/dashboard/questions",
|
||||
},
|
||||
{
|
||||
label: "Tests",
|
||||
icon: <QuizIcon />,
|
||||
to: "/dashboard/tests",
|
||||
},
|
||||
const adminMenu: MenuItem[] = [
|
||||
{ label: "Users", icon: <PeopleIcon />, to: "/dashboard/users" },
|
||||
{ label: "Questions", icon: <HelpOutlineIcon />, to: "/dashboard/questions" },
|
||||
{ label: "Tests", icon: <QuizIcon />, to: "/dashboard/tests" },
|
||||
{
|
||||
label: "User Tests",
|
||||
icon: <AssignmentIndIcon />,
|
||||
to: "/dashboard/user-tests",
|
||||
},
|
||||
{
|
||||
label: "Categories",
|
||||
icon: <CategoryIcon />,
|
||||
to: "/dashboard/categories",
|
||||
},
|
||||
{
|
||||
label: "Hitcounts",
|
||||
icon: <FmdBadIcon />,
|
||||
to: "/dashboard/hitcounts",
|
||||
},
|
||||
{
|
||||
label: "Logs",
|
||||
icon: <ArticleIcon />,
|
||||
to: "/dashboard/logs",
|
||||
},
|
||||
{ label: "Categories", icon: <CategoryIcon />, to: "/dashboard/categories" },
|
||||
{ label: "Hitcounts", icon: <FmdBadIcon />, to: "/dashboard/hitcounts" },
|
||||
{ label: "Logs", icon: <ArticleIcon />, to: "/dashboard/logs" },
|
||||
];
|
||||
|
||||
const creatorMenu: MenuItem[] = [
|
||||
{ label: "Questions", icon: <HelpOutlineIcon />, to: "/dashboard/questions" },
|
||||
{ label: "Tests", icon: <QuizIcon />, to: "/dashboard/tests" },
|
||||
];
|
||||
|
||||
export const AdminSidebar = () => {
|
||||
const { user } = useAuth();
|
||||
|
||||
const menuItems =
|
||||
user?.type === "admin"
|
||||
? adminMenu
|
||||
: user?.type === "creator"
|
||||
? creatorMenu
|
||||
: [];
|
||||
return (
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
|
||||
@ -95,16 +95,14 @@ function Header() {
|
||||
>
|
||||
<Typography textAlign="center">Profile</Typography>
|
||||
</MenuItem>
|
||||
{user.type === "admin" && (
|
||||
{(user.type === "admin" || user.type === "creator") && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
handleCloseNavMenu();
|
||||
goToDashboard();
|
||||
}}
|
||||
>
|
||||
<Typography textAlign="center">
|
||||
Dashboard
|
||||
</Typography>
|
||||
<Typography textAlign="center">Dashboard</Typography>
|
||||
</MenuItem>
|
||||
)}
|
||||
</span>
|
||||
@ -159,7 +157,7 @@ function Header() {
|
||||
>
|
||||
Profile
|
||||
</Button>
|
||||
{user.type === "admin" && (
|
||||
{(user.type === "admin" || user.type === "creator") && (
|
||||
<Button
|
||||
onClick={goToDashboard}
|
||||
sx={{ my: 2, color: "white", display: "block" }}
|
||||
@ -170,13 +168,13 @@ function Header() {
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
sx={{ my: 2, color: "white", display: "block" }}
|
||||
sx={{ my: 2, color: "white", display: "block" }}
|
||||
onClick={() => {
|
||||
handleCloseNavMenu();
|
||||
navigate("/tests");
|
||||
}}
|
||||
>
|
||||
Tests
|
||||
Tests
|
||||
</Button>
|
||||
</Box>
|
||||
</Toolbar>
|
||||
|
||||
18
src/components/ProtectedRoute/AdminProtectedRoute.tsx
Normal file
18
src/components/ProtectedRoute/AdminProtectedRoute.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { useAuth } from "../../context/AuthContext";
|
||||
import Container from "../shared/Container";
|
||||
import Forbidden from "../../pages/Unauthorized/Forbidden";
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const AdminProtectedRoute = ({ children }: ProtectedRouteProps) => {
|
||||
const { user, isLoading } = useAuth();
|
||||
|
||||
if (isLoading) return <Container>Loading...</Container>;
|
||||
|
||||
if (user?.type === "user" || user?.type === "banned") return <Forbidden />;
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
41
src/components/TestQrModal/TestQrModal.tsx
Normal file
41
src/components/TestQrModal/TestQrModal.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import QRCode from "react-qr-code";
|
||||
import { Modal, Box, Button } from "@mui/material";
|
||||
|
||||
import type { TestType } from "../shared/types/TestTypes";
|
||||
|
||||
interface QrProps {
|
||||
open: boolean,
|
||||
onClose: () => void,
|
||||
test: TestType
|
||||
}
|
||||
|
||||
const TestQrModal = ({ open, onClose, test }: QrProps) => {
|
||||
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
bgcolor: "white",
|
||||
p: 3,
|
||||
borderRadius: 2,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
|
||||
}}
|
||||
>
|
||||
<QRCode value={String(test.id)} size={200} />
|
||||
<Button onClick={onClose} sx={{ mt: 2 }}>
|
||||
Close
|
||||
</Button>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestQrModal;
|
||||
@ -3,6 +3,7 @@ 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";
|
||||
import { useAuth } from "../../context/AuthContext";
|
||||
|
||||
type Props = {
|
||||
userTest: UserTestType;
|
||||
@ -11,6 +12,7 @@ type Props = {
|
||||
|
||||
const UserTestRow = ({ userTest, onDelete }: Props) => {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
|
||||
const testId = userTest.test_id;
|
||||
let title = "User Test";
|
||||
@ -38,9 +40,7 @@ const UserTestRow = ({ userTest, onDelete }: Props) => {
|
||||
|
||||
<TableCell>{userTest.is_completed ? "Yes" : "No"}</TableCell>
|
||||
|
||||
<TableCell>
|
||||
{formatDate(userTest.created_at)}
|
||||
</TableCell>
|
||||
<TableCell>{formatDate(userTest.created_at)}</TableCell>
|
||||
<TableCell>{formatDate(userTest.closed_at)}</TableCell>
|
||||
|
||||
<TableCell>
|
||||
@ -52,15 +52,16 @@ const UserTestRow = ({ userTest, onDelete }: Props) => {
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
color="error"
|
||||
variant="outlined"
|
||||
onClick={() => onDelete(userTest.id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
{user?.type === "admin" && (
|
||||
<Button
|
||||
size="small"
|
||||
color="error"
|
||||
variant="outlined"
|
||||
onClick={() => onDelete(userTest.id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
@ -25,10 +25,12 @@ 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";
|
||||
import { useAuth } from "../../context/AuthContext";
|
||||
|
||||
const AdminTestsPage = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [deleteTestId, setDeleteTestId] = useState<number | null>(null);
|
||||
const { user } = useAuth();
|
||||
|
||||
const { data, isLoading, isError } = useGetTests({ page: currentPage });
|
||||
const deleteMutation = useDeleteTest();
|
||||
@ -109,14 +111,16 @@ const AdminTestsPage = () => {
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={() => setDeleteTestId(test.id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
{(user?.type === "admin" || user?.id == test.author_id) && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={() => setDeleteTestId(test.id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@ -44,7 +44,7 @@ const ProfilePage = () => {
|
||||
sx={{ marginLeft: "10px" }}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={() => navigate("/admin")}
|
||||
onClick={() => navigate("/dashboard")}
|
||||
>
|
||||
Admin Dashboard
|
||||
</Button>
|
||||
|
||||
@ -22,6 +22,7 @@ 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";
|
||||
import { useAuth } from "../../context/AuthContext";
|
||||
|
||||
const QuestionsPage = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
@ -29,6 +30,7 @@ const QuestionsPage = () => {
|
||||
const { data, isLoading, isError } = useQuestions({ page: currentPage });
|
||||
const deleteMutation = useDeleteQuestion();
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
|
||||
const handleCreateQuestion = () => {
|
||||
navigate("/dashboard/questions/create");
|
||||
@ -111,14 +113,17 @@ const QuestionsPage = () => {
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
size="small"
|
||||
onClick={() => setDeleteQuestionId(question.id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
{(user?.type === "admin" ||
|
||||
user?.id == question.author.id) && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
size="small"
|
||||
onClick={() => setDeleteQuestionId(question.id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@ -4,10 +4,6 @@ import Container from "../../components/shared/Container";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Pagination,
|
||||
Paper,
|
||||
Table,
|
||||
@ -24,24 +20,17 @@ 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";
|
||||
import TestQrModal from "../../components/TestQrModal/TestQrModal";
|
||||
|
||||
export const SingleTestPage = () => {
|
||||
const { id } = useParams();
|
||||
const { user } = useAuth();
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [deleteId, setDeleteId] = useState<number | null>(null);
|
||||
const deleteMutation = useDeleteUserTest();
|
||||
const { data: tests } = useGetAllUserTests(currentPage, Number(id));
|
||||
|
||||
const handleDelete = () => {
|
||||
if (deleteId !== null) {
|
||||
deleteMutation.mutate(deleteId, {
|
||||
onSuccess: () => setDeleteId(null),
|
||||
});
|
||||
}
|
||||
};
|
||||
const [qrOpen, setQrOpen] = useState(false);
|
||||
|
||||
|
||||
const { data: test } = useGetTestById(Number(id));
|
||||
if (!test) {
|
||||
@ -49,9 +38,15 @@ export const SingleTestPage = () => {
|
||||
}
|
||||
return (
|
||||
<Container>
|
||||
<Typography variant="h2" sx={{ mb: 3 }}>
|
||||
{test?.title}
|
||||
</Typography>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", mb: 3 }}>
|
||||
<Typography variant="h2">{test.title}</Typography>
|
||||
|
||||
{(user?.type === "admin" || user?.type === "creator") && (
|
||||
<Button variant="outlined" onClick={() => setQrOpen(true)}>
|
||||
Show QR
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{test?.questions?.map((q) => (
|
||||
<Box key={q.id} sx={{ mb: 4 }}>
|
||||
@ -95,7 +90,7 @@ export const SingleTestPage = () => {
|
||||
<UserTestRow
|
||||
key={ut.id}
|
||||
userTest={ut}
|
||||
onDelete={(id) => setDeleteId(id)}
|
||||
onDelete={() => {}}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
@ -124,24 +119,10 @@ export const SingleTestPage = () => {
|
||||
justifyContent: "center",
|
||||
}}
|
||||
/>
|
||||
<Dialog open={deleteId !== null} onClose={() => setDeleteId(null)}>
|
||||
<DialogTitle>Confirm Delete</DialogTitle>
|
||||
<DialogContent>
|
||||
Are you sure you want to delete this user test?
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDeleteId(null)}>Cancel</Button>
|
||||
<Button
|
||||
color="error"
|
||||
onClick={handleDelete}
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
</span>
|
||||
)}
|
||||
<TestQrModal open={qrOpen} onClose={() => setQrOpen(false)} test={test} />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
19
src/pages/Unauthorized/Forbidden.tsx
Normal file
19
src/pages/Unauthorized/Forbidden.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { Box, Typography } from "@mui/material";
|
||||
|
||||
export default function Forbidden() {
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
flexDirection="column"
|
||||
minHeight="80vh"
|
||||
>
|
||||
<Typography variant="h2" color="error" mb={2}>
|
||||
403
|
||||
</Typography>
|
||||
<Typography variant="h5">Forbidden</Typography>
|
||||
<Typography>You do not have access to this page.</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@ -25,6 +25,7 @@ import AdminTestsPage from "../pages/AdminTestsPage/AdminTestsPage";
|
||||
import TestForm from "../pages/TestForm/TestForm";
|
||||
import QuestionsPage from "../pages/QuestionsPage/QuestionsPage";
|
||||
import QuestionForm from "../pages/QuestionForm/QuestionForm";
|
||||
import { AdminProtectedRoute } from "../components/ProtectedRoute/AdminProtectedRoute";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@ -89,9 +90,13 @@ const router = createBrowserRouter([
|
||||
},
|
||||
{
|
||||
path: "/dashboard",
|
||||
element: <AdminLayout />,
|
||||
element: (
|
||||
<AdminProtectedRoute>
|
||||
<AdminLayout />
|
||||
</AdminProtectedRoute>
|
||||
),
|
||||
children: [
|
||||
{ index: true, element: <UsersPage /> },
|
||||
{ index: true, element: <QuestionsPage /> },
|
||||
{
|
||||
path: "users",
|
||||
children: [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user