diff --git a/WebApp/frontend/quiz-master/api/api-routes.ts b/WebApp/frontend/quiz-master/api/api-routes.ts index a68aadc7..2012572b 100644 --- a/WebApp/frontend/quiz-master/api/api-routes.ts +++ b/WebApp/frontend/quiz-master/api/api-routes.ts @@ -1,87 +1,91 @@ //#region Account API endpoints -const QUIZMASTER_ACCOUNT=`${process.env.QUIZMASTER_GATEWAY}/gateway/api/account` -const QUIZMASTER_ACCOUNT_GET=`${QUIZMASTER_ACCOUNT}` -const QUIZMASTER_ACCOUNT_POST=`${QUIZMASTER_ACCOUNT}/create` -const QUIZMASTER_ACCOUNT_POST_PARTIAL=`${QUIZMASTER_ACCOUNT}/create_partial` -const QUIZMASTER_ACCOUNT_DELETE=`${QUIZMASTER_ACCOUNT}/delete/` -const QUIZMASTER_ACCOUNT_PATCH=`${QUIZMASTER_ACCOUNT}/update/` -const QUIZMASTER_ACCOUNT_PASSWORD_RESET_POST= (id: Number) => {return `${QUIZMASTER_ACCOUNT}/${id}/update_password`} -const QUIZMASTER_ACCOUNT_POST_SET_ADMIN=`${QUIZMASTER_ACCOUNT}/set_admin/` +const QUIZMASTER_ACCOUNT = `${process.env.QUIZMASTER_GATEWAY}/gateway/api/account`; +const QUIZMASTER_ACCOUNT_GET = `${QUIZMASTER_ACCOUNT}`; +const QUIZMASTER_ACCOUNT_POST = `${QUIZMASTER_ACCOUNT}/create`; +const QUIZMASTER_ACCOUNT_POST_PARTIAL = `${QUIZMASTER_ACCOUNT}/create_partial`; +const QUIZMASTER_ACCOUNT_DELETE = `${QUIZMASTER_ACCOUNT}/delete/`; +const QUIZMASTER_ACCOUNT_PATCH = `${QUIZMASTER_ACCOUNT}/update/`; +const QUIZMASTER_ACCOUNT_PASSWORD_RESET_POST = (id: Number) => { + return `${QUIZMASTER_ACCOUNT}/${id}/update_password`; +}; +const QUIZMASTER_ACCOUNT_POST_SET_ADMIN = `${QUIZMASTER_ACCOUNT}/set_admin/`; //#endregion //#region Authentication API endpoints -const QUIZMASTER_AUTH=`${process.env.QUIZMASTER_GATEWAY}/gateway/api/auth` -const QUIZMASTER_AUTH_POST_LOGIN=`${QUIZMASTER_AUTH}/login` -const QUIZMASTER_AUTH_POST_PARTIAL_LOGIN=`${QUIZMASTER_AUTH}/partialLogin` -const QUIZMASTER_AUTH_POST_PARTIAL_LOGOUT=`${QUIZMASTER_AUTH}/logout` -const QUIZMASTER_AUTH_GET_COOKIE_INFO=`${QUIZMASTER_AUTH}/info` -const QUIZMASTER_AUTH_POST_SET_ADMIN=`${QUIZMASTER_AUTH}/set_admin/` +const QUIZMASTER_AUTH = `${process.env.QUIZMASTER_GATEWAY}/gateway/api/auth`; +const QUIZMASTER_AUTH_POST_LOGIN = `${QUIZMASTER_AUTH}/login`; +const QUIZMASTER_AUTH_POST_PARTIAL_LOGIN = `${QUIZMASTER_AUTH}/partialLogin`; +const QUIZMASTER_AUTH_POST_PARTIAL_LOGOUT = `${QUIZMASTER_AUTH}/logout`; +const QUIZMASTER_AUTH_GET_COOKIE_INFO = `${QUIZMASTER_AUTH}/info`; +const QUIZMASTER_AUTH_POST_SET_ADMIN = `${QUIZMASTER_AUTH}/set_admin/`; //#endregion -//#region Media API endpoints -const QUIZMASTER_MEDIA=`${process.env.QUIZMASTER_GATEWAY}/gateway/api/media` -const QUIZMASTER_MEDIA_POST=`${QUIZMASTER_MEDIA}/upload` -const QUIZMASTER_MEDIA_GET_MEDIAS=`${QUIZMASTER_MEDIA}/get_all_media` -const QUIZMASTER_MEDIA_GET_MEDIA=`${QUIZMASTER_MEDIA}/get_media/` -const QUIZMASTER_MEDIA_GET_DOWNLOAD=`${QUIZMASTER_MEDIA}/download_media/` -const QUIZMASTER_MEDIA_DELETE=`${QUIZMASTER_MEDIA}/delete_media/` +//#region Media API endpoints +const QUIZMASTER_MEDIA = `${process.env.QUIZMASTER_GATEWAY}/gateway/api/media`; +const QUIZMASTER_MEDIA_POST = `${QUIZMASTER_MEDIA}/upload`; +const QUIZMASTER_MEDIA_GET_MEDIAS = `${QUIZMASTER_MEDIA}/get_all_media`; +const QUIZMASTER_MEDIA_GET_MEDIA = `${QUIZMASTER_MEDIA}/get_media/`; +const QUIZMASTER_MEDIA_GET_DOWNLOAD = `${QUIZMASTER_MEDIA}/download_media/`; +const QUIZMASTER_MEDIA_DELETE = `${QUIZMASTER_MEDIA}/delete_media/`; //#endregion //#region Question API endpoints -const QUIZMASTER_QUESTION=`${process.env.QUIZMASTER_GATEWAY}/api/gateway/question` -const QUIZMASTER_QUESTION_GET_QUESTIONS=`${QUIZMASTER_QUESTION}/get_questions` -const QUIZMASTER_QUESTION_GET_QUESTION=`${QUIZMASTER_QUESTION}/get_question/` -const QUIZMASTER_QUESTION_POST=`${QUIZMASTER_QUESTION}/add_question` -const QUIZMASTER_QUESTION_DELETE=`${QUIZMASTER_QUESTION}/delete_question/` -const QUIZMASTER_QUESTION_PATCH=`${QUIZMASTER_QUESTION}/update_qustion/` +const QUIZMASTER_QUESTION = `${process.env.QUIZMASTER_GATEWAY}/api/gateway/question`; +const QUIZMASTER_QUESTION_GET_QUESTIONS = `${QUIZMASTER_QUESTION}/get_questions`; +const QUIZMASTER_QUESTION_GET_QUESTION = `${QUIZMASTER_QUESTION}/get_question/`; +const QUIZMASTER_QUESTION_POST = `${QUIZMASTER_QUESTION}/add_question`; +const QUIZMASTER_QUESTION_DELETE = `${QUIZMASTER_QUESTION}/delete_question/`; +const QUIZMASTER_QUESTION_PATCH = `${QUIZMASTER_QUESTION}/update_qustion/`; //#endregion -//#region Difficulty API endpoints -const QUIZMASTER_QDIFFICULTY=`${process.env.QUIZMASTER_GATEWAY}/api/gateway/question/difficulty` -const QUIZMASTER_QDIFFICULTY_GET_DIFFICULTIES=`${QUIZMASTER_QDIFFICULTY}/get_difficulties` -const QUIZMASTER_QDIFFICULTY_GET_DIFFICULTY=`${QUIZMASTER_QDIFFICULTY}/get_difficulty/` -const QUIZMASTER_QDIFFICULTY_POST=`${QUIZMASTER_QDIFFICULTY}/add_difficulty` -const QUIZMASTER_QDIFFICULTY_DELETE=`${QUIZMASTER_QDIFFICULTY}/delete_difficulty/` -const QUIZMASTER_QDIFFICULTY_PATCH=`${QUIZMASTER_QDIFFICULTY}/update_difficulty/` +//#region Difficulty API endpoints +const QUIZMASTER_QDIFFICULTY = `${process.env.QUIZMASTER_GATEWAY}/api/gateway/question/difficulty`; +const QUIZMASTER_QDIFFICULTY_GET_DIFFICULTIES = `${QUIZMASTER_QDIFFICULTY}/get_difficulties`; +const QUIZMASTER_QDIFFICULTY_GET_DIFFICULTY = `${QUIZMASTER_QDIFFICULTY}/get_difficulty/`; +const QUIZMASTER_QDIFFICULTY_POST = `${QUIZMASTER_QDIFFICULTY}/add_difficulty`; +const QUIZMASTER_QDIFFICULTY_DELETE = `${QUIZMASTER_QDIFFICULTY}/delete_difficulty/`; +const QUIZMASTER_QDIFFICULTY_PATCH = `${QUIZMASTER_QDIFFICULTY}/update_difficulty/`; //#endregion //#region Question Type API endpoints -const QUIZMASTER_QTYPE=`${process.env.QUIZMASTER_GATEWAY}/api/gateway/question/type` -const QUIZMASTER_QTYPE_GET_TYPES=`${QUIZMASTER_QTYPE}/get_all_type` -const QUIZMASTER_QTYPE_GET_TYPE=`${QUIZMASTER_QTYPE}/get_type/` +const QUIZMASTER_QTYPE = `${process.env.QUIZMASTER_GATEWAY}/api/gateway/question/type`; +const QUIZMASTER_QTYPE_GET_TYPES = `${QUIZMASTER_QTYPE}/get_all_type`; +const QUIZMASTER_QTYPE_GET_TYPE = `${QUIZMASTER_QTYPE}/get_type/`; //#endregion //#region Question Category API endpoints -const QUIZMASTER_QCATEGORY=`${process.env.QUIZMASTER_GATEWAY}/gateway/api/question/category` -const QUIZMASTER_QCATEGORY_GET_CATEGORIES=`${QUIZMASTER_QCATEGORY}/get_all_category` -const QUIZMASTER_QCATEGORY_GET_CATEGORY=`${QUIZMASTER_QCATEGORY}/get_category/` -const QUIZMASTER_QCATEGORY_POST=`${QUIZMASTER_QCATEGORY}/create_category` -const QUIZMASTER_QCATEGORY_DELETE=`${QUIZMASTER_QCATEGORY}/delete/` -const QUIZMASTER_QCATEGORY_PATCH=`${QUIZMASTER_QCATEGORY}/update_category/` +const QUIZMASTER_QCATEGORY = `${process.env.QUIZMASTER_GATEWAY}/gateway/api/question/category`; +const QUIZMASTER_QCATEGORY_GET_CATEGORIES = `${QUIZMASTER_QCATEGORY}/get_all_category`; +const QUIZMASTER_QCATEGORY_GET_CATEGORY = `${QUIZMASTER_QCATEGORY}/get_category/`; +const QUIZMASTER_QCATEGORY_POST = `${QUIZMASTER_QCATEGORY}/create_category`; +const QUIZMASTER_QCATEGORY_DELETE = `${QUIZMASTER_QCATEGORY}/delete/`; +const QUIZMASTER_QCATEGORY_PATCH = `${QUIZMASTER_QCATEGORY}/update_category/`; //#endregion //#region Question Detail API endpoints -const QUIZMASTER_QUESTIONDETAIL="question_detail" -const QUIZMASTER_QUESTIONDETAIL_GET_QUESTIONDETAILS=`${QUIZMASTER_QUESTIONDETAIL}/get_question_details` -const QUIZMASTER_QUESTIONDETAIL_GET_QUESTIONDETAIL=`${QUIZMASTER_QUESTIONDETAIL}/get_question_detail/` -const QUIZMASTER_QUESTIONDETAIL_POST=`${QUIZMASTER_QUESTIONDETAIL}/add_question_detail` -const QUIZMASTER_QUESTIONDETAIL_DELETE=`${QUIZMASTER_QUESTIONDETAIL}/delete_question_detail/` -const QUIZMASTER_QUESTIONDETAIL_PATCH=`${QUIZMASTER_QUESTIONDETAIL}/update_qustion_detail/` +const QUIZMASTER_QUESTIONDETAIL = "question_detail"; +const QUIZMASTER_QUESTIONDETAIL_GET_QUESTIONDETAILS = `${QUIZMASTER_QUESTIONDETAIL}/get_question_details`; +const QUIZMASTER_QUESTIONDETAIL_GET_QUESTIONDETAIL = `${QUIZMASTER_QUESTIONDETAIL}/get_question_detail/`; +const QUIZMASTER_QUESTIONDETAIL_POST = `${QUIZMASTER_QUESTIONDETAIL}/add_question_detail`; +const QUIZMASTER_QUESTIONDETAIL_DELETE = `${QUIZMASTER_QUESTIONDETAIL}/delete_question_detail/`; +const QUIZMASTER_QUESTIONDETAIL_PATCH = `${QUIZMASTER_QUESTIONDETAIL}/update_qustion_detail/`; //#endregion //#region Set API endpoints -const QUIZMASTER_SET=`${process.env.QUIZMASTER_GATEWAY}/gateway/api/set` -const QUIZMASTER_SET_POST=`${QUIZMASTER_SET}/create` -const QUIZMASTER_SET_POST_SUBMIT_ANSWER=`${QUIZMASTER_SET}/submitAnswer` -const QUIZMASTER_SET_GET_SETS=`${QUIZMASTER_SET}/all_set` -const QUIZMASTER_SET_GET_SET=`${QUIZMASTER_SET}/` -const QUIZMASTER_SET_GET_SETQUESTIONS=`${QUIZMASTER_SET}/all_question_set` -const QUIZMASTER_SET_GET_SETQUESTION=`${QUIZMASTER_SET}/get_question_set/` -const QUIZMASTER_SET_PUT=`${QUIZMASTER_SET}/update_set/` -const QUIZMASTER_SET_DELETE=`${QUIZMASTER_SET}/delete_set/` +const QUIZMASTER_SET = `${process.env.QUIZMASTER_GATEWAY}/gateway/api/set`; +const QUIZMASTER_SET_POST = `${QUIZMASTER_SET}/create`; +const QUIZMASTER_SET_POST_SUBMIT_ANSWER = `${QUIZMASTER_SET}/submitAnswer`; +const QUIZMASTER_SET_GET_SETS = `${QUIZMASTER_SET}/all_set`; +const QUIZMASTER_SET_GET_SET = `${QUIZMASTER_SET}/`; +const QUIZMASTER_SET_GET_SETQUESTIONS = `${QUIZMASTER_SET}/all_question_set`; +const QUIZMASTER_SET_GET_SETQUESTION = `${QUIZMASTER_SET}/get_question_set/`; +const QUIZMASTER_SET_PUT = `${QUIZMASTER_SET}/update_set/`; +const QUIZMASTER_SET_DELETE = `${QUIZMASTER_SET}/delete_set/`; //#endregion - +// #region Hub +const QUIZMASTER_GATEWAY_SESSION_HUB = `${process.env.QUIZMASTER_GATEWAY}/gateway/hub/session`; +//#endregion export { QUIZMASTER_ACCOUNT_GET, QUIZMASTER_ACCOUNT_POST, @@ -131,4 +135,5 @@ export { QUIZMASTER_SET_POST_SUBMIT_ANSWER, QUIZMASTER_SET_PUT, QUIZMASTER_SET_DELETE, -}; \ No newline at end of file + QUIZMASTER_GATEWAY_SESSION_HUB, +}; diff --git a/WebApp/frontend/quiz-master/app/auth/[authPage]/page.tsx b/WebApp/frontend/quiz-master/app/auth/[authPage]/page.tsx index 7a160bb1..4991d52b 100644 --- a/WebApp/frontend/quiz-master/app/auth/[authPage]/page.tsx +++ b/WebApp/frontend/quiz-master/app/auth/[authPage]/page.tsx @@ -16,7 +16,8 @@ export default function Page({ const searchParams = useSearchParams(); const authPage = params.authPage || "login"; - const callbackUrl = searchParams.get("callbackUrl") || "/dashboard"; + const callbackUrl = + searchParams.get("callbackUrl") || "http://localhost:3000/dashboard"; const { status } = useSession(); if (status === "loading") { diff --git a/WebApp/frontend/quiz-master/app/quiz-rooms/create-quiz-room/page.tsx b/WebApp/frontend/quiz-master/app/quiz-rooms/create-quiz-room/page.tsx new file mode 100644 index 00000000..1a528c71 --- /dev/null +++ b/WebApp/frontend/quiz-master/app/quiz-rooms/create-quiz-room/page.tsx @@ -0,0 +1,229 @@ +"use client"; + +import { + Anchor, + Breadcrumbs, + Button, + Checkbox, + InputLabel, + LoadingOverlay, + TextInput, + Tooltip, +} from "@mantine/core"; +import { useForm } from "@mantine/form"; +import { useDisclosure } from "@mantine/hooks"; +import { useCallback } from "react"; +import styles from "@/styles/input.module.css"; +import { CreateQuizRoom, RoomOptionTypes } from "@/lib/definitions/quizRoom"; +import { validatorFactory } from "@/lib/validation/creators"; +import { isRequired } from "@/lib/validation/regex"; +import { validate } from "@/lib/validation/validate"; +import { InformationCircleIcon } from "@heroicons/react/24/outline"; + +const items = [ + { label: "All", href: "/quiz-rooms" }, + { label: "Create a Quiz Room", href: "#" }, + { label: "", href: "#" }, +].map((item, index) => ( + +

{item.label}

+
+)); + +const maxChar = validatorFactory(100, "max"); +const minChar = validatorFactory(3, "min"); + +export default function Page() { + const [visible, { close, open }] = useDisclosure(false); + + const form = useForm({ + initialValues: { + roomName: "", + questionSets: [], + roomOptions: [ + "mode:normal", + "displaytop10only:false", + "allowjoinonquizstarted:false", + "allowreconnect:false", + "showLeaderboardEachRound:false", + ], + }, + clearInputErrorOnChange: true, + validateInputOnChange: true, + validate: { + roomName: (value, values) => { + const validators = [isRequired, minChar, maxChar]; + return validate(value, validators); + }, + }, + }); + + // Creates new quiz room + const handelSubmit = useCallback(async () => { + open(); + + close(); + }, [form.values]); + + // toggles the mode for quizRoom + const toggleMode = useCallback(() => { + var options = form.values.roomOptions; + const modeIndex = options.findIndex( + (o) => o === "mode:normal" || o === "mode:elimination" + ); + if (modeIndex !== -1) { + const deleted = options.splice(modeIndex, 1); + if (deleted[0] === "mode:normal") { + options = [...options, "mode:elimination"]; + } else { + options = [...options, "mode:normal"]; + } + } else { + options = [...options, "mode:normal"]; + } + form.setFieldValue("roomOptions", options); + }, [form.values]); + + const toogleOptions = useCallback( + ( + option: + | "showLeaderboardEachRound" + | "displaytop10only" + | "allowreconnect" + | "allowjoinonquizstarted" + ) => { + var options = form.values.roomOptions; + var optionIndex = options.findIndex((o) => o.startsWith(option)); + if (optionIndex !== -1) { + const deleted = options.splice(optionIndex, 1); + if (deleted[0].endsWith("false")) { + options = [...options, `${option}:true`]; + } else { + options = [...options, `${option}:false`]; + } + } else { + options = [...options, `${option}:true`]; + } + form.setFieldValue("roomOptions", options); + }, + [form.values] + ); + + return ( +
+ {items} +
+

Create New Question

+
+
{ + handelSubmit(); + })} + onReset={() => form.reset()} + > + + +
+ Mode +
+ { + toggleMode(); + }} + /> +
+ { + toggleMode(); + }} + /> + + + +
+
+
+
+ { + toogleOptions("allowreconnect"); + }} + /> + { + toogleOptions("showLeaderboardEachRound"); + }} + /> + { + toogleOptions("allowjoinonquizstarted"); + }} + checked={form.values.roomOptions.includes( + "allowjoinonquizstarted:true" + )} + /> + { + toogleOptions("displaytop10only"); + }} + checked={form.values.roomOptions.includes( + "displaytop10only:true" + )} + /> +
+ +
+ + +
+ +
+ ); +} diff --git a/WebApp/frontend/quiz-master/app/quiz-rooms/layout.tsx b/WebApp/frontend/quiz-master/app/quiz-rooms/layout.tsx new file mode 100644 index 00000000..ab82089e --- /dev/null +++ b/WebApp/frontend/quiz-master/app/quiz-rooms/layout.tsx @@ -0,0 +1,21 @@ +"use client"; + +import PageHeader from "@/components/Commons/headers/PageHeader"; +import SideNav from "@/components/Commons/navbars/sidenav"; +import { Suspense } from "react"; +import Loading from "./loading"; +import ErrorContainer from "@/components/pages/ErrorContainer"; + +export default function Layout({ children }: { children: React.ReactNode }) { + return ( +
+ +
+ Quiz Rooms + + }>{children} + +
+
+ ); +} diff --git a/WebApp/frontend/quiz-master/app/quiz-rooms/loading.tsx b/WebApp/frontend/quiz-master/app/quiz-rooms/loading.tsx new file mode 100644 index 00000000..00f57b42 --- /dev/null +++ b/WebApp/frontend/quiz-master/app/quiz-rooms/loading.tsx @@ -0,0 +1,5 @@ +import LoadingPage from "@/components/pages/Loading"; + +export default function Loading() { + return ; +} diff --git a/WebApp/frontend/quiz-master/app/quiz-rooms/page.tsx b/WebApp/frontend/quiz-master/app/quiz-rooms/page.tsx new file mode 100644 index 00000000..5af24b2b --- /dev/null +++ b/WebApp/frontend/quiz-master/app/quiz-rooms/page.tsx @@ -0,0 +1,141 @@ +"use client"; + +import QuizRoomTable from "@/components/Commons/tables/QuizRoomTable"; +import { QuizRoom } from "@/lib/definitions/quizRoom"; +import { notification } from "@/lib/notifications"; +import { QuizRoomPageData } from "@/lib/pagesData"; +import { useConnectionStore } from "@/store/ConnectionStore"; +import { useQuizRoomsStore } from "@/store/QuizRoomStore"; +import { PlusIcon } from "@heroicons/react/24/outline"; +import { Anchor, Breadcrumbs } from "@mantine/core"; +import Pagination from "@/components/Commons/Pagination"; +import { useDebouncedValue, useDisclosure } from "@mantine/hooks"; +import Link from "next/link"; +import { useCallback, useEffect, useState } from "react"; +import { useForm } from "@mantine/form"; +import { ResourceParameter } from "@/lib/definitions"; +import SearchField from "@/components/Commons/SearchField"; + +const items = [ + { label: "All", href: "#" }, + { label: "", href: "#" }, +].map((item, index) => ( + +

{item.label}

+
+)); + +export default function Page() { + const [visible, { close, open }] = useDisclosure(true); + + const [searchQuery2, setSearchQuery] = useState(""); + const [debouncedSearchQuery] = useDebouncedValue(searchQuery2, 200); + + const { connection, init } = useConnectionStore(); + const { + quizRooms, + pageNumber, + pageSize, + searchQuery, + setQuizRooms, + getTotalPages, + setPagination, + } = useQuizRoomsStore(); + const [totalPages, setTotalPages] = useState(); + + const form = useForm({ + initialValues: { + pageSize: "10", + searchQuery: "", + pageNumber: 1, + }, + }); + + const startConnection = useCallback(async () => { + if (connection && connection.state !== "Connected") { + await connection.start(); + const token = localStorage.getItem("token"); + connection.invoke("Login", token); + connection.on("notif", (data) => { + console.log("notif", data); + notification({ type: "success", title: data }); + }); + connection.on("auth_data", (data) => { + console.log("auth_data", data); + + notification({ type: "success", title: data }); + }); + connection.on("QuizRooms", (data: QuizRoom[]) => { + data.forEach((data) => { + data.dateCreated = new Date(data.dateCreated); + data.dateUpdated = data.dateUpdated + ? new Date(data.dateUpdated) + : undefined; + }); + setQuizRooms(data); + }); + getAllRooms(); + } + }, [connection]); + + const getAllRooms = useCallback(async () => { + if (connection) { + await connection.invoke("GetAllRooms"); + } + }, [connection]); + + useEffect(() => { + if (!connection) { + init(); + } + + try { + startConnection(); + } catch (error) { + notification({ + type: "error", + title: "Unable to communicate to the hub", + }); + } + }, [connection]); + + useEffect(() => { + setTotalPages(getTotalPages()); + }, [quizRooms, pageNumber, pageSize, searchQuery]); + + useEffect(() => { + setPagination({ + pageNumber: form.values.pageNumber, + pageSize: parseInt(form.values.pageSize), + searchQuery: debouncedSearchQuery, + }); + }, [form.values, debouncedSearchQuery]); + + return ( +
+ {items} +
+ + +

Create Quiz Room

+ +
+ +
+ { + setSearchQuery(e.target.value); + }} + onKeyDown={(e) => {}} + /> +
+
+ + +
+ ); +} diff --git a/WebApp/frontend/quiz-master/components/Commons/Pagination.tsx b/WebApp/frontend/quiz-master/components/Commons/Pagination.tsx index 711ba7b5..cc2a51d2 100644 --- a/WebApp/frontend/quiz-master/components/Commons/Pagination.tsx +++ b/WebApp/frontend/quiz-master/components/Commons/Pagination.tsx @@ -11,9 +11,11 @@ const pageSizes = ["10", "20", "30", "40", "50"]; export default function CustomPagination({ form, metadata, + totalPages, }: { form: UseFormReturnType; - metadata: PaginationMetadata | undefined; + metadata?: PaginationMetadata; + totalPages?: number; }) { return (
@@ -31,7 +33,7 @@ export default function CustomPagination({ />
diff --git a/WebApp/frontend/quiz-master/components/Commons/tables/QuizRoomTable.tsx b/WebApp/frontend/quiz-master/components/Commons/tables/QuizRoomTable.tsx new file mode 100644 index 00000000..6187c15c --- /dev/null +++ b/WebApp/frontend/quiz-master/components/Commons/tables/QuizRoomTable.tsx @@ -0,0 +1,155 @@ +import { QuizRoom } from "@/lib/definitions/quizRoom"; +import { Checkbox, LoadingOverlay, Table, Text } from "@mantine/core"; +import { useCallback, useEffect, useState } from "react"; + +import { notification } from "@/lib/notifications"; +import { useQuizRoomsStore } from "@/store/QuizRoomStore"; +import PromptModal from "../modals/PromptModal"; + +export default function QuizRoomTable() { + const { quizRooms, getPaginatedRooms, pageNumber, pageSize, searchQuery } = + useQuizRoomsStore(); + const [paginatedRooms, setPaginatedRooms] = useState([]); + const [deleteQuizRoom, setDeleteQuizRoom] = useState(); + const [selectedRows, setSelectedRows] = useState([]); + const [viewQuizRoom, setViewQuizRoom] = useState(); + + const handelDelete = useCallback(async () => { + if (deleteQuizRoom) { + try { + // const res = await removeQuizRoom({ id: deleteQuizRoom.id }); + // if (res.type === "success") { + // notification({ type: "success", title: res.message }); + // } else { + // notification({ type: "error", title: res.message }); + // } + setDeleteQuizRoom(undefined); + } catch (error) { + notification({ type: "error", title: "Something went wrong" }); + } + } + }, [deleteQuizRoom]); + + useEffect(() => { + setPaginatedRooms( + getPaginatedRooms({ pageNumber, pageSize, searchQuery }) + ); + }, [quizRooms, pageNumber, pageSize, searchQuery]); + + const rows = + quizRooms && + paginatedRooms.map((quizRoom) => ( + + + + setSelectedRows( + event.currentTarget.checked + ? [...selectedRows, quizRoom.id] + : selectedRows.filter( + (id) => id !== quizRoom.id + ) + ) + } + /> + + setViewQuizRoom(quizRoom)} + > + {quizRoom.qRoomDesc} + + {quizRoom.qRoomPin} + {quizRoom.dateCreated.toDateString()} + + {quizRoom.dateUpdated?.toDateString() || "null"} + + "Question sets count here" + + "Questions count in here" + + )); + + return ( +
+ + + + + + setSelectedRows( + event.currentTarget.checked && quizRooms + ? paginatedRooms.map( + (quizRoom) => quizRoom.id + ) + : [] + ) + } + /> + + Room Name + Pin + Created on + Updated on + Sets + Questions + + + + {quizRooms === undefined ? ( + + +
+ +
+
+
+ ) : quizRooms.length === 0 ? ( + + +
+ No Quiz Rooms found. +
+
+
+ ) : ( + rows + )} +
+
+ + Are you sure want to delete. + {deleteQuizRoom?.qRoomDesc} +
+ } + action="Delete" + onConfirm={handelDelete} + opened={deleteQuizRoom ? true : false} + onClose={() => { + setDeleteQuizRoom(undefined); + }} + title="Delete QuizRoom" + /> + + ); +} diff --git a/WebApp/frontend/quiz-master/lib/definitions/quizRoom.ts b/WebApp/frontend/quiz-master/lib/definitions/quizRoom.ts new file mode 100644 index 00000000..ba2022a8 --- /dev/null +++ b/WebApp/frontend/quiz-master/lib/definitions/quizRoom.ts @@ -0,0 +1,29 @@ +export type RoomOptionTypes = ( + | "mode:normal" + | "mode:elimination" + | "showLeaderboardEachRound:true" + | "showLeaderboardEachRound:false" + | "displaytop10only:true" + | "displaytop10only:false" + | "allowreconnect:true" + | "allowreconnect:false" + | "allowjoinonquizstarted:true" + | "allowjoinonquizstarted:false" +)[]; + +export interface CreateQuizRoom { + roomName: string; + questionSets: number[]; + roomOptions: RoomOptionTypes; +} + +export interface QuizRoom extends CreateQuizRoom { + id: number; + roomName: ""; + qRoomDesc: string; + qRoomPin: number; + dateCreated: Date; + dateUpdated?: Date; + createdByUserId: number; + updatedByUserId?: number; +} diff --git a/WebApp/frontend/quiz-master/lib/validation/creators.ts b/WebApp/frontend/quiz-master/lib/validation/creators.ts index f61e183a..acde7e63 100644 --- a/WebApp/frontend/quiz-master/lib/validation/creators.ts +++ b/WebApp/frontend/quiz-master/lib/validation/creators.ts @@ -1,33 +1,41 @@ import { InputValidator } from "./regex"; -const minimumCharacter = (minLen: number, error?: string) : InputValidator => { +const minimumCharacter = (minLen: number, error?: string): InputValidator => { return { - ErrorMessage: error? error: `Must contain at least ${minLen} characters.`, - Regex: new RegExp(`.{${minLen},}`) - } -} + ErrorMessage: error + ? error + : `Must contain at least ${minLen} characters.`, + Regex: new RegExp(`.{${minLen},}`), + }; +}; -const maximumCharater = (maxLen: number, error?: string) : InputValidator => { +const maximumCharater = (maxLen: number, error?: string): InputValidator => { return { - ErrorMessage: error? error: `Must contain at least ${maxLen} characters.`, - Regex: new RegExp(`.{0,${maxLen}}`) - } -} + ErrorMessage: error + ? error + : `Must contain at least ${maxLen} characters.`, + Regex: new RegExp(`.{0,${maxLen}}`), + }; +}; -const emptyValidator = () : InputValidator=> { +const emptyValidator = (): InputValidator => { return { ErrorMessage: "", - Regex: new RegExp(".*") - } -} + Regex: new RegExp(".*"), + }; +}; -export const validatorFactory = (len: number, type: "min" | "max", error?: string) => { +export const validatorFactory = ( + len: number, + type: "min" | "max", + error?: string +) => { switch (type) { case "max": - return maximumCharater(len, error) + return maximumCharater(len, error); case "min": - return minimumCharacter(len, error) - default: - return emptyValidator() + return minimumCharacter(len, error); + default: + return emptyValidator(); } -} \ No newline at end of file +}; diff --git a/WebApp/frontend/quiz-master/package-lock.json b/WebApp/frontend/quiz-master/package-lock.json index af188344..99cbde61 100644 --- a/WebApp/frontend/quiz-master/package-lock.json +++ b/WebApp/frontend/quiz-master/package-lock.json @@ -14,6 +14,7 @@ "@mantine/hooks": "^7.1.5", "@mantine/modals": "^7.1.5", "@mantine/notifications": "^7.2.2", + "@microsoft/signalr": "^8.0.0", "@mui/icons-material": "^5.14.18", "@types/node": "^20", "@types/react": "^18", @@ -1461,6 +1462,38 @@ "react": "^18.2.0" } }, + "node_modules/@microsoft/signalr": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-8.0.0.tgz", + "integrity": "sha512-K/wS/VmzRWePCGqGh8MU8OWbS1Zvu7DG7LSJS62fBB8rJUXwwj4axQtqrAAwKGUZHQF6CuteuQR9xMsVpM2JNA==", + "dependencies": { + "abort-controller": "^3.0.0", + "eventsource": "^2.0.2", + "fetch-cookie": "^2.0.3", + "node-fetch": "^2.6.7", + "ws": "^7.4.5" + } + }, + "node_modules/@microsoft/signalr/node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@mui/base": { "version": "5.0.0-beta.24", "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.24.tgz", @@ -2545,6 +2578,17 @@ "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", "dev": true }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -4363,6 +4407,22 @@ "node": ">=0.10.0" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -4469,6 +4529,15 @@ "bser": "2.1.1" } }, + "node_modules/fetch-cookie": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-2.1.0.tgz", + "integrity": "sha512-39+cZRbWfbibmj22R2Jy6dmTbAWC+oqun1f1FzQaNurkPDUP4C38jpeZbiXCR88RKRVDp8UcDrbFXkNhN+NjYg==", + "dependencies": { + "set-cookie-parser": "^2.4.8", + "tough-cookie": "^4.0.0" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -6896,6 +6965,44 @@ } } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -7657,8 +7764,7 @@ "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" }, "node_modules/punycode": { "version": "2.3.0", @@ -7687,8 +7793,7 @@ "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" }, "node_modules/queue-microtask": { "version": "1.2.3", @@ -8012,8 +8117,7 @@ "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, "node_modules/resolve": { "version": "1.22.8", @@ -8208,6 +8312,11 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/set-cookie-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" + }, "node_modules/set-function-length": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", @@ -8754,7 +8863,6 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", - "dev": true, "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", @@ -9020,7 +9128,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true, "engines": { "node": ">= 4.0.0" } @@ -9067,7 +9174,6 @@ "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dev": true, "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" diff --git a/WebApp/frontend/quiz-master/package.json b/WebApp/frontend/quiz-master/package.json index 62a4b19b..8f640acd 100644 --- a/WebApp/frontend/quiz-master/package.json +++ b/WebApp/frontend/quiz-master/package.json @@ -17,6 +17,7 @@ "@mantine/hooks": "^7.1.5", "@mantine/modals": "^7.1.5", "@mantine/notifications": "^7.2.2", + "@microsoft/signalr": "^8.0.0", "@mui/icons-material": "^5.14.18", "@types/node": "^20", "@types/react": "^18", diff --git a/WebApp/frontend/quiz-master/store/ConnectionStore.tsx b/WebApp/frontend/quiz-master/store/ConnectionStore.tsx new file mode 100644 index 00000000..b7a02c91 --- /dev/null +++ b/WebApp/frontend/quiz-master/store/ConnectionStore.tsx @@ -0,0 +1,43 @@ +import { create } from "zustand"; +import { + HubConnection, + HubConnectionBuilder, + LogLevel, +} from "@microsoft/signalr"; +import { QUIZMASTER_GATEWAY_SESSION_HUB } from "@/api/api-routes"; + +interface IConnectionStore { + connection?: HubConnection; + setConnection: () => void; + init: () => void; + startConnection: () => void; + loginConnection: (token: string) => void; +} + +export const useConnectionStore = create((set, get) => ({ + connection: undefined, + setConnection: () => {}, + init: () => { + if (!get().connection) { + console.log("Creating connection to gateway session hub."); + set({ + connection: new HubConnectionBuilder() + .withUrl(QUIZMASTER_GATEWAY_SESSION_HUB) + .configureLogging(LogLevel.Information) + .build(), + }); + } + }, + startConnection: () => { + if (get().connection !== undefined) { + console.log("Staring connection..."); + get().connection?.start(); + console.log("Connection started"); + } + }, + loginConnection: (token: string) => { + if (get().connection !== undefined) { + get().connection?.invoke("login", token); + } + }, +})); diff --git a/WebApp/frontend/quiz-master/store/QuizRoomStore.tsx b/WebApp/frontend/quiz-master/store/QuizRoomStore.tsx new file mode 100644 index 00000000..dacf4d95 --- /dev/null +++ b/WebApp/frontend/quiz-master/store/QuizRoomStore.tsx @@ -0,0 +1,105 @@ +import { QuestionDifficulty } from "@/lib/definitions"; +import { QuizRoom } from "@/lib/definitions/quizRoom"; +import { create } from "zustand"; + +interface IQuizRoomsStore { + quizRooms?: QuizRoom[]; + getPaginatedRooms: ({ + pageNumber, + pageSize, + searchQuery, + }: { + pageNumber: number; + pageSize: number; + searchQuery?: string; + }) => QuizRoom[]; + setQuizRooms: (fetchedQuizRooms: QuizRoom[]) => void; + pageNumber: number; + pageSize: number; + searchQuery?: string; + getTotalPages: () => number; + setPagination: ({ + pageNumber, + pageSize, + searchQuery, + }: { + pageNumber: number; + pageSize: number; + searchQuery?: string; + }) => void; +} + +export const useQuizRoomsStore = create((set, get) => ({ + quizRooms: undefined, + setQuizRooms: (fetchedQuizRooms: QuizRoom[]) => { + set({ + quizRooms: fetchedQuizRooms, + }); + }, + getPaginatedRooms: ({ + pageNumber, + pageSize, + searchQuery, + }: { + pageNumber: number; + pageSize: number; + searchQuery?: string; + }) => { + return get().quizRooms + ? get() + .quizRooms!.filter((qR) => + searchQuery + ? qR.qRoomDesc + .trim() + .toLowerCase() + .includes(searchQuery) + : true + ) + .slice( + (pageNumber - 1) * pageSize, + (pageNumber - 1) * pageSize + pageSize + ) + : []; + }, + pageNumber: 1, + pageSize: 10, + searchQuery: "", + getTotalPages: () => + get().quizRooms !== undefined + ? Math.ceil( + get().quizRooms!.filter((qR) => + get().searchQuery + ? qR.qRoomDesc + .trim() + .toLowerCase() + .includes(get().searchQuery!) + : true + ).length / get().pageSize + ) + : 0, + setPagination: ({ + pageNumber, + pageSize, + searchQuery, + }: { + pageNumber: number; + pageSize: number; + searchQuery?: string; + }) => { + if ( + get().quizRooms === undefined || + pageNumber > Math.ceil(get().quizRooms!.length / pageSize) + ) { + return; + } + + set({ + pageNumber: + searchQuery !== undefined || searchQuery !== "" + ? 1 + : pageNumber, + pageSize: pageSize, + searchQuery: searchQuery, + }); + }, +}));