diff --git a/scripts/updatePrivateFieldExams.js b/scripts/updatePrivateFieldExams.js new file mode 100644 index 00000000..0fba9d8f --- /dev/null +++ b/scripts/updatePrivateFieldExams.js @@ -0,0 +1,51 @@ +import dotenv from "dotenv"; +dotenv.config(); +import { MongoClient } from "mongodb"; +const uri = process.env.MONGODB_URI || ""; +const options = { + maxPoolSize: 10, +}; +const dbName = process.env.MONGODB_DB; // change this to prod db when needed +async function migrateData() { + const MODULE_ARRAY = ["reading", "listening", "writing", "speaking", "level"]; + const client = new MongoClient(uri, options); + + try { + await client.connect(); + console.log("Connected to MongoDB"); + if (!process.env.MONGODB_DB) { + throw new Error("Missing env var: MONGODB_DB"); + } + const db = client.db(dbName); + for (const string of MODULE_ARRAY) { + const collection = db.collection(string); + const result = await collection.updateMany( + { private: { $exists: false } }, + { $set: { access: "public" } } + ); + const result2 = await collection.updateMany( + { private: true }, + { $set: { access: "private" }, $unset: { private: "" } } + ); + const result1 = await collection.updateMany( + { private: { $exists: true } }, + { $set: { access: "public" } } + ); + console.log( + `Updated ${ + result.modifiedCount + result1.modifiedCount + } documents to "access: public" in ${string}` + ); + console.log( + `Updated ${result2.modifiedCount} documents to "access: private" and removed private var in ${string}` + ); + } + console.log("Migration completed successfully!"); + } catch (error) { + console.error("Migration failed:", error); + } finally { + await client.close(); + console.log("MongoDB connection closed."); + } +} +//migrateData(); // uncomment to run the migration diff --git a/src/components/Diagnostic.tsx b/src/components/Diagnostic.tsx index 1ca52b73..af022262 100644 --- a/src/components/Diagnostic.tsx +++ b/src/components/Diagnostic.tsx @@ -1,17 +1,12 @@ -import {infoButtonStyle} from "@/constants/buttonStyles"; -import {Module} from "@/interfaces"; import {User} from "@/interfaces/user"; import useExamStore from "@/stores/exam"; -import {getExam, getExamById} from "@/utils/exams"; +import {getExam} from "@/utils/exams"; import {MODULE_ARRAY} from "@/utils/moduleUtils"; -import {writingMarking} from "@/utils/score"; -import {Menu} from "@headlessui/react"; import axios from "axios"; import clsx from "clsx"; -import {capitalize} from "lodash"; import {useRouter} from "next/router"; -import {useEffect, useState} from "react"; -import {BsBook, BsChevronDown, BsHeadphones, BsMegaphone, BsPen, BsQuestionSquare} from "react-icons/bs"; +import { useState} from "react"; +import { BsQuestionSquare} from "react-icons/bs"; import {toast} from "react-toastify"; import Button from "./Low/Button"; import ModuleLevelSelector from "./Medium/ModuleLevelSelector"; diff --git a/src/components/ExamEditor/SettingsEditor/level.tsx b/src/components/ExamEditor/SettingsEditor/level.tsx index d192c629..02e97fd6 100644 --- a/src/components/ExamEditor/SettingsEditor/level.tsx +++ b/src/components/ExamEditor/SettingsEditor/level.tsx @@ -38,7 +38,7 @@ const LevelSettings: React.FC = () => { difficulty, sections, minTimer, - isPrivate, + access, } = useExamEditorStore(state => state.modules[currentModule]); const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState( @@ -200,7 +200,7 @@ const LevelSettings: React.FC = () => { module: "level", id: title, difficulty, - private: isPrivate, + access, }; const result = await axios.post('/api/exam/level', exam); @@ -243,7 +243,7 @@ const LevelSettings: React.FC = () => { isDiagnostic: false, variant: undefined, difficulty, - private: isPrivate, + access, } as LevelExam); setExerciseIndex(0); setQuestionIndex(0); diff --git a/src/components/ExamEditor/SettingsEditor/listening/index.tsx b/src/components/ExamEditor/SettingsEditor/listening/index.tsx index 0e1e733e..c1a5fa7c 100644 --- a/src/components/ExamEditor/SettingsEditor/listening/index.tsx +++ b/src/components/ExamEditor/SettingsEditor/listening/index.tsx @@ -27,7 +27,7 @@ const ListeningSettings: React.FC = () => { difficulty, sections, minTimer, - isPrivate, + access, instructionsState } = useExamEditorStore(state => state.modules[currentModule]); @@ -144,7 +144,7 @@ const ListeningSettings: React.FC = () => { id: title, variant: sections.length === 4 ? "full" : "partial", difficulty, - private: isPrivate, + access, instructions: instructionsURL }; @@ -191,7 +191,7 @@ const ListeningSettings: React.FC = () => { isDiagnostic: false, variant: sections.length === 4 ? "full" : "partial", difficulty, - private: isPrivate, + access, instructions: instructionsState.currentInstructionsURL } as ListeningExam); setExerciseIndex(0); diff --git a/src/components/ExamEditor/SettingsEditor/reading/index.tsx b/src/components/ExamEditor/SettingsEditor/reading/index.tsx index 45b00931..30424552 100644 --- a/src/components/ExamEditor/SettingsEditor/reading/index.tsx +++ b/src/components/ExamEditor/SettingsEditor/reading/index.tsx @@ -15,135 +15,137 @@ import ReadingComponents from "./components"; import { getExamById } from "@/utils/exams"; const ReadingSettings: React.FC = () => { - const router = useRouter(); + const router = useRouter(); - const { - setExam, - setExerciseIndex, - setPartIndex, - setQuestionIndex, - setBgColor, - } = usePersistentExamStore(); + const { + setExam, + setExerciseIndex, + setPartIndex, + setQuestionIndex, + setBgColor, + } = usePersistentExamStore(); - const { currentModule, title } = useExamEditorStore(); - const { - focusedSection, - difficulty, - sections, - minTimer, - isPrivate, - type, - } = useExamEditorStore(state => state.modules[currentModule]); + const { currentModule, title } = useExamEditorStore(); + const { focusedSection, difficulty, sections, minTimer, access, type } = + useExamEditorStore((state) => state.modules[currentModule]); - const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState( - currentModule, - focusedSection - ); + const { localSettings, updateLocalAndScheduleGlobal } = + useSettingsState(currentModule, focusedSection); - const currentSection = sections.find((section) => section.sectionId == focusedSection)?.state as ReadingPart; + const currentSection = sections.find( + (section) => section.sectionId == focusedSection + )?.state as ReadingPart; + const defaultPresets: Option[] = [ + { + label: "Preset: Reading Passage 1", + value: + "Welcome to {part} of the {label}. You will read texts relating to everyday topics and situations. These may include advertisements, brochures, manuals, or official documents. Answer questions that test your ability to locate specific information and understand main ideas.", + }, + { + label: "Preset: Reading Passage 2", + value: + "Welcome to {part} of the {label}. You will read texts dealing with general interest topics that may include news articles, company policies, or workplace documents. Answer questions testing your understanding of main ideas, specific details, and the author's views.", + }, + { + label: "Preset: Reading Passage 3", + value: + "Welcome to {part} of the {label}. You will read longer academic texts that may include journal articles, academic essays, or research papers. Answer questions testing your ability to understand complex arguments, identify key points, and follow the development of ideas.", + }, + ]; - const defaultPresets: Option[] = [ - { - label: "Preset: Reading Passage 1", - value: "Welcome to {part} of the {label}. You will read texts relating to everyday topics and situations. These may include advertisements, brochures, manuals, or official documents. Answer questions that test your ability to locate specific information and understand main ideas." - }, - { - label: "Preset: Reading Passage 2", - value: "Welcome to {part} of the {label}. You will read texts dealing with general interest topics that may include news articles, company policies, or workplace documents. Answer questions testing your understanding of main ideas, specific details, and the author's views." - }, - { - label: "Preset: Reading Passage 3", - value: "Welcome to {part} of the {label}. You will read longer academic texts that may include journal articles, academic essays, or research papers. Answer questions testing your ability to understand complex arguments, identify key points, and follow the development of ideas." - } - ]; + const canPreviewOrSubmit = sections.some( + (s) => + (s.state as ReadingPart).exercises && + (s.state as ReadingPart).exercises.length > 0 + ); - const canPreviewOrSubmit = sections.some( - (s) => (s.state as ReadingPart).exercises && (s.state as ReadingPart).exercises.length > 0 - ); - - const submitReading = () => { - if (title === "") { - toast.error("Enter a title for the exam!"); - return; - } - const exam: ReadingExam = { - parts: sections.map((s) => { - const exercise = s.state as ReadingPart; - return { - ...exercise, - intro: localSettings.currentIntro, - category: localSettings.category - }; - }), - isDiagnostic: true, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed. - minTimer, - module: "reading", - id: title, - variant: sections.length === 3 ? "full" : "partial", - difficulty, - private: isPrivate, - type: type! + const submitReading = () => { + if (title === "") { + toast.error("Enter a title for the exam!"); + return; + } + const exam: ReadingExam = { + parts: sections.map((s) => { + const exercise = s.state as ReadingPart; + return { + ...exercise, + intro: localSettings.currentIntro, + category: localSettings.category, }; + }), + isDiagnostic: true, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed. + minTimer, + module: "reading", + id: title, + variant: sections.length === 3 ? "full" : "partial", + difficulty, + access, + type: type!, + }; - axios.post(`/api/exam/reading`, exam) - .then((result) => { - playSound("sent"); - // Successfully submitted exam - if (result.status === 200) { - toast.success(result.data.message); - } else if (result.status === 207) { - toast.warning(result.data.message); - } - }) - .catch((error) => { - console.log(error); - toast.error(error.response.data.error || "Something went wrong while submitting, please try again later."); - }) - } + axios + .post(`/api/exam/reading`, exam) + .then((result) => { + playSound("sent"); + // Successfully submitted exam + if (result.status === 200) { + toast.success(result.data.message); + } else if (result.status === 207) { + toast.warning(result.data.message); + } + }) + .catch((error) => { + console.log(error); + toast.error( + error.response.data.error || + "Something went wrong while submitting, please try again later." + ); + }); + }; - const preview = () => { - setExam({ - parts: sections.map((s) => { - const exercises = s.state as ReadingPart; - return { - ...exercises, - intro: s.settings.currentIntro, - category: s.settings.category - }; - }), - minTimer, - module: "reading", - id: title, - isDiagnostic: false, - variant: undefined, - difficulty, - private: isPrivate, - type: type! - } as ReadingExam); - setExerciseIndex(0); - setQuestionIndex(0); - setPartIndex(0); - setBgColor("bg-white"); - openDetachedTab("popout?type=Exam&module=reading", router) - } + const preview = () => { + setExam({ + parts: sections.map((s) => { + const exercises = s.state as ReadingPart; + return { + ...exercises, + intro: s.settings.currentIntro, + category: s.settings.category, + }; + }), + minTimer, + module: "reading", + id: title, + isDiagnostic: false, + variant: undefined, + difficulty, + access: access, + type: type!, + } as ReadingExam); + setExerciseIndex(0); + setQuestionIndex(0); + setPartIndex(0); + setBgColor("bg-white"); + openDetachedTab("popout?type=Exam&module=reading", router); + }; - return ( - - - - ); + return ( + + + + ); }; export default ReadingSettings; diff --git a/src/components/ExamEditor/SettingsEditor/speaking/index.tsx b/src/components/ExamEditor/SettingsEditor/speaking/index.tsx index 513b30b1..297e864c 100644 --- a/src/components/ExamEditor/SettingsEditor/speaking/index.tsx +++ b/src/components/ExamEditor/SettingsEditor/speaking/index.tsx @@ -30,7 +30,7 @@ const SpeakingSettings: React.FC = () => { } = usePersistentExamStore(); const { title, currentModule } = useExamEditorStore(); - const { focusedSection, difficulty, sections, minTimer, isPrivate } = useExamEditorStore((store) => store.modules[currentModule]) + const { focusedSection, difficulty, sections, minTimer, access } = useExamEditorStore((store) => store.modules[currentModule]) const section = sections.find((section) => section.sectionId == focusedSection)?.state; @@ -185,7 +185,7 @@ const SpeakingSettings: React.FC = () => { variant: undefined, difficulty, instructorGender: "varied", - private: isPrivate, + access, }; const result = await axios.post('/api/exam/speaking', exam); @@ -238,7 +238,7 @@ const SpeakingSettings: React.FC = () => { isDiagnostic: false, variant: undefined, difficulty, - private: isPrivate, + access, } as SpeakingExam); setExerciseIndex(0); setQuestionIndex(0); diff --git a/src/components/ExamEditor/SettingsEditor/writing/index.tsx b/src/components/ExamEditor/SettingsEditor/writing/index.tsx index 911d1817..8f077d2f 100644 --- a/src/components/ExamEditor/SettingsEditor/writing/index.tsx +++ b/src/components/ExamEditor/SettingsEditor/writing/index.tsx @@ -23,7 +23,7 @@ const WritingSettings: React.FC = () => { const { minTimer, difficulty, - isPrivate, + access, sections, focusedSection, type, @@ -81,7 +81,7 @@ const WritingSettings: React.FC = () => { isDiagnostic: false, variant: undefined, difficulty, - private: isPrivate, + access, type: type! }); setExerciseIndex(0); @@ -134,7 +134,7 @@ const WritingSettings: React.FC = () => { isDiagnostic: true, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed. variant: undefined, difficulty, - private: isPrivate, + access, type: type! }; diff --git a/src/components/ExamEditor/index.tsx b/src/components/ExamEditor/index.tsx index 599f804b..c53af7a9 100644 --- a/src/components/ExamEditor/index.tsx +++ b/src/components/ExamEditor/index.tsx @@ -3,12 +3,12 @@ import SectionRenderer from "./SectionRenderer"; import Checkbox from "../Low/Checkbox"; import Input from "../Low/Input"; import Select from "../Low/Select"; -import {capitalize} from "lodash"; -import {Difficulty} from "@/interfaces/exam"; -import {useCallback, useEffect, useMemo, useState} from "react"; -import {toast} from "react-toastify"; -import {ModuleState, SectionState} from "@/stores/examEditor/types"; -import {Module} from "@/interfaces"; +import { capitalize } from "lodash"; +import { AccessType, ACCESSTYPE, Difficulty } from "@/interfaces/exam"; +import { useCallback, useEffect, useState } from "react"; +import { toast } from "react-toastify"; +import { ModuleState, SectionState } from "@/stores/examEditor/types"; +import { Module } from "@/interfaces"; import useExamEditorStore from "@/stores/examEditor"; import WritingSettings from "./SettingsEditor/writing"; import ReadingSettings from "./SettingsEditor/reading"; @@ -16,243 +16,286 @@ import LevelSettings from "./SettingsEditor/level"; import ListeningSettings from "./SettingsEditor/listening"; import SpeakingSettings from "./SettingsEditor/speaking"; import ImportOrStartFromScratch from "./ImportExam/ImportOrFromScratch"; -import {defaultSectionSettings} from "@/stores/examEditor/defaults"; +import { defaultSectionSettings } from "@/stores/examEditor/defaults"; import Button from "../Low/Button"; import ResetModule from "./Standalone/ResetModule"; import ListeningInstructions from "./Standalone/ListeningInstructions"; -import {EntityWithRoles} from "@/interfaces/entity"; +import { EntityWithRoles } from "@/interfaces/entity"; const DIFFICULTIES: Difficulty[] = ["A1", "A2", "B1", "B2", "C1", "C2"]; -const ExamEditor: React.FC<{levelParts?: number; entitiesAllowEditPrivacy: EntityWithRoles[]}> = ({ - levelParts = 0, - entitiesAllowEditPrivacy = [], -}) => { - const {currentModule, dispatch} = useExamEditorStore(); - const {sections, minTimer, expandedSections, examLabel, isPrivate, difficulty, sectionLabels, importModule} = useExamEditorStore( - (state) => state.modules[currentModule], - ); +const ExamEditor: React.FC<{ + levelParts?: number; + entitiesAllowEditPrivacy: EntityWithRoles[]; +}> = ({ levelParts = 0, entitiesAllowEditPrivacy = [] }) => { + const { currentModule, dispatch } = useExamEditorStore(); + const { + sections, + minTimer, + expandedSections, + examLabel, + access, + difficulty, + sectionLabels, + importModule, + } = useExamEditorStore((state) => state.modules[currentModule]); - const [numberOfLevelParts, setNumberOfLevelParts] = useState(levelParts !== 0 ? levelParts : 1); - const [isResetModuleOpen, setIsResetModuleOpen] = useState(false); + const [numberOfLevelParts, setNumberOfLevelParts] = useState( + levelParts !== 0 ? levelParts : 1 + ); + const [isResetModuleOpen, setIsResetModuleOpen] = useState(false); - // For exam edits - useEffect(() => { - if (levelParts !== 0) { - setNumberOfLevelParts(levelParts); - dispatch({ - type: "UPDATE_MODULE", - payload: { - updates: { - sectionLabels: Array.from({length: levelParts}).map((_, i) => ({ - id: i + 1, - label: `Part ${i + 1}`, - })), - }, - module: "level", - }, - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [levelParts]); + // For exam edits + useEffect(() => { + if (levelParts !== 0) { + setNumberOfLevelParts(levelParts); + dispatch({ + type: "UPDATE_MODULE", + payload: { + updates: { + sectionLabels: Array.from({ length: levelParts }).map((_, i) => ({ + id: i + 1, + label: `Part ${i + 1}`, + })), + }, + module: "level", + }, + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [levelParts]); - useEffect(() => { - const currentSections = sections; - const currentLabels = sectionLabels; - let updatedSections: SectionState[]; - let updatedLabels: any; - if ((currentModule === "level" && currentSections.length !== currentLabels.length) || numberOfLevelParts !== currentSections.length) { - const newSections = [...currentSections]; - const newLabels = [...currentLabels]; - for (let i = currentLabels.length; i < numberOfLevelParts; i++) { - if (currentSections.length !== numberOfLevelParts) newSections.push(defaultSectionSettings(currentModule, i + 1)); - newLabels.push({ - id: i + 1, - label: `Part ${i + 1}`, - }); - } - updatedSections = newSections; - updatedLabels = newLabels; - } else if (numberOfLevelParts < currentSections.length) { - updatedSections = currentSections.slice(0, numberOfLevelParts); - updatedLabels = currentLabels.slice(0, numberOfLevelParts); - } else { - return; - } + useEffect(() => { + const currentSections = sections; + const currentLabels = sectionLabels; + let updatedSections: SectionState[]; + let updatedLabels: any; + if ( + (currentModule === "level" && + currentSections.length !== currentLabels.length) || + numberOfLevelParts !== currentSections.length + ) { + const newSections = [...currentSections]; + const newLabels = [...currentLabels]; + for (let i = currentLabels.length; i < numberOfLevelParts; i++) { + if (currentSections.length !== numberOfLevelParts) + newSections.push(defaultSectionSettings(currentModule, i + 1)); + newLabels.push({ + id: i + 1, + label: `Part ${i + 1}`, + }); + } + updatedSections = newSections; + updatedLabels = newLabels; + } else if (numberOfLevelParts < currentSections.length) { + updatedSections = currentSections.slice(0, numberOfLevelParts); + updatedLabels = currentLabels.slice(0, numberOfLevelParts); + } else { + return; + } - const updatedExpandedSections = expandedSections.filter((sectionId) => updatedSections.some((section) => section.sectionId === sectionId)); + const updatedExpandedSections = expandedSections.filter((sectionId) => + updatedSections.some((section) => section.sectionId === sectionId) + ); - dispatch({ - type: "UPDATE_MODULE", - payload: { - updates: { - sections: updatedSections, - sectionLabels: updatedLabels, - expandedSections: updatedExpandedSections, - }, - }, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [numberOfLevelParts]); + dispatch({ + type: "UPDATE_MODULE", + payload: { + updates: { + sections: updatedSections, + sectionLabels: updatedLabels, + expandedSections: updatedExpandedSections, + }, + }, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [numberOfLevelParts]); - const sectionIds = sections.map((section) => section.sectionId); + const sectionIds = sections.map((section) => section.sectionId); - const updateModule = useCallback( - (updates: Partial) => { - dispatch({type: "UPDATE_MODULE", payload: {updates}}); - }, - [dispatch], - ); + const updateModule = useCallback( + (updates: Partial) => { + dispatch({ type: "UPDATE_MODULE", payload: { updates } }); + }, + [dispatch] + ); - const toggleSection = (sectionId: number) => { - if (expandedSections.length === 1 && sectionIds.includes(sectionId)) { - toast.error("Include at least one section!"); - return; - } - dispatch({type: "TOGGLE_SECTION", payload: {sectionId}}); - }; + const toggleSection = (sectionId: number) => { + if (expandedSections.length === 1 && sectionIds.includes(sectionId)) { + toast.error("Include at least one section!"); + return; + } + dispatch({ type: "TOGGLE_SECTION", payload: { sectionId } }); + }; - const ModuleSettings: Record = { - reading: ReadingSettings, - writing: WritingSettings, - speaking: SpeakingSettings, - listening: ListeningSettings, - level: LevelSettings, - }; + const ModuleSettings: Record = { + reading: ReadingSettings, + writing: WritingSettings, + speaking: SpeakingSettings, + listening: ListeningSettings, + level: LevelSettings, + }; - const Settings = ModuleSettings[currentModule]; - const showImport = importModule && ["reading", "listening", "level"].includes(currentModule); + const Settings = ModuleSettings[currentModule]; + const showImport = + importModule && ["reading", "listening", "level"].includes(currentModule); - const updateLevelParts = (parts: number) => { - setNumberOfLevelParts(parts); - }; + const updateLevelParts = (parts: number) => { + setNumberOfLevelParts(parts); + }; - return ( - <> - {showImport ? ( - - ) : ( - <> - {isResetModuleOpen && ( - - )} -
-
-
- - - updateModule({ - minTimer: parseInt(e) < 15 ? 15 : parseInt(e), - }) - } - value={minTimer} - className="max-w-[300px]" - /> -
-
- - setNumberOfLevelParts(parseInt(v))} - value={numberOfLevelParts} - /> -
- )} -
-
- updateModule({isPrivate: checked})} - disabled={entitiesAllowEditPrivacy.length === 0}> - Privacy (Only available for Assignments) - -
-
-
-
- - updateModule({examLabel: text})} - roundness="xl" - value={examLabel} - required - /> -
- {currentModule === "listening" && } - -
-
- -
- -
-
- - )} - - ); + return ( + <> + {showImport ? ( + + ) : ( + <> + {isResetModuleOpen && ( + + )} +
+
+
+ + + updateModule({ + minTimer: parseInt(e) < 15 ? 15 : parseInt(e), + }) + } + value={minTimer} + className="max-w-[300px]" + /> +
+
+ + setNumberOfLevelParts(parseInt(v))} + value={numberOfLevelParts} + /> +
+ )} +
+
+ updateModule({ examLabel: text })} + roundness="xl" + value={examLabel} + required + /> +
+ {currentModule === "listening" && } + +
+
+ +
+ +
+
+ + )} + + ); }; export default ExamEditor; diff --git a/src/components/High/Table.tsx b/src/components/High/Table.tsx index 2d588ffc..572e619a 100644 --- a/src/components/High/Table.tsx +++ b/src/components/High/Table.tsx @@ -149,10 +149,16 @@ export default function Table({ ))} - {isLoading && ( + {isLoading ? (
+ ) : ( + rows.length === 0 && ( +
+ No data found... +
+ ) )}
); diff --git a/src/hooks/useExams.tsx b/src/hooks/useExams.tsx index c9e001b5..befc474d 100644 --- a/src/hooks/useExams.tsx +++ b/src/hooks/useExams.tsx @@ -1,21 +1,21 @@ -import {Exam} from "@/interfaces/exam"; +import { Exam } from "@/interfaces/exam"; import axios from "axios"; -import {useEffect, useState} from "react"; +import { useEffect, useState } from "react"; export default function useExams() { - const [exams, setExams] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [isError, setIsError] = useState(false); + const [exams, setExams] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); - const getData = () => { - setIsLoading(true); - axios - .get("/api/exam") - .then((response) => setExams(response.data)) - .finally(() => setIsLoading(false)); - }; + const getData = () => { + setIsLoading(true); + axios + .get(`/api/exam`) + .then((response) => setExams(response.data)) + .finally(() => setIsLoading(false)); + }; - useEffect(getData, []); + useEffect(getData, []); - return {exams, isLoading, isError, reload: getData}; + return { exams, isLoading, isError, reload: getData }; } diff --git a/src/hooks/usePagination.tsx b/src/hooks/usePagination.tsx index 9075dbbe..3c307f33 100644 --- a/src/hooks/usePagination.tsx +++ b/src/hooks/usePagination.tsx @@ -1,60 +1,141 @@ import Button from "@/components/Low/Button"; -import {useMemo, useState} from "react"; -import {BiChevronLeft} from "react-icons/bi"; -import {BsChevronDoubleLeft, BsChevronDoubleRight, BsChevronLeft, BsChevronRight} from "react-icons/bs"; +import { useEffect, useMemo, useState } from "react"; +import { + BsChevronDoubleLeft, + BsChevronDoubleRight, + BsChevronLeft, + BsChevronRight, +} from "react-icons/bs"; +import Select from "../components/Low/Select"; export default function usePagination(list: T[], size = 25) { - const [page, setPage] = useState(0); + const [page, setPage] = useState(0); + const [itemsPerPage, setItemsPerPage] = useState(size); - const items = useMemo(() => list.slice(page * size, (page + 1) * size), [page, size, list]); + const items = useMemo( + () => list.slice(page * itemsPerPage, (page + 1) * itemsPerPage), + [list, page, itemsPerPage] + ); + useEffect(() => { + if (page * itemsPerPage >= list.length) setPage(0); + }, [items, itemsPerPage, list.length, page]); - const render = () => ( -
-
- -
-
- - {page * size + 1} - {(page + 1) * size > list.length ? list.length : (page + 1) * size} / {list.length} - - -
-
- ); + const itemsPerPageOptions = [25, 50, 100, 200]; - const renderMinimal = () => ( -
-
- - -
- - {page * size + 1} - {(page + 1) * size > list.length ? list.length : (page + 1) * size} / {list.length} - -
- - -
-
- ); + const render = () => ( +
+
+ +
+
+
+ setItemsPerPage(parseInt(value!.value ?? "25"))} + options={itemsPerPageOptions.map((size) => ({ + label: size.toString(), + value: size.toString(), + }))} + isClearable={false} + styles={{ + control: (styles) => ({ ...styles, width: "100px" }), + container: (styles) => ({ ...styles, width: "100px" }), + }} + /> + + {page * itemsPerPage + 1} -{" "} + {itemsPerPage * (page + 1) > list.length + ? list.length + : itemsPerPage * (page + 1)} + / {list.length} + +
+
+ + +
+
+ ); + + return { page, items, setPage, render, renderMinimal }; } diff --git a/src/interfaces/exam.ts b/src/interfaces/exam.ts index 965ad45b..201ce888 100644 --- a/src/interfaces/exam.ts +++ b/src/interfaces/exam.ts @@ -1,4 +1,3 @@ -import instructions from "@/pages/api/exam/media/instructions"; import { Module } from "."; export type Exam = ReadingExam | ListeningExam | WritingExam | SpeakingExam | LevelExam; @@ -10,6 +9,9 @@ export type Difficulty = BasicDifficulty | CEFRLevels; // Left easy, medium and hard to support older exam versions export type BasicDifficulty = "easy" | "medium" | "hard"; export type CEFRLevels = "A1" | "A2" | "B1" | "B2" | "C1" | "C2"; +export const ACCESSTYPE = ["public", "private", "confidential"] as const; +export type AccessType = typeof ACCESSTYPE[number]; + export interface ExamBase { @@ -24,7 +26,7 @@ export interface ExamBase { shuffle?: boolean; createdBy?: string; // option as it has been added later createdAt?: string; // option as it has been added later - private?: boolean; + access: AccessType; label?: string; } export interface ReadingExam extends ExamBase { diff --git a/src/pages/(admin)/CorporateGradingSystem.tsx b/src/pages/(admin)/CorporateGradingSystem.tsx index 9506108d..e4e9c829 100644 --- a/src/pages/(admin)/CorporateGradingSystem.tsx +++ b/src/pages/(admin)/CorporateGradingSystem.tsx @@ -265,7 +265,7 @@ export default function CorporateGradingSystem({ <>