Merge branch 'develop' into ENCOA-83_MasterStatistical
This commit is contained in:
@@ -44,44 +44,43 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
const calculateScore = () => {
|
const calculateScore = () => {
|
||||||
const total = text.match(/({{\d+}})/g)?.length || 0;
|
const total = text.match(/({{\d+}})/g)?.length || 0;
|
||||||
const correct = userSolutions.filter((x) => {
|
const correct = userSolutions.filter((x) => {
|
||||||
const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution;
|
const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution;
|
||||||
if (!solution) return false;
|
if (!solution) return false;
|
||||||
|
|
||||||
const option = words.find((w) => {
|
const option = words.find((w) => {
|
||||||
if (typeof w === "string") {
|
if (typeof w === "string") {
|
||||||
return w.toLowerCase() === x.solution.toLowerCase();
|
return w.toLowerCase() === x.solution.toLowerCase();
|
||||||
} else if ('letter' in w) {
|
} else if ('letter' in w) {
|
||||||
return w.word.toLowerCase() === x.solution.toLowerCase();
|
return w.word.toLowerCase() === x.solution.toLowerCase();
|
||||||
} else {
|
} else {
|
||||||
return w.id === x.id;
|
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();
|
if (!option) return false;
|
||||||
}*/
|
|
||||||
|
|
||||||
return solution.toLowerCase() === (option.options[x.solution as keyof typeof option.options] || '').toLowerCase();
|
if (typeof option === "string") {
|
||||||
}
|
return solution.toLowerCase() === option.toLowerCase();
|
||||||
return false;
|
} 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;
|
}).length;
|
||||||
|
|
||||||
const missing = total - userSolutions.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length;
|
const missing = total - userSolutions.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length;
|
||||||
|
|
||||||
return { total, correct, missing };
|
return { total, correct, missing };
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderLines = (line: string) => {
|
const renderLines = (line: string) => {
|
||||||
return (
|
return (
|
||||||
@@ -96,24 +95,27 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
variant === "mc" ? (
|
variant === "mc" ? (
|
||||||
<button
|
<>
|
||||||
className={styles}
|
{/*<span className="mr-2">{`(${id})`}</span>*/}
|
||||||
onClick={() => {
|
<button
|
||||||
setCurrentMCSelection(
|
className={styles}
|
||||||
{
|
onClick={() => {
|
||||||
id: id,
|
setCurrentMCSelection(
|
||||||
selection: words.find((x) => {
|
{
|
||||||
if (typeof x !== "string" && 'id' in x) {
|
id: id,
|
||||||
return (x as FillBlanksMCOption).id.toString() == id.toString();
|
selection: words.find((x) => {
|
||||||
}
|
if (typeof x !== "string" && 'id' in x) {
|
||||||
return false;
|
return (x as FillBlanksMCOption).id.toString() == id.toString();
|
||||||
}) as FillBlanksMCOption
|
}
|
||||||
}
|
return false;
|
||||||
);
|
}) as FillBlanksMCOption
|
||||||
}}
|
}
|
||||||
>
|
);
|
||||||
{userSolution?.solution === undefined ? <span className="text-transparent select-none">placeholder</span> : <span> {userSolution.solution} </span>}
|
}}
|
||||||
</button>
|
>
|
||||||
|
{userSolution?.solution === undefined ? <span className="text-transparent select-none">placeholder</span> : <span> {userSolution.solution} </span>}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<input
|
<input
|
||||||
className={styles}
|
className={styles}
|
||||||
@@ -131,28 +133,28 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*const getShuffles = () => {
|
/*const getShuffles = () => {
|
||||||
let shuffle = {};
|
let shuffle = {};
|
||||||
if (shuffleMaps.length !== 0) {
|
if (shuffleMaps.length !== 0) {
|
||||||
shuffle = {
|
shuffle = {
|
||||||
shuffleMaps: shuffleMaps.filter((map) =>
|
shuffleMaps: shuffleMaps.filter((map) =>
|
||||||
answers.some(answer => answer.id === map.id)
|
answers.some(answer => answer.id === map.id)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return shuffle;
|
return shuffle;
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
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,21 +166,33 @@ 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) &&
|
||||||
"bg-mti-purple-dark text-white",
|
"bg-mti-purple-dark text-white",
|
||||||
)}
|
)}
|
||||||
key={v4()}
|
key={v4()}
|
||||||
onClick={() => onSelection(currentMCSelection.id, value)}
|
onClick={() => onSelection(currentMCSelection.id, value)}
|
||||||
>
|
>
|
||||||
{value}
|
{value}
|
||||||
</button>;
|
</button>;*/
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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]}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import {Module} from "@/interfaces";
|
import { Module } from "@/interfaces";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import {moduleLabels} from "@/utils/moduleUtils";
|
import { moduleLabels } from "@/utils/moduleUtils";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {motion} from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import {ReactNode, useEffect, useState} from "react";
|
import { ReactNode, useEffect, useState } from "react";
|
||||||
import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsStopwatch} from "react-icons/bs";
|
import { BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsStopwatch } from "react-icons/bs";
|
||||||
import ProgressBar from "../Low/ProgressBar";
|
import ProgressBar from "../Low/ProgressBar";
|
||||||
import TimerEndedModal from "../TimerEndedModal";
|
import TimerEndedModal from "../TimerEndedModal";
|
||||||
|
|
||||||
@@ -15,15 +15,18 @@ 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);
|
||||||
|
|
||||||
const setHasExamEnded = useExamStore((state) => state.setHasExamEnded);
|
const setHasExamEnded = useExamStore((state) => state.setHasExamEnded);
|
||||||
const {timeSpent} = useExamStore((state) => state);
|
const { timeSpent } = useExamStore((state) => state);
|
||||||
|
|
||||||
useEffect(() => setTimer((prev) => prev - timeSpent), [timeSpent]);
|
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);
|
if (timer < 300 && !warningMode) setWarningMode(true);
|
||||||
}, [timer, warningMode]);
|
}, [timer, warningMode]);
|
||||||
|
|
||||||
const moduleIcon: {[key in Module]: ReactNode} = {
|
const moduleIcon: { [key in Module]: ReactNode } = {
|
||||||
reading: <BsBook className="text-ielts-reading w-6 h-6" />,
|
reading: <BsBook className="text-ielts-reading w-6 h-6" />,
|
||||||
listening: <BsHeadphones className="text-ielts-listening w-6 h-6" />,
|
listening: <BsHeadphones className="text-ielts-listening w-6 h-6" />,
|
||||||
writing: <BsPen className="text-ielts-writing w-6 h-6" />,
|
writing: <BsPen className="text-ielts-writing w-6 h-6" />,
|
||||||
@@ -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",
|
"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",
|
warningMode && !disableTimer && "bg-mti-red-light text-mti-gray-seasalt",
|
||||||
)}
|
)}
|
||||||
initial={{scale: warningMode && !disableTimer ? 0.8 : 1}}
|
initial={{ scale: warningMode && !disableTimer ? 0.8 : 1 }}
|
||||||
animate={{scale: warningMode && !disableTimer ? 1.1 : 1}}
|
animate={{ scale: warningMode && !disableTimer ? 1.1 : 1 }}
|
||||||
transition={{repeat: Infinity, repeatType: "reverse", duration: 0.5, ease: "easeInOut"}}>
|
transition={{ repeat: Infinity, repeatType: "reverse", duration: 0.5, ease: "easeInOut" }}>
|
||||||
<BsStopwatch className="w-6 h-6" />
|
<BsStopwatch className="w-6 h-6" />
|
||||||
<span className="text-base font-semibold w-12">
|
<span className="text-base font-semibold w-12">
|
||||||
{timer > 0 && (
|
{timer > 0 && (
|
||||||
@@ -86,18 +89,24 @@ 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">
|
||||||
<div className="w-12 h-12 bg-mti-gray-smoke flex items-center justify-center rounded-lg">{moduleIcon[module]}</div>
|
{partLabel && <div className="text-3xl space-y-4">{partLabel.split('\n\n').map((line, index) => {
|
||||||
<div className="flex flex-col gap-3 w-full">
|
if(index == 0) return <p className="font-bold">{line}</p>
|
||||||
<div className="w-full flex justify-between">
|
else return <p className="text-2xl font-semibold">{line}</p>
|
||||||
<span className="text-base font-semibold">
|
})}</div>}
|
||||||
{moduleLabels[module]} exam {label && `- ${label}`}
|
<div className={clsx("flex gap-6 w-full h-fit items-center", partLabel ? "mt-10" : "mt-5" )}>
|
||||||
</span>
|
<div className="w-12 h-12 bg-mti-gray-smoke flex items-center justify-center rounded-lg">{moduleIcon[module]}</div>
|
||||||
<span className="text-sm font-semibold self-end">
|
<div className="flex flex-col gap-3 w-full">
|
||||||
Exercise {exerciseIndex}/{totalExercises}
|
<div className="w-full flex justify-between">
|
||||||
</span>
|
<span className="text-base font-semibold">
|
||||||
|
{moduleLabels[module]} exam {label && `- ${label}`}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-semibold self-end">
|
||||||
|
Question {exerciseIndex}/{totalExercises}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ProgressBar color={module} label="" percentage={(exerciseIndex * 100) / totalExercises} className="h-2 w-full" />
|
||||||
</div>
|
</div>
|
||||||
<ProgressBar color={module} label="" percentage={(exerciseIndex * 100) / totalExercises} className="h-2 w-full" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,105 +1,82 @@
|
|||||||
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";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
permissions: Permission[];
|
permissions: Permission[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<Permission>();
|
const columnHelper = createColumnHelper<Permission>();
|
||||||
|
|
||||||
const defaultColumns = [
|
const defaultColumns = [
|
||||||
columnHelper.accessor("type", {
|
columnHelper.accessor("type", {
|
||||||
header: () => <span>Type</span>,
|
header: () => <span>Type</span>,
|
||||||
cell: ({ row, getValue }) => (
|
cell: ({row, getValue}) => (
|
||||||
<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>
|
),
|
||||||
),
|
}),
|
||||||
}),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function PermissionList({ permissions }: Props) {
|
export default function PermissionList({permissions}: Props) {
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: permissions,
|
data: permissions,
|
||||||
columns: defaultColumns,
|
columns: defaultColumns,
|
||||||
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()
|
const parent = row.original.topic;
|
||||||
.rows.reduce((groups: { [key: string]: Row<Permission>[] }, row) => {
|
if (!groups[parent]) {
|
||||||
const parent = row.original.topic;
|
groups[parent] = [];
|
||||||
if (!groups[parent]) {
|
}
|
||||||
groups[parent] = [];
|
groups[parent].push(row);
|
||||||
}
|
return groups;
|
||||||
groups[parent].push(row);
|
}, {});
|
||||||
return groups;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
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>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<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
|
</th>
|
||||||
: flexRender(
|
))}
|
||||||
header.column.columnDef.header,
|
</tr>
|
||||||
header.getContext()
|
))}
|
||||||
)}
|
</thead>
|
||||||
</th>
|
<tbody className="px-2">
|
||||||
))}
|
{Object.keys(groupedData).map((parent) => (
|
||||||
</tr>
|
<React.Fragment key={parent}>
|
||||||
))}
|
<tr>
|
||||||
</thead>
|
<td className="px-2 py-2 items-center w-fit">
|
||||||
<tbody className="px-2">
|
<strong>{parent}</strong>
|
||||||
{Object.keys(groupedData).map((parent) => (
|
</td>
|
||||||
<React.Fragment key={parent}>
|
</tr>
|
||||||
<tr>
|
{groupedData[parent].map((row, i) => (
|
||||||
<td className="px-2 py-2 items-center w-fit">
|
<tr className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2" key={row.id}>
|
||||||
<strong>{parent}</strong>
|
{row.getVisibleCells().map((cell) => (
|
||||||
</td>
|
<td className="px-4 py-2 items-center w-fit" key={cell.id}>
|
||||||
</tr>
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
{groupedData[parent].map((row, i) => (
|
</td>
|
||||||
<tr
|
))}
|
||||||
className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2"
|
</tr>
|
||||||
key={row.id}
|
))}
|
||||||
>
|
</React.Fragment>
|
||||||
{row.getVisibleCells().map((cell) => (
|
))}
|
||||||
<td
|
</tbody>
|
||||||
className="px-4 py-2 items-center w-fit"
|
</table>
|
||||||
key={cell.id}
|
</div>
|
||||||
>
|
</div>
|
||||||
{flexRender(
|
);
|
||||||
cell.column.columnDef.cell,
|
|
||||||
cell.getContext()
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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")}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
/* 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 { withIronSessionSsr } from "iron-session/next";
|
import {withIronSessionSsr} from "iron-session/next";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import useUser from "@/hooks/useUser";
|
import useUser from "@/hooks/useUser";
|
||||||
import { toast, ToastContainer } from "react-toastify";
|
import {toast, ToastContainer} from "react-toastify";
|
||||||
import Layout from "@/components/High/Layout";
|
import Layout from "@/components/High/Layout";
|
||||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
||||||
import { useState } from "react";
|
import {useState} from "react";
|
||||||
import { Module } from "@/interfaces";
|
import {Module} from "@/interfaces";
|
||||||
import { RadioGroup, Tab } from "@headlessui/react";
|
import {RadioGroup, Tab} from "@headlessui/react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { MODULE_ARRAY } from "@/utils/moduleUtils";
|
import {MODULE_ARRAY} from "@/utils/moduleUtils";
|
||||||
import { capitalize } from "lodash";
|
import {capitalize} from "lodash";
|
||||||
import Button from "@/components/Low/Button";
|
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 Input from "@/components/Low/Input";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import ReadingGeneration from "./(generation)/ReadingGeneration";
|
import ReadingGeneration from "./(generation)/ReadingGeneration";
|
||||||
@@ -21,121 +21,114 @@ 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;
|
||||||
|
|
||||||
if (!user || !user.isVerified) {
|
if (!user || !user.isVerified) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
destination: "/login",
|
destination: "/login",
|
||||||
permanent: false,
|
permanent: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (shouldRedirectHome(user) || !checkAccess(user, ["admin", "mastercorporate", "developer", "corporate"])) {
|
||||||
shouldRedirectHome(user) ||
|
return {
|
||||||
checkAccess(user, getTypesOfUser(["developer"]))
|
redirect: {
|
||||||
) {
|
destination: "/",
|
||||||
return {
|
permanent: false,
|
||||||
redirect: {
|
},
|
||||||
destination: "/",
|
};
|
||||||
permanent: false,
|
}
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: { user: req.session.user },
|
props: {user: req.session.user},
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
export default function Generation() {
|
export default function Generation() {
|
||||||
const [module, setModule] = useState<Module>("reading");
|
const [module, setModule] = useState<Module>("reading");
|
||||||
|
|
||||||
const { user } = useUser({ redirectTo: "/login" });
|
const {user} = useUser({redirectTo: "/login"});
|
||||||
|
|
||||||
const [title, setTitle] = useState<string>("");
|
const [title, setTitle] = useState<string>("");
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Exam Generation | EnCoach</title>
|
<title>Exam Generation | EnCoach</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
||||||
/>
|
/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
{user && (
|
{user && (
|
||||||
<Layout user={user} className="gap-6">
|
<Layout user={user} className="gap-6">
|
||||||
<h1 className="text-2xl font-semibold">Exam Generation</h1>
|
<h1 className="text-2xl font-semibold">Exam Generation</h1>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Insert a title here"
|
placeholder="Insert a title here"
|
||||||
name="title"
|
name="title"
|
||||||
label="Title"
|
label="Title"
|
||||||
onChange={setTitle}
|
onChange={setTitle}
|
||||||
roundness="xl"
|
roundness="xl"
|
||||||
defaultValue={title}
|
defaultValue={title}
|
||||||
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
|
<RadioGroup
|
||||||
</label>
|
value={module}
|
||||||
<RadioGroup
|
onChange={setModule}
|
||||||
value={module}
|
className="flex flex-row -2xl:flex-wrap w-full gap-4 -md:justify-center justify-between">
|
||||||
onChange={setModule}
|
{[...MODULE_ARRAY].map((x) => (
|
||||||
className="flex flex-row -2xl:flex-wrap w-full gap-4 -md:justify-center justify-between"
|
<RadioGroup.Option value={x} key={x}>
|
||||||
>
|
{({checked}) => (
|
||||||
{[...MODULE_ARRAY].map((x) => (
|
<span
|
||||||
<RadioGroup.Option value={x} key={x}>
|
className={clsx(
|
||||||
{({ checked }) => (
|
"px-6 py-4 w-64 flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
||||||
<span
|
"transition duration-300 ease-in-out",
|
||||||
className={clsx(
|
x === "reading" &&
|
||||||
"px-6 py-4 w-64 flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
(!checked
|
||||||
"transition duration-300 ease-in-out",
|
? "bg-white border-mti-gray-platinum"
|
||||||
x === "reading" &&
|
: "bg-ielts-reading/70 border-ielts-reading text-white"),
|
||||||
(!checked
|
x === "listening" &&
|
||||||
? "bg-white border-mti-gray-platinum"
|
(!checked
|
||||||
: "bg-ielts-reading/70 border-ielts-reading text-white"),
|
? "bg-white border-mti-gray-platinum"
|
||||||
x === "listening" &&
|
: "bg-ielts-listening/70 border-ielts-listening text-white"),
|
||||||
(!checked
|
x === "writing" &&
|
||||||
? "bg-white border-mti-gray-platinum"
|
(!checked
|
||||||
: "bg-ielts-listening/70 border-ielts-listening text-white"),
|
? "bg-white border-mti-gray-platinum"
|
||||||
x === "writing" &&
|
: "bg-ielts-writing/70 border-ielts-writing text-white"),
|
||||||
(!checked
|
x === "speaking" &&
|
||||||
? "bg-white border-mti-gray-platinum"
|
(!checked
|
||||||
: "bg-ielts-writing/70 border-ielts-writing text-white"),
|
? "bg-white border-mti-gray-platinum"
|
||||||
x === "speaking" &&
|
: "bg-ielts-speaking/70 border-ielts-speaking text-white"),
|
||||||
(!checked
|
x === "level" &&
|
||||||
? "bg-white border-mti-gray-platinum"
|
(!checked
|
||||||
: "bg-ielts-speaking/70 border-ielts-speaking text-white"),
|
? "bg-white border-mti-gray-platinum"
|
||||||
x === "level" &&
|
: "bg-ielts-level/70 border-ielts-level text-white"),
|
||||||
(!checked
|
)}>
|
||||||
? "bg-white border-mti-gray-platinum"
|
{capitalize(x)}
|
||||||
: "bg-ielts-level/70 border-ielts-level text-white")
|
</span>
|
||||||
)}
|
)}
|
||||||
>
|
</RadioGroup.Option>
|
||||||
{capitalize(x)}
|
))}
|
||||||
</span>
|
</RadioGroup>
|
||||||
)}
|
</div>
|
||||||
</RadioGroup.Option>
|
{module === "reading" && <ReadingGeneration id={title} />}
|
||||||
))}
|
{module === "listening" && <ListeningGeneration id={title} />}
|
||||||
</RadioGroup>
|
{module === "writing" && <WritingGeneration id={title} />}
|
||||||
</div>
|
{module === "speaking" && <SpeakingGeneration id={title} />}
|
||||||
{module === "reading" && <ReadingGeneration id={title} />}
|
{module === "level" && <LevelGeneration id={title} />}
|
||||||
{module === "listening" && <ListeningGeneration id={title} />}
|
</Layout>
|
||||||
{module === "writing" && <WritingGeneration id={title} />}
|
)}
|
||||||
{module === "speaking" && <SpeakingGeneration id={title} />}
|
</>
|
||||||
{module === "level" && <LevelGeneration id={title} />}
|
);
|
||||||
</Layout>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,209 +1,209 @@
|
|||||||
/* 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";
|
||||||
import { Permission, PermissionType } from "@/interfaces/permissions";
|
import {Permission, PermissionType} from "@/interfaces/permissions";
|
||||||
import { getPermissionDoc } from "@/utils/permissions.be";
|
import {getPermissionDoc} 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 { getUsers } from "@/utils/users.be";
|
import {getUsers} from "@/utils/users.be";
|
||||||
import { BsTrash } from "react-icons/bs";
|
import {BsTrash} from "react-icons/bs";
|
||||||
import Select from "@/components/Low/Select";
|
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 {
|
||||||
id: string;
|
id: string;
|
||||||
type: PermissionType;
|
type: PermissionType;
|
||||||
users: BasicUser[];
|
users: BasicUser[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async (context) => {
|
export const getServerSideProps = withIronSessionSsr(async (context) => {
|
||||||
const { req, params } = context;
|
const {req, params} = context;
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
|
|
||||||
if (!user || !user.isVerified) {
|
if (!user || !user.isVerified) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
destination: "/login",
|
destination: "/login",
|
||||||
permanent: false,
|
permanent: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldRedirectHome(user)) {
|
if (shouldRedirectHome(user)) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
destination: "/",
|
destination: "/",
|
||||||
permanent: false,
|
permanent: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!params?.id) {
|
if (!params?.id) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
destination: "/permissions",
|
destination: "/permissions",
|
||||||
permanent: false,
|
permanent: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch data from external API
|
// Fetch data from external API
|
||||||
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 users = allUserData.map((u) => ({
|
const userGroups = groups.filter((x) => x.admin === user.id);
|
||||||
id: u.id,
|
const filteredGroups =
|
||||||
name: u.name,
|
user.type === "corporate"
|
||||||
type: u.type
|
? userGroups
|
||||||
})) as BasicUser[];
|
: user.type === "mastercorporate"
|
||||||
|
? groups.filter((x) => userGroups.flatMap((y) => y.participants).includes(x.admin))
|
||||||
|
: groups;
|
||||||
|
|
||||||
// const res = await fetch("api/permissions");
|
const users = allUserData.map((u) => ({
|
||||||
// const permissions: Permission[] = await res.json();
|
id: u.id,
|
||||||
// Pass data to the page via props
|
name: u.name,
|
||||||
const usersData: BasicUser[] = permission.users.reduce(
|
type: u.type,
|
||||||
(acc: BasicUser[], userId) => {
|
})) as BasicUser[];
|
||||||
const user = users.find((u) => u.id === userId) as BasicUser;
|
|
||||||
if (user) {
|
|
||||||
acc.push(user);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
const filteredUsers = ["mastercorporate", "corporate"].includes(user.type)
|
||||||
props: {
|
? users.filter((u) => filteredGroups.flatMap((g) => g.participants).includes(u.id))
|
||||||
// permissions: permissions.map((p) => ({ id: p.id, type: p.type })),
|
: users;
|
||||||
permission: {
|
|
||||||
...permission,
|
// const res = await fetch("api/permissions");
|
||||||
id: params.id,
|
// const permissions: Permission[] = await res.json();
|
||||||
users: usersData,
|
// Pass data to the page via props
|
||||||
},
|
const usersData: BasicUser[] = permission.users.reduce((acc: BasicUser[], userId) => {
|
||||||
user: req.session.user,
|
const user = filteredUsers.find((u) => u.id === userId) as BasicUser;
|
||||||
users,
|
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);
|
}, sessionOptions);
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
permission: PermissionWithBasicUsers;
|
permission: PermissionWithBasicUsers;
|
||||||
user: User;
|
user: User;
|
||||||
users: BasicUser[];
|
users: BasicUser[];
|
||||||
}
|
}
|
||||||
|
|
||||||
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[]>(() =>
|
const onChange = (value: any) => {
|
||||||
permission.users.map((u) => u.id)
|
setSelectedUsers((prev) => {
|
||||||
);
|
if (value?.value) {
|
||||||
|
return [...prev, value?.value];
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const removeUser = (id: string) => {
|
||||||
|
setSelectedUsers((prev) => prev.filter((u) => u !== id));
|
||||||
|
};
|
||||||
|
|
||||||
const onChange = (value: any) => {
|
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");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
setSelectedUsers((prev) => {
|
return (
|
||||||
if (value?.value) {
|
<>
|
||||||
return [...prev, value?.value];
|
<Head>
|
||||||
}
|
<title>EnCoach</title>
|
||||||
return prev;
|
<meta
|
||||||
});
|
name="description"
|
||||||
};
|
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
||||||
const removeUser = (id: string) => {
|
/>
|
||||||
setSelectedUsers((prev) => prev.filter((u) => u !== id));
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
};
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
</Head>
|
||||||
const update = async () => {
|
<ToastContainer />
|
||||||
|
<Layout user={user} className="gap-6">
|
||||||
try {
|
<div className="flex flex-col gap-6 w-full h-[88vh] overflow-y-scroll scrollbar-hide rounded-xl">
|
||||||
await axios.patch(`/api/permissions/${permission.id}`, {
|
<h1 className="text-2xl font-semibold">Permission: {permission.type as string}</h1>
|
||||||
users: selectedUsers,
|
<div className="flex gap-3">
|
||||||
});
|
<Select
|
||||||
toast.success("Permission updated");
|
value={null}
|
||||||
} catch (err) {
|
options={users
|
||||||
toast.error("Failed to update permission");
|
.filter((u) => !selectedUsers.includes(u.id))
|
||||||
}
|
.map((u) => ({
|
||||||
};
|
label: `${u?.type}-${u?.name}`,
|
||||||
|
value: u.id,
|
||||||
return (
|
}))}
|
||||||
<>
|
onChange={onChange}
|
||||||
<Head>
|
/>
|
||||||
<title>EnCoach</title>
|
<Button onClick={update}>Update</Button>
|
||||||
<meta
|
</div>
|
||||||
name="description"
|
<div className="flex flex-row justify-between">
|
||||||
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
<div className="flex flex-col gap-3">
|
||||||
/>
|
<h2>Blacklisted Users</h2>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<div className="flex gap-3 flex-wrap">
|
||||||
<link rel="icon" href="/favicon.ico" />
|
{selectedUsers.map((userId) => {
|
||||||
</Head>
|
const user = users.find((u) => u.id === userId);
|
||||||
<ToastContainer />
|
return (
|
||||||
<Layout user={user} className="gap-6">
|
<div className="flex p-4 rounded-xl w-auto bg-mti-purple-light text-white gap-4" key={userId}>
|
||||||
<h1 className="text-2xl font-semibold">
|
<span className="text-base first-letter:uppercase">
|
||||||
Permission: {permission.type as string}
|
{user?.type}-{user?.name}
|
||||||
</h1>
|
</span>
|
||||||
<div className="flex gap-3">
|
<BsTrash style={{cursor: "pointer"}} onClick={() => removeUser(userId)} size={20} />
|
||||||
<Select
|
</div>
|
||||||
value={null}
|
);
|
||||||
options={users
|
})}
|
||||||
.filter((u) => !selectedUsers.includes(u.id))
|
</div>
|
||||||
.map((u) => ({
|
</div>
|
||||||
label: `${u?.type}-${u?.name}`,
|
<div className="flex flex-col gap-3">
|
||||||
value: u.id,
|
<h2>Whitelisted Users</h2>
|
||||||
}))}
|
<div className="flex flex-col gap-3 flex-wrap">
|
||||||
onChange={onChange}
|
{users
|
||||||
/>
|
.filter((user) => !selectedUsers.includes(user.id))
|
||||||
<Button onClick={update}>Update</Button>
|
.map((user) => {
|
||||||
</div>
|
return (
|
||||||
<div className="flex flex-row justify-between">
|
<div className="flex p-4 rounded-xl w-auto bg-mti-purple-light text-white gap-4" key={user.id}>
|
||||||
<div className="flex flex-col gap-3">
|
<span className="text-base first-letter:uppercase">
|
||||||
<h2>Blacklisted Users</h2>
|
{user?.type}-{user?.name}
|
||||||
<div className="flex gap-3 flex-wrap">
|
</span>
|
||||||
{selectedUsers.map((userId) => {
|
</div>
|
||||||
const user = users.find((u) => u.id === userId);
|
);
|
||||||
return (
|
})}
|
||||||
<div
|
</div>
|
||||||
className="flex p-4 rounded-xl w-auto bg-mti-purple-light text-white gap-4"
|
</div>
|
||||||
key={userId}
|
</div>
|
||||||
>
|
</div>
|
||||||
<span className="text-base first-letter:uppercase">{user?.type}-{user?.name}</span>
|
</Layout>
|
||||||
<BsTrash
|
</>
|
||||||
style={{ cursor: "pointer" }}
|
);
|
||||||
onClick={() => removeUser(userId)}
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<h2>Whitelisted Users</h2>
|
|
||||||
<div className="flex flex-col gap-3 flex-wrap">
|
|
||||||
{users.filter(user => !selectedUsers.includes(user.id)).map((user) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="flex p-4 rounded-xl w-auto bg-mti-purple-light text-white gap-4"
|
|
||||||
key={user.id}
|
|
||||||
>
|
|
||||||
<span className="text-base first-letter:uppercase">{user?.type}-{user?.name}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,78 +1,86 @@
|
|||||||
/* 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 { 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";
|
||||||
import { Permission } from "@/interfaces/permissions";
|
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;
|
||||||
|
|
||||||
if (!user || !user.isVerified) {
|
if (!user || !user.isVerified) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
destination: "/login",
|
destination: "/login",
|
||||||
permanent: false,
|
permanent: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldRedirectHome(user)) {
|
if (shouldRedirectHome(user)) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
destination: "/",
|
destination: "/",
|
||||||
permanent: false,
|
permanent: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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");
|
||||||
|
|
||||||
// const res = await fetch("api/permissions");
|
return true;
|
||||||
// const permissions: Permission[] = await res.json();
|
});
|
||||||
// Pass data to the page via props
|
|
||||||
return {
|
// const res = await fetch("api/permissions");
|
||||||
props: {
|
// const permissions: Permission[] = await res.json();
|
||||||
// permissions: permissions.map((p) => ({ id: p.id, type: p.type })),
|
// Pass data to the page via props
|
||||||
permissions: permissions.map((p) => {
|
return {
|
||||||
const { users, ...rest } = p;
|
props: {
|
||||||
return rest;
|
// permissions: permissions.map((p) => ({ id: p.id, type: p.type })),
|
||||||
}),
|
permissions: filteredPermissions.map((p) => {
|
||||||
user: req.session.user,
|
const {users, ...rest} = p;
|
||||||
},
|
return rest;
|
||||||
};
|
}),
|
||||||
|
user: req.session.user,
|
||||||
|
},
|
||||||
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
permissions: Permission[];
|
permissions: Permission[];
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Page(props: Props) {
|
export default function Page(props: Props) {
|
||||||
const { permissions, user } = props;
|
const {permissions, user} = props;
|
||||||
return (
|
|
||||||
<>
|
return (
|
||||||
<Head>
|
<>
|
||||||
<title>EnCoach</title>
|
<Head>
|
||||||
<meta
|
<title>EnCoach</title>
|
||||||
name="description"
|
<meta
|
||||||
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
name="description"
|
||||||
/>
|
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
/>
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
</Head>
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<Layout user={user} className="gap-6">
|
</Head>
|
||||||
<h1 className="text-2xl font-semibold">Permissions</h1>
|
<Layout user={user} className="gap-6">
|
||||||
<div className="flex gap-3 flex-wrap">
|
<h1 className="text-2xl font-semibold">Permissions</h1>
|
||||||
<PermissionList permissions={permissions} />
|
<div className="flex gap-3 flex-wrap overflow-y-scroll scrollbar-hide h-[80vh] rounded-xl">
|
||||||
</div>
|
<PermissionList permissions={permissions} />
|
||||||
</Layout>
|
</div>
|
||||||
</>
|
</Layout>
|
||||||
);
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user