This commit is contained in:
David Katrinka 2026-01-02 21:49:46 +01:00
parent 69382f2f4c
commit bf919daaf8
14 changed files with 200 additions and 106 deletions

20
package-lock.json generated
View File

@ -16,6 +16,7 @@
"axios": "^1.13.2", "axios": "^1.13.2",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-qr-code": "^2.0.18",
"react-router-dom": "^7.9.5", "react-router-dom": "^7.9.5",
"react-toastify": "^11.0.5" "react-toastify": "^11.0.5"
}, },
@ -3797,6 +3798,12 @@
"node": ">=6" "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": { "node_modules/queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@ -3845,6 +3852,19 @@
"integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==", "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==",
"license": "MIT" "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": { "node_modules/react-refresh": {
"version": "0.18.0", "version": "0.18.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",

View File

@ -18,6 +18,7 @@
"axios": "^1.13.2", "axios": "^1.13.2",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-qr-code": "^2.0.18",
"react-router-dom": "^7.9.5", "react-router-dom": "^7.9.5",
"react-toastify": "^11.0.5" "react-toastify": "^11.0.5"
}, },

View File

@ -38,7 +38,13 @@ export const deleteUser = async (id: number) => {
}; };
export const createUser = async (payload: UserPayload) => { 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; return res.data;
}; };

View File

@ -12,50 +12,45 @@ import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
import QuizIcon from "@mui/icons-material/Quiz"; import QuizIcon from "@mui/icons-material/Quiz";
import CategoryIcon from "@mui/icons-material/Category"; import CategoryIcon from "@mui/icons-material/Category";
import ArticleIcon from "@mui/icons-material/Article"; import ArticleIcon from "@mui/icons-material/Article";
import AssignmentIndIcon from '@mui/icons-material/AssignmentInd'; import AssignmentIndIcon from "@mui/icons-material/AssignmentInd";
import FmdBadIcon from '@mui/icons-material/FmdBad'; 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 drawerWidth = 240;
const menuItems = [ const adminMenu: MenuItem[] = [
{ { label: "Users", icon: <PeopleIcon />, to: "/dashboard/users" },
label: "Users", { label: "Questions", icon: <HelpOutlineIcon />, to: "/dashboard/questions" },
icon: <PeopleIcon />, { label: "Tests", icon: <QuizIcon />, to: "/dashboard/tests" },
to: "/dashboard/users",
},
{
label: "Questions",
icon: <HelpOutlineIcon />,
to: "/dashboard/questions",
},
{
label: "Tests",
icon: <QuizIcon />,
to: "/dashboard/tests",
},
{ {
label: "User Tests", label: "User Tests",
icon: <AssignmentIndIcon />, icon: <AssignmentIndIcon />,
to: "/dashboard/user-tests", to: "/dashboard/user-tests",
}, },
{ { label: "Categories", icon: <CategoryIcon />, to: "/dashboard/categories" },
label: "Categories", { label: "Hitcounts", icon: <FmdBadIcon />, to: "/dashboard/hitcounts" },
icon: <CategoryIcon />, { label: "Logs", icon: <ArticleIcon />, to: "/dashboard/logs" },
to: "/dashboard/categories", ];
},
{ const creatorMenu: MenuItem[] = [
label: "Hitcounts", { label: "Questions", icon: <HelpOutlineIcon />, to: "/dashboard/questions" },
icon: <FmdBadIcon />, { label: "Tests", icon: <QuizIcon />, to: "/dashboard/tests" },
to: "/dashboard/hitcounts",
},
{
label: "Logs",
icon: <ArticleIcon />,
to: "/dashboard/logs",
},
]; ];
export const AdminSidebar = () => { export const AdminSidebar = () => {
const { user } = useAuth();
const menuItems =
user?.type === "admin"
? adminMenu
: user?.type === "creator"
? creatorMenu
: [];
return ( return (
<Drawer <Drawer
variant="permanent" variant="permanent"

View File

@ -95,16 +95,14 @@ function Header() {
> >
<Typography textAlign="center">Profile</Typography> <Typography textAlign="center">Profile</Typography>
</MenuItem> </MenuItem>
{user.type === "admin" && ( {(user.type === "admin" || user.type === "creator") && (
<MenuItem <MenuItem
onClick={() => { onClick={() => {
handleCloseNavMenu(); handleCloseNavMenu();
goToDashboard(); goToDashboard();
}} }}
> >
<Typography textAlign="center"> <Typography textAlign="center">Dashboard</Typography>
Dashboard
</Typography>
</MenuItem> </MenuItem>
)} )}
</span> </span>
@ -159,7 +157,7 @@ function Header() {
> >
Profile Profile
</Button> </Button>
{user.type === "admin" && ( {(user.type === "admin" || user.type === "creator") && (
<Button <Button
onClick={goToDashboard} onClick={goToDashboard}
sx={{ my: 2, color: "white", display: "block" }} sx={{ my: 2, color: "white", display: "block" }}

View 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}</>;
};

View 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;

View File

@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom";
import type { UserTestType } from "../../components/shared/types/TestTypes"; 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"; import { formatDate } from "../../utils/functions";
import { useAuth } from "../../context/AuthContext";
type Props = { type Props = {
userTest: UserTestType; userTest: UserTestType;
@ -11,6 +12,7 @@ type Props = {
const UserTestRow = ({ userTest, onDelete }: Props) => { const UserTestRow = ({ userTest, onDelete }: Props) => {
const navigate = useNavigate(); const navigate = useNavigate();
const { user } = useAuth();
const testId = userTest.test_id; const testId = userTest.test_id;
let title = "User Test"; let title = "User Test";
@ -38,9 +40,7 @@ const UserTestRow = ({ userTest, onDelete }: Props) => {
<TableCell>{userTest.is_completed ? "Yes" : "No"}</TableCell> <TableCell>{userTest.is_completed ? "Yes" : "No"}</TableCell>
<TableCell> <TableCell>{formatDate(userTest.created_at)}</TableCell>
{formatDate(userTest.created_at)}
</TableCell>
<TableCell>{formatDate(userTest.closed_at)}</TableCell> <TableCell>{formatDate(userTest.closed_at)}</TableCell>
<TableCell> <TableCell>
@ -52,7 +52,7 @@ const UserTestRow = ({ userTest, onDelete }: Props) => {
> >
View View
</Button> </Button>
{user?.type === "admin" && (
<Button <Button
size="small" size="small"
color="error" color="error"
@ -61,6 +61,7 @@ const UserTestRow = ({ userTest, onDelete }: Props) => {
> >
Delete Delete
</Button> </Button>
)}
</TableCell> </TableCell>
</TableRow> </TableRow>
); );

View File

@ -25,10 +25,12 @@ import type { TestType } from "../../components/shared/types/TestTypes";
import { useGetTests } from "../../hooks/tests/useGetTests"; import { useGetTests } from "../../hooks/tests/useGetTests";
import { useDeleteTest } from "../../hooks/tests/useDeleteTest"; import { useDeleteTest } from "../../hooks/tests/useDeleteTest";
import { formatDate } from "../../utils/functions"; import { formatDate } from "../../utils/functions";
import { useAuth } from "../../context/AuthContext";
const AdminTestsPage = () => { const AdminTestsPage = () => {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [deleteTestId, setDeleteTestId] = useState<number | null>(null); const [deleteTestId, setDeleteTestId] = useState<number | null>(null);
const { user } = useAuth();
const { data, isLoading, isError } = useGetTests({ page: currentPage }); const { data, isLoading, isError } = useGetTests({ page: currentPage });
const deleteMutation = useDeleteTest(); const deleteMutation = useDeleteTest();
@ -109,6 +111,7 @@ const AdminTestsPage = () => {
> >
Update Update
</Button> </Button>
{(user?.type === "admin" || user?.id == test.author_id) && (
<Button <Button
size="small" size="small"
variant="outlined" variant="outlined"
@ -117,6 +120,7 @@ const AdminTestsPage = () => {
> >
Delete Delete
</Button> </Button>
)}
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}

View File

@ -44,7 +44,7 @@ const ProfilePage = () => {
sx={{ marginLeft: "10px" }} sx={{ marginLeft: "10px" }}
variant="contained" variant="contained"
color="secondary" color="secondary"
onClick={() => navigate("/admin")} onClick={() => navigate("/dashboard")}
> >
Admin Dashboard Admin Dashboard
</Button> </Button>

View File

@ -22,6 +22,7 @@ import { useDeleteQuestion } from "../../hooks/questions/useDeleteQuestion";
import Container from "../../components/shared/Container"; import Container from "../../components/shared/Container";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import type { QuestionType } from "../../components/shared/types/QuestionTypes"; import type { QuestionType } from "../../components/shared/types/QuestionTypes";
import { useAuth } from "../../context/AuthContext";
const QuestionsPage = () => { const QuestionsPage = () => {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
@ -29,6 +30,7 @@ const QuestionsPage = () => {
const { data, isLoading, isError } = useQuestions({ page: currentPage }); const { data, isLoading, isError } = useQuestions({ page: currentPage });
const deleteMutation = useDeleteQuestion(); const deleteMutation = useDeleteQuestion();
const navigate = useNavigate(); const navigate = useNavigate();
const { user } = useAuth();
const handleCreateQuestion = () => { const handleCreateQuestion = () => {
navigate("/dashboard/questions/create"); navigate("/dashboard/questions/create");
@ -111,6 +113,8 @@ const QuestionsPage = () => {
> >
Update Update
</Button> </Button>
{(user?.type === "admin" ||
user?.id == question.author.id) && (
<Button <Button
variant="outlined" variant="outlined"
color="error" color="error"
@ -119,6 +123,7 @@ const QuestionsPage = () => {
> >
Delete Delete
</Button> </Button>
)}
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}

View File

@ -4,10 +4,6 @@ import Container from "../../components/shared/Container";
import { import {
Box, Box,
Button, Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Pagination, Pagination,
Paper, Paper,
Table, Table,
@ -24,24 +20,17 @@ import { useAuth } from "../../context/AuthContext";
import { useState } from "react"; import { useState } from "react";
import { useGetAllUserTests } from "../../hooks/tests/useGetAllUserTests"; import { useGetAllUserTests } from "../../hooks/tests/useGetAllUserTests";
import UserTestRow from "../../components/UserTestRow/UserTestRow"; import UserTestRow from "../../components/UserTestRow/UserTestRow";
import { useDeleteUserTest } from "../../hooks/tests/useDeteleUserTest"; import TestQrModal from "../../components/TestQrModal/TestQrModal";
export const SingleTestPage = () => { export const SingleTestPage = () => {
const { id } = useParams(); const { id } = useParams();
const { user } = useAuth(); const { user } = useAuth();
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [deleteId, setDeleteId] = useState<number | null>(null);
const deleteMutation = useDeleteUserTest();
const { data: tests } = useGetAllUserTests(currentPage, Number(id)); const { data: tests } = useGetAllUserTests(currentPage, Number(id));
const handleDelete = () => { const [qrOpen, setQrOpen] = useState(false);
if (deleteId !== null) {
deleteMutation.mutate(deleteId, {
onSuccess: () => setDeleteId(null),
});
}
};
const { data: test } = useGetTestById(Number(id)); const { data: test } = useGetTestById(Number(id));
if (!test) { if (!test) {
@ -49,9 +38,15 @@ export const SingleTestPage = () => {
} }
return ( return (
<Container> <Container>
<Typography variant="h2" sx={{ mb: 3 }}> <Box sx={{ display: "flex", justifyContent: "space-between", mb: 3 }}>
{test?.title} <Typography variant="h2">{test.title}</Typography>
</Typography>
{(user?.type === "admin" || user?.type === "creator") && (
<Button variant="outlined" onClick={() => setQrOpen(true)}>
Show QR
</Button>
)}
</Box>
{test?.questions?.map((q) => ( {test?.questions?.map((q) => (
<Box key={q.id} sx={{ mb: 4 }}> <Box key={q.id} sx={{ mb: 4 }}>
@ -95,7 +90,7 @@ export const SingleTestPage = () => {
<UserTestRow <UserTestRow
key={ut.id} key={ut.id}
userTest={ut} userTest={ut}
onDelete={(id) => setDeleteId(id)} onDelete={() => {}}
/> />
)) ))
) : ( ) : (
@ -124,24 +119,10 @@ export const SingleTestPage = () => {
justifyContent: "center", 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> </span>
)} )}
<TestQrModal open={qrOpen} onClose={() => setQrOpen(false)} test={test} />
</Container> </Container>
); );
}; };

View 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>
);
}

View File

@ -25,6 +25,7 @@ import AdminTestsPage from "../pages/AdminTestsPage/AdminTestsPage";
import TestForm from "../pages/TestForm/TestForm"; import TestForm from "../pages/TestForm/TestForm";
import QuestionsPage from "../pages/QuestionsPage/QuestionsPage"; import QuestionsPage from "../pages/QuestionsPage/QuestionsPage";
import QuestionForm from "../pages/QuestionForm/QuestionForm"; import QuestionForm from "../pages/QuestionForm/QuestionForm";
import { AdminProtectedRoute } from "../components/ProtectedRoute/AdminProtectedRoute";
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
@ -89,9 +90,13 @@ const router = createBrowserRouter([
}, },
{ {
path: "/dashboard", path: "/dashboard",
element: <AdminLayout />, element: (
<AdminProtectedRoute>
<AdminLayout />
</AdminProtectedRoute>
),
children: [ children: [
{ index: true, element: <UsersPage /> }, { index: true, element: <QuestionsPage /> },
{ {
path: "users", path: "users",
children: [ children: [