Merge branch 'develop' into ENCOA-83_MasterStatistical

This commit is contained in:
Joao Ramos
2024-08-20 01:16:46 +01:00
14 changed files with 684 additions and 581 deletions

View File

@@ -72,7 +72,6 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
const original = shuffleMap[x.solution as keyof typeof shuffleMap]; 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[original as keyof typeof option.options] || '').toLowerCase();
}*/ }*/
return solution.toLowerCase() === (option.options[x.solution as keyof typeof option.options] || '').toLowerCase(); return solution.toLowerCase() === (option.options[x.solution as keyof typeof option.options] || '').toLowerCase();
} }
return false; return false;
@@ -96,6 +95,8 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
) )
return ( return (
variant === "mc" ? ( variant === "mc" ? (
<>
{/*<span className="mr-2">{`(${id})`}</span>*/}
<button <button
className={styles} className={styles}
onClick={() => { onClick={() => {
@@ -114,6 +115,7 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
> >
{userSolution?.solution === undefined ? <span className="text-transparent select-none">placeholder</span> : <span> {userSolution.solution} </span>} {userSolution?.solution === undefined ? <span className="text-transparent select-none">placeholder</span> : <span> {userSolution.solution} </span>}
</button> </button>
</>
) : ( ) : (
<input <input
className={styles} className={styles}
@@ -145,14 +147,14 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
return ( return (
<> <>
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20"> <div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
<span className="text-sm w-full leading-6"> {false && <span className="text-sm w-full leading-6">
{prompt.split("\\n").map((line, index) => ( {prompt.split("\\n").map((line, index) => (
<Fragment key={index}> <Fragment key={index}>
{line} {line}
<br /> <br />
</Fragment> </Fragment>
))} ))}
</span> </span>}
<span className="bg-mti-gray-smoke rounded-xl px-5 py-6"> <span className="bg-mti-gray-smoke rounded-xl px-5 py-6">
{text.split("\\n").map((line, index) => ( {text.split("\\n").map((line, index) => (
<p key={index} className={clsx(variant === "mc" && "whitespace-pre-wrap")}> <p key={index} className={clsx(variant === "mc" && "whitespace-pre-wrap")}>
@@ -164,11 +166,23 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
{variant === "mc" && typeCheckWordsMC(words) ? ( {variant === "mc" && typeCheckWordsMC(words) ? (
<> <>
{currentMCSelection && ( {currentMCSelection && (
<div className="bg-mti-gray-smoke rounded-xl px-5 py-6 flex flex-col gap-4"> <div className="bg-mti-gray-smoke rounded-xl flex flex-col gap-4 px-16 py-8">
<span className="font-medium text-mti-purple-dark">Options</span> <span className="font-medium text-lg text-mti-purple-dark mb-4 px-2">{`${currentMCSelection.id} - Select the appropriate word.`}</span>
<div className="flex gap-4 flex-wrap"> <div className="flex gap-4 flex-wrap justify-between">
{currentMCSelection.selection?.options && Object.entries(currentMCSelection.selection.options).sort((a, b) => a[0].localeCompare(b[0])).map(([key, value]) => { {currentMCSelection.selection?.options && Object.entries(currentMCSelection.selection.options).sort((a, b) => a[0].localeCompare(b[0])).map(([key, value]) => {
return <button return <div
key={v4()}
onClick={() => onSelection(currentMCSelection.id, value)}
className={clsx(
"flex border p-4 rounded-xl gap-2 cursor-pointer bg-white text-base",
!!answers.find((x) => x.solution.toLocaleLowerCase() === value.toLocaleLowerCase() && x.id === currentMCSelection.id) &&
"border-mti-purple-light",
)}>
<span className="font-semibold">{key}.</span>
<span>{value}</span>
</div>
/*<button
className={clsx( className={clsx(
"border border-mti-purple-light rounded-full px-3 py-0.5 transition ease-in-out duration-300", "border border-mti-purple-light rounded-full px-3 py-0.5 transition ease-in-out duration-300",
!!answers.find((x) => x.solution.toLocaleLowerCase() === value.toLocaleLowerCase() && x.id === currentMCSelection.id) && !!answers.find((x) => x.solution.toLocaleLowerCase() === value.toLocaleLowerCase() && x.id === currentMCSelection.id) &&
@@ -178,7 +192,7 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
onClick={() => onSelection(currentMCSelection.id, value)} onClick={() => onSelection(currentMCSelection.id, value)}
> >
{value} {value}
</button>; </button>;*/
})} })}
</div> </div>
</div> </div>

View File

@@ -20,24 +20,21 @@ function Question({
showSolution?: boolean, showSolution?: boolean,
}) { }) {
/*
const renderPrompt = (prompt: string) => { const renderPrompt = (prompt: string) => {
return reactStringReplace(prompt, /((<u>)[\w\s']+(<\/u>))/g, (match) => { return reactStringReplace(prompt, /(<u>.*?<\/u>)/g, (match) => {
const word = match.replaceAll("<u>", "").replaceAll("</u>", ""); const word = match.replaceAll("<u>", "").replaceAll("</u>", "");
return word.length > 0 ? <u>{word}</u> : null; return word.length > 0 ? <u>{word}</u> : null;
}); });
}; };
*/
return ( return (
// {renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")} <div className="flex flex-col gap-8">
<div className="flex flex-col gap-10">
{isNaN(Number(id)) ? ( {isNaN(Number(id)) ? (
<span dangerouslySetInnerHTML={{ __html: prompt }} /> <span className={clsx(true ? "text-lg" : "text-base")}>{renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")}</span>
) : ( ) : (
<span className=""> <span className={clsx(true ? "text-lg" : "text-base")}>
<> <>
{id} - <span dangerouslySetInnerHTML={{ __html: prompt }} /> {id} - <span>{renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")}</span>
</> </>
</span> </span>
)} )}
@@ -61,7 +58,7 @@ function Question({
key={option.id.toString()} key={option.id.toString()}
onClick={() => (onSelectOption ? onSelectOption(option.id.toString()) : null)} onClick={() => (onSelectOption ? onSelectOption(option.id.toString()) : null)}
className={clsx( 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", userSolution === option.id.toString() && "border-mti-purple-light",
)}> )}>
<span className="font-semibold">{option.id.toString()}.</span> <span className="font-semibold">{option.id.toString()}.</span>
@@ -156,7 +153,7 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti
return ( return (
<> <>
<div className="flex flex-col gap-2 mt-4 h-fit w-full mb-20 bg-mti-gray-smoke rounded-xl px-16 py-8"> <div className="flex flex-col gap-2 mt-4 h-fit w-full mb-20 bg-mti-gray-smoke rounded-xl px-16 py-8">
<span className="text-xl font-semibold">{prompt}</span> {/*<span className="text-xl font-semibold mb-2">{"Select the appropriate option."}</span>*/}
{questionIndex < questions.length && ( {questionIndex < questions.length && (
<Question <Question
{...questions[questionIndex]} {...questions[questionIndex]}

View File

@@ -15,9 +15,12 @@ interface Props {
exerciseIndex: number; exerciseIndex: number;
totalExercises: number; totalExercises: number;
disableTimer?: boolean; disableTimer?: boolean;
partLabel?: string;
} }
export default function ModuleTitle({minTimer, module, label, exerciseIndex, totalExercises, disableTimer = false}: Props) { export default function ModuleTitle({
minTimer, module, label, exerciseIndex, totalExercises, disableTimer = false, partLabel
}: Props) {
const [timer, setTimer] = useState(minTimer * 60); const [timer, setTimer] = useState(minTimer * 60);
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [warningMode, setWarningMode] = useState(false); const [warningMode, setWarningMode] = useState(false);
@@ -86,7 +89,12 @@ export default function ModuleTitle({minTimer, module, label, exerciseIndex, tot
{timer <= 0 && <>00:00</>} {timer <= 0 && <>00:00</>}
</span> </span>
</motion.div> </motion.div>
<div className="flex gap-6 w-full h-fit items-center mt-5"> <div className="w-full">
{partLabel && <div className="text-3xl space-y-4">{partLabel.split('\n\n').map((line, index) => {
if(index == 0) return <p className="font-bold">{line}</p>
else return <p className="text-2xl font-semibold">{line}</p>
})}</div>}
<div className={clsx("flex gap-6 w-full h-fit items-center", partLabel ? "mt-10" : "mt-5" )}>
<div className="w-12 h-12 bg-mti-gray-smoke flex items-center justify-center rounded-lg">{moduleIcon[module]}</div> <div className="w-12 h-12 bg-mti-gray-smoke flex items-center justify-center rounded-lg">{moduleIcon[module]}</div>
<div className="flex flex-col gap-3 w-full"> <div className="flex flex-col gap-3 w-full">
<div className="w-full flex justify-between"> <div className="w-full flex justify-between">
@@ -94,12 +102,13 @@ export default function ModuleTitle({minTimer, module, label, exerciseIndex, tot
{moduleLabels[module]} exam {label && `- ${label}`} {moduleLabels[module]} exam {label && `- ${label}`}
</span> </span>
<span className="text-sm font-semibold self-end"> <span className="text-sm font-semibold self-end">
Exercise {exerciseIndex}/{totalExercises} Question {exerciseIndex}/{totalExercises}
</span> </span>
</div> </div>
<ProgressBar color={module} label="" percentage={(exerciseIndex * 100) / totalExercises} className="h-2 w-full" /> <ProgressBar color={module} label="" percentage={(exerciseIndex * 100) / totalExercises} className="h-2 w-full" />
</div> </div>
</div> </div>
</div>
</> </>
); );
} }

View File

@@ -1,12 +1,6 @@
import React from "react"; import React from "react";
import {Permission} from "@/interfaces/permissions"; import {Permission} from "@/interfaces/permissions";
import { import {createColumnHelper, flexRender, getCoreRowModel, useReactTable, Row} from "@tanstack/react-table";
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
Row,
} from "@tanstack/react-table";
import Link from "next/link"; import Link from "next/link";
import {convertCamelCaseToReadable} from "@/utils/string"; import {convertCamelCaseToReadable} from "@/utils/string";
@@ -23,8 +17,7 @@ const defaultColumns = [
<Link <Link
href={`/permissions/${row.original.id}`} href={`/permissions/${row.original.id}`}
key={row.id} key={row.id}
className="underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer" className="underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer">
>
{convertCamelCaseToReadable(getValue() as string)} {convertCamelCaseToReadable(getValue() as string)}
</Link> </Link>
), ),
@@ -38,9 +31,7 @@ export default function PermissionList({ permissions }: Props) {
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
}); });
const groupedData: { [key: string]: Row<Permission>[] } = table const groupedData: {[key: string]: Row<Permission>[]} = table.getRowModel().rows.reduce((groups: {[key: string]: Row<Permission>[]}, row) => {
.getRowModel()
.rows.reduce((groups: { [key: string]: Row<Permission>[] }, row) => {
const parent = row.original.topic; const parent = row.original.topic;
if (!groups[parent]) { if (!groups[parent]) {
groups[parent] = []; groups[parent] = [];
@@ -50,7 +41,7 @@ export default function PermissionList({ permissions }: Props) {
}, {}); }, {});
return ( return (
<div className="w-full"> <div className="w-full h-full">
<div className="w-full flex flex-col gap-2"> <div className="w-full flex flex-col gap-2">
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full"> <table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
<thead> <thead>
@@ -58,12 +49,7 @@ export default function PermissionList({ permissions }: Props) {
<tr key={headerGroup.id}> <tr key={headerGroup.id}>
{headerGroup.headers.map((header) => ( {headerGroup.headers.map((header) => (
<th className="py-4 px-4 text-left" key={header.id}> <th className="py-4 px-4 text-left" key={header.id}>
{header.isPlaceholder {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th> </th>
))} ))}
</tr> </tr>
@@ -78,19 +64,10 @@ export default function PermissionList({ permissions }: Props) {
</td> </td>
</tr> </tr>
{groupedData[parent].map((row, i) => ( {groupedData[parent].map((row, i) => (
<tr <tr className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2" key={row.id}>
className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2"
key={row.id}
>
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<td <td className="px-4 py-2 items-center w-fit" key={cell.id}>
className="px-4 py-2 items-center w-fit" {flexRender(cell.column.columnDef.cell, cell.getContext())}
key={cell.id}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</td> </td>
))} ))}
</tr> </tr>

View File

@@ -49,7 +49,7 @@ function Question({
const newSolution = solution; //questionShuffleMap ? getShuffledSolution(solution, questionShuffleMap) : solution; const newSolution = solution; //questionShuffleMap ? getShuffledSolution(solution, questionShuffleMap) : solution;
const renderPrompt = (prompt: string) => { const renderPrompt = (prompt: string) => {
return reactStringReplace(prompt, /((<u>)[\w\s']+(<\/u>))/g, (match) => { return reactStringReplace(prompt, /(<u>.*?<\/u>)/g, (match) => {
const word = match.replaceAll("<u>", "").replaceAll("</u>", ""); const word = match.replaceAll("<u>", "").replaceAll("</u>", "");
return word.length > 0 ? <u>{word}</u> : null; return word.length > 0 ? <u>{word}</u> : null;
}); });

View File

@@ -121,7 +121,7 @@ export default function TeacherDashboard({user}: Props) {
}; };
const GroupsList = () => { const GroupsList = () => {
const filter = (x: Group) => x.admin === user.id || x.participants.includes(user.id); const filter = (x: Group) => x.admin === user.id;
return ( return (
<> <>
@@ -318,7 +318,13 @@ export default function TeacherDashboard({user}: Props) {
color="purple" color="purple"
/> />
{checkAccess(user, ["teacher", "developer"], permissions, "viewGroup") && ( {checkAccess(user, ["teacher", "developer"], permissions, "viewGroup") && (
<IconCard Icon={BsPeople} label="Groups" value={groups.length} color="purple" onClick={() => setPage("groups")} /> <IconCard
Icon={BsPeople}
label="Groups"
value={groups.filter((x) => x.admin === user.id).length}
color="purple"
onClick={() => setPage("groups")}
/>
)} )}
<div <div
onClick={() => setPage("assignments")} onClick={() => setPage("assignments")}

View File

@@ -16,6 +16,7 @@ import clsx from "clsx";
import { Dispatch, Fragment, SetStateAction, use, useEffect, useMemo, useRef, useState } from "react"; import { Dispatch, Fragment, SetStateAction, use, useEffect, useMemo, useRef, useState } from "react";
import { BsChevronDown, BsChevronUp } from "react-icons/bs"; import { BsChevronDown, BsChevronUp } from "react-icons/bs";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { v4 } from "uuid";
interface Props { interface Props {
exam: LevelExam; exam: LevelExam;
@@ -25,9 +26,9 @@ interface Props {
} }
function TextComponent({ function TextComponent({
part, highlightPhrases, contextWord, setContextWordLine part, contextWord, setContextWordLine
}: { }: {
part: LevelPart, highlightPhrases: string[], contextWord: string | undefined, setContextWordLine: React.Dispatch<React.SetStateAction<number | undefined>> part: LevelPart, contextWord: string | undefined, setContextWordLine: React.Dispatch<React.SetStateAction<number | undefined>>
}) { }) {
const textRef = useRef<HTMLDivElement>(null); const textRef = useRef<HTMLDivElement>(null);
const [lineNumbers, setLineNumbers] = useState<number[]>([]); const [lineNumbers, setLineNumbers] = useState<number[]>([]);
@@ -49,13 +50,14 @@ function TextComponent({
offscreenElement.style.width = `${containerWidth}px`; offscreenElement.style.width = `${containerWidth}px`;
offscreenElement.style.font = computedStyle.font; offscreenElement.style.font = computedStyle.font;
offscreenElement.style.lineHeight = computedStyle.lineHeight; offscreenElement.style.lineHeight = computedStyle.lineHeight;
offscreenElement.style.wordWrap = 'break-word';
offscreenElement.style.textAlign = computedStyle.textAlign as CanvasTextAlign; offscreenElement.style.textAlign = computedStyle.textAlign as CanvasTextAlign;
const textContent = textRef.current.textContent || ''; const textContent = textRef.current.textContent || '';
textContent.split(/(\s+)/).forEach((word: string) => { textContent.split(/(\s+)/).forEach((word: string) => {
const span = document.createElement('span'); const span = document.createElement('span');
span.textContent = word; span.textContent = word;
span.style.display = 'inline-block';
span.style.height = `calc(1em + 16px)`;
offscreenElement.appendChild(span); offscreenElement.appendChild(span);
}); });
@@ -139,15 +141,10 @@ function TextComponent({
<div className="flex flex-col gap-2 w-full"> <div className="flex flex-col gap-2 w-full">
<div className="border border-mti-gray-dim w-full rounded-full opacity-10" /> <div className="border border-mti-gray-dim w-full rounded-full opacity-10" />
<div className="flex mt-2"> <div className="flex mt-2">
<div className="flex-shrink-0 w-8 pr-2"> <div ref={textRef} className="h-fit ml-2 flex flex-col gap-4">
{lineNumbers.map(num => ( {part.context!.split('\n\n').map((line, index) => {
<div key={num} className="text-gray-400 flex justify-end" style={{ lineHeight: `${lineHeight}px` }}> return <p key={`line-${index}`}><span className="mr-6">{index + 1}</span>{line}</p>
{num} })}
</div>
))}
</div>
<div ref={textRef} className="h-fit whitespace-pre-wrap ml-2">
<HighlightContent html={part.context!} highlightPhrases={highlightPhrases} firstOccurence={true} />
</div> </div>
</div> </div>
</div> </div>
@@ -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 scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
const [highlightPhrases, setContextHighlight] = useState<string[]>([]);
const [contextWord, setContextWord] = useState<string | undefined>(undefined); const [contextWord, setContextWord] = useState<string | undefined>(undefined);
const [contextWordLine, setContextWordLine] = useState<number | undefined>(undefined); const [contextWordLine, setContextWordLine] = useState<number | undefined>(undefined);
@@ -304,7 +300,6 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
if (match) { if (match) {
const word = match[1]; const word = match[1];
const originalLineNumber = match[2]; const originalLineNumber = match[2];
setContextHighlight([word]);
if (word !== contextWord) { if (word !== contextWord) {
setContextWord(word); setContextWord(word);
@@ -317,7 +312,6 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
currentExercise.questions[storeQuestionIndex].prompt = updatedPrompt; currentExercise.questions[storeQuestionIndex].prompt = updatedPrompt;
} else { } else {
setContextHighlight([]);
setContextWord(undefined); 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 }]); setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level", exam: exam.id }]);
} }
if (storeQuestionIndex > 0) { if (storeQuestionIndex > 0 || currentExercise?.type == "fillBlanks") {
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: storeQuestionIndex }]); setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: currentExercise?.type == "fillBlanks" ? currentExercise.words.length - 1 : storeQuestionIndex }]);
} }
setStoreQuestionIndex(0); 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 }]); setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level", exam: exam.id }]);
} }
if (storeQuestionIndex > 0) { if (storeQuestionIndex > 0 || currentExercise?.type == "fillBlanks") {
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: storeQuestionIndex }]); setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: currentExercise?.type == "fillBlanks" ? currentExercise.words.length - 1 : storeQuestionIndex }]);
} }
setStoreQuestionIndex(0); setStoreQuestionIndex(0);
@@ -397,7 +391,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
exercisesDone + exercisesDone +
(exerciseIndex === -1 ? 0 : exerciseIndex + 1) + (exerciseIndex === -1 ? 0 : exerciseIndex + 1) +
storeQuestionIndex + 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 =
</div> </div>
<TextComponent <TextComponent
part={exam.parts[partIndex]} part={exam.parts[partIndex]}
highlightPhrases={highlightPhrases}
contextWord={contextWord} contextWord={contextWord}
setContextWordLine={setContextWordLine} setContextWordLine={setContextWordLine}
/> />
@@ -420,11 +413,19 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
</div> </div>
); );
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 ( return (
<> <>
<div className="flex flex-col h-full w-full gap-8 items-center"> <div className="flex flex-col h-full w-full gap-8 items-center">
<BlankQuestionsModal isOpen={showBlankModal} onClose={confirmFinishModule} /> <BlankQuestionsModal isOpen={showBlankModal} onClose={confirmFinishModule} />
<ModuleTitle <ModuleTitle
partLabel={partLabel()}
minTimer={exam.minTimer} minTimer={exam.minTimer}
exerciseIndex={calculateExerciseIndex()} exerciseIndex={calculateExerciseIndex()}
module="level" module="level"

View File

@@ -210,7 +210,12 @@ export default function GroupList({user}: {user: User}) {
const {groups, reload} = useGroups({ const {groups, reload} = useGroups({
admin: user && filterTypes.includes(user?.type) ? user.id : undefined, admin: user && filterTypes.includes(user?.type) ? user.id : undefined,
userType: user?.type, userType: user?.type,
adminAdmins: user?.type === "teacher" ? user?.id : undefined, });
const {groups: corporateGroups} = useGroups({
admin: user && filterTypes.includes(user?.type) ? user.id : undefined,
userType: user?.type,
adminAdmins: user?.id,
}); });
useEffect(() => { useEffect(() => {
@@ -310,7 +315,8 @@ export default function GroupList({user}: {user: User}) {
groups groups
.filter((g) => g.admin === user.id) .filter((g) => g.admin === user.id)
.flatMap((g) => g.participants) .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 : users
} }

View File

@@ -21,7 +21,7 @@ import ListeningGeneration from "./(generation)/ListeningGeneration";
import WritingGeneration from "./(generation)/WritingGeneration"; import WritingGeneration from "./(generation)/WritingGeneration";
import LevelGeneration from "./(generation)/LevelGeneration"; import LevelGeneration from "./(generation)/LevelGeneration";
import SpeakingGeneration from "./(generation)/SpeakingGeneration"; import SpeakingGeneration from "./(generation)/SpeakingGeneration";
import { checkAccess, getTypesOfUser } from "@/utils/permissions"; import {checkAccess} from "@/utils/permissions";
export const getServerSideProps = withIronSessionSsr(({req, res}) => { export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user; const user = req.session.user;
@@ -35,10 +35,7 @@ export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
}; };
} }
if ( if (shouldRedirectHome(user) || !checkAccess(user, ["admin", "mastercorporate", "developer", "corporate"])) {
shouldRedirectHome(user) ||
checkAccess(user, getTypesOfUser(["developer"]))
) {
return { return {
redirect: { redirect: {
destination: "/", destination: "/",
@@ -85,14 +82,11 @@ export default function Generation() {
required required
/> />
<label className="font-normal text-base text-mti-gray-dim"> <label className="font-normal text-base text-mti-gray-dim">Module</label>
Module
</label>
<RadioGroup <RadioGroup
value={module} value={module}
onChange={setModule} onChange={setModule}
className="flex flex-row -2xl:flex-wrap w-full gap-4 -md:justify-center justify-between" className="flex flex-row -2xl:flex-wrap w-full gap-4 -md:justify-center justify-between">
>
{[...MODULE_ARRAY].map((x) => ( {[...MODULE_ARRAY].map((x) => (
<RadioGroup.Option value={x} key={x}> <RadioGroup.Option value={x} key={x}>
{({checked}) => ( {({checked}) => (
@@ -119,9 +113,8 @@ export default function Generation() {
x === "level" && x === "level" &&
(!checked (!checked
? "bg-white border-mti-gray-platinum" ? "bg-white border-mti-gray-platinum"
: "bg-ielts-level/70 border-ielts-level text-white") : "bg-ielts-level/70 border-ielts-level text-white"),
)} )}>
>
{capitalize(x)} {capitalize(x)}
</span> </span>
)} )}

View File

@@ -1,6 +1,6 @@
/* eslint-disable @next/next/no-img-element */ /* eslint-disable @next/next/no-img-element */
import Head from "next/head"; import Head from "next/head";
import { useState } from "react"; import {useEffect, useState} from "react";
import {withIronSessionSsr} from "iron-session/next"; import {withIronSessionSsr} from "iron-session/next";
import {sessionOptions} from "@/lib/session"; import {sessionOptions} from "@/lib/session";
import {shouldRedirectHome} from "@/utils/navigation.disabled"; import {shouldRedirectHome} from "@/utils/navigation.disabled";
@@ -14,11 +14,12 @@ import Select from "@/components/Low/Select";
import Button from "@/components/Low/Button"; import Button from "@/components/Low/Button";
import axios from "axios"; import axios from "axios";
import {toast, ToastContainer} from "react-toastify"; import {toast, ToastContainer} from "react-toastify";
import {Type as UserType} from '@/interfaces/user' import {Type as UserType} from "@/interfaces/user";
import {getGroups} from "@/utils/groups.be";
interface BasicUser { interface BasicUser {
id: string; id: string;
name: string; name: string;
type: UserType type: UserType;
} }
interface PermissionWithBasicUsers { interface PermissionWithBasicUsers {
@@ -62,26 +63,34 @@ export const getServerSideProps = withIronSessionSsr(async (context) => {
const permission: Permission = await getPermissionDoc(params.id as string); const permission: Permission = await getPermissionDoc(params.id as string);
const allUserData: User[] = await getUsers(); const allUserData: User[] = await getUsers();
const groups = await getGroups();
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) => ({ const users = allUserData.map((u) => ({
id: u.id, id: u.id,
name: u.name, name: u.name,
type: u.type type: u.type,
})) as BasicUser[]; })) 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 res = await fetch("api/permissions");
// const permissions: Permission[] = await res.json(); // const permissions: Permission[] = await res.json();
// Pass data to the page via props // Pass data to the page via props
const usersData: BasicUser[] = permission.users.reduce( const usersData: BasicUser[] = permission.users.reduce((acc: BasicUser[], userId) => {
(acc: BasicUser[], userId) => { const user = filteredUsers.find((u) => u.id === userId) as BasicUser;
const user = users.find((u) => u.id === userId) as BasicUser; if (!!user) acc.push(user);
if (user) {
acc.push(user);
}
return acc; return acc;
}, }, []);
[]
);
return { return {
props: { props: {
@@ -92,7 +101,7 @@ export const getServerSideProps = withIronSessionSsr(async (context) => {
users: usersData, users: usersData,
}, },
user: req.session.user, user: req.session.user,
users, users: filteredUsers,
}, },
}; };
}, sessionOptions); }, sessionOptions);
@@ -106,13 +115,9 @@ interface Props {
export default function Page(props: Props) { export default function Page(props: Props) {
const {permission, user, users} = props; const {permission, user, users} = props;
const [selectedUsers, setSelectedUsers] = useState<string[]>(() => permission.users.map((u) => u.id));
const [selectedUsers, setSelectedUsers] = useState<string[]>(() =>
permission.users.map((u) => u.id)
);
const onChange = (value: any) => { const onChange = (value: any) => {
setSelectedUsers((prev) => { setSelectedUsers((prev) => {
if (value?.value) { if (value?.value) {
return [...prev, value?.value]; return [...prev, value?.value];
@@ -125,7 +130,6 @@ export default function Page(props: Props) {
}; };
const update = async () => { const update = async () => {
try { try {
await axios.patch(`/api/permissions/${permission.id}`, { await axios.patch(`/api/permissions/${permission.id}`, {
users: selectedUsers, users: selectedUsers,
@@ -149,9 +153,8 @@ export default function Page(props: Props) {
</Head> </Head>
<ToastContainer /> <ToastContainer />
<Layout user={user} className="gap-6"> <Layout user={user} className="gap-6">
<h1 className="text-2xl font-semibold"> <div className="flex flex-col gap-6 w-full h-[88vh] overflow-y-scroll scrollbar-hide rounded-xl">
Permission: {permission.type as string} <h1 className="text-2xl font-semibold">Permission: {permission.type as string}</h1>
</h1>
<div className="flex gap-3"> <div className="flex gap-3">
<Select <Select
value={null} value={null}
@@ -172,16 +175,11 @@ export default function Page(props: Props) {
{selectedUsers.map((userId) => { {selectedUsers.map((userId) => {
const user = users.find((u) => u.id === userId); const user = users.find((u) => u.id === userId);
return ( return (
<div <div className="flex p-4 rounded-xl w-auto bg-mti-purple-light text-white gap-4" key={userId}>
className="flex p-4 rounded-xl w-auto bg-mti-purple-light text-white gap-4" <span className="text-base first-letter:uppercase">
key={userId} {user?.type}-{user?.name}
> </span>
<span className="text-base first-letter:uppercase">{user?.type}-{user?.name}</span> <BsTrash style={{cursor: "pointer"}} onClick={() => removeUser(userId)} size={20} />
<BsTrash
style={{ cursor: "pointer" }}
onClick={() => removeUser(userId)}
size={20}
/>
</div> </div>
); );
})} })}
@@ -190,19 +188,21 @@ export default function Page(props: Props) {
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<h2>Whitelisted Users</h2> <h2>Whitelisted Users</h2>
<div className="flex flex-col gap-3 flex-wrap"> <div className="flex flex-col gap-3 flex-wrap">
{users.filter(user => !selectedUsers.includes(user.id)).map((user) => { {users
.filter((user) => !selectedUsers.includes(user.id))
.map((user) => {
return ( return (
<div <div className="flex p-4 rounded-xl w-auto bg-mti-purple-light text-white gap-4" key={user.id}>
className="flex p-4 rounded-xl w-auto bg-mti-purple-light text-white gap-4" <span className="text-base first-letter:uppercase">
key={user.id} {user?.type}-{user?.name}
> </span>
<span className="text-base first-letter:uppercase">{user?.type}-{user?.name}</span>
</div> </div>
); );
})} })}
</div> </div>
</div> </div>
</div> </div>
</div>
</Layout> </Layout>
</> </>
); );

View File

@@ -7,7 +7,7 @@ import { Permission } from "@/interfaces/permissions";
import {getPermissionDocs} from "@/utils/permissions.be"; import {getPermissionDocs} from "@/utils/permissions.be";
import {User} from "@/interfaces/user"; import {User} from "@/interfaces/user";
import Layout from "@/components/High/Layout"; import Layout from "@/components/High/Layout";
import PermissionList from '@/components/PermissionList' import PermissionList from "@/components/PermissionList";
export const getServerSideProps = withIronSessionSsr(async ({req}) => { export const getServerSideProps = withIronSessionSsr(async ({req}) => {
const user = req.session.user; const user = req.session.user;
@@ -32,7 +32,14 @@ export const getServerSideProps = withIronSessionSsr(async ({ req }) => {
// Fetch data from external API // Fetch data from external API
const permissions: Permission[] = await getPermissionDocs(); 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");
return true;
});
// const res = await fetch("api/permissions"); // const res = await fetch("api/permissions");
// const permissions: Permission[] = await res.json(); // const permissions: Permission[] = await res.json();
@@ -40,7 +47,7 @@ export const getServerSideProps = withIronSessionSsr(async ({ req }) => {
return { return {
props: { props: {
// permissions: permissions.map((p) => ({ id: p.id, type: p.type })), // permissions: permissions.map((p) => ({ id: p.id, type: p.type })),
permissions: permissions.map((p) => { permissions: filteredPermissions.map((p) => {
const {users, ...rest} = p; const {users, ...rest} = p;
return rest; return rest;
}), }),
@@ -56,6 +63,7 @@ interface Props {
export default function Page(props: Props) { export default function Page(props: Props) {
const {permissions, user} = props; const {permissions, user} = props;
return ( return (
<> <>
<Head> <Head>
@@ -69,7 +77,7 @@ export default function Page(props: Props) {
</Head> </Head>
<Layout user={user} className="gap-6"> <Layout user={user} className="gap-6">
<h1 className="text-2xl font-semibold">Permissions</h1> <h1 className="text-2xl font-semibold">Permissions</h1>
<div className="flex gap-3 flex-wrap"> <div className="flex gap-3 flex-wrap overflow-y-scroll scrollbar-hide h-[80vh] rounded-xl">
<PermissionList permissions={permissions} /> <PermissionList permissions={permissions} />
</div> </div>
</Layout> </Layout>

View File

@@ -2,3 +2,89 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@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;
}

View File

@@ -33,6 +33,11 @@ export const updateExpiryDateOnGroup = async (participantID: string, corporateID
return; 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<Group[]> => { export const getUserGroups = async (id: string): Promise<Group[]> => {
const groupDocs = await getDocs(query(collection(db, "groups"), where("admin", "==", id))); const groupDocs = await getDocs(query(collection(db, "groups"), where("admin", "==", id)));
return groupDocs.docs.map((x) => ({...x.data(), id})) as Group[]; return groupDocs.docs.map((x) => ({...x.data(), id})) as Group[];

View File

@@ -26,6 +26,7 @@ export const countExercises = (exercises: Exercise[]) => {
const lengthMap = exercises.map((e) => { const lengthMap = exercises.map((e) => {
if (e.type === "multipleChoice") return e.questions.length; if (e.type === "multipleChoice") return e.questions.length;
if (e.type === "interactiveSpeaking") return e.prompts.length; if (e.type === "interactiveSpeaking") return e.prompts.length;
if (e.type === "fillBlanks") return e.words.length;
return 1; return 1;
}); });