diff --git a/package-lock.json b/package-lock.json index 6c941e4..efb7655 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 63042d5..135a2bc 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/src/api/usersApi.ts b/src/api/usersApi.ts index ffe4208..4138d7b 100644 --- a/src/api/usersApi.ts +++ b/src/api/usersApi.ts @@ -38,7 +38,13 @@ export const deleteUser = async (id: number) => { }; export const createUser = async (payload: UserPayload) => { - const res = await axiosInstance.post("/api/users", payload); + const res = await axiosInstance.post("/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; }; diff --git a/src/components/AdminSidebar/AdminSidebar.tsx b/src/components/AdminSidebar/AdminSidebar.tsx index 96e4466..f3ca8ef 100644 --- a/src/components/AdminSidebar/AdminSidebar.tsx +++ b/src/components/AdminSidebar/AdminSidebar.tsx @@ -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: , - to: "/dashboard/users", - }, - { - label: "Questions", - icon: , - to: "/dashboard/questions", - }, - { - label: "Tests", - icon: , - to: "/dashboard/tests", - }, +const adminMenu: MenuItem[] = [ + { label: "Users", icon: , to: "/dashboard/users" }, + { label: "Questions", icon: , to: "/dashboard/questions" }, + { label: "Tests", icon: , to: "/dashboard/tests" }, { label: "User Tests", icon: , to: "/dashboard/user-tests", }, - { - label: "Categories", - icon: , - to: "/dashboard/categories", - }, - { - label: "Hitcounts", - icon: , - to: "/dashboard/hitcounts", - }, - { - label: "Logs", - icon: , - to: "/dashboard/logs", - }, + { label: "Categories", icon: , to: "/dashboard/categories" }, + { label: "Hitcounts", icon: , to: "/dashboard/hitcounts" }, + { label: "Logs", icon: , to: "/dashboard/logs" }, +]; + +const creatorMenu: MenuItem[] = [ + { label: "Questions", icon: , to: "/dashboard/questions" }, + { label: "Tests", icon: , to: "/dashboard/tests" }, ]; export const AdminSidebar = () => { + const { user } = useAuth(); + + const menuItems = + user?.type === "admin" + ? adminMenu + : user?.type === "creator" + ? creatorMenu + : []; return ( Profile - {user.type === "admin" && ( + {(user.type === "admin" || user.type === "creator") && ( { handleCloseNavMenu(); goToDashboard(); }} > - - Dashboard - + Dashboard )} @@ -159,7 +157,7 @@ function Header() { > Profile - {user.type === "admin" && ( + {(user.type === "admin" || user.type === "creator") && ( diff --git a/src/components/ProtectedRoute/AdminProtectedRoute.tsx b/src/components/ProtectedRoute/AdminProtectedRoute.tsx new file mode 100644 index 0000000..38eb4ff --- /dev/null +++ b/src/components/ProtectedRoute/AdminProtectedRoute.tsx @@ -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 Loading...; + + if (user?.type === "user" || user?.type === "banned") return ; + + return <>{children}; +}; diff --git a/src/components/TestQrModal/TestQrModal.tsx b/src/components/TestQrModal/TestQrModal.tsx new file mode 100644 index 0000000..f09674e --- /dev/null +++ b/src/components/TestQrModal/TestQrModal.tsx @@ -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 ( + + + + + + + ); +}; + +export default TestQrModal; \ No newline at end of file diff --git a/src/components/UserTestRow/UserTestRow.tsx b/src/components/UserTestRow/UserTestRow.tsx index 9ef318e..451be4e 100644 --- a/src/components/UserTestRow/UserTestRow.tsx +++ b/src/components/UserTestRow/UserTestRow.tsx @@ -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) => { {userTest.is_completed ? "Yes" : "No"} - - {formatDate(userTest.created_at)} - + {formatDate(userTest.created_at)} {formatDate(userTest.closed_at)} @@ -52,15 +52,16 @@ const UserTestRow = ({ userTest, onDelete }: Props) => { > View - - + {user?.type === "admin" && ( + + )} ); diff --git a/src/pages/AdminTestsPage/AdminTestsPage.tsx b/src/pages/AdminTestsPage/AdminTestsPage.tsx index 3fe6371..9cbd372 100644 --- a/src/pages/AdminTestsPage/AdminTestsPage.tsx +++ b/src/pages/AdminTestsPage/AdminTestsPage.tsx @@ -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(null); + const { user } = useAuth(); const { data, isLoading, isError } = useGetTests({ page: currentPage }); const deleteMutation = useDeleteTest(); @@ -109,14 +111,16 @@ const AdminTestsPage = () => { > Update - + {(user?.type === "admin" || user?.id == test.author_id) && ( + + )} ))} diff --git a/src/pages/ProfilePage/Profilepage.tsx b/src/pages/ProfilePage/Profilepage.tsx index 3d24134..9f72ddd 100644 --- a/src/pages/ProfilePage/Profilepage.tsx +++ b/src/pages/ProfilePage/Profilepage.tsx @@ -44,7 +44,7 @@ const ProfilePage = () => { sx={{ marginLeft: "10px" }} variant="contained" color="secondary" - onClick={() => navigate("/admin")} + onClick={() => navigate("/dashboard")} > Admin Dashboard diff --git a/src/pages/QuestionsPage/QuestionsPage.tsx b/src/pages/QuestionsPage/QuestionsPage.tsx index ddbaaf5..d9ecc62 100644 --- a/src/pages/QuestionsPage/QuestionsPage.tsx +++ b/src/pages/QuestionsPage/QuestionsPage.tsx @@ -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 - + {(user?.type === "admin" || + user?.id == question.author.id) && ( + + )} ))} diff --git a/src/pages/SingleTestPage/SingleTestPage.tsx b/src/pages/SingleTestPage/SingleTestPage.tsx index 354c2e3..08a4756 100644 --- a/src/pages/SingleTestPage/SingleTestPage.tsx +++ b/src/pages/SingleTestPage/SingleTestPage.tsx @@ -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(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 ( - - {test?.title} - + + {test.title} + + {(user?.type === "admin" || user?.type === "creator") && ( + + )} + {test?.questions?.map((q) => ( @@ -95,7 +90,7 @@ export const SingleTestPage = () => { setDeleteId(id)} + onDelete={() => {}} /> )) ) : ( @@ -124,24 +119,10 @@ export const SingleTestPage = () => { justifyContent: "center", }} /> - setDeleteId(null)}> - Confirm Delete - - Are you sure you want to delete this user test? - - - - - - + )} + setQrOpen(false)} test={test} /> ); }; diff --git a/src/pages/Unauthorized/Forbidden.tsx b/src/pages/Unauthorized/Forbidden.tsx new file mode 100644 index 0000000..9b7990d --- /dev/null +++ b/src/pages/Unauthorized/Forbidden.tsx @@ -0,0 +1,19 @@ +import { Box, Typography } from "@mui/material"; + +export default function Forbidden() { + return ( + + + 403 + + Forbidden + You do not have access to this page. + + ); +} diff --git a/src/router/router.tsx b/src/router/router.tsx index f7a7626..a99b947 100644 --- a/src/router/router.tsx +++ b/src/router/router.tsx @@ -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: , + element: ( + + + + ), children: [ - { index: true, element: }, + { index: true, element: }, { path: "users", children: [