diff --git a/src/components/Exercises/FillBlanks/index.tsx b/src/components/Exercises/FillBlanks/index.tsx index 44f8d693..fe95bb51 100644 --- a/src/components/Exercises/FillBlanks/index.tsx +++ b/src/components/Exercises/FillBlanks/index.tsx @@ -44,44 +44,43 @@ const FillBlanks: React.FC = ({ const calculateScore = () => { const total = text.match(/({{\d+}})/g)?.length || 0; const correct = userSolutions.filter((x) => { - const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution; - if (!solution) return false; - - const option = words.find((w) => { - if (typeof w === "string") { - return w.toLowerCase() === x.solution.toLowerCase(); - } else if ('letter' in w) { - return w.word.toLowerCase() === x.solution.toLowerCase(); - } else { - return w.id === x.id; - } - }); - if (!option) return false; - - if (typeof option === "string") { - return solution.toLowerCase() === option.toLowerCase(); - } else if ('letter' in option) { - return solution.toLowerCase() === option.word.toLowerCase(); - } else if ('options' in option) { - /* - if (shuffleMaps.length !== 0) { - const shuffleMap = shuffleMaps.find((map) => map.id == x.id) - if (!shuffleMap) { - return false; - } - const original = shuffleMap[x.solution as keyof typeof shuffleMap]; - return solution.toLowerCase() === (option.options[original as keyof typeof option.options] || '').toLowerCase(); - }*/ + const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution; + if (!solution) return false; - return solution.toLowerCase() === (option.options[x.solution as keyof typeof option.options] || '').toLowerCase(); - } - return false; + const option = words.find((w) => { + if (typeof w === "string") { + return w.toLowerCase() === x.solution.toLowerCase(); + } else if ('letter' in w) { + return w.word.toLowerCase() === x.solution.toLowerCase(); + } else { + return w.id === x.id; + } + }); + if (!option) return false; + + if (typeof option === "string") { + return solution.toLowerCase() === option.toLowerCase(); + } else if ('letter' in option) { + return solution.toLowerCase() === option.word.toLowerCase(); + } else if ('options' in option) { + /* + if (shuffleMaps.length !== 0) { + const shuffleMap = shuffleMaps.find((map) => map.id == x.id) + if (!shuffleMap) { + return false; + } + const original = shuffleMap[x.solution as keyof typeof shuffleMap]; + return solution.toLowerCase() === (option.options[original as keyof typeof option.options] || '').toLowerCase(); + }*/ + return solution.toLowerCase() === (option.options[x.solution as keyof typeof option.options] || '').toLowerCase(); + } + return false; }).length; - + const missing = total - userSolutions.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length; - + return { total, correct, missing }; - }; + }; const renderLines = (line: string) => { return ( @@ -96,24 +95,27 @@ const FillBlanks: React.FC = ({ ) return ( variant === "mc" ? ( - + <> + {/*{`(${id})`}*/} + + ) : ( = ({ } /*const getShuffles = () => { - let shuffle = {}; - if (shuffleMaps.length !== 0) { - shuffle = { - shuffleMaps: shuffleMaps.filter((map) => - answers.some(answer => answer.id === map.id) - ) - } - } - return shuffle; - }*/ + let shuffle = {}; + if (shuffleMaps.length !== 0) { + shuffle = { + shuffleMaps: shuffleMaps.filter((map) => + answers.some(answer => answer.id === map.id) + ) + } + } + return shuffle; + }*/ return ( <>
- + {false && {prompt.split("\\n").map((line, index) => ( {line}
))} -
+
} {text.split("\\n").map((line, index) => (

@@ -162,23 +164,35 @@ const FillBlanks: React.FC = ({ ))} {variant === "mc" && typeCheckWordsMC(words) ? ( - <> + <> {currentMCSelection && ( -

- Options -
+
+ {`${currentMCSelection.id} - Select the appropriate word.`} +
{currentMCSelection.selection?.options && Object.entries(currentMCSelection.selection.options).sort((a, b) => a[0].localeCompare(b[0])).map(([key, value]) => { - return
+ + /*; + ;*/ })}
diff --git a/src/components/Exercises/MultipleChoice.tsx b/src/components/Exercises/MultipleChoice.tsx index 70d8a3ba..e818a60b 100644 --- a/src/components/Exercises/MultipleChoice.tsx +++ b/src/components/Exercises/MultipleChoice.tsx @@ -20,24 +20,21 @@ function Question({ showSolution?: boolean, }) { - /* const renderPrompt = (prompt: string) => { - return reactStringReplace(prompt, /(()[\w\s']+(<\/u>))/g, (match) => { + return reactStringReplace(prompt, /(.*?<\/u>)/g, (match) => { const word = match.replaceAll("", "").replaceAll("", ""); return word.length > 0 ? {word} : null; }); }; - */ return ( - // {renderPrompt(prompt).filter((x) => x?.toString() !== "")} -
+
{isNaN(Number(id)) ? ( - + {renderPrompt(prompt).filter((x) => x?.toString() !== "")} ) : ( - + <> - {id} - + {id} - {renderPrompt(prompt).filter((x) => x?.toString() !== "")} )} @@ -61,7 +58,7 @@ function Question({ key={option.id.toString()} onClick={() => (onSelectOption ? onSelectOption(option.id.toString()) : null)} className={clsx( - "flex border p-4 rounded-xl gap-2 cursor-pointer bg-white text-sm", + "flex border p-4 rounded-xl gap-2 cursor-pointer bg-white text-base", userSolution === option.id.toString() && "border-mti-purple-light", )}> {option.id.toString()}. @@ -156,7 +153,7 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti return ( <>
- {prompt} + {/*{"Select the appropriate option."}*/} {questionIndex < questions.length && ( state.setHasExamEnded); - const {timeSpent} = useExamStore((state) => state); + const { timeSpent } = useExamStore((state) => state); useEffect(() => setTimer((prev) => prev - timeSpent), [timeSpent]); @@ -45,7 +48,7 @@ export default function ModuleTitle({minTimer, module, label, exerciseIndex, tot if (timer < 300 && !warningMode) setWarningMode(true); }, [timer, warningMode]); - const moduleIcon: {[key in Module]: ReactNode} = { + const moduleIcon: { [key in Module]: ReactNode } = { reading: , listening: , writing: , @@ -67,9 +70,9 @@ export default function ModuleTitle({minTimer, module, label, exerciseIndex, tot "absolute top-4 right-6 bg-mti-gray-seasalt px-4 py-3 flex items-center gap-2 rounded-full text-mti-gray-davy", warningMode && !disableTimer && "bg-mti-red-light text-mti-gray-seasalt", )} - initial={{scale: warningMode && !disableTimer ? 0.8 : 1}} - animate={{scale: warningMode && !disableTimer ? 1.1 : 1}} - transition={{repeat: Infinity, repeatType: "reverse", duration: 0.5, ease: "easeInOut"}}> + initial={{ scale: warningMode && !disableTimer ? 0.8 : 1 }} + animate={{ scale: warningMode && !disableTimer ? 1.1 : 1 }} + transition={{ repeat: Infinity, repeatType: "reverse", duration: 0.5, ease: "easeInOut" }}> {timer > 0 && ( @@ -86,18 +89,24 @@ export default function ModuleTitle({minTimer, module, label, exerciseIndex, tot {timer <= 0 && <>00:00} -
-
{moduleIcon[module]}
-
-
- - {moduleLabels[module]} exam {label && `- ${label}`} - - - Exercise {exerciseIndex}/{totalExercises} - +
+ {partLabel &&
{partLabel.split('\n\n').map((line, index) => { + if(index == 0) return

{line}

+ else return

{line}

+ })}
} +
+
{moduleIcon[module]}
+
+
+ + {moduleLabels[module]} exam {label && `- ${label}`} + + + Question {exerciseIndex}/{totalExercises} + +
+
-
diff --git a/src/components/PermissionList.tsx b/src/components/PermissionList.tsx index 2911b22f..e3424c08 100644 --- a/src/components/PermissionList.tsx +++ b/src/components/PermissionList.tsx @@ -1,105 +1,82 @@ import React from "react"; -import { Permission } from "@/interfaces/permissions"; -import { - createColumnHelper, - flexRender, - getCoreRowModel, - useReactTable, - Row, -} from "@tanstack/react-table"; +import {Permission} from "@/interfaces/permissions"; +import {createColumnHelper, flexRender, getCoreRowModel, useReactTable, Row} from "@tanstack/react-table"; import Link from "next/link"; -import { convertCamelCaseToReadable } from "@/utils/string"; +import {convertCamelCaseToReadable} from "@/utils/string"; interface Props { - permissions: Permission[]; + permissions: Permission[]; } const columnHelper = createColumnHelper(); const defaultColumns = [ - columnHelper.accessor("type", { - header: () => Type, - cell: ({ row, getValue }) => ( - - {convertCamelCaseToReadable(getValue() as string)} - - ), - }), + columnHelper.accessor("type", { + header: () => Type, + cell: ({row, getValue}) => ( + + {convertCamelCaseToReadable(getValue() as string)} + + ), + }), ]; -export default function PermissionList({ permissions }: Props) { - const table = useReactTable({ - data: permissions, - columns: defaultColumns, - getCoreRowModel: getCoreRowModel(), - }); +export default function PermissionList({permissions}: Props) { + const table = useReactTable({ + data: permissions, + columns: defaultColumns, + getCoreRowModel: getCoreRowModel(), + }); - const groupedData: { [key: string]: Row[] } = table - .getRowModel() - .rows.reduce((groups: { [key: string]: Row[] }, row) => { - const parent = row.original.topic; - if (!groups[parent]) { - groups[parent] = []; - } - groups[parent].push(row); - return groups; - }, {}); + const groupedData: {[key: string]: Row[]} = table.getRowModel().rows.reduce((groups: {[key: string]: Row[]}, row) => { + const parent = row.original.topic; + if (!groups[parent]) { + groups[parent] = []; + } + groups[parent].push(row); + return groups; + }, {}); - return ( -
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - ))} - - ))} - - - {Object.keys(groupedData).map((parent) => ( - - - - - {groupedData[parent].map((row, i) => ( - - {row.getVisibleCells().map((cell) => ( - - ))} - - ))} - - ))} - -
- {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} -
- {parent} -
- {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} -
-
-
- ); + return ( +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {Object.keys(groupedData).map((parent) => ( + + + + + {groupedData[parent].map((row, i) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + + ))} + +
+ {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} +
+ {parent} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+
+ ); } diff --git a/src/components/Solutions/MultipleChoice.tsx b/src/components/Solutions/MultipleChoice.tsx index d3ecfabc..095e3a5e 100644 --- a/src/components/Solutions/MultipleChoice.tsx +++ b/src/components/Solutions/MultipleChoice.tsx @@ -49,7 +49,7 @@ function Question({ const newSolution = solution; //questionShuffleMap ? getShuffledSolution(solution, questionShuffleMap) : solution; const renderPrompt = (prompt: string) => { - return reactStringReplace(prompt, /(()[\w\s']+(<\/u>))/g, (match) => { + return reactStringReplace(prompt, /(.*?<\/u>)/g, (match) => { const word = match.replaceAll("", "").replaceAll("", ""); return word.length > 0 ? {word} : null; }); diff --git a/src/dashboards/Teacher.tsx b/src/dashboards/Teacher.tsx index e1ddbbb5..436696d5 100644 --- a/src/dashboards/Teacher.tsx +++ b/src/dashboards/Teacher.tsx @@ -121,7 +121,7 @@ export default function TeacherDashboard({user}: Props) { }; const GroupsList = () => { - const filter = (x: Group) => x.admin === user.id || x.participants.includes(user.id); + const filter = (x: Group) => x.admin === user.id; return ( <> @@ -318,7 +318,13 @@ export default function TeacherDashboard({user}: Props) { color="purple" /> {checkAccess(user, ["teacher", "developer"], permissions, "viewGroup") && ( - setPage("groups")} /> + x.admin === user.id).length} + color="purple" + onClick={() => setPage("groups")} + /> )}
setPage("assignments")} diff --git a/src/exams/Level.tsx b/src/exams/Level.tsx index ea360929..a5bdf478 100644 --- a/src/exams/Level.tsx +++ b/src/exams/Level.tsx @@ -16,6 +16,7 @@ import clsx from "clsx"; import { Dispatch, Fragment, SetStateAction, use, useEffect, useMemo, useRef, useState } from "react"; import { BsChevronDown, BsChevronUp } from "react-icons/bs"; import { toast } from "react-toastify"; +import { v4 } from "uuid"; interface Props { exam: LevelExam; @@ -25,9 +26,9 @@ interface Props { } function TextComponent({ - part, highlightPhrases, contextWord, setContextWordLine + part, contextWord, setContextWordLine }: { - part: LevelPart, highlightPhrases: string[], contextWord: string | undefined, setContextWordLine: React.Dispatch> + part: LevelPart, contextWord: string | undefined, setContextWordLine: React.Dispatch> }) { const textRef = useRef(null); const [lineNumbers, setLineNumbers] = useState([]); @@ -49,13 +50,14 @@ function TextComponent({ offscreenElement.style.width = `${containerWidth}px`; offscreenElement.style.font = computedStyle.font; offscreenElement.style.lineHeight = computedStyle.lineHeight; - offscreenElement.style.wordWrap = 'break-word'; offscreenElement.style.textAlign = computedStyle.textAlign as CanvasTextAlign; const textContent = textRef.current.textContent || ''; textContent.split(/(\s+)/).forEach((word: string) => { const span = document.createElement('span'); span.textContent = word; + span.style.display = 'inline-block'; + span.style.height = `calc(1em + 16px)`; offscreenElement.appendChild(span); }); @@ -139,15 +141,10 @@ function TextComponent({
-
- {lineNumbers.map(num => ( -
- {num} -
- ))} -
-
- +
+ {part.context!.split('\n\n').map((line, index) => { + return

{index + 1}{line}

+ })}
@@ -176,7 +173,6 @@ export default function Level({ exam, showSolutions = false, onFinish, editing = const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0)); - const [highlightPhrases, setContextHighlight] = useState([]); const [contextWord, setContextWord] = useState(undefined); const [contextWordLine, setContextWordLine] = useState(undefined); @@ -304,7 +300,6 @@ export default function Level({ exam, showSolutions = false, onFinish, editing = if (match) { const word = match[1]; const originalLineNumber = match[2]; - setContextHighlight([word]); if (word !== contextWord) { setContextWord(word); @@ -317,7 +312,6 @@ export default function Level({ exam, showSolutions = false, onFinish, editing = currentExercise.questions[storeQuestionIndex].prompt = updatedPrompt; } else { - setContextHighlight([]); setContextWord(undefined); } } @@ -329,8 +323,8 @@ export default function Level({ exam, showSolutions = false, onFinish, editing = setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level", exam: exam.id }]); } - if (storeQuestionIndex > 0) { - setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: storeQuestionIndex }]); + if (storeQuestionIndex > 0 || currentExercise?.type == "fillBlanks") { + setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: currentExercise?.type == "fillBlanks" ? currentExercise.words.length - 1 : storeQuestionIndex }]); } setStoreQuestionIndex(0); @@ -377,8 +371,8 @@ export default function Level({ exam, showSolutions = false, onFinish, editing = setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level", exam: exam.id }]); } - if (storeQuestionIndex > 0) { - setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: storeQuestionIndex }]); + if (storeQuestionIndex > 0 || currentExercise?.type == "fillBlanks") { + setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: currentExercise?.type == "fillBlanks" ? currentExercise.words.length - 1 : storeQuestionIndex }]); } setStoreQuestionIndex(0); @@ -397,7 +391,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing = exercisesDone + (exerciseIndex === -1 ? 0 : exerciseIndex + 1) + storeQuestionIndex + - multipleChoicesDone.reduce((acc, curr) => acc + curr.amount, 0) + multipleChoicesDone.reduce((acc, curr) => { return acc + curr.amount}, 0) ); }; @@ -412,7 +406,6 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
@@ -420,11 +413,19 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
); + const partLabel = () => { + if (currentExercise?.type === "fillBlanks" && typeCheckWordsMC(currentExercise.words)) + return `Part ${partIndex + 1} (Questions ${currentExercise.words[0].id} - ${currentExercise.words[currentExercise.words.length - 1].id})\n\n${currentExercise.prompt}` + if (currentExercise?.type === "multipleChoice") + return `Part ${partIndex + 1} (Questions ${currentExercise.questions[0].id} - ${currentExercise.questions[currentExercise.questions.length - 1].id})\n\n${currentExercise.prompt}` + } + return ( <>
{ @@ -310,7 +315,8 @@ export default function GroupList({user}: {user: User}) { groups .filter((g) => g.admin === user.id) .flatMap((g) => g.participants) - .includes(u.id) || groups.flatMap((g) => g.participants).includes(u.id), + .includes(u.id) || + (user?.type === "teacher" ? corporateGroups : groups).flatMap((g) => g.participants).includes(u.id), ) : users } diff --git a/src/pages/generation.tsx b/src/pages/generation.tsx index 2fd4660d..82577a61 100644 --- a/src/pages/generation.tsx +++ b/src/pages/generation.tsx @@ -1,19 +1,19 @@ /* eslint-disable @next/next/no-img-element */ import Head from "next/head"; -import { withIronSessionSsr } from "iron-session/next"; -import { sessionOptions } from "@/lib/session"; +import {withIronSessionSsr} from "iron-session/next"; +import {sessionOptions} from "@/lib/session"; import useUser from "@/hooks/useUser"; -import { toast, ToastContainer } from "react-toastify"; +import {toast, ToastContainer} from "react-toastify"; import Layout from "@/components/High/Layout"; -import { shouldRedirectHome } from "@/utils/navigation.disabled"; -import { useState } from "react"; -import { Module } from "@/interfaces"; -import { RadioGroup, Tab } from "@headlessui/react"; +import {shouldRedirectHome} from "@/utils/navigation.disabled"; +import {useState} from "react"; +import {Module} from "@/interfaces"; +import {RadioGroup, Tab} from "@headlessui/react"; import clsx from "clsx"; -import { MODULE_ARRAY } from "@/utils/moduleUtils"; -import { capitalize } from "lodash"; +import {MODULE_ARRAY} from "@/utils/moduleUtils"; +import {capitalize} from "lodash"; import Button from "@/components/Low/Button"; -import { Exercise, ReadingPart } from "@/interfaces/exam"; +import {Exercise, ReadingPart} from "@/interfaces/exam"; import Input from "@/components/Low/Input"; import axios from "axios"; import ReadingGeneration from "./(generation)/ReadingGeneration"; @@ -21,121 +21,114 @@ import ListeningGeneration from "./(generation)/ListeningGeneration"; import WritingGeneration from "./(generation)/WritingGeneration"; import LevelGeneration from "./(generation)/LevelGeneration"; import SpeakingGeneration from "./(generation)/SpeakingGeneration"; -import { checkAccess, getTypesOfUser } from "@/utils/permissions"; +import {checkAccess} from "@/utils/permissions"; -export const getServerSideProps = withIronSessionSsr(({ req, res }) => { - const user = req.session.user; +export const getServerSideProps = withIronSessionSsr(({req, res}) => { + const user = req.session.user; - if (!user || !user.isVerified) { - return { - redirect: { - destination: "/login", - permanent: false, - }, - }; - } + if (!user || !user.isVerified) { + return { + redirect: { + destination: "/login", + permanent: false, + }, + }; + } - if ( - shouldRedirectHome(user) || - checkAccess(user, getTypesOfUser(["developer"])) - ) { - return { - redirect: { - destination: "/", - permanent: false, - }, - }; - } + if (shouldRedirectHome(user) || !checkAccess(user, ["admin", "mastercorporate", "developer", "corporate"])) { + return { + redirect: { + destination: "/", + permanent: false, + }, + }; + } - return { - props: { user: req.session.user }, - }; + return { + props: {user: req.session.user}, + }; }, sessionOptions); export default function Generation() { - const [module, setModule] = useState("reading"); + const [module, setModule] = useState("reading"); - const { user } = useUser({ redirectTo: "/login" }); + const {user} = useUser({redirectTo: "/login"}); - const [title, setTitle] = useState(""); - return ( - <> - - Exam Generation | EnCoach - - - - - - {user && ( - -

Exam Generation

-
- + const [title, setTitle] = useState(""); + return ( + <> + + Exam Generation | EnCoach + + + + + + {user && ( + +

Exam Generation

+
+ - - - {[...MODULE_ARRAY].map((x) => ( - - {({ checked }) => ( - - {capitalize(x)} - - )} - - ))} - -
- {module === "reading" && } - {module === "listening" && } - {module === "writing" && } - {module === "speaking" && } - {module === "level" && } -
- )} - - ); + + + {[...MODULE_ARRAY].map((x) => ( + + {({checked}) => ( + + {capitalize(x)} + + )} + + ))} + +
+ {module === "reading" && } + {module === "listening" && } + {module === "writing" && } + {module === "speaking" && } + {module === "level" && } +
+ )} + + ); } diff --git a/src/pages/permissions/[id].tsx b/src/pages/permissions/[id].tsx index bafcaaf8..022ba933 100644 --- a/src/pages/permissions/[id].tsx +++ b/src/pages/permissions/[id].tsx @@ -1,209 +1,209 @@ /* eslint-disable @next/next/no-img-element */ import Head from "next/head"; -import { useState } from "react"; -import { withIronSessionSsr } from "iron-session/next"; -import { sessionOptions } from "@/lib/session"; -import { shouldRedirectHome } from "@/utils/navigation.disabled"; -import { Permission, PermissionType } from "@/interfaces/permissions"; -import { getPermissionDoc } from "@/utils/permissions.be"; -import { User } from "@/interfaces/user"; +import {useEffect, useState} from "react"; +import {withIronSessionSsr} from "iron-session/next"; +import {sessionOptions} from "@/lib/session"; +import {shouldRedirectHome} from "@/utils/navigation.disabled"; +import {Permission, PermissionType} from "@/interfaces/permissions"; +import {getPermissionDoc} from "@/utils/permissions.be"; +import {User} from "@/interfaces/user"; import Layout from "@/components/High/Layout"; -import { getUsers } from "@/utils/users.be"; -import { BsTrash } from "react-icons/bs"; +import {getUsers} from "@/utils/users.be"; +import {BsTrash} from "react-icons/bs"; import Select from "@/components/Low/Select"; import Button from "@/components/Low/Button"; import axios from "axios"; -import { toast, ToastContainer } from "react-toastify"; -import {Type as UserType} from '@/interfaces/user' +import {toast, ToastContainer} from "react-toastify"; +import {Type as UserType} from "@/interfaces/user"; +import {getGroups} from "@/utils/groups.be"; interface BasicUser { - id: string; - name: string; - type: UserType + id: string; + name: string; + type: UserType; } interface PermissionWithBasicUsers { - id: string; - type: PermissionType; - users: BasicUser[]; + id: string; + type: PermissionType; + users: BasicUser[]; } export const getServerSideProps = withIronSessionSsr(async (context) => { - const { req, params } = context; - const user = req.session.user; + const {req, params} = context; + const user = req.session.user; - if (!user || !user.isVerified) { - return { - redirect: { - destination: "/login", - permanent: false, - }, - }; - } + if (!user || !user.isVerified) { + return { + redirect: { + destination: "/login", + permanent: false, + }, + }; + } - if (shouldRedirectHome(user)) { - return { - redirect: { - destination: "/", - permanent: false, - }, - }; - } + if (shouldRedirectHome(user)) { + return { + redirect: { + destination: "/", + permanent: false, + }, + }; + } - if (!params?.id) { - return { - redirect: { - destination: "/permissions", - permanent: false, - }, - }; - } + if (!params?.id) { + return { + redirect: { + destination: "/permissions", + permanent: false, + }, + }; + } - // Fetch data from external API - const permission: Permission = await getPermissionDoc(params.id as string); + // Fetch data from external API + const permission: Permission = await getPermissionDoc(params.id as string); - const allUserData: User[] = await getUsers(); - - const users = allUserData.map((u) => ({ - id: u.id, - name: u.name, - type: u.type - })) as BasicUser[]; - - // const res = await fetch("api/permissions"); - // const permissions: Permission[] = await res.json(); - // Pass data to the page via props - const usersData: BasicUser[] = permission.users.reduce( - (acc: BasicUser[], userId) => { - const user = users.find((u) => u.id === userId) as BasicUser; - if (user) { - acc.push(user); - } - return acc; - }, - [] - ); + const allUserData: User[] = await getUsers(); + const groups = await getGroups(); - return { - props: { - // permissions: permissions.map((p) => ({ id: p.id, type: p.type })), - permission: { - ...permission, - id: params.id, - users: usersData, - }, - user: req.session.user, - users, - }, - }; + const userGroups = groups.filter((x) => x.admin === user.id); + const filteredGroups = + user.type === "corporate" + ? userGroups + : user.type === "mastercorporate" + ? groups.filter((x) => userGroups.flatMap((y) => y.participants).includes(x.admin)) + : groups; + + const users = allUserData.map((u) => ({ + id: u.id, + name: u.name, + type: u.type, + })) as BasicUser[]; + + const filteredUsers = ["mastercorporate", "corporate"].includes(user.type) + ? users.filter((u) => filteredGroups.flatMap((g) => g.participants).includes(u.id)) + : users; + + // const res = await fetch("api/permissions"); + // const permissions: Permission[] = await res.json(); + // Pass data to the page via props + const usersData: BasicUser[] = permission.users.reduce((acc: BasicUser[], userId) => { + const user = filteredUsers.find((u) => u.id === userId) as BasicUser; + if (!!user) acc.push(user); + return acc; + }, []); + + return { + props: { + // permissions: permissions.map((p) => ({ id: p.id, type: p.type })), + permission: { + ...permission, + id: params.id, + users: usersData, + }, + user: req.session.user, + users: filteredUsers, + }, + }; }, sessionOptions); interface Props { - permission: PermissionWithBasicUsers; - user: User; - users: BasicUser[]; + permission: PermissionWithBasicUsers; + user: User; + users: BasicUser[]; } export default function Page(props: Props) { - const { permission, user, users } = props; - - - const [selectedUsers, setSelectedUsers] = useState(() => - permission.users.map((u) => u.id) - ); + const {permission, user, users} = props; - const onChange = (value: any) => { - - setSelectedUsers((prev) => { - if (value?.value) { - return [...prev, value?.value]; - } - return prev; - }); - }; - const removeUser = (id: string) => { - setSelectedUsers((prev) => prev.filter((u) => u !== id)); - }; + const [selectedUsers, setSelectedUsers] = useState(() => permission.users.map((u) => u.id)); - const update = async () => { - - try { - await axios.patch(`/api/permissions/${permission.id}`, { - users: selectedUsers, - }); - toast.success("Permission updated"); - } catch (err) { - toast.error("Failed to update permission"); - } - }; - - return ( - <> - - EnCoach - - - - - - -

- Permission: {permission.type as string} -

-
- !selectedUsers.includes(u.id)) + .map((u) => ({ + label: `${u?.type}-${u?.name}`, + value: u.id, + }))} + onChange={onChange} + /> + +
+
+
+

Blacklisted Users

+
+ {selectedUsers.map((userId) => { + const user = users.find((u) => u.id === userId); + return ( +
+ + {user?.type}-{user?.name} + + removeUser(userId)} size={20} /> +
+ ); + })} +
+
+
+

Whitelisted Users

+
+ {users + .filter((user) => !selectedUsers.includes(user.id)) + .map((user) => { + return ( +
+ + {user?.type}-{user?.name} + +
+ ); + })} +
+
+
+
+ + + ); } diff --git a/src/pages/permissions/index.tsx b/src/pages/permissions/index.tsx index bce6ed26..373707e7 100644 --- a/src/pages/permissions/index.tsx +++ b/src/pages/permissions/index.tsx @@ -1,78 +1,86 @@ /* eslint-disable @next/next/no-img-element */ import Head from "next/head"; -import { withIronSessionSsr } from "iron-session/next"; -import { sessionOptions } from "@/lib/session"; -import { shouldRedirectHome } from "@/utils/navigation.disabled"; -import { Permission } from "@/interfaces/permissions"; -import { getPermissionDocs } from "@/utils/permissions.be"; -import { User } from "@/interfaces/user"; +import {withIronSessionSsr} from "iron-session/next"; +import {sessionOptions} from "@/lib/session"; +import {shouldRedirectHome} from "@/utils/navigation.disabled"; +import {Permission} from "@/interfaces/permissions"; +import {getPermissionDocs} from "@/utils/permissions.be"; +import {User} from "@/interfaces/user"; import Layout from "@/components/High/Layout"; -import PermissionList from '@/components/PermissionList' +import PermissionList from "@/components/PermissionList"; -export const getServerSideProps = withIronSessionSsr(async ({ req }) => { - const user = req.session.user; +export const getServerSideProps = withIronSessionSsr(async ({req}) => { + const user = req.session.user; - if (!user || !user.isVerified) { - return { - redirect: { - destination: "/login", - permanent: false, - }, - }; - } + if (!user || !user.isVerified) { + return { + redirect: { + destination: "/login", + permanent: false, + }, + }; + } - if (shouldRedirectHome(user)) { - return { - redirect: { - destination: "/", - permanent: false, - }, - }; - } + if (shouldRedirectHome(user)) { + return { + redirect: { + destination: "/", + permanent: false, + }, + }; + } - // Fetch data from external API - const permissions: Permission[] = await getPermissionDocs(); + // Fetch data from external API + const permissions: Permission[] = await getPermissionDocs(); + const filteredPermissions = permissions.filter((p) => { + const permissionType = p.type.toString().toLowerCase(); + if (user.type === "corporate") return !permissionType.includes("corporate") && !permissionType.includes("admin"); + if (user.type === "mastercorporate") return !permissionType.includes("mastercorporate") && !permissionType.includes("admin"); - // const res = await fetch("api/permissions"); - // const permissions: Permission[] = await res.json(); - // Pass data to the page via props - return { - props: { - // permissions: permissions.map((p) => ({ id: p.id, type: p.type })), - permissions: permissions.map((p) => { - const { users, ...rest } = p; - return rest; - }), - user: req.session.user, - }, - }; + return true; + }); + + // const res = await fetch("api/permissions"); + // const permissions: Permission[] = await res.json(); + // Pass data to the page via props + return { + props: { + // permissions: permissions.map((p) => ({ id: p.id, type: p.type })), + permissions: filteredPermissions.map((p) => { + const {users, ...rest} = p; + return rest; + }), + user: req.session.user, + }, + }; }, sessionOptions); interface Props { - permissions: Permission[]; - user: User; + permissions: Permission[]; + user: User; } export default function Page(props: Props) { - const { permissions, user } = props; - return ( - <> - - EnCoach - - - - - -

Permissions

-
- -
-
- - ); + const {permissions, user} = props; + + return ( + <> + + EnCoach + + + + + +

Permissions

+
+ +
+
+ + ); } diff --git a/src/styles/globals.css b/src/styles/globals.css index ee65b924..1db0b8a7 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -1,4 +1,90 @@ @tailwind base; @tailwind components; @tailwind utilities; - \ No newline at end of file + +@layer utilities { + .scrollbar-hide { + -ms-overflow-style: none; + /* IE and Edge */ + scrollbar-width: none; + /* Firefox */ + } + + .scrollbar-hide::-webkit-scrollbar { + display: none; + /* Chrome, Safari and Opera */ + } +} + +.training-scrollbar::-webkit-scrollbar { + @apply w-1.5; +} + +.training-scrollbar::-webkit-scrollbar-track { + @apply bg-transparent; +} + +.training-scrollbar::-webkit-scrollbar-thumb { + @apply bg-gray-400 hover:bg-gray-500 rounded-full transition-colors opacity-50 hover:opacity-75; +} + +.training-scrollbar { + scrollbar-width: thin; + scrollbar-color: rgba(156, 163, 175, 0.5) transparent; +} + +:root { + --max-width: 1100px; + --border-radius: 12px; + --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", + "Fira Mono", "Droid Sans Mono", "Courier New", monospace; + + --foreground-rgb: 53, 51, 56; + --background-start-rgb: 245, 245, 245; + --background-end-rgb: 245, 245, 245; + + --primary-glow: conic-gradient(from 180deg at 50% 50%, #16abff33 0deg, #0885ff33 55deg, #54d6ff33 120deg, #0071ff33 160deg, transparent 360deg); + --secondary-glow: radial-gradient(rgba(255, 255, 255, 1), rgba(255, 255, 255, 0)); + + --tile-start-rgb: 239, 245, 249; + --tile-end-rgb: 228, 232, 233; + --tile-border: conic-gradient(#00000080, #00000040, #00000030, #00000020, #00000010, #00000010, #00000080); + + --callout-rgb: 238, 240, 241; + --callout-border-rgb: 172, 175, 176; + --card-rgb: 180, 185, 188; + --card-border-rgb: 131, 134, 135; +} + +* { + box-sizing: border-box; + padding: 0; + margin: 0; +} + +html { + min-height: 100vh !important; + height: 100%; + max-width: 100vw; + overflow-x: hidden; + overflow-y: auto; + font-family: "Open Sans", system-ui, -apple-system, "Helvetica Neue", sans-serif; +} + +body { + min-height: 100vh !important; + height: 100%; + max-width: 100vw; + overflow-x: hidden; + font-family: "Open Sans", system-ui, -apple-system, "Helvetica Neue", sans-serif; +} + +body { + color: rgb(var(--foreground-rgb)); + background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb)); +} + +a { + color: inherit; + text-decoration: none; +} diff --git a/src/utils/groups.be.ts b/src/utils/groups.be.ts index 77fc5d23..5b793f8d 100644 --- a/src/utils/groups.be.ts +++ b/src/utils/groups.be.ts @@ -33,6 +33,11 @@ export const updateExpiryDateOnGroup = async (participantID: string, corporateID return; }; +export const getGroups = async () => { + const groupDocs = await getDocs(collection(db, "groups")); + return groupDocs.docs.map((x) => ({...x.data(), id: x.id})) as Group[]; +}; + export const getUserGroups = async (id: string): Promise => { const groupDocs = await getDocs(query(collection(db, "groups"), where("admin", "==", id))); return groupDocs.docs.map((x) => ({...x.data(), id})) as Group[]; diff --git a/src/utils/moduleUtils.ts b/src/utils/moduleUtils.ts index 5ee11d48..59843034 100644 --- a/src/utils/moduleUtils.ts +++ b/src/utils/moduleUtils.ts @@ -26,6 +26,7 @@ export const countExercises = (exercises: Exercise[]) => { const lengthMap = exercises.map((e) => { if (e.type === "multipleChoice") return e.questions.length; if (e.type === "interactiveSpeaking") return e.prompts.length; + if (e.type === "fillBlanks") return e.words.length; return 1; });