Merge branch 'develop' into ENCOA-83_MasterStatistical

This commit is contained in:
Joao Ramos
2024-08-19 23:38:46 +01:00
54 changed files with 4951 additions and 3823 deletions

17
components.json Normal file
View File

@@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/styles/globals.css",
"baseColor": "neutral",
"cssVariables": false,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

23
package-lock.json generated
View File

@@ -10,6 +10,7 @@
"dependencies": {
"@beam-australia/react-env": "^3.1.1",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@firebase/util": "^1.9.7",
"@headlessui/react": "^1.7.13",
"@mdi/js": "^7.1.96",
@@ -453,6 +454,19 @@
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/sortable": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-8.0.0.tgz",
"integrity": "sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==",
"dependencies": {
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.1.0",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
@@ -10430,6 +10444,15 @@
"tslib": "^2.0.0"
}
},
"@dnd-kit/sortable": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-8.0.0.tgz",
"integrity": "sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==",
"requires": {
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
}
},
"@dnd-kit/utilities": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",

View File

@@ -12,13 +12,15 @@
"dependencies": {
"@beam-australia/react-env": "^3.1.1",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@firebase/util": "^1.9.7",
"@headlessui/react": "^1.7.13",
"@headlessui/react": "^2.1.2",
"@mdi/js": "^7.1.96",
"@mdi/react": "^1.6.1",
"@next/font": "13.1.6",
"@paypal/paypal-js": "^7.1.0",
"@paypal/react-paypal-js": "^8.1.3",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-popover": "^1.1.1",
"@react-pdf/renderer": "^3.1.14",
"@react-spring/web": "^9.7.4",
"@tanstack/react-table": "^8.10.1",
@@ -29,7 +31,8 @@
"axios": "^1.3.5",
"bcrypt": "^5.1.1",
"chart.js": "^4.2.1",
"clsx": "^1.2.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"countries-list": "^3.0.1",
"country-codes-list": "^1.6.11",
"currency-symbol-map": "^5.1.0",
@@ -48,7 +51,7 @@
"lodash": "^4.17.21",
"moment": "^2.29.4",
"moment-timezone": "^0.5.44",
"next": "13.1.6",
"next": "^14.2.5",
"nodemailer": "^6.9.5",
"nodemailer-express-handlebars": "^6.1.0",
"primeicons": "^6.0.1",
@@ -77,7 +80,9 @@
"short-unique-id": "5.0.2",
"stripe": "^13.10.0",
"swr": "^2.1.3",
"tailwind-merge": "^2.5.2",
"tailwind-scrollbar-hide": "^1.1.7",
"tailwindcss-animate": "^1.0.7",
"typescript": "4.9.5",
"use-file-picker": "^2.1.0",
"uuid": "^9.0.0",

View File

@@ -1,187 +0,0 @@
import {FillBlanksExercise} from "@/interfaces/exam";
import useExamStore from "@/stores/examStore";
import clsx from "clsx";
import {Fragment, useEffect, useState} from "react";
import reactStringReplace from "react-string-replace";
import {CommonProps} from ".";
import Button from "../Low/Button";
interface WordsDrawerProps {
words: {word: string; isDisabled: boolean}[];
isOpen: boolean;
blankId?: string;
previouslySelectedWord?: string;
onCancel: () => void;
onAnswer: (answer: string) => void;
}
function WordsDrawer({words, isOpen, blankId, previouslySelectedWord, onCancel, onAnswer}: WordsDrawerProps) {
const [selectedWord, setSelectedWord] = useState<string | undefined>(previouslySelectedWord);
return (
<>
<div
className={clsx(
"w-full h-full absolute top-0 left-0 bg-gradient-to-t from-mti-black to-transparent z-10",
isOpen ? "visible opacity-10" : "invisible opacity-0",
)}
/>
<div
className={clsx(
"absolute w-full bg-white px-7 py-8 bottom-0 left-0 shadow-2xl rounded-2xl z-20 flex flex-col gap-8 transition-opacity duration-300 ease-in-out",
isOpen ? "visible opacity-100" : "invisible opacity-0",
)}>
<div className="w-full flex gap-2">
<div className="rounded-full w-6 h-6 flex items-center justify-center text-white bg-mti-purple-light">{blankId}</div>
<span> Choose the correct word:</span>
</div>
<div className="grid grid-cols-6 gap-6" key="word-array">
{words.map(({word, isDisabled}) => (
<button
key={`${word}_${blankId}`}
onClick={() => setSelectedWord((prev) => (prev === word ? undefined : word))}
className={clsx(
"rounded-full py-3 text-center transition duration-300 ease-in-out",
selectedWord === word ? "text-white bg-mti-purple-light" : "bg-mti-purple-ultralight",
!isDisabled && "hover:text-white hover:bg-mti-purple",
"disabled:cursor-not-allowed disabled:text-mti-gray-dim",
)}
disabled={isDisabled}>
{word}
</button>
))}
</div>
<div className="flex justify-between w-full">
<Button color="purple" variant="outline" className="max-w-[200px] w-full" onClick={onCancel}>
Cancel
</Button>
<Button color="purple" className="max-w-[200px] w-full" onClick={() => onAnswer(selectedWord!)} disabled={!selectedWord}>
Confirm
</Button>
</div>
</div>
</>
);
}
export default function FillBlanks({
id,
allowRepetition,
type,
prompt,
solutions,
text,
words,
userSolutions,
onNext,
onBack,
}: FillBlanksExercise & CommonProps) {
const [answers, setAnswers] = useState<{id: string; solution: string}[]>(userSolutions);
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
useEffect(() => {
if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasExamEnded]);
const calculateScore = () => {
const total = text.match(/({{\d+}})/g)?.length || 0;
const correct = answers.filter((x) => {
const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution.toLowerCase();
if (!solution) return false;
const option = words.find((w) =>
typeof w === "string" ? w.toLowerCase() === x.solution.toLowerCase() : w.letter.toLowerCase() === x.solution.toLowerCase(),
);
if (!option) return false;
return solution === (typeof option === "string" ? option.toLowerCase() : option.word.toLowerCase());
}).length;
const missing = total - answers.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length;
return {total, correct, missing};
};
const renderLines = (line: string) => {
return (
<div className="text-base leading-5">
{reactStringReplace(line, /({{\d+}})/g, (match) => {
const id = match.replaceAll(/[\{\}]/g, "");
const userSolution = answers.find((x) => x.id === id);
return (
<input
className={clsx(
"rounded-full hover:text-white focus:ring-0 focus:outline-none focus:!text-white focus:bg-mti-purple transition duration-300 ease-in-out my-1 px-5 py-2 text-center",
!userSolution && "text-center text-mti-purple-light bg-mti-purple-ultralight",
userSolution && "px-5 py-2 text-center text-mti-purple-dark bg-mti-purple-ultralight",
)}
onChange={(e) => setAnswers((prev) => [...prev.filter((x) => x.id !== id), {id, solution: e.target.value}])}
value={userSolution?.solution}></input>
);
})}
</div>
);
};
return (
<>
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
<span className="text-sm w-full leading-6">
{prompt.split("\\n").map((line, index) => (
<Fragment key={index}>
{line}
<br />
</Fragment>
))}
</span>
<span className="bg-mti-gray-smoke rounded-xl px-5 py-6">
{text.split("\\n").map((line, index) => (
<p key={index}>
{renderLines(line)}
<br />
</p>
))}
</span>
<div className="bg-mti-gray-smoke rounded-xl px-5 py-6 flex flex-col gap-4">
<span className="font-medium text-mti-purple-dark">Options</span>
<div className="flex gap-4 flex-wrap">
{words.map((v) => {
const text = typeof v === "string" ? v : `${v.letter} - ${v.word}`;
return (
<span
className={clsx(
"border border-mti-purple-light rounded-full px-3 py-0.5 transition ease-in-out duration-300",
!!answers.find((x) => x.solution.toLowerCase() === (typeof v === "string" ? v : v.letter).toLowerCase()) &&
"bg-mti-purple-dark text-white",
)}
key={text}>
{text}
</span>
);
})}
</div>
</div>
</div>
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
<Button
color="purple"
variant="outline"
onClick={() => onBack({exercise: id, solutions: answers, score: calculateScore(), type})}
className="max-w-[200px] w-full">
Back
</Button>
<Button
color="purple"
onClick={() => onNext({exercise: id, solutions: answers, score: calculateScore(), type})}
className="max-w-[200px] self-end w-full">
Next
</Button>
</div>
</>
);
}

View File

@@ -0,0 +1,64 @@
import Button from "@/components/Low/Button";
import clsx from "clsx";
import { useState } from "react";
interface WordsDrawerProps {
words: {word: string; isDisabled: boolean}[];
isOpen: boolean;
blankId?: string;
previouslySelectedWord?: string;
onCancel: () => void;
onAnswer: (answer: string) => void;
}
const WordsDrawer: React.FC<WordsDrawerProps> = ({words, isOpen, blankId, previouslySelectedWord, onCancel, onAnswer}) => {
const [selectedWord, setSelectedWord] = useState<string | undefined>(previouslySelectedWord);
return (
<>
<div
className={clsx(
"w-full h-full absolute top-0 left-0 bg-gradient-to-t from-mti-black to-transparent z-10",
isOpen ? "visible opacity-10" : "invisible opacity-0",
)}
/>
<div
className={clsx(
"absolute w-full bg-white px-7 py-8 bottom-0 left-0 shadow-2xl rounded-2xl z-20 flex flex-col gap-8 transition-opacity duration-300 ease-in-out",
isOpen ? "visible opacity-100" : "invisible opacity-0",
)}>
<div className="w-full flex gap-2">
<div className="rounded-full w-6 h-6 flex items-center justify-center text-white bg-mti-purple-light">{blankId}</div>
<span> Choose the correct word:</span>
</div>
<div className="grid grid-cols-6 gap-6" key="word-array">
{words.map(({word, isDisabled}) => (
<button
key={`${word}_${blankId}`}
onClick={() => setSelectedWord((prev) => (prev === word ? undefined : word))}
className={clsx(
"rounded-full py-3 text-center transition duration-300 ease-in-out",
selectedWord === word ? "text-white bg-mti-purple-light" : "bg-mti-purple-ultralight",
!isDisabled && "hover:text-white hover:bg-mti-purple",
"disabled:cursor-not-allowed disabled:text-mti-gray-dim",
)}
disabled={isDisabled}>
{word}
</button>
))}
</div>
<div className="flex justify-between w-full">
<Button color="purple" variant="outline" className="max-w-[200px] w-full" onClick={onCancel}>
Cancel
</Button>
<Button color="purple" className="max-w-[200px] w-full" onClick={() => onAnswer(selectedWord!)} disabled={!selectedWord}>
Confirm
</Button>
</div>
</div>
</>
);
}
export default WordsDrawer;

View File

@@ -0,0 +1,232 @@
import { FillBlanksExercise, FillBlanksMCOption } from "@/interfaces/exam";
import useExamStore from "@/stores/examStore";
import clsx from "clsx";
import { Fragment, useEffect, useState } from "react";
import reactStringReplace from "react-string-replace";
import { CommonProps } from "..";
import Button from "../../Low/Button";
import { v4 } from "uuid";
const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
id,
type,
prompt,
solutions,
text,
words,
userSolutions,
variant,
onNext,
onBack,
}) => {
//const { shuffleMaps } = useExamStore((state) => state);
const [answers, setAnswers] = useState<{ id: string; solution: string }[]>(userSolutions);
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
const [currentMCSelection, setCurrentMCSelection] = useState<{ id: string, selection: FillBlanksMCOption }>();
const typeCheckWordsMC = (words: any[]): words is FillBlanksMCOption[] => {
return Array.isArray(words) && words.every(
word => word && typeof word === 'object' && 'id' in word && 'options' in word
);
}
const excludeWordMCType = (x: any) => {
return typeof x === "string" ? x : x as { letter: string; word: string };
}
useEffect(() => {
if (hasExamEnded) onNext({ exercise: id, solutions: answers, score: calculateScore(), type });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasExamEnded]);
const calculateScore = () => {
const total = text.match(/({{\d+}})/g)?.length || 0;
const correct = userSolutions.filter((x) => {
const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution;
if (!solution) return false;
const option = words.find((w) => {
if (typeof w === "string") {
return w.toLowerCase() === x.solution.toLowerCase();
} else if ('letter' in w) {
return w.word.toLowerCase() === x.solution.toLowerCase();
} else {
return w.id === x.id;
}
});
if (!option) return false;
if (typeof option === "string") {
return solution.toLowerCase() === option.toLowerCase();
} else if ('letter' in option) {
return solution.toLowerCase() === option.word.toLowerCase();
} else if ('options' in option) {
/*
if (shuffleMaps.length !== 0) {
const shuffleMap = shuffleMaps.find((map) => map.id == x.id)
if (!shuffleMap) {
return false;
}
const original = shuffleMap[x.solution as keyof typeof shuffleMap];
return solution.toLowerCase() === (option.options[original as keyof typeof option.options] || '').toLowerCase();
}*/
return solution.toLowerCase() === (option.options[x.solution as keyof typeof option.options] || '').toLowerCase();
}
return false;
}).length;
const missing = total - userSolutions.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length;
return { total, correct, missing };
};
const renderLines = (line: string) => {
return (
<div className="text-base leading-5">
{reactStringReplace(line, /({{\d+}})/g, (match) => {
const id = match.replaceAll(/[\{\}]/g, "");
const userSolution = answers.find((x) => x.id === id);
const styles = clsx(
"rounded-full hover:text-white focus:ring-0 focus:outline-none focus:!text-white focus:bg-mti-purple transition duration-300 ease-in-out my-1 px-5 py-2 text-center",
!userSolution && "text-center text-mti-purple-light bg-mti-purple-ultralight",
userSolution && "text-center text-mti-purple-dark bg-mti-purple-ultralight",
)
return (
variant === "mc" ? (
<button
className={styles}
onClick={() => {
setCurrentMCSelection(
{
id: id,
selection: words.find((x) => {
if (typeof x !== "string" && 'id' in x) {
return (x as FillBlanksMCOption).id.toString() == id.toString();
}
return false;
}) as FillBlanksMCOption
}
);
}}
>
{userSolution?.solution === undefined ? <span className="text-transparent select-none">placeholder</span> : <span> {userSolution.solution} </span>}
</button>
) : (
<input
className={styles}
onChange={(e) => setAnswers((prev) => [...prev.filter((x) => x.id !== id), { id, solution: e.target.value }])}
value={userSolution?.solution} />
)
);
})}
</div>
);
};
const onSelection = (id: string, value: string) => {
setAnswers((prev) => [...prev.filter((x) => x.id !== id), { id: id, solution: value }]);
}
/*const getShuffles = () => {
let shuffle = {};
if (shuffleMaps.length !== 0) {
shuffle = {
shuffleMaps: shuffleMaps.filter((map) =>
answers.some(answer => answer.id === map.id)
)
}
}
return shuffle;
}*/
return (
<>
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
<span className="text-sm w-full leading-6">
{prompt.split("\\n").map((line, index) => (
<Fragment key={index}>
{line}
<br />
</Fragment>
))}
</span>
<span className="bg-mti-gray-smoke rounded-xl px-5 py-6">
{text.split("\\n").map((line, index) => (
<p key={index} className={clsx(variant === "mc" && "whitespace-pre-wrap")}>
{renderLines(line)}
<br />
</p>
))}
</span>
{variant === "mc" && typeCheckWordsMC(words) ? (
<>
{currentMCSelection && (
<div className="bg-mti-gray-smoke rounded-xl px-5 py-6 flex flex-col gap-4">
<span className="font-medium text-mti-purple-dark">Options</span>
<div className="flex gap-4 flex-wrap">
{currentMCSelection.selection?.options && Object.entries(currentMCSelection.selection.options).sort((a, b) => a[0].localeCompare(b[0])).map(([key, value]) => {
return <button
className={clsx(
"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 ) &&
"bg-mti-purple-dark text-white",
)}
key={v4()}
onClick={() => onSelection(currentMCSelection.id, value)}
>
{value}
</button>;
})}
</div>
</div>
)}
</>
) : (
<div className="bg-mti-gray-smoke rounded-xl px-5 py-6 flex flex-col gap-4">
<span className="font-medium text-mti-purple-dark">Options</span>
<div className="flex gap-4 flex-wrap">
{words.map((v) => {
v = excludeWordMCType(v);
const text = typeof v === "string" ? v : `${v.letter} - ${v.word}`;
return (
<span
className={clsx(
"border border-mti-purple-light rounded-full px-3 py-0.5 transition ease-in-out duration-300",
!!answers.find((x) => x.solution.toLowerCase() === (typeof v === "string" ? v : ("letter" in v ? v.letter : "")).toLowerCase()) &&
"bg-mti-purple-dark text-white",
)}
key={v4()}
>
{text}
</span>
)
})}
</div>
</div >
)}
</div>
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
<Button
color="purple"
variant="outline"
onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type, })}//...getShuffles() })}
className="max-w-[200px] w-full">
Back
</Button>
<Button
color="purple"
onClick={() => onNext({ exercise: id, solutions: answers, score: calculateScore(), type, })}//...getShuffles() })}
className="max-w-[200px] self-end w-full">
Next
</Button>
</div>
</>
);
}
export default FillBlanks;

View File

@@ -1,10 +1,10 @@
/* eslint-disable @next/next/no-img-element */
import {MultipleChoiceExercise, MultipleChoiceQuestion} from "@/interfaces/exam";
import { MultipleChoiceExercise, MultipleChoiceQuestion } from "@/interfaces/exam";
import useExamStore from "@/stores/examStore";
import clsx from "clsx";
import {useEffect, useState} from "react";
import { useEffect, useState } from "react";
import reactStringReplace from "react-string-replace";
import {CommonProps} from ".";
import { CommonProps } from ".";
import Button from "../Low/Button";
function Question({
@@ -14,22 +14,30 @@ function Question({
options,
userSolution,
onSelectOption,
}: MultipleChoiceQuestion & {userSolution: string | undefined; onSelectOption?: (option: string) => void; showSolution?: boolean}) {
}: MultipleChoiceQuestion & {
userSolution: string | undefined;
onSelectOption?: (option: string) => void;
showSolution?: boolean,
}) {
/*
const renderPrompt = (prompt: string) => {
return reactStringReplace(prompt, /((<u>)[\w\s']+(<\/u>))/g, (match) => {
const word = match.replaceAll("<u>", "").replaceAll("</u>", "");
return word.length > 0 ? <u>{word}</u> : null;
});
};
*/
return (
// {renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")}
<div className="flex flex-col gap-10">
{isNaN(Number(id)) ? (
<span>{renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")} </span>
<span dangerouslySetInnerHTML={{ __html: prompt }} />
) : (
<span className="">
<>
{id} - <span>{renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")} </span>
{id} - <span dangerouslySetInnerHTML={{ __html: prompt }} />
</>
</span>
)}
@@ -65,53 +73,79 @@ function Question({
);
}
export default function MultipleChoice({id, prompt, type, questions, userSolutions, onNext, onBack}: MultipleChoiceExercise & CommonProps) {
const [answers, setAnswers] = useState<{question: string; option: string}[]>(userSolutions);
export default function MultipleChoice({ id, prompt, type, questions, userSolutions, onNext, onBack }: MultipleChoiceExercise & CommonProps) {
const [answers, setAnswers] = useState<{ question: string; option: string }[]>(userSolutions);
const {questionIndex, setQuestionIndex} = useExamStore((state) => state);
const {userSolutions: storeUserSolutions, setUserSolutions} = useExamStore((state) => state);
//const { shuffleMaps } = useExamStore((state) => state);
const { questionIndex, setQuestionIndex } = useExamStore((state) => state);
const { userSolutions: storeUserSolutions, setUserSolutions } = useExamStore((state) => state);
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
useEffect(() => {
setUserSolutions([...storeUserSolutions.filter((x) => x.exercise !== id), {exercise: id, solutions: answers, score: calculateScore(), type}]);
setUserSolutions(
[...storeUserSolutions.filter((x) => x.exercise !== id), {
exercise: id, solutions: answers, score: calculateScore(), type
}]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [answers]);
useEffect(() => {
if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type});
if (hasExamEnded) onNext({ exercise: id, solutions: answers, score: calculateScore(), type });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasExamEnded]);
const onSelectOption = (option: string) => {
const question = questions[questionIndex];
setAnswers((prev) => [...prev.filter((x) => x.question !== question.id), {option, question: question.id}]);
setAnswers((prev) => [...prev.filter((x) => x.question !== question.id), { option, question: question.id }]);
};
const calculateScore = () => {
const total = questions.length;
const correct = answers.filter(
(x) => questions.find((y) => y.id.toString() === x.question.toString())?.solution === x.option || false,
).length;
const missing = total - answers.filter((x) => questions.find((y) => y.id.toString() === x.question.toString())).length;
const correct = answers.filter((x) => {
const matchingQuestion = questions.find((y) => {
return y.id.toString() === x.question.toString();
});
return {total, correct, missing};
let isSolutionCorrect;
//if (shuffleMaps.length == 0) {
isSolutionCorrect = matchingQuestion?.solution === x.option;
//} else {
// const shuffleMap = shuffleMaps.find((map) => map.id == x.question)
// isSolutionCorrect = shuffleMap?.map[x.option] == matchingQuestion?.solution;
//}
return isSolutionCorrect || false;
}).length;
const missing = total - correct;
return { total, correct, missing };
};
/*const getShuffles = () => {
let shuffle = {};
if (shuffleMaps.length !== 0) {
shuffle = {
shuffleMaps: shuffleMaps.filter((map) =>
answers.some(answer => answer.question === map.id)
)
}
}
return shuffle;
}*/
const next = () => {
if (questionIndex === questions.length - 1) {
onNext({exercise: id, solutions: answers, score: calculateScore(), type});
onNext({ exercise: id, solutions: answers, score: calculateScore(), type, });//...getShuffles() });
} else {
setQuestionIndex(questionIndex + 1);
}
scrollToTop();
};
const back = () => {
if (questionIndex === 0) {
onBack({exercise: id, solutions: answers, score: calculateScore(), type});
onBack({ exercise: id, solutions: answers, score: calculateScore(), type, });// ...getShuffles() });
} else {
setQuestionIndex(questionIndex - 1);
}

View File

@@ -63,7 +63,7 @@ const FillBlanksEdit = (props: Props) => {
label={`Word ${index + 1}`}
name="word"
required
value={typeof word === "string" ? word : word.word}
value={typeof word === "string" ? word : ("word" in word ? word.word : "")}
onChange={(value) =>
updateExercise({
words: exercise.words.map((sol, idx) =>

View File

@@ -0,0 +1,39 @@
import { useCallback } from "react";
const HighlightContent: React.FC<{
html: string;
highlightPhrases: string[],
firstOccurence?: boolean
}> = ({
html,
highlightPhrases,
firstOccurence = false
}) => {
const createHighlightedContent = useCallback(() => {
if (highlightPhrases.length === 0) {
return { __html: html };
}
const escapeRegExp = (string: string) => {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
};
const regex = new RegExp(`(${highlightPhrases.map(escapeRegExp).join('|')})`, 'i');
const globalRegex = new RegExp(`(${highlightPhrases.map(escapeRegExp).join('|')})`, 'gi');
let highlightedHtml = html;
if (firstOccurence) {
highlightedHtml = html.replace(regex, (match) => `<span style="background-color: yellow;">${match}</span>`);
} else {
highlightedHtml = html.replace(globalRegex, (match) => `<span style="background-color: yellow;">${match}</span>`);
}
return { __html: highlightedHtml };
}, [html, highlightPhrases, firstOccurence]);
return <div dangerouslySetInnerHTML={createHighlightedContent()} />;
};
export default HighlightContent;

51
src/components/List.tsx Normal file
View File

@@ -0,0 +1,51 @@
import {Column, flexRender, getCoreRowModel, getSortedRowModel, useReactTable} from "@tanstack/react-table";
export default function List<T>({data, columns}: {data: T[]; columns: any[]}) {
const table = useReactTable({
data,
columns: columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
});
return (
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id} colSpan={header.colSpan}>
{header.isPlaceholder ? null : (
<>
<div
{...{
className: header.column.getCanSort() ? "cursor-pointer select-none py-4 text-left first:pl-4" : "",
onClick: header.column.getToggleSortingHandler(),
}}>
{flexRender(header.column.columnDef.header, header.getContext())}
{{
asc: " 🔼",
desc: " 🔽",
}[header.column.getIsSorted() as string] ?? null}
</div>
</>
)}
</th>
))}
</tr>
))}
</thead>
<tbody className="px-2">
{table.getRowModel().rows.map((row) => (
<tr className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2" key={row.id}>
{row.getVisibleCells().map((cell) => (
<td className="px-4 py-2" key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}

View File

@@ -13,6 +13,7 @@ import {
BsCurrencyDollar,
BsClipboardData,
BsFileLock,
BsPeople,
} from "react-icons/bs";
import {CiDumbbell} from "react-icons/ci";
import {RiLogoutBoxFill} from "react-icons/ri";
@@ -109,6 +110,9 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, u
{checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewStats") && (
<Nav disabled={disableNavigation} Icon={BsGraphUp} label="Stats" path={path} keyPath="/stats" isMinimized={isMinimized} />
)}
{checkAccess(user, ["developer", "admin", "teacher", "student"], permissions) && (
<Nav disabled={disableNavigation} Icon={BsPeople} label="Groups" path={path} keyPath="/groups" isMinimized={isMinimized} />
)}
{checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewRecords") && (
<Nav disabled={disableNavigation} Icon={BsClockHistory} label="Record" path={path} keyPath="/record" isMinimized={isMinimized} />
)}
@@ -146,8 +150,7 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, u
badge={totalAssignedTickets}
/>
)}
{checkAccess(user, ["developer", "admin"]) && (
<>
{checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"]) && (
<Nav
disabled={disableNavigation}
Icon={BsCloudFill}
@@ -156,6 +159,8 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, u
keyPath="/generation"
isMinimized={isMinimized}
/>
)}
{checkAccess(user, ["developer", "admin", "corporate", "mastercorporate", "agent"]) && (
<Nav
disabled={disableNavigation}
Icon={BsFileLock}
@@ -164,7 +169,6 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, u
keyPath="/permissions"
isMinimized={isMinimized}
/>
</>
)}
</div>
<div className="-xl:flex flex-col gap-3 xl:hidden">

View File

@@ -1,8 +1,8 @@
import {FillBlanksExercise} from "@/interfaces/exam";
import { FillBlanksExercise, FillBlanksMCOption } from "@/interfaces/exam";
import clsx from "clsx";
import reactStringReplace from "react-string-replace";
import {CommonProps} from ".";
import {Fragment} from "react";
import { CommonProps } from ".";
import { Fragment } from "react";
import Button from "../Low/Button";
export default function FillBlanksSolutions({
@@ -19,21 +19,42 @@ export default function FillBlanksSolutions({
const calculateScore = () => {
const total = text.match(/({{\d+}})/g)?.length || 0;
const correct = userSolutions.filter((x) => {
const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution.toLowerCase();
const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution;
if (!solution) return false;
const option = words.find((w) =>
typeof w === "string" ? w.toLowerCase() === x.solution.toLowerCase() : w.letter.toLowerCase() === x.solution.toLowerCase(),
);
const option = words.find((w) => {
if (typeof w === "string") {
return w.toLowerCase() === x.solution.toLowerCase();
} else if ('letter' in w) {
return w.word.toLowerCase() === x.solution.toLowerCase();
} else {
return w.id === x.id;
}
});
if (!option) return false;
return solution === (typeof option === "string" ? option.toLowerCase() : option.word.toLowerCase());
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) {
return option.options[solution as keyof typeof option.options] == x.solution;
}
return false;
}).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 typeCheckWordsMC = (words: any[]): words is FillBlanksMCOption[] => {
return Array.isArray(words) && words.every(
word => word && typeof word === 'object' && 'id' in word && 'options' in word
);
}
const renderLines = (line: string) => {
return (
<span>
@@ -56,23 +77,53 @@ export default function FillBlanksSolutions({
const userSolutionWord = words.find((w) =>
typeof w === "string"
? w.toLowerCase() === userSolution.solution.toLowerCase()
: w.letter.toLowerCase() === userSolution.solution.toLowerCase(),
: 'letter' in w
? w.letter.toLowerCase() === userSolution.solution.toLowerCase()
: 'options' in w
? w.id === userSolution.id
: false
);
const userSolutionText = typeof userSolutionWord === "string" ? userSolutionWord : userSolutionWord?.word;
if (userSolutionText === solution.solution) {
const userSolutionText =
typeof userSolutionWord === "string"
? userSolutionWord
: userSolutionWord && 'letter' in userSolutionWord
? userSolutionWord.word
: userSolutionWord && 'options' in userSolutionWord
? userSolution.solution
: userSolution.solution;
let correct;
let solutionText;
if (typeCheckWordsMC(words)) {
const options = words.find((x) => x.id === id);
if (options) {
const correctKey = Object.keys(options.options).find(key =>
key.toLowerCase() === solution.solution.toLowerCase()
);
correct = userSolution.solution == options.options[correctKey as keyof typeof options.options];
solutionText = options.options[correctKey as keyof typeof options.options] || solution.solution;
} else {
correct = false;
solutionText = solution?.solution;
}
} else {
correct = userSolutionText === solution.solution;
solutionText = solution.solution;
}
if (correct) {
return (
<button
className={clsx(
"rounded-full hover:text-white hover:bg-mti-purple transition duration-300 ease-in-out my-1",
userSolution && "px-5 py-2 text-center text-white bg-mti-purple-light",
)}>
{solution.solution}
{solutionText}
</button>
);
}
if (userSolutionText !== solution.solution) {
} else {
return (
<>
<button
@@ -88,7 +139,7 @@ export default function FillBlanksSolutions({
"rounded-full hover:text-white hover:bg-mti-purple transition duration-300 ease-in-out my-1",
userSolution && "px-5 py-2 text-center text-white bg-mti-purple-light",
)}>
{solution.solution}
{solutionText}
</button>
</>
);
@@ -138,14 +189,14 @@ export default function FillBlanksSolutions({
<Button
color="purple"
variant="outline"
onClick={() => onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
onClick={() => onBack({ exercise: id, solutions: userSolutions, score: calculateScore(), type })}
className="max-w-[200px] w-full">
Back
</Button>
<Button
color="purple"
onClick={() => onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
onClick={() => onNext({ exercise: id, solutions: userSolutions, score: calculateScore(), type })}
className="max-w-[200px] self-end w-full">
Next
</Button>

View File

@@ -1,10 +1,10 @@
/* eslint-disable @next/next/no-img-element */
import {MultipleChoiceExercise, MultipleChoiceQuestion} from "@/interfaces/exam";
import { MultipleChoiceExercise, MultipleChoiceQuestion, ShuffleMap } from "@/interfaces/exam";
import useExamStore from "@/stores/examStore";
import clsx from "clsx";
import {useEffect, useState} from "react";
import { useEffect, useState } from "react";
import reactStringReplace from "react-string-replace";
import {CommonProps} from ".";
import { CommonProps } from ".";
import Button from "../Low/Button";
function Question({
@@ -14,7 +14,40 @@ function Question({
solution,
options,
userSolution,
}: MultipleChoiceQuestion & {userSolution: string | undefined; onSelectOption?: (option: string) => void; showSolution?: boolean}) {
}: MultipleChoiceQuestion & { userSolution: string | undefined; onSelectOption?: (option: string) => void; showSolution?: boolean }) {
const { userSolutions } = useExamStore((state) => state);
/*
const getShuffledOptions = (options: {id: string, text: string}[], questionShuffleMap: ShuffleMap) => {
const shuffledOptions = ['A', 'B', 'C', 'D'].map(newId => {
const originalId = questionShuffleMap.map[newId];
const originalOption = options.find(option => option.id === originalId);
return {
id: newId,
text: originalOption!.text
};
});
return shuffledOptions;
}
const getShuffledSolution = (originalSolution: string, questionShuffleMap: ShuffleMap) => {
for (const [newPosition, originalPosition] of Object.entries(questionShuffleMap.map)) {
if (originalPosition === originalSolution) {
return newPosition;
}
}
return originalSolution;
}
const questionShuffleMap = userSolutions.reduce((foundMap, userSolution) => {
if (foundMap) return foundMap;
return userSolution.shuffleMaps?.find(map => map.id === id) || null;
}, null as ShuffleMap | null);
*/
const questionOptions = options; // questionShuffleMap ? getShuffledOptions(options as {id: string, text: string}[], questionShuffleMap) : options;
const newSolution = solution; //questionShuffleMap ? getShuffledSolution(solution, questionShuffleMap) : solution;
const renderPrompt = (prompt: string) => {
return reactStringReplace(prompt, /((<u>)[\w\s']+(<\/u>))/g, (match) => {
const word = match.replaceAll("<u>", "").replaceAll("</u>", "");
@@ -23,11 +56,11 @@ function Question({
};
const optionColor = (option: string) => {
if (option === solution && !userSolution) {
if (option === newSolution && !userSolution) {
return "!border-mti-gray-davy !text-mti-gray-davy";
}
if (option === solution) {
if (option === newSolution) {
return "!border-mti-purple-light !text-mti-purple-light";
}
@@ -47,24 +80,24 @@ function Question({
)}
<div className="grid grid-cols-4 gap-4 place-items-center">
{variant === "image" &&
options.map((option) => (
questionOptions.map((option) => (
<div
key={option.id}
key={option?.id}
className={clsx(
"flex flex-col items-center border border-mti-gray-platinum p-4 px-8 rounded-xl gap-4 cursor-pointer bg-white relative",
optionColor(option.id),
optionColor(option!.id),
)}>
<span className={clsx("text-sm", solution !== option.id && userSolution !== option.id && "opacity-50")}>{option.id}</span>
<img src={option.src!} alt={`Option ${option.id}`} />
<span className={clsx("text-sm", newSolution !== option?.id && userSolution !== option?.id && "opacity-50")}>{option?.id}</span>
{"src" in option && <img src={option?.src!} alt={`Option ${option?.id}`} />}
</div>
))}
{variant === "text" &&
options.map((option) => (
questionOptions.map((option) => (
<div
key={option.id}
className={clsx("flex border p-4 rounded-xl gap-2 cursor-pointer bg-white text-sm", optionColor(option.id))}>
<span className="font-semibold">{option.id}.</span>
<span>{option.text}</span>
key={option?.id}
className={clsx("flex border p-4 rounded-xl gap-2 cursor-pointer bg-white text-sm", optionColor(option!.id))}>
<span className="font-semibold">{option?.id}.</span>
<span>{option?.text}</span>
</div>
))}
</div>
@@ -72,8 +105,8 @@ function Question({
);
}
export default function MultipleChoice({id, type, prompt, questions, userSolutions, onNext, onBack}: MultipleChoiceExercise & CommonProps) {
const {questionIndex, setQuestionIndex} = useExamStore((state) => state);
export default function MultipleChoice({ id, type, prompt, questions, userSolutions, onNext, onBack }: MultipleChoiceExercise & CommonProps) {
const { questionIndex, setQuestionIndex } = useExamStore((state) => state);
const calculateScore = () => {
const total = questions.length;
@@ -82,12 +115,12 @@ export default function MultipleChoice({id, type, prompt, questions, userSolutio
).length;
const missing = total - userSolutions.filter((x) => questions.find((y) => y.id.toString() === x.question.toString())).length;
return {total, correct, missing};
return { total, correct, missing };
};
const next = () => {
if (questionIndex === questions.length - 1) {
onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type});
onNext({ exercise: id, solutions: userSolutions, score: calculateScore(), type });
} else {
setQuestionIndex(questionIndex + 1);
}
@@ -95,7 +128,7 @@ export default function MultipleChoice({id, type, prompt, questions, userSolutio
const back = () => {
if (questionIndex === 0) {
onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type});
onBack({ exercise: id, solutions: userSolutions, score: calculateScore(), type });
} else {
setQuestionIndex(questionIndex - 1);
}

View File

@@ -1,23 +0,0 @@
import { useCallback } from "react";
const HighlightedContent: React.FC<{ html: string; highlightPhrases: string[] }> = ({ html, highlightPhrases }) => {
const createHighlightedContent = useCallback(() => {
if (highlightPhrases.length === 0) {
return { __html: html };
}
const escapeRegExp = (string: string) => {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
};
const regex = new RegExp(`(${highlightPhrases.map(escapeRegExp).join('|')})`, 'gi');
const highlightedHtml = html.replace(regex, (match) => `<span style="background-color: yellow;">${match}</span>`);
return { __html: highlightedHtml };
}, [html, highlightPhrases]);
return <div dangerouslySetInnerHTML={createHighlightedContent()} />;
};
export default HighlightedContent;

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { animated } from '@react-spring/web';
import { FaRegCirclePlay, FaRegCircleStop } from "react-icons/fa6";
import HighlightedContent from './AnimatedHighlight';
import HighlightContent from '../HighlightContent';
import { ITrainingTip, SegmentRef, TimelineEvent } from './TrainingInterfaces';
@@ -267,7 +267,7 @@ const ExerciseWalkthrough: React.FC<ITrainingTip> = (tip: ITrainingTip) => {
<div className='flex-1 bg-white p-6 rounded-lg shadow'>
{/*<h2 className="text-xl font-bold mb-4">Question</h2>*/}
<div className="mb-4" dangerouslySetInnerHTML={{ __html: tip.exercise.question }} />
<HighlightedContent html={tip.exercise.highlightable} highlightPhrases={highlightedPhrases} />
<HighlightContent html={tip.exercise.highlightable} highlightPhrases={highlightedPhrases} />
</div>
<div className='flex-1'>
<div className='bg-gray-50 rounded-lg shadow'>

View File

@@ -525,7 +525,9 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
<Checkbox
isChecked={!!expiryDate}
onChange={(checked) => setExpiryDate(checked ? user.subscriptionExpirationDate || new Date() : null)}
disabled={disabled}>
disabled={
disabled || (!["admin", "developer"].includes(loggedInUser.type) && !!loggedInUser.subscriptionExpirationDate)
}>
Enabled
</Checkbox>
</div>
@@ -564,7 +566,12 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
</div>
</div>
</div>
{checkAccess(loggedInUser, ["developer", "admin"]) && (
{checkAccess(
loggedInUser,
["developer", "admin", "corporate", "mastercorporate"],
permissions,
user.type === "teacher" ? "editTeacher" : user.type === "student" ? "editStudent" : undefined,
) && (
<>
<Divider className="w-full !m-0" />
<div className="flex flex-col md:flex-row gap-8 w-full">
@@ -572,7 +579,10 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
<label className="font-normal text-base text-mti-gray-dim">Status</label>
<Select
className="px-4 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed bg-white rounded-full border border-mti-gray-platinum focus:outline-none"
options={USER_STATUS_OPTIONS}
options={USER_STATUS_OPTIONS.filter((x) => {
if (checkAccess(loggedInUser, ["admin", "developer"])) return true;
return x.value !== "paymentDue";
})}
menuPortalTarget={document?.body}
value={USER_STATUS_OPTIONS.find((o) => o.value === status)}
onChange={(value) => setStatus(value?.value as typeof user.status)}
@@ -600,7 +610,28 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
<label className="font-normal text-base text-mti-gray-dim">Type</label>
<Select
className="px-4 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed bg-white rounded-full border border-mti-gray-platinum focus:outline-none"
options={USER_TYPE_OPTIONS}
options={USER_TYPE_OPTIONS.filter((x) => {
if (x.value === "student")
return checkAccess(
loggedInUser,
["developer", "admin", "corporate", "mastercorporate"],
permissions,
"editStudent",
);
if (x.value === "teacher")
return checkAccess(
loggedInUser,
["developer", "admin", "corporate", "mastercorporate"],
permissions,
"editTeacher",
);
if (x.value === "corporate")
return checkAccess(loggedInUser, ["developer", "admin", "mastercorporate"], permissions, "editCorporate");
return checkAccess(loggedInUser, ["developer", "admin"]);
})}
menuPortalTarget={document?.body}
value={USER_TYPE_OPTIONS.find((o) => o.value === type)}
onChange={(value) => setType(value?.value as typeof user.type)}

View File

@@ -0,0 +1,31 @@
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverAnchor = PopoverPrimitive.Anchor
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border border-neutral-200 bg-white p-4 text-neutral-950 shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

View File

@@ -2,11 +2,11 @@
import Modal from "@/components/Modal";
import useStats from "@/hooks/useStats";
import useUsers from "@/hooks/useUsers";
import { User } from "@/interfaces/user";
import {User} from "@/interfaces/user";
import UserList from "@/pages/(admin)/Lists/UserList";
import { dateSorter } from "@/utils";
import {dateSorter} from "@/utils";
import moment from "moment";
import { useEffect, useState } from "react";
import {useEffect, useState} from "react";
import {
BsArrowLeft,
BsBriefcaseFill,
@@ -23,7 +23,7 @@ import UserCard from "@/components/UserCard";
import useGroups from "@/hooks/useGroups";
import IconCard from "./IconCard";
import useFilterStore from "@/stores/listFilterStore";
import { useRouter } from "next/router";
import {useRouter} from "next/router";
import usePaymentStatusUsers from "@/hooks/usePaymentStatusUsers";
import CorporateStudentsLevels from "./CorporateStudentsLevels";
@@ -31,15 +31,15 @@ interface Props {
user: User;
}
export default function AdminDashboard({ user }: Props) {
export default function AdminDashboard({user}: Props) {
const [page, setPage] = useState("");
const [selectedUser, setSelectedUser] = useState<User>();
const [showModal, setShowModal] = useState(false);
const { stats } = useStats(user.id);
const { users, reload } = useUsers();
const { groups } = useGroups();
const { pending, done } = usePaymentStatusUsers();
const {stats} = useStats(user.id);
const {users, reload} = useUsers();
const {groups} = useGroups({});
const {pending, done} = usePaymentStatusUsers();
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
const router = useRouter();
@@ -52,24 +52,17 @@ export default function AdminDashboard({ user }: Props) {
useEffect(reload, [page]);
const inactiveCountryManagerFilter = (x: User) =>
x.type === "agent" &&
(x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
x.type === "agent" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
const UserDisplay = (displayUser: User) => (
<div
onClick={() => setSelectedUser(displayUser)}
className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300"
>
<img
src={displayUser.profilePicture}
alt={displayUser.name}
className="rounded-full w-10 h-10"
/>
className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300">
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
<div className="flex flex-col gap-1 items-start">
<span>
{displayUser.type === "corporate"
? displayUser.corporateInformation?.companyInformation?.name ||
displayUser.name
? displayUser.corporateInformation?.companyInformation?.name || displayUser.name
: displayUser.name}
</span>
<span className="text-sm opacity-75">{displayUser.email}</span>
@@ -82,11 +75,7 @@ export default function AdminDashboard({ user }: Props) {
x.type === "student" &&
(!!selectedUser
? groups
.filter(
(g) =>
g.admin === selectedUser.id ||
g.participants.includes(selectedUser.id)
)
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
.flatMap((g) => g.participants)
.includes(x.id)
: true);
@@ -99,8 +88,7 @@ export default function AdminDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<div
onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
>
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
@@ -116,11 +104,7 @@ export default function AdminDashboard({ user }: Props) {
x.type === "teacher" &&
(!!selectedUser
? groups
.filter(
(g) =>
g.admin === selectedUser.id ||
g.participants.includes(selectedUser.id)
)
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
.flatMap((g) => g.participants)
.includes(x.id) || false
: true);
@@ -133,8 +117,7 @@ export default function AdminDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<div
onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
>
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
@@ -156,14 +139,11 @@ export default function AdminDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<div
onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
>
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">
Country Managers ({total})
</h2>
<h2 className="text-2xl font-semibold">Country Managers ({total})</h2>
</div>
)}
/>
@@ -178,8 +158,7 @@ export default function AdminDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<div
onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
>
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
@@ -189,7 +168,7 @@ export default function AdminDashboard({ user }: Props) {
/>
);
const CorporatePaidStatusList = ({ paid }: { paid: Boolean }) => {
const CorporatePaidStatusList = ({paid}: {paid: Boolean}) => {
const list = paid ? done : pending;
const filter = (x: User) => x.type === "corporate" && list.includes(x.id);
@@ -201,8 +180,7 @@ export default function AdminDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<div
onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
>
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
@@ -224,14 +202,11 @@ export default function AdminDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<div
onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
>
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">
Inactive Country Managers ({total})
</h2>
<h2 className="text-2xl font-semibold">Inactive Country Managers ({total})</h2>
</div>
)}
/>
@@ -239,10 +214,7 @@ export default function AdminDashboard({ user }: Props) {
};
const InactiveStudentsList = () => {
const filter = (x: User) =>
x.type === "student" &&
(x.status === "disabled" ||
moment().isAfter(x.subscriptionExpirationDate));
const filter = (x: User) => x.type === "student" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
return (
<UserList
@@ -252,14 +224,11 @@ export default function AdminDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<div
onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
>
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">
Inactive Students ({total})
</h2>
<h2 className="text-2xl font-semibold">Inactive Students ({total})</h2>
</div>
)}
/>
@@ -267,10 +236,7 @@ export default function AdminDashboard({ user }: Props) {
};
const InactiveCorporateList = () => {
const filter = (x: User) =>
x.type === "corporate" &&
(x.status === "disabled" ||
moment().isAfter(x.subscriptionExpirationDate));
const filter = (x: User) => x.type === "corporate" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
return (
<UserList
@@ -280,14 +246,11 @@ export default function AdminDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<div
onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
>
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">
Inactive Corporate ({total})
</h2>
<h2 className="text-2xl font-semibold">Inactive Corporate ({total})</h2>
</div>
)}
/>
@@ -300,14 +263,11 @@ export default function AdminDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<div
onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
>
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">
Corporate Students Levels
</h2>
<h2 className="text-2xl font-semibold">Corporate Students Levels</h2>
</div>
<CorporateStudentsLevels />
</>
@@ -348,15 +308,7 @@ export default function AdminDashboard({ user }: Props) {
<IconCard
Icon={BsGlobeCentralSouthAsia}
label="Countries"
value={
[
...new Set(
users
.filter((x) => x.demographicInformation)
.map((x) => x.demographicInformation?.country)
),
].length
}
value={[...new Set(users.filter((x) => x.demographicInformation).map((x) => x.demographicInformation?.country))].length}
color="purple"
/>
<IconCard
@@ -364,12 +316,8 @@ export default function AdminDashboard({ user }: Props) {
Icon={BsPersonFill}
label="Inactive Students"
value={
users.filter(
(x) =>
x.type === "student" &&
(x.status === "disabled" ||
moment().isAfter(x.subscriptionExpirationDate))
).length
users.filter((x) => x.type === "student" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate)))
.length
}
color="rose"
/>
@@ -385,22 +333,12 @@ export default function AdminDashboard({ user }: Props) {
Icon={BsBank}
label="Inactive Corporate"
value={
users.filter(
(x) =>
x.type === "corporate" &&
(x.status === "disabled" ||
moment().isAfter(x.subscriptionExpirationDate))
).length
users.filter((x) => x.type === "corporate" && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate)))
.length
}
color="rose"
/>
<IconCard
onClick={() => setPage("paymentdone")}
Icon={BsCurrencyDollar}
label="Payment Done"
value={done.length}
color="purple"
/>
<IconCard onClick={() => setPage("paymentdone")} Icon={BsCurrencyDollar} label="Payment Done" value={done.length} color="purple" />
<IconCard
onClick={() => setPage("paymentpending")}
Icon={BsCurrencyDollar}
@@ -414,12 +352,7 @@ export default function AdminDashboard({ user }: Props) {
label="Content Management System (CMS)"
color="green"
/>
<IconCard
onClick={() => setPage("corporatestudentslevels")}
Icon={BsPersonFill}
label="Corporate Students Levels"
color="purple"
/>
<IconCard onClick={() => setPage("corporatestudentslevels")} Icon={BsPersonFill} label="Corporate Students Levels" color="purple" />
</section>
<section className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 w-full justify-between">
@@ -464,9 +397,7 @@ export default function AdminDashboard({ user }: Props) {
<span className="p-4">Unpaid Corporate</span>
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users
.filter(
(x) => x.type === "corporate" && x.status === "paymentDue"
)
.filter((x) => x.type === "corporate" && x.status === "paymentDue")
.map((x) => (
<UserDisplay key={x.id} {...x} />
))}
@@ -480,10 +411,8 @@ export default function AdminDashboard({ user }: Props) {
(x) =>
x.type === "student" &&
x.subscriptionExpirationDate &&
moment().isAfter(
moment(x.subscriptionExpirationDate).subtract(30, "days")
) &&
moment().isBefore(moment(x.subscriptionExpirationDate))
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
moment().isBefore(moment(x.subscriptionExpirationDate)),
)
.map((x) => (
<UserDisplay key={x.id} {...x} />
@@ -498,10 +427,8 @@ export default function AdminDashboard({ user }: Props) {
(x) =>
x.type === "teacher" &&
x.subscriptionExpirationDate &&
moment().isAfter(
moment(x.subscriptionExpirationDate).subtract(30, "days")
) &&
moment().isBefore(moment(x.subscriptionExpirationDate))
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
moment().isBefore(moment(x.subscriptionExpirationDate)),
)
.map((x) => (
<UserDisplay key={x.id} {...x} />
@@ -516,10 +443,8 @@ export default function AdminDashboard({ user }: Props) {
(x) =>
x.type === "agent" &&
x.subscriptionExpirationDate &&
moment().isAfter(
moment(x.subscriptionExpirationDate).subtract(30, "days")
) &&
moment().isBefore(moment(x.subscriptionExpirationDate))
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
moment().isBefore(moment(x.subscriptionExpirationDate)),
)
.map((x) => (
<UserDisplay key={x.id} {...x} />
@@ -534,10 +459,8 @@ export default function AdminDashboard({ user }: Props) {
(x) =>
x.type === "corporate" &&
x.subscriptionExpirationDate &&
moment().isAfter(
moment(x.subscriptionExpirationDate).subtract(30, "days")
) &&
moment().isBefore(moment(x.subscriptionExpirationDate))
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
moment().isBefore(moment(x.subscriptionExpirationDate)),
)
.map((x) => (
<UserDisplay key={x.id} {...x} />
@@ -549,10 +472,7 @@ export default function AdminDashboard({ user }: Props) {
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users
.filter(
(x) =>
x.type === "student" &&
x.subscriptionExpirationDate &&
moment().isAfter(moment(x.subscriptionExpirationDate))
(x) => x.type === "student" && x.subscriptionExpirationDate && moment().isAfter(moment(x.subscriptionExpirationDate)),
)
.map((x) => (
<UserDisplay key={x.id} {...x} />
@@ -564,10 +484,7 @@ export default function AdminDashboard({ user }: Props) {
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users
.filter(
(x) =>
x.type === "teacher" &&
x.subscriptionExpirationDate &&
moment().isAfter(moment(x.subscriptionExpirationDate))
(x) => x.type === "teacher" && x.subscriptionExpirationDate && moment().isAfter(moment(x.subscriptionExpirationDate)),
)
.map((x) => (
<UserDisplay key={x.id} {...x} />
@@ -579,10 +496,7 @@ export default function AdminDashboard({ user }: Props) {
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users
.filter(
(x) =>
x.type === "agent" &&
x.subscriptionExpirationDate &&
moment().isAfter(moment(x.subscriptionExpirationDate))
(x) => x.type === "agent" && x.subscriptionExpirationDate && moment().isAfter(moment(x.subscriptionExpirationDate)),
)
.map((x) => (
<UserDisplay key={x.id} {...x} />
@@ -595,9 +509,7 @@ export default function AdminDashboard({ user }: Props) {
{users
.filter(
(x) =>
x.type === "corporate" &&
x.subscriptionExpirationDate &&
moment().isAfter(moment(x.subscriptionExpirationDate))
x.type === "corporate" && x.subscriptionExpirationDate && moment().isAfter(moment(x.subscriptionExpirationDate)),
)
.map((x) => (
<UserDisplay key={x.id} {...x} />
@@ -621,8 +533,7 @@ export default function AdminDashboard({ user }: Props) {
if (shouldReload) reload();
}}
onViewStudents={
selectedUser.type === "corporate" ||
selectedUser.type === "teacher"
selectedUser.type === "corporate" || selectedUser.type === "teacher"
? () => {
appendUserFilters({
id: "view-students",
@@ -632,11 +543,7 @@ export default function AdminDashboard({ user }: Props) {
id: "belongs-to-admin",
filter: (x: User) =>
groups
.filter(
(g) =>
g.admin === selectedUser.id ||
g.participants.includes(selectedUser.id)
)
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
.flatMap((g) => g.participants)
.includes(x.id),
});
@@ -646,8 +553,7 @@ export default function AdminDashboard({ user }: Props) {
: undefined
}
onViewTeachers={
selectedUser.type === "corporate" ||
selectedUser.type === "student"
selectedUser.type === "corporate" || selectedUser.type === "student"
? () => {
appendUserFilters({
id: "view-teachers",
@@ -657,11 +563,7 @@ export default function AdminDashboard({ user }: Props) {
id: "belongs-to-admin",
filter: (x: User) =>
groups
.filter(
(g) =>
g.admin === selectedUser.id ||
g.participants.includes(selectedUser.id)
)
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
.flatMap((g) => g.participants)
.includes(x.id),
});
@@ -671,8 +573,7 @@ export default function AdminDashboard({ user }: Props) {
: undefined
}
onViewCorporate={
selectedUser.type === "teacher" ||
selectedUser.type === "student"
selectedUser.type === "teacher" || selectedUser.type === "student"
? () => {
appendUserFilters({
id: "view-corporate",
@@ -682,9 +583,7 @@ export default function AdminDashboard({ user }: Props) {
id: "belongs-to-admin",
filter: (x: User) =>
groups
.filter((g) =>
g.participants.includes(selectedUser.id)
)
.filter((g) => g.participants.includes(selectedUser.id))
.flatMap((g) => [g.admin, ...g.participants])
.includes(x.id),
});

View File

@@ -2,17 +2,12 @@
import Modal from "@/components/Modal";
import useStats from "@/hooks/useStats";
import useUsers from "@/hooks/useUsers";
import { User } from "@/interfaces/user";
import {User} from "@/interfaces/user";
import UserList from "@/pages/(admin)/Lists/UserList";
import { dateSorter } from "@/utils";
import {dateSorter} from "@/utils";
import moment from "moment";
import { useEffect, useState } from "react";
import {
BsArrowLeft,
BsPersonFill,
BsBank,
BsCurrencyDollar,
} from "react-icons/bs";
import {useEffect, useState} from "react";
import {BsArrowLeft, BsPersonFill, BsBank, BsCurrencyDollar} from "react-icons/bs";
import UserCard from "@/components/UserCard";
import useGroups from "@/hooks/useGroups";
@@ -23,15 +18,14 @@ interface Props {
user: User;
}
export default function AgentDashboard({ user }: Props) {
export default function AgentDashboard({user}: Props) {
const [page, setPage] = useState("");
const [selectedUser, setSelectedUser] = useState<User>();
const [showModal, setShowModal] = useState(false);
const { stats } = useStats();
const { users, reload } = useUsers();
const { groups } = useGroups(user.id);
const { pending, done } = usePaymentStatusUsers();
const {stats} = useStats();
const {users, reload} = useUsers();
const {pending, done} = usePaymentStatusUsers();
useEffect(() => {
setShowModal(!!selectedUser && page === "");
@@ -39,34 +33,19 @@ export default function AgentDashboard({ user }: Props) {
const corporateFilter = (user: User) => user.type === "corporate";
const referredCorporateFilter = (x: User) =>
x.type === "corporate" &&
!!x.corporateInformation &&
x.corporateInformation.referralAgent === user.id;
x.type === "corporate" && !!x.corporateInformation && x.corporateInformation.referralAgent === user.id;
const inactiveReferredCorporateFilter = (x: User) =>
referredCorporateFilter(x) &&
(x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
referredCorporateFilter(x) && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
const UserDisplay = ({
displayUser,
allowClick = true,
}: {
displayUser: User;
allowClick?: boolean;
}) => (
const UserDisplay = ({displayUser, allowClick = true}: {displayUser: User; allowClick?: boolean}) => (
<div
onClick={() => allowClick && setSelectedUser(displayUser)}
className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300"
>
<img
src={displayUser.profilePicture}
alt={displayUser.name}
className="rounded-full w-10 h-10"
/>
className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300">
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
<div className="flex flex-col gap-1 items-start">
<span>
{displayUser.type === "corporate"
? displayUser.corporateInformation?.companyInformation?.name ||
displayUser.name
? displayUser.corporateInformation?.companyInformation?.name || displayUser.name
: displayUser.name}
</span>
<span className="text-sm opacity-75">{displayUser.email}</span>
@@ -83,14 +62,11 @@ export default function AgentDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<div
onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
>
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">
Referred Corporate ({total})
</h2>
<h2 className="text-2xl font-semibold">Referred Corporate ({total})</h2>
</div>
)}
/>
@@ -106,14 +82,11 @@ export default function AgentDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<div
onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
>
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<h2 className="text-2xl font-semibold">
Inactive Referred Corporate ({total})
</h2>
<h2 className="text-2xl font-semibold">Inactive Referred Corporate ({total})</h2>
</div>
)}
/>
@@ -131,8 +104,7 @@ export default function AgentDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<div
onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
>
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
@@ -143,7 +115,7 @@ export default function AgentDashboard({ user }: Props) {
);
};
const CorporatePaidStatusList = ({ paid }: { paid: Boolean }) => {
const CorporatePaidStatusList = ({paid}: {paid: Boolean}) => {
const list = paid ? done : pending;
const filter = (x: User) => x.type === "corporate" && list.includes(x.id);
@@ -155,8 +127,7 @@ export default function AgentDashboard({ user }: Props) {
<div className="flex flex-col gap-4">
<div
onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
>
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
@@ -193,13 +164,7 @@ export default function AgentDashboard({ user }: Props) {
value={users.filter(corporateFilter).length}
color="purple"
/>
<IconCard
onClick={() => setPage("paymentdone")}
Icon={BsCurrencyDollar}
label="Payment Done"
value={done.length}
color="purple"
/>
<IconCard onClick={() => setPage("paymentdone")} Icon={BsCurrencyDollar} label="Payment Done" value={done.length} color="purple" />
<IconCard
onClick={() => setPage("paymentpending")}
Icon={BsCurrencyDollar}
@@ -239,10 +204,8 @@ export default function AgentDashboard({ user }: Props) {
.filter(
(x) =>
referredCorporateFilter(x) &&
moment().isAfter(
moment(x.subscriptionExpirationDate).subtract(30, "days")
) &&
moment().isBefore(moment(x.subscriptionExpirationDate))
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
moment().isBefore(moment(x.subscriptionExpirationDate)),
)
.map((x) => (
<UserDisplay key={x.id} displayUser={x} />
@@ -266,16 +229,9 @@ export default function AgentDashboard({ user }: Props) {
if (shouldReload) reload();
}}
onViewStudents={
selectedUser.type === "corporate" ||
selectedUser.type === "teacher"
? () => setPage("students")
: undefined
}
onViewTeachers={
selectedUser.type === "corporate"
? () => setPage("teachers")
: undefined
selectedUser.type === "corporate" || selectedUser.type === "teacher" ? () => setPage("students") : undefined
}
onViewTeachers={selectedUser.type === "corporate" ? () => setPage("teachers") : undefined}
user={selectedUser}
/>
</div>
@@ -284,9 +240,7 @@ export default function AgentDashboard({ user }: Props) {
</Modal>
{page === "referredCorporate" && <ReferredCorporateList />}
{page === "corporate" && <CorporateList />}
{page === "inactiveReferredCorporate" && (
<InactiveReferredCorporateList />
)}
{page === "inactiveReferredCorporate" && <InactiveReferredCorporateList />}
{page === "paymentdone" && <CorporatePaidStatusList paid={true} />}
{page === "paymentpending" && <CorporatePaidStatusList paid={false} />}
{page === "" && <DefaultDashboard />}

View File

@@ -11,8 +11,10 @@ import {useAssignmentArchive} from "@/hooks/useAssignmentArchive";
import {uniqBy} from "lodash";
import {useAssignmentUnarchive} from "@/hooks/useAssignmentUnarchive";
import {getUserName} from "@/utils/users";
import {User} from "@/interfaces/user";
interface Props {
users: User[];
onClick?: () => void;
allowDownload?: boolean;
reload?: Function;
@@ -37,9 +39,8 @@ export default function AssignmentCard({
allowArchive,
allowUnarchive,
allowExcelDownload,
users,
}: Assignment & Props) {
const {users} = useUsers();
const renderPdfIcon = usePDFDownload("assignments");
const renderExcelIcon = usePDFDownload("assignments", "excel");
const renderArchiveIcon = useAssignmentArchive(id, reload);

View File

@@ -371,7 +371,7 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
!startDate ||
!endDate ||
assignees.length === 0 ||
(!!examIDs && examIDs.length < selectedModules.length)
(!useRandomExams && examIDs.length < selectedModules.length)
}
className="w-full max-w-[200px]"
onClick={createAssignment}

View File

@@ -29,7 +29,7 @@ import {
} from "react-icons/bs";
import UserCard from "@/components/UserCard";
import useGroups from "@/hooks/useGroups";
import {calculateAverageLevel, calculateBandScore} from "@/utils/score";
import {averageLevelCalculator, calculateAverageLevel, calculateBandScore} from "@/utils/score";
import {MODULE_ARRAY} from "@/utils/moduleUtils";
import {Module} from "@/interfaces";
import {groupByExam} from "@/utils/stats";
@@ -45,11 +45,113 @@ import AssignmentView from "./AssignmentView";
import AssignmentCreator from "./AssignmentCreator";
import clsx from "clsx";
import AssignmentCard from "./AssignmentCard";
import {createColumnHelper} from "@tanstack/react-table";
import Checkbox from "@/components/Low/Checkbox";
import List from "@/components/List";
import {getUserCompanyName} from "@/resources/user";
interface Props {
user: CorporateUser;
}
type StudentPerformanceItem = User & {corporateName: string; group: string};
const StudentPerformanceList = ({items, stats, users}: {items: StudentPerformanceItem[]; stats: Stat[]; users: User[]}) => {
const [isShowingAmount, setIsShowingAmount] = useState(false);
const columnHelper = createColumnHelper<StudentPerformanceItem>();
const columns = [
columnHelper.accessor("name", {
header: "Student Name",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("email", {
header: "E-mail",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("demographicInformation.passport_id", {
header: "ID",
cell: (info) => info.getValue() || "N/A",
}),
columnHelper.accessor("group", {
header: "Group",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("corporateName", {
header: "Corporate",
cell: (info) => info.getValue() || "N/A",
}),
columnHelper.accessor("levels.reading", {
header: "Reading",
cell: (info) =>
!isShowingAmount
? info.getValue() || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "reading" && x.user === info.row.original.id))).length} exams`,
}),
columnHelper.accessor("levels.listening", {
header: "Listening",
cell: (info) =>
!isShowingAmount
? info.getValue() || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "listening" && x.user === info.row.original.id))).length} exams`,
}),
columnHelper.accessor("levels.writing", {
header: "Writing",
cell: (info) =>
!isShowingAmount
? info.getValue() || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "writing" && x.user === info.row.original.id))).length} exams`,
}),
columnHelper.accessor("levels.speaking", {
header: "Speaking",
cell: (info) =>
!isShowingAmount
? info.getValue() || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "speaking" && x.user === info.row.original.id))).length} exams`,
}),
columnHelper.accessor("levels.level", {
header: "Level",
cell: (info) =>
!isShowingAmount
? info.getValue() || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "level" && x.user === info.row.original.id))).length} exams`,
}),
columnHelper.accessor("levels", {
id: "overall_level",
header: "Overall",
cell: (info) =>
!isShowingAmount
? averageLevelCalculator(
users,
stats.filter((x) => x.user === info.row.original.id),
).toFixed(1)
: `${Object.keys(groupByExam(stats.filter((x) => x.user === info.row.original.id))).length} exams`,
}),
];
return (
<div className="flex flex-col gap-4 w-full h-full">
<Checkbox isChecked={isShowingAmount} onChange={setIsShowingAmount}>
Show Utilization
</Checkbox>
<List<StudentPerformanceItem>
data={items.sort(
(a, b) =>
averageLevelCalculator(
users,
stats.filter((x) => x.user === b.id),
) -
averageLevelCalculator(
users,
stats.filter((x) => x.user === a.id),
),
)}
columns={columns}
/>
</div>
);
};
export default function CorporateDashboard({user}: Props) {
const [page, setPage] = useState("");
const [selectedUser, setSelectedUser] = useState<User>();
@@ -57,11 +159,12 @@ export default function CorporateDashboard({user}: Props) {
const [corporateUserToShow, setCorporateUserToShow] = useState<CorporateUser>();
const [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
const [userBalance, setUserBalance] = useState(0);
const {stats} = useStats();
const {users, reload} = useUsers();
const {users, reload, isLoading} = useUsers();
const {codes} = useCodes(user.id);
const {groups} = useGroups(user.id);
const {groups} = useGroups({admin: user.id});
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({corporate: user.id});
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
@@ -71,6 +174,14 @@ export default function CorporateDashboard({user}: Props) {
setShowModal(!!selectedUser && page === "");
}, [selectedUser, page]);
useEffect(() => {
const relatedGroups = groups.filter((x) => x.name === "Students" || x.name === "Teachers" || x.name === "Corporate");
const usersInGroups = relatedGroups.map((x) => x.participants).flat();
const filteredCodes = codes.filter((x) => !x.userId || !usersInGroups.includes(x.userId));
setUserBalance(usersInGroups.length + filteredCodes.length);
}, [codes, groups]);
useEffect(() => {
// in this case it fetches the master corporate account
getUserCorporate(user.id).then(setCorporateUserToShow);
@@ -228,7 +339,7 @@ export default function CorporateDashboard({user}: Props) {
<h2 className="text-2xl font-semibold">Active Assignments ({assignments.filter(activeFilter).length})</h2>
<div className="flex flex-wrap gap-2">
{assignments.filter(activeFilter).map((a) => (
<AssignmentCard {...a} onClick={() => setSelectedAssignment(a)} key={a.id} />
<AssignmentCard {...a} users={users} onClick={() => setSelectedAssignment(a)} key={a.id} />
))}
</div>
</section>
@@ -244,6 +355,7 @@ export default function CorporateDashboard({user}: Props) {
{assignments.filter(futureFilter).map((a) => (
<AssignmentCard
{...a}
users={users}
onClick={() => {
setSelectedAssignment(a);
setIsCreatingAssignment(true);
@@ -259,6 +371,7 @@ export default function CorporateDashboard({user}: Props) {
{assignments.filter(pastFilter).map((a) => (
<AssignmentCard
{...a}
users={users}
onClick={() => setSelectedAssignment(a)}
key={a.id}
allowDownload
@@ -275,6 +388,7 @@ export default function CorporateDashboard({user}: Props) {
{assignments.filter(archivedFilter).map((a) => (
<AssignmentCard
{...a}
users={users}
onClick={() => setSelectedAssignment(a)}
key={a.id}
allowDownload
@@ -289,6 +403,36 @@ export default function CorporateDashboard({user}: Props) {
);
};
const StudentPerformancePage = () => {
const students = users
.filter((x) => x.type === "student" && groups.flatMap((g) => g.participants).includes(x.id))
.map((u) => ({
...u,
group: groups.find((x) => x.participants.includes(u.id))?.name || "N/A",
corporateName: getUserCompanyName(u, users, groups),
}));
return (
<>
<div className="w-full flex justify-between items-center">
<div
onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<div
onClick={reload}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<span>Reload</span>
<BsArrowRepeat className={clsx("text-xl", isLoading && "animate-spin")} />
</div>
</div>
<StudentPerformanceList items={students} stats={stats} users={users} />
</>
);
};
const averageLevelCalculator = (studentStats: Stat[]) => {
const formattedStats = studentStats
.map((s) => ({
@@ -352,7 +496,7 @@ export default function CorporateDashboard({user}: Props) {
<IconCard
Icon={BsPersonCheck}
label="User Balance"
value={`${codes.length}/${user.corporateInformation?.companyInformation?.userAmount || 0}`}
value={`${userBalance}/${user.corporateInformation?.companyInformation?.userAmount || 0}`}
color="purple"
/>
<IconCard
@@ -361,6 +505,13 @@ export default function CorporateDashboard({user}: Props) {
value={user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy") : "Unlimited"}
color="rose"
/>
<IconCard
Icon={BsPersonFillGear}
label="Student Performance"
value={users.filter(studentFilter).length}
color="purple"
onClick={() => setPage("studentsPerformance")}
/>
<button
disabled={isAssignmentsLoading}
onClick={() => setPage("assignments")}
@@ -489,6 +640,7 @@ export default function CorporateDashboard({user}: Props) {
{page === "teachers" && <TeachersList />}
{page === "groups" && <GroupsList />}
{page === "assignments" && <AssignmentsPage />}
{page === "studentsPerformance" && <StudentPerformancePage />}
{page === "" && <DefaultDashboard />}
</>
);

View File

@@ -1,23 +1,17 @@
import React from "react";
import useUsers from "@/hooks/useUsers";
import useGroups from "@/hooks/useGroups";
import { User } from "@/interfaces/user";
import {User} from "@/interfaces/user";
import Select from "@/components/Low/Select";
import ProgressBar from "@/components/Low/ProgressBar";
import {
BsBook,
BsClipboard,
BsHeadphones,
BsMegaphone,
BsPen,
} from "react-icons/bs";
import { MODULE_ARRAY } from "@/utils/moduleUtils";
import { capitalize } from "lodash";
import { getLevelLabel } from "@/utils/score";
import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs";
import {MODULE_ARRAY} from "@/utils/moduleUtils";
import {capitalize} from "lodash";
import {getLevelLabel} from "@/utils/score";
const Card = ({ user }: { user: User }) => {
const Card = ({user}: {user: User}) => {
return (
<div className="border-mti-gray-platinum flex flex-col h-fit w-full cursor-pointer flex-col gap-6 rounded-xl border bg-white p-4 transition duration-300 ease-in-out hover:drop-shadow">
<div className="border-mti-gray-platinum flex flex-col h-fit w-full cursor-pointer gap-6 rounded-xl border bg-white p-4 transition duration-300 ease-in-out hover:drop-shadow">
<div className="flex flex-col gap-3">
<h3 className="text-xl font-semibold">{user.name}</h3>
</div>
@@ -26,38 +20,19 @@ const Card = ({ user }: { user: User }) => {
const desiredLevel = user.desiredLevels[module] || 9;
const level = user.levels[module] || 0;
return (
<div
className="border-mti-gray-anti-flash flex flex-col gap-2 rounded-xl border p-4 min-w-[250px]"
key={module}
>
<div className="border-mti-gray-anti-flash flex flex-col gap-2 rounded-xl border p-4 min-w-[250px]" key={module}>
<div className="flex items-center gap-2 md:gap-3">
<div className="bg-mti-gray-smoke flex h-8 w-8 items-center justify-center rounded-lg md:h-12 md:w-12 md:rounded-xl">
{module === "reading" && (
<BsBook className="text-ielts-reading h-4 w-4 md:h-5 md:w-5" />
)}
{module === "listening" && (
<BsHeadphones className="text-ielts-listening h-4 w-4 md:h-5 md:w-5" />
)}
{module === "writing" && (
<BsPen className="text-ielts-writing h-4 w-4 md:h-5 md:w-5" />
)}
{module === "speaking" && (
<BsMegaphone className="text-ielts-speaking h-4 w-4 md:h-5 md:w-5" />
)}
{module === "level" && (
<BsClipboard className="text-ielts-level h-4 w-4 md:h-5 md:w-5" />
)}
{module === "reading" && <BsBook className="text-ielts-reading h-4 w-4 md:h-5 md:w-5" />}
{module === "listening" && <BsHeadphones className="text-ielts-listening h-4 w-4 md:h-5 md:w-5" />}
{module === "writing" && <BsPen className="text-ielts-writing h-4 w-4 md:h-5 md:w-5" />}
{module === "speaking" && <BsMegaphone className="text-ielts-speaking h-4 w-4 md:h-5 md:w-5" />}
{module === "level" && <BsClipboard className="text-ielts-level h-4 w-4 md:h-5 md:w-5" />}
</div>
<div className="flex w-full flex-col">
<span className="text-sm font-bold md:font-extrabold w-full">
{capitalize(module)}
</span>
<span className="text-sm font-bold md:font-extrabold w-full">{capitalize(module)}</span>
<div className="text-mti-gray-dim text-sm font-normal">
{module === "level" && (
<span>
English Level: {getLevelLabel(level).join(" / ")}
</span>
)}
{module === "level" && <span>English Level: {getLevelLabel(level).join(" / ")}</span>}
{module !== "level" && (
<div className="flex flex-col">
<span>Level {level} / Level 9</span>
@@ -86,17 +61,14 @@ const Card = ({ user }: { user: User }) => {
};
const CorporateStudentsLevels = () => {
const { users } = useUsers();
const { groups } = useGroups();
const {users} = useUsers();
const {groups} = useGroups({});
const corporateUsers = users.filter((u) => u.type === "corporate") as User[];
const [corporateId, setCorporateId] = React.useState<string>("");
const corporate =
corporateUsers.find((u) => u.id === corporateId) || corporateUsers[0];
const corporate = corporateUsers.find((u) => u.id === corporateId) || corporateUsers[0];
const groupsFromCorporate = corporate
? groups.filter((g) => g.admin === corporate.id)
: [];
const groupsFromCorporate = corporate ? groups.filter((g) => g.admin === corporate.id) : [];
const groupsParticipants = groupsFromCorporate
.flatMap((g) => g.participants)
@@ -115,17 +87,13 @@ const CorporateStudentsLevels = () => {
value: x.id,
label: `${x.name} - ${x.email}`,
}))}
value={corporate ? { value: corporate.id, label: corporate.name } : null}
value={corporate ? {value: corporate.id, label: corporate.name} : null}
onChange={(value) => setCorporateId(value?.value!)}
styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
menuPortal: (base) => ({...base, zIndex: 9999}),
option: (styles, state) => ({
...styles,
backgroundColor: state.isFocused
? "#D5D9F0"
: state.isSelected
? "#7872BF"
: "white",
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
color: state.isFocused ? "black" : styles.color,
}),
}}

View File

@@ -2,13 +2,7 @@
import Modal from "@/components/Modal";
import useStats from "@/hooks/useStats";
import useUsers from "@/hooks/useUsers";
import {
Group,
MasterCorporateUser,
Stat,
User,
CorporateUser,
} from "@/interfaces/user";
import {CorporateUser, Group, MasterCorporateUser, Stat, User} from "@/interfaces/user";
import UserList from "@/pages/(admin)/Lists/UserList";
import { dateSorter } from "@/utils";
import moment from "moment";
@@ -26,15 +20,17 @@ import {
BsEnvelopePaper,
BsArrowRepeat,
BsPlus,
BsDatabase,
BsPersonFillGear,
BsFilter,
BsDatabase
} from "react-icons/bs";
import UserCard from "@/components/UserCard";
import useGroups from "@/hooks/useGroups";
import { calculateAverageLevel, calculateBandScore } from "@/utils/score";
import { MODULE_ARRAY } from "@/utils/moduleUtils";
import { Module } from "@/interfaces";
import { groupByExam } from "@/utils/stats";
import {averageLevelCalculator, calculateAverageLevel, calculateBandScore} from "@/utils/score";
import {MODULE_ARRAY} from "@/utils/moduleUtils";
import {Module} from "@/interfaces";
import {groupByExam} from "@/utils/stats";
import IconCard from "./IconCard";
import GroupList from "@/pages/(admin)/Lists/GroupList";
import useFilterStore from "@/stores/listFilterStore";
@@ -46,28 +42,279 @@ import AssignmentView from "./AssignmentView";
import AssignmentCreator from "./AssignmentCreator";
import clsx from "clsx";
import AssignmentCard from "./AssignmentCard";
import {createColumn, createColumnHelper} from "@tanstack/react-table";
import List from "@/components/List";
import {getUserCorporate} from "@/utils/groups";
import {getCorporateUser, getUserCompanyName} from "@/resources/user";
import Checkbox from "@/components/Low/Checkbox";
import {groupBy, uniq, uniqBy} from "lodash";
import Select from "@/components/Low/Select";
import {Menu, MenuButton, MenuItem, MenuItems} from "@headlessui/react";
import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover";
import MasterStatistical from "./MasterStatistical";
interface Props {
user: MasterCorporateUser;
}
export default function MasterCorporateDashboard({ user }: Props) {
const activeFilter = (a: Assignment) =>
moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment()) && a.assignees.length > a.results.length;
const pastFilter = (a: Assignment) => (moment(a.endDate).isBefore(moment()) || a.assignees.length === a.results.length) && !a.archived;
const archivedFilter = (a: Assignment) => a.archived;
const futureFilter = (a: Assignment) => moment(a.startDate).isAfter(moment());
type StudentPerformanceItem = User & {corporate?: CorporateUser; group?: Group};
const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPerformanceItem[]; stats: Stat[]; users: User[]; groups: Group[]}) => {
const [isShowingAmount, setIsShowingAmount] = useState(false);
const [availableCorporates] = useState(
uniqBy(
items.map((x) => x.corporate),
"id",
),
);
const [availableGroups] = useState(
uniqBy(
items.map((x) => x.group),
"id",
),
);
const [selectedCorporate, setSelectedCorporate] = useState<CorporateUser | null | undefined>(null);
const [selectedGroup, setSelectedGroup] = useState<Group | null | undefined>(null);
const columnHelper = createColumnHelper<StudentPerformanceItem>();
const columns = [
columnHelper.accessor("name", {
header: "Student Name",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("email", {
header: "E-mail",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("demographicInformation.passport_id", {
header: "ID",
cell: (info) => info.getValue() || "N/A",
}),
columnHelper.accessor("group", {
header: "Group",
cell: (info) => info.getValue()?.name || "N/A",
}),
columnHelper.accessor("corporate", {
header: "Corporate",
cell: (info) => (!!info.getValue() ? getUserCompanyName(info.getValue() as User, users, groups) : "N/A"),
}),
columnHelper.accessor("levels.reading", {
header: "Reading",
cell: (info) =>
!isShowingAmount
? calculateBandScore(
stats
.filter((x) => x.module === "reading" && x.user === info.row.original.id)
.reduce((acc, curr) => acc + curr.score.correct, 0),
stats
.filter((x) => x.module === "reading" && x.user === info.row.original.id)
.reduce((acc, curr) => acc + curr.score.total, 0),
"level",
info.row.original.focus || "academic",
) || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "reading" && x.user === info.row.original.id))).length} exams`,
}),
columnHelper.accessor("levels.listening", {
header: "Listening",
cell: (info) =>
!isShowingAmount
? calculateBandScore(
stats
.filter((x) => x.module === "listening" && x.user === info.row.original.id)
.reduce((acc, curr) => acc + curr.score.correct, 0),
stats
.filter((x) => x.module === "listening" && x.user === info.row.original.id)
.reduce((acc, curr) => acc + curr.score.total, 0),
"level",
info.row.original.focus || "academic",
) || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "listening" && x.user === info.row.original.id))).length} exams`,
}),
columnHelper.accessor("levels.writing", {
header: "Writing",
cell: (info) =>
!isShowingAmount
? calculateBandScore(
stats
.filter((x) => x.module === "writing" && x.user === info.row.original.id)
.reduce((acc, curr) => acc + curr.score.correct, 0),
stats
.filter((x) => x.module === "writing" && x.user === info.row.original.id)
.reduce((acc, curr) => acc + curr.score.total, 0),
"level",
info.row.original.focus || "academic",
) || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "writing" && x.user === info.row.original.id))).length} exams`,
}),
columnHelper.accessor("levels.speaking", {
header: "Speaking",
cell: (info) =>
!isShowingAmount
? calculateBandScore(
stats
.filter((x) => x.module === "speaking" && x.user === info.row.original.id)
.reduce((acc, curr) => acc + curr.score.correct, 0),
stats
.filter((x) => x.module === "speaking" && x.user === info.row.original.id)
.reduce((acc, curr) => acc + curr.score.total, 0),
"level",
info.row.original.focus || "academic",
) || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "speaking" && x.user === info.row.original.id))).length} exams`,
}),
columnHelper.accessor("levels.level", {
header: "Level",
cell: (info) =>
!isShowingAmount
? calculateBandScore(
stats
.filter((x) => x.module === "level" && x.user === info.row.original.id)
.reduce((acc, curr) => acc + curr.score.correct, 0),
stats
.filter((x) => x.module === "level" && x.user === info.row.original.id)
.reduce((acc, curr) => acc + curr.score.total, 0),
"level",
info.row.original.focus || "academic",
) || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "level" && x.user === info.row.original.id))).length} exams`,
}),
columnHelper.accessor("levels", {
id: "overall_level",
header: "Overall",
cell: (info) =>
!isShowingAmount
? averageLevelCalculator(
users,
stats.filter((x) => x.user === info.row.original.id),
).toFixed(1)
: `${Object.keys(groupByExam(stats.filter((x) => x.user === info.row.original.id))).length} exams`,
}),
];
const filterUsers = (data: StudentPerformanceItem[]) => {
console.log(data, selectedCorporate);
const filterByCorporate = (item: StudentPerformanceItem) => item.corporate?.id === selectedCorporate?.id;
const filterByGroup = (item: StudentPerformanceItem) => item.group?.id === selectedGroup?.id;
const filters: ((item: StudentPerformanceItem) => boolean)[] = [];
if (selectedCorporate !== null) filters.push(filterByCorporate);
if (selectedGroup !== null) filters.push(filterByGroup);
return filters.reduce((d, f) => d.filter(f), data);
};
return (
<div className="flex flex-col gap-4 w-full h-full">
<div className="w-full flex gap-4 justify-between items-center">
<Checkbox isChecked={isShowingAmount} onChange={setIsShowingAmount}>
Show Utilization
</Checkbox>
<Popover>
<PopoverTrigger>
<div className="flex items-center justify-center p-2 hover:bg-neutral-300/50 rounded-full transition ease-in-out duration-300">
<BsFilter size={20} />
</div>
</PopoverTrigger>
<PopoverContent className="w-96">
<div className="flex flex-col gap-4">
<span className="font-bold text-lg">Filters</span>
<Select
options={availableCorporates.map((x) => ({
value: x?.id || "N/A",
label: x?.corporateInformation?.companyInformation?.name || x?.name || "N/A",
}))}
isClearable
value={
selectedCorporate === null
? null
: {
value: selectedCorporate?.id || "N/A",
label:
selectedCorporate?.corporateInformation?.companyInformation?.name ||
selectedCorporate?.name ||
"N/A",
}
}
placeholder="Select a Corporate..."
onChange={(value) =>
!value
? setSelectedCorporate(null)
: setSelectedCorporate(
value.value === "N/A" ? undefined : availableCorporates.find((x) => x?.id === value.value),
)
}
/>
<Select
options={availableGroups.map((x) => ({
value: x?.id || "N/A",
label: x?.name || "N/A",
}))}
isClearable
value={
selectedGroup === null
? null
: {
value: selectedGroup?.id || "N/A",
label: selectedGroup?.name || "N/A",
}
}
placeholder="Select a Group..."
onChange={(value) =>
!value
? setSelectedGroup(null)
: setSelectedGroup(value.value === "N/A" ? undefined : availableGroups.find((x) => x?.id === value.value))
}
/>
</div>
</PopoverContent>
</Popover>
</div>
<List<StudentPerformanceItem>
data={filterUsers(
items.sort(
(a, b) =>
averageLevelCalculator(
users,
stats.filter((x) => x.user === b.id),
) -
averageLevelCalculator(
users,
stats.filter((x) => x.user === a.id),
),
),
)}
columns={columns}
/>
</div>
);
};
export default function MasterCorporateDashboard({user}: Props) {
const [page, setPage] = useState("");
const [selectedUser, setSelectedUser] = useState<User>();
const [showModal, setShowModal] = useState(false);
const [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
const [corporateAssignments, setCorporateAssignments] = useState<(Assignment & {corporate?: CorporateUser})[]>([]);
const { stats } = useStats();
const { users, reload } = useUsers();
const { codes } = useCodes(user.id);
const { groups } = useGroups(user.id, user.type);
const {stats} = useStats();
const {users, reload} = useUsers();
const {codes} = useCodes(user.id);
const {groups} = useGroups({admin: user.id, userType: user.type});
const masterCorporateUserGroups = [
...new Set(
groups.filter((u) => u.admin === user.id).flatMap((g) => g.participants)
),
];
const corporateUserGroups = [
...new Set(groups.flatMap((g) => g.participants)),
];
@@ -85,13 +332,20 @@ export default function MasterCorporateDashboard({ user }: Props) {
setShowModal(!!selectedUser && page === "");
}, [selectedUser, page]);
const studentFilter = (user: User) =>
user.type === "student" && corporateUserGroups.includes(user.id);
const teacherFilter = (user: User) =>
user.type === "teacher" && corporateUserGroups.includes(user.id);
useEffect(() => {
setCorporateAssignments(
assignments.filter(activeFilter).map((a) => ({
...a,
corporate: !!users.find((x) => x.id === a.assigner)
? getCorporateUser(users.find((x) => x.id === a.assigner)!, users, groups)
: undefined,
})),
);
}, [assignments, groups, users]);
const getStatsByStudent = (user: User) =>
stats.filter((s) => s.user === user.id);
const studentFilter = (user: User) => user.type === "student" && corporateUserGroups.includes(user.id);
const teacherFilter = (user: User) => user.type === "teacher" && corporateUserGroups.includes(user.id);
const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id);
const UserDisplay = (displayUser: User) => (
<div
@@ -210,19 +464,50 @@ export default function MasterCorporateDashboard({ user }: Props) {
);
};
const AssignmentsPage = () => {
const activeFilter = (a: Assignment) =>
moment(a.endDate).isAfter(moment()) &&
moment(a.startDate).isBefore(moment()) &&
a.assignees.length > a.results.length;
const pastFilter = (a: Assignment) =>
(moment(a.endDate).isBefore(moment()) ||
a.assignees.length === a.results.length) &&
!a.archived;
const archivedFilter = (a: Assignment) => a.archived;
const futureFilter = (a: Assignment) =>
moment(a.startDate).isAfter(moment());
// const AssignmentsPage = () => {
// const activeFilter = (a: Assignment) =>
// moment(a.endDate).isAfter(moment()) &&
// moment(a.startDate).isBefore(moment()) &&
// a.assignees.length > a.results.length;
// const pastFilter = (a: Assignment) =>
// (moment(a.endDate).isBefore(moment()) ||
// a.assignees.length === a.results.length) &&
// !a.archived;
// const archivedFilter = (a: Assignment) => a.archived;
// const futureFilter = (a: Assignment) =>
// moment(a.startDate).isAfter(moment());
const StudentPerformancePage = () => {
const students = users
.filter((x) => x.type === "student" && groups.flatMap((g) => g.participants).includes(x.id))
.map((u) => ({
...u,
group: groups.find((x) => x.participants.includes(u.id)),
corporate: getCorporateUser(u, users, groups),
}));
return (
<>
<div className="w-full flex justify-between items-center">
<div
onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<div
onClick={reloadAssignments}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<span>Reload</span>
<BsArrowRepeat className={clsx("text-xl", isAssignmentsLoading && "animate-spin")} />
</div>
</div>
<StudentPerformanceList items={students} stats={stats} users={users} groups={groups} />
</>
);
};
const AssignmentsPage = () => {
return (
<>
<AssignmentView
@@ -236,9 +521,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
/>
<AssignmentCreator
assignment={selectedAssignment}
groups={groups.filter(
(x) => x.admin === user.id || x.participants.includes(user.id)
)}
groups={groups.filter((x) => x.admin === user.id || x.participants.includes(user.id))}
users={users.filter(
(x) =>
x.type === "student" &&
@@ -247,7 +530,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
.filter((g) => g.admin === selectedUser.id)
.flatMap((g) => g.participants)
.includes(x.id) || false
: groups.flatMap((g) => g.participants).includes(x.id))
: groups.flatMap((g) => g.participants).includes(x.id)),
)}
assigner={user.id}
isCreating={isCreatingAssignment}
@@ -260,53 +543,56 @@ export default function MasterCorporateDashboard({ user }: Props) {
<div className="w-full flex justify-between items-center">
<div
onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
>
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<BsArrowLeft className="text-xl" />
<span>Back</span>
</div>
<div
onClick={reloadAssignments}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
>
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
<span>Reload</span>
<BsArrowRepeat
className={clsx(
"text-xl",
isAssignmentsLoading && "animate-spin"
)}
/>
<BsArrowRepeat className={clsx("text-xl", isAssignmentsLoading && "animate-spin")} />
</div>
</div>
<div className="flex flex-col gap-2">
<span className="text-lg font-bold">Active Assignments Status</span>
<div className="flex items-center gap-4">
<span>
<b>Total:</b> {assignments.filter(activeFilter).reduce((acc, curr) => acc + curr.results.length, 0)}/
{assignments.filter(activeFilter).reduce((acc, curr) => curr.exams.length + acc, 0)}
</span>
{Object.keys(groupBy(corporateAssignments, (x) => x.corporate?.id)).map((x) => (
<div key={x}>
<span className="font-semibold">{getUserCompanyName(users.find((u) => u.id === x)!, users, groups)}: </span>
<span>
{groupBy(corporateAssignments, (x) => x.corporate?.id)[x].reduce((acc, curr) => curr.results.length + acc, 0)}/
{groupBy(corporateAssignments, (x) => x.corporate?.id)[x].reduce((acc, curr) => curr.exams.length + acc, 0)}
</span>
</div>
))}
</div>
</div>
<section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold">
Active Assignments ({assignments.filter(activeFilter).length})
</h2>
<h2 className="text-2xl font-semibold">Active Assignments ({assignments.filter(activeFilter).length})</h2>
<div className="flex flex-wrap gap-2">
{assignments.filter(activeFilter).map((a) => (
<AssignmentCard
{...a}
onClick={() => setSelectedAssignment(a)}
key={a.id}
/>
<AssignmentCard {...a} users={users} onClick={() => setSelectedAssignment(a)} key={a.id} />
))}
</div>
</section>
<section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold">
Planned Assignments ({assignments.filter(futureFilter).length})
</h2>
<h2 className="text-2xl font-semibold">Planned Assignments ({assignments.filter(futureFilter).length})</h2>
<div className="flex flex-wrap gap-2">
<div
onClick={() => setIsCreatingAssignment(true)}
className="w-[250px] h-[200px] flex flex-col gap-2 items-center justify-center bg-white hover:bg-mti-purple-ultralight text-mti-purple-light hover:text-mti-purple-dark border border-mti-gray-platinum hover:drop-shadow p-4 cursor-pointer rounded-xl transition ease-in-out duration-300"
>
className="w-[250px] h-[200px] flex flex-col gap-2 items-center justify-center bg-white hover:bg-mti-purple-ultralight text-mti-purple-light hover:text-mti-purple-dark border border-mti-gray-platinum hover:drop-shadow p-4 cursor-pointer rounded-xl transition ease-in-out duration-300">
<BsPlus className="text-6xl" />
<span className="text-lg">New Assignment</span>
</div>
{assignments.filter(futureFilter).map((a) => (
<AssignmentCard
{...a}
users={users}
onClick={() => {
setSelectedAssignment(a);
setIsCreatingAssignment(true);
@@ -317,13 +603,12 @@ export default function MasterCorporateDashboard({ user }: Props) {
</div>
</section>
<section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold">
Past Assignments ({assignments.filter(pastFilter).length})
</h2>
<h2 className="text-2xl font-semibold">Past Assignments ({assignments.filter(pastFilter).length})</h2>
<div className="flex flex-wrap gap-2">
{assignments.filter(pastFilter).map((a) => (
<AssignmentCard
{...a}
users={users}
onClick={() => setSelectedAssignment(a)}
key={a.id}
allowDownload
@@ -334,13 +619,12 @@ export default function MasterCorporateDashboard({ user }: Props) {
</div>
</section>
<section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold">
Archived Assignments ({assignments.filter(archivedFilter).length})
</h2>
<h2 className="text-2xl font-semibold">Archived Assignments ({assignments.filter(archivedFilter).length})</h2>
<div className="flex flex-wrap gap-2">
{assignments.filter(archivedFilter).map((a) => (
<AssignmentCard
{...a}
users={users}
onClick={() => setSelectedAssignment(a)}
key={a.id}
allowDownload
@@ -381,36 +665,6 @@ export default function MasterCorporateDashboard({ user }: Props) {
);
};
const averageLevelCalculator = (studentStats: Stat[]) => {
const formattedStats = studentStats
.map((s) => ({
focus: users.find((u) => u.id === s.user)?.focus,
score: s.score,
module: s.module,
}))
.filter((f) => !!f.focus);
const bandScores = formattedStats.map((s) => ({
module: s.module,
level: calculateBandScore(
s.score.correct,
s.score.total,
s.module,
s.focus!
),
}));
const levels: { [key in Module]: number } = {
reading: 0,
listening: 0,
writing: 0,
speaking: 0,
level: 0,
};
bandScores.forEach((b) => (levels[b.module] += b.level));
return calculateAverageLevel(levels);
};
const DefaultDashboard = () => (
<>
<section className="flex flex-wrap gap-2 items-center -lg:justify-center lg:justify-between text-center">
@@ -431,46 +685,29 @@ export default function MasterCorporateDashboard({ user }: Props) {
<IconCard
Icon={BsClipboard2Data}
label="Exams Performed"
value={
stats.filter((s) =>
groups.flatMap((g) => g.participants).includes(s.user)
).length
}
value={stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)).length}
color="purple"
/>
<IconCard
Icon={BsPaperclip}
label="Average Level"
value={averageLevelCalculator(
stats.filter((s) =>
groups.flatMap((g) => g.participants).includes(s.user)
)
users,
stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)),
).toFixed(1)}
color="purple"
/>
<IconCard
onClick={() => setPage("groups")}
Icon={BsPeople}
label="Groups"
value={groups.length}
color="purple"
/>
<IconCard onClick={() => setPage("groups")} Icon={BsPeople} label="Groups" value={groups.length} color="purple" />
<IconCard
Icon={BsPersonCheck}
label="User Balance"
value={`${codes.length}/${
user.corporateInformation?.companyInformation?.userAmount || 0
}`}
value={`${codes.length}/${user.corporateInformation?.companyInformation?.userAmount || 0}`}
color="purple"
/>
<IconCard
Icon={BsClock}
label="Expiration Date"
value={
user.subscriptionExpirationDate
? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy")
: "Unlimited"
}
value={user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy") : "Unlimited"}
color="rose"
/>
<IconCard
@@ -480,6 +717,13 @@ export default function MasterCorporateDashboard({ user }: Props) {
color="purple"
onClick={() => setPage("corporate")}
/>
<IconCard
Icon={BsPersonFillGear}
label="Student Performance"
value={users.filter(studentFilter).length}
color="purple"
onClick={() => setPage("studentsPerformance")}
/>
<IconCard
Icon={BsDatabase}
label="Master Statistical"
@@ -490,15 +734,12 @@ export default function MasterCorporateDashboard({ user }: Props) {
<button
disabled={isAssignmentsLoading}
onClick={() => setPage("assignments")}
className="bg-white col-span-2 rounded-xl shadow p-4 flex flex-col gap-4 items-center w-96 h-52 justify-center cursor-pointer hover:shadow-xl transition ease-in-out duration-300"
>
className="bg-white col-span-2 rounded-xl shadow p-4 flex flex-col gap-4 items-center w-96 h-52 justify-center cursor-pointer hover:shadow-xl transition ease-in-out duration-300">
<BsEnvelopePaper className="text-6xl text-mti-purple-light" />
<span className="flex flex-col gap-1 items-center text-xl">
<span className="text-lg">Assignments</span>
<span className="font-semibold text-mti-purple-light">
{isAssignmentsLoading
? "Loading..."
: assignments.filter((a) => !a.archived).length}
{isAssignmentsLoading ? "Loading..." : assignments.filter((a) => !a.archived).length}
</span>
</span>
</button>
@@ -634,6 +875,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
{page === "groups" && <GroupsList />}
{page === "corporate" && <CorporateList />}
{page === "assignments" && <AssignmentsPage />}
{page === "studentsPerformance" && <StudentPerformancePage />}
{page === "statistical" && <MasterStatisticalPage />}
{page === "" && <DefaultDashboard />}
</>

View File

@@ -63,7 +63,7 @@ export default function TeacherDashboard({user}: Props) {
const {stats} = useStats();
const {users, reload} = useUsers();
const {groups} = useGroups(user.id);
const {groups} = useGroups({adminAdmins: user.id});
const {permissions} = usePermissions(user.id);
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assigner: user.id});
@@ -193,7 +193,7 @@ export default function TeacherDashboard({user}: Props) {
? groups
.filter((g) => g.admin === selectedUser.id)
.flatMap((g) => g.participants)
.includes(x.id) || false
.includes(x.id)
: groups.flatMap((g) => g.participants).includes(x.id)),
)}
assigner={user.id}
@@ -222,7 +222,7 @@ export default function TeacherDashboard({user}: Props) {
<h2 className="text-2xl font-semibold">Active Assignments ({assignments.filter(activeFilter).length})</h2>
<div className="flex flex-wrap gap-2">
{assignments.filter(activeFilter).map((a) => (
<AssignmentCard {...a} onClick={() => setSelectedAssignment(a)} key={a.id} />
<AssignmentCard {...a} users={users} onClick={() => setSelectedAssignment(a)} key={a.id} />
))}
</div>
</section>
@@ -238,6 +238,7 @@ export default function TeacherDashboard({user}: Props) {
{assignments.filter(futureFilter).map((a) => (
<AssignmentCard
{...a}
users={users}
onClick={() => {
setSelectedAssignment(a);
setIsCreatingAssignment(true);
@@ -253,6 +254,7 @@ export default function TeacherDashboard({user}: Props) {
{assignments.filter(pastFilter).map((a) => (
<AssignmentCard
{...a}
users={users}
onClick={() => setSelectedAssignment(a)}
key={a.id}
allowDownload
@@ -268,6 +270,7 @@ export default function TeacherDashboard({user}: Props) {
{assignments.filter(archivedFilter).map((a) => (
<AssignmentCard
{...a}
users={users}
onClick={() => setSelectedAssignment(a)}
key={a.id}
allowDownload

View File

@@ -1,27 +1,124 @@
import BlankQuestionsModal from "@/components/BlankQuestionsModal";
import {renderExercise} from "@/components/Exercises";
import { renderExercise } from "@/components/Exercises";
import HighlightContent from "@/components/HighlightContent";
import Button from "@/components/Low/Button";
import ModuleTitle from "@/components/Medium/ModuleTitle";
import {renderSolution} from "@/components/Solutions";
import {infoButtonStyle} from "@/constants/buttonStyles";
import {LevelExam, LevelPart, UserSolution, WritingExam} from "@/interfaces/exam";
import { renderSolution } from "@/components/Solutions";
import { infoButtonStyle } from "@/constants/buttonStyles";
import { Module } from "@/interfaces";
import { Exercise, FillBlanksExercise, FillBlanksMCOption, LevelExam, LevelPart, MultipleChoiceExercise, ShuffleMap, UserSolution, WritingExam } from "@/interfaces/exam";
import useExamStore from "@/stores/examStore";
import {defaultUserSolutions} from "@/utils/exams";
import {countExercises} from "@/utils/moduleUtils";
import {mdiArrowRight} from "@mdi/js";
import { defaultUserSolutions } from "@/utils/exams";
import { countExercises } from "@/utils/moduleUtils";
import { mdiArrowRight } from "@mdi/js";
import Icon from "@mdi/react";
import clsx from "clsx";
import {Fragment, useEffect, useState} from "react";
import {BsChevronDown, BsChevronUp} from "react-icons/bs";
import {toast} from "react-toastify";
import { Dispatch, Fragment, SetStateAction, use, useEffect, useMemo, useRef, useState } from "react";
import { BsChevronDown, BsChevronUp } from "react-icons/bs";
import { toast } from "react-toastify";
interface Props {
exam: LevelExam;
showSolutions?: boolean;
onFinish: (userSolutions: UserSolution[]) => void;
editing?: boolean;
}
function TextComponent({part}: {part: LevelPart}) {
function TextComponent({
part, highlightPhrases, contextWord, setContextWordLine
}: {
part: LevelPart, highlightPhrases: string[], contextWord: string | undefined, setContextWordLine: React.Dispatch<React.SetStateAction<number | undefined>>
}) {
const textRef = useRef<HTMLDivElement>(null);
const [lineNumbers, setLineNumbers] = useState<number[]>([]);
const [lineHeight, setLineHeight] = useState<number>(0);
part.showContextLines = true;
const calculateLineNumbers = () => {
if (textRef.current) {
const computedStyle = window.getComputedStyle(textRef.current);
const lineHeightValue = parseFloat(computedStyle.lineHeight);
const containerWidth = textRef.current.clientWidth;
setLineHeight(lineHeightValue);
const offscreenElement = document.createElement('div');
offscreenElement.style.position = 'absolute';
offscreenElement.style.top = '-9999px';
offscreenElement.style.left = '-9999px';
offscreenElement.style.whiteSpace = 'pre-wrap';
offscreenElement.style.width = `${containerWidth}px`;
offscreenElement.style.font = computedStyle.font;
offscreenElement.style.lineHeight = computedStyle.lineHeight;
offscreenElement.style.wordWrap = 'break-word';
offscreenElement.style.textAlign = computedStyle.textAlign as CanvasTextAlign;
const textContent = textRef.current.textContent || '';
textContent.split(/(\s+)/).forEach((word: string) => {
const span = document.createElement('span');
span.textContent = word;
offscreenElement.appendChild(span);
});
document.body.appendChild(offscreenElement);
const lines: string[][] = [[]];
let currentLine = 1;
let currentLineTop: number | undefined;
let contextWordLine: number | null = null;
const firstChild = offscreenElement.firstChild as HTMLElement;
if (firstChild) {
currentLineTop = firstChild.getBoundingClientRect().top;
}
const spans = offscreenElement.querySelectorAll<HTMLSpanElement>('span');
spans.forEach(span => {
const rect = span.getBoundingClientRect();
const top = rect.top;
if (currentLineTop !== undefined && top > currentLineTop) {
currentLine++;
currentLineTop = top;
lines.push([]);
}
lines[lines.length - 1].push(span.textContent?.trim() || '');
if (contextWord && contextWordLine === null && span.textContent?.includes(contextWord)) {
contextWordLine = currentLine;
}
});
setLineNumbers(lines.map((_, index) => index + 1));
if (contextWordLine) {
setContextWordLine(contextWordLine);
}
document.body.removeChild(offscreenElement);
}
};
useEffect(() => {
calculateLineNumbers();
const resizeObserver = new ResizeObserver(() => {
calculateLineNumbers();
});
if (textRef.current) {
resizeObserver.observe(textRef.current);
}
return () => {
if (textRef.current) {
resizeObserver.unobserve(textRef.current);
}
};
}, [part.context, part.showContextLines, contextWord]);
if (typeof part.showContextLines === "undefined") {
return (
<div className="flex flex-col gap-2 w-full">
<div className="border border-mti-gray-dim w-full rounded-full opacity-10" />
@@ -36,20 +133,59 @@ function TextComponent({part}: {part: LevelPart}) {
))}
</div>
);
}
return (
<div className="flex flex-col gap-2 w-full">
<div className="border border-mti-gray-dim w-full rounded-full opacity-10" />
<div className="flex mt-2">
<div className="flex-shrink-0 w-8 pr-2">
{lineNumbers.map(num => (
<div key={num} className="text-gray-400 flex justify-end" style={{ lineHeight: `${lineHeight}px` }}>
{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>
);
}
export default function Level({exam, showSolutions = false, onFinish}: Props) {
const [multipleChoicesDone, setMultipleChoicesDone] = useState<{id: string; amount: number}[]>([]);
const typeCheckWordsMC = (words: any[]): words is FillBlanksMCOption[] => {
return Array.isArray(words) && words.every(
word => word && typeof word === 'object' && 'id' in word && 'options' in word
);
}
export default function Level({ exam, showSolutions = false, onFinish, editing = false }: Props) {
const [multipleChoicesDone, setMultipleChoicesDone] = useState<{ id: string; amount: number }[]>([]);
const [showBlankModal, setShowBlankModal] = useState(false);
const {userSolutions, setUserSolutions} = useExamStore((state) => state);
const {hasExamEnded, setHasExamEnded} = useExamStore((state) => state);
const {partIndex, setPartIndex} = useExamStore((state) => state);
const {exerciseIndex, setExerciseIndex} = useExamStore((state) => state);
const { userSolutions, setUserSolutions } = useExamStore((state) => state);
const { hasExamEnded, setHasExamEnded } = useExamStore((state) => state);
const { partIndex, setPartIndex } = useExamStore((state) => state);
const { exerciseIndex, setExerciseIndex } = useExamStore((state) => state);
const [storeQuestionIndex, setStoreQuestionIndex] = useExamStore((state) => [state.questionIndex, state.setQuestionIndex]);
//const [shuffleMaps, setShuffleMaps] = useExamStore((state) => [state.shuffleMaps, state.setShuffleMaps])
const [currentExercise, setCurrentExercise] = useState<Exercise>();
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 [contextWordLine, setContextWordLine] = useState<number | undefined>(undefined);
/*useEffect(() => {
if (showSolutions && userSolutions[exerciseIndex].shuffleMaps) {
setShuffleMaps(userSolutions[exerciseIndex].shuffleMaps as ShuffleMap[])
}
}, [showSolutions])*/
useEffect(() => {
if (hasExamEnded && exerciseIndex === -1) {
setExerciseIndex(exerciseIndex + 1);
@@ -65,15 +201,136 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) {
onFinish(userSolutions);
};
const getExercise = () => {
if (exerciseIndex === -1) {
return undefined;
}
let exercise = exam.parts[partIndex]?.exercises[exerciseIndex];
if (!exercise) return undefined;
exercise = {
...exercise,
userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [],
};
/*if (exam.shuffle && exercise.type === "multipleChoice") {
if (shuffleMaps.length == 0 && !showSolutions) {
console.log("Shuffling answers");
const newShuffleMaps: ShuffleMap[] = [];
exercise.questions = exercise.questions.map(question => {
const options = [...question.options];
let shuffledOptions = [...options].sort(() => Math.random() - 0.5);
const newOptions = options.map((option, index) => ({
id: option.id,
text: shuffledOptions[index].text
}));
const optionMapping = options.reduce<{ [key: string]: string }>((acc, originalOption) => {
const shuffledPosition = newOptions.find(newOpt => newOpt.text === originalOption.text)?.id;
if (shuffledPosition) {
acc[shuffledPosition] = originalOption.id;
}
return acc;
}, {});
newShuffleMaps.push({ id: question.id, map: optionMapping });
return { ...question, options: newOptions };
});
setShuffleMaps(newShuffleMaps);
} else {
console.log("Retrieving shuffles");
exercise.questions = exercise.questions.map(question => {
const questionShuffleMap = shuffleMaps.find(map => map.id === question.id);
if (questionShuffleMap) {
const newOptions = question.options.map(option => ({
id: option.id,
text: question.options.find(o => questionShuffleMap.map[o.id] === option.id)?.text || option.text
}));
return { ...question, options: newOptions };
}
return question;
});
}
} else if (exam.shuffle && exercise.type === "fillBlanks" && typeCheckWordsMC(exercise.words)) {
if (shuffleMaps.length === 0 && !showSolutions) {
const newShuffleMaps: ShuffleMap[] = [];
exercise.words = exercise.words.map(word => {
if ('options' in word) {
const options = { ...word.options };
const originalKeys = Object.keys(options);
const shuffledKeys = [...originalKeys].sort(() => Math.random() - 0.5);
const newOptions = shuffledKeys.reduce((acc, key, index) => {
acc[key as keyof typeof options] = options[originalKeys[index] as keyof typeof options];
return acc;
}, {} as { [key in keyof typeof options]: string });
const optionMapping = originalKeys.reduce((acc, key, index) => {
acc[key as keyof typeof options] = shuffledKeys[index];
return acc;
}, {} as { [key in keyof typeof options]: string });
newShuffleMaps.push({ id: word.id, map: optionMapping });
return { ...word, options: newOptions };
}
return word;
});
setShuffleMaps(newShuffleMaps);
}
}
*/
return exercise;
};
useEffect(() => {
//console.log("Getting another exercise");
//setShuffleMaps([]);
setCurrentExercise(getExercise());
}, [partIndex, exerciseIndex]);
useEffect(() => {
const regex = /.*?['"](.*?)['"] in line (\d+)\?$/;
if (currentExercise && currentExercise.type === "multipleChoice") {
const match = currentExercise.questions[storeQuestionIndex].prompt.match(regex);
if (match) {
const word = match[1];
const originalLineNumber = match[2];
setContextHighlight([word]);
if (word !== contextWord) {
setContextWord(word);
}
const updatedPrompt = currentExercise.questions[storeQuestionIndex].prompt.replace(
`in line ${originalLineNumber}`,
`in line ${contextWordLine || originalLineNumber}`
);
currentExercise.questions[storeQuestionIndex].prompt = updatedPrompt;
} else {
setContextHighlight([]);
setContextWord(undefined);
}
}
}, [storeQuestionIndex, contextWordLine, exerciseIndex, partIndex]); //, shuffleMaps]);
const nextExercise = (solution?: UserSolution) => {
scrollToTop();
if (solution) {
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) {
const exercise = getExercise();
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== exercise.id), {id: exercise.id, amount: storeQuestionIndex}]);
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: storeQuestionIndex }]);
}
setStoreQuestionIndex(0);
@@ -94,6 +351,7 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) {
(x) => x === 0,
) &&
!showSolutions &&
!editing &&
!hasExamEnded
) {
setShowBlankModal(true);
@@ -103,7 +361,11 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) {
setHasExamEnded(false);
if (solution) {
onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "level", exam: exam.id}]);
let stat = { ...solution, module: "level" as Module, exam: exam.id }
/*if (exam.shuffle) {
stat.shuffleMaps = shuffleMaps
}*/
onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...stat }]);
} else {
onFinish(userSolutions);
}
@@ -112,26 +374,17 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) {
const previousExercise = (solution?: UserSolution) => {
scrollToTop();
if (solution) {
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) {
const exercise = getExercise();
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== exercise.id), {id: exercise.id, amount: storeQuestionIndex}]);
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: storeQuestionIndex }]);
}
setStoreQuestionIndex(0);
setExerciseIndex(exerciseIndex - 1);
};
const getExercise = () => {
const exercise = exam.parts[partIndex].exercises[exerciseIndex];
return {
...exercise,
userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [],
};
};
const calculateExerciseIndex = () => {
if (partIndex === 0)
return (
@@ -157,7 +410,12 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) {
</h4>
<span className="text-base">You will be allowed to read the text while doing the exercises</span>
</div>
<TextComponent part={exam.parts[partIndex]} />
<TextComponent
part={exam.parts[partIndex]}
highlightPhrases={highlightPhrases}
contextWord={contextWord}
setContextWordLine={setContextWordLine}
/>
</>
</div>
);
@@ -171,7 +429,7 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) {
exerciseIndex={calculateExerciseIndex()}
module="level"
totalExercises={countExercises(exam.parts.flatMap((x) => x.exercises))}
disableTimer={showSolutions}
disableTimer={showSolutions || editing}
/>
<div
className={clsx(
@@ -184,12 +442,14 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) {
partIndex > -1 &&
exerciseIndex < exam.parts[partIndex].exercises.length &&
!showSolutions &&
renderExercise(getExercise(), exam.id, nextExercise, previousExercise)}
!editing &&
currentExercise &&
renderExercise(currentExercise, exam.id, nextExercise, previousExercise)}
{exerciseIndex > -1 &&
partIndex > -1 &&
exerciseIndex < exam.parts[partIndex].exercises.length &&
showSolutions &&
(showSolutions || editing) &&
renderSolution(exam.parts[partIndex].exercises[exerciseIndex], nextExercise, previousExercise)}
</div>
{exerciseIndex === -1 && partIndex > 0 && (

View File

@@ -1,29 +1,19 @@
/* eslint-disable @next/next/no-img-element */
import { useState } from "react";
import { Module } from "@/interfaces";
import {useState} from "react";
import {Module} from "@/interfaces";
import clsx from "clsx";
import { User } from "@/interfaces/user";
import {User} from "@/interfaces/user";
import ProgressBar from "@/components/Low/ProgressBar";
import {
BsArrowRepeat,
BsBook,
BsCheck,
BsCheckCircle,
BsClipboard,
BsHeadphones,
BsMegaphone,
BsPen,
BsXCircle,
} from "react-icons/bs";
import { totalExamsByModule } from "@/utils/stats";
import {BsArrowRepeat, BsBook, BsCheck, BsCheckCircle, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle} from "react-icons/bs";
import {totalExamsByModule} from "@/utils/stats";
import useStats from "@/hooks/useStats";
import Button from "@/components/Low/Button";
import { calculateAverageLevel } from "@/utils/score";
import { sortByModuleName } from "@/utils/moduleUtils";
import { capitalize } from "lodash";
import {calculateAverageLevel} from "@/utils/score";
import {sortByModuleName} from "@/utils/moduleUtils";
import {capitalize} from "lodash";
import ProfileSummary from "@/components/ProfileSummary";
import { Variant } from "@/interfaces/exam";
import useSessions, { Session } from "@/hooks/useSessions";
import {Variant} from "@/interfaces/exam";
import useSessions, {Session} from "@/hooks/useSessions";
import SessionCard from "@/components/Medium/SessionCard";
import useExamStore from "@/stores/examStore";
import moment from "moment";
@@ -31,34 +21,23 @@ import moment from "moment";
interface Props {
user: User;
page: "exercises" | "exams";
onStart: (
modules: Module[],
avoidRepeated: boolean,
variant: Variant,
) => void;
onStart: (modules: Module[], avoidRepeated: boolean, variant: Variant) => void;
disableSelection?: boolean;
}
export default function Selection({
user,
page,
onStart,
disableSelection = false,
}: Props) {
export default function Selection({user, page, onStart, disableSelection = false}: Props) {
const [selectedModules, setSelectedModules] = useState<Module[]>([]);
const [avoidRepeatedExams, setAvoidRepeatedExams] = useState(true);
const [variant, setVariant] = useState<Variant>("full");
const { stats } = useStats(user?.id);
const { sessions, isLoading, reload } = useSessions(user.id);
const {stats} = useStats(user?.id);
const {sessions, isLoading, reload} = useSessions(user.id);
const state = useExamStore((state) => state);
const toggleModule = (module: Module) => {
const modules = selectedModules.filter((x) => x !== module);
setSelectedModules((prev) =>
prev.includes(module) ? modules : [...modules, module],
);
setSelectedModules((prev) => (prev.includes(module) ? modules : [...modules, module]));
};
const loadSession = async (session: Session) => {
@@ -84,41 +63,31 @@ export default function Selection({
user={user}
items={[
{
icon: (
<BsBook className="text-ielts-reading h-6 w-6 md:h-8 md:w-8" />
),
icon: <BsBook className="text-ielts-reading h-6 w-6 md:h-8 md:w-8" />,
label: "Reading",
value: totalExamsByModule(stats, "reading"),
tooltip: "The amount of reading exams performed.",
},
{
icon: (
<BsHeadphones className="text-ielts-listening h-6 w-6 md:h-8 md:w-8" />
),
icon: <BsHeadphones className="text-ielts-listening h-6 w-6 md:h-8 md:w-8" />,
label: "Listening",
value: totalExamsByModule(stats, "listening"),
tooltip: "The amount of listening exams performed.",
},
{
icon: (
<BsPen className="text-ielts-writing h-6 w-6 md:h-8 md:w-8" />
),
icon: <BsPen className="text-ielts-writing h-6 w-6 md:h-8 md:w-8" />,
label: "Writing",
value: totalExamsByModule(stats, "writing"),
tooltip: "The amount of writing exams performed.",
},
{
icon: (
<BsMegaphone className="text-ielts-speaking h-6 w-6 md:h-8 md:w-8" />
),
icon: <BsMegaphone className="text-ielts-speaking h-6 w-6 md:h-8 md:w-8" />,
label: "Speaking",
value: totalExamsByModule(stats, "speaking"),
tooltip: "The amount of speaking exams performed.",
},
{
icon: (
<BsClipboard className="text-ielts-level h-6 w-6 md:h-8 md:w-8" />
),
icon: <BsClipboard className="text-ielts-level h-6 w-6 md:h-8 md:w-8" />,
label: "Level",
value: totalExamsByModule(stats, "level"),
tooltip: "The amount of level exams performed.",
@@ -132,35 +101,23 @@ export default function Selection({
<span className="text-mti-gray-taupe">
{page === "exercises" && (
<>
In the realm of language acquisition, practice makes perfect,
and our exercises are the key to unlocking your full potential.
Dive into a world of interactive and engaging exercises that
cater to diverse learning styles. From grammar drills that build
a strong foundation to vocabulary challenges that broaden your
lexicon, our exercises are carefully designed to make learning
English both enjoyable and effective. Whether you&apos;re
looking to reinforce specific skills or embark on a holistic
language journey, our exercises are your companions in the
pursuit of excellence. Embrace the joy of learning as you
navigate through a variety of activities that cater to every
facet of language acquisition. Your linguistic adventure starts
here!
In the realm of language acquisition, practice makes perfect, and our exercises are the key to unlocking your full
potential. Dive into a world of interactive and engaging exercises that cater to diverse learning styles. From grammar
drills that build a strong foundation to vocabulary challenges that broaden your lexicon, our exercises are carefully
designed to make learning English both enjoyable and effective. Whether you&apos;re looking to reinforce specific
skills or embark on a holistic language journey, our exercises are your companions in the pursuit of excellence.
Embrace the joy of learning as you navigate through a variety of activities that cater to every facet of language
acquisition. Your linguistic adventure starts here!
</>
)}
{page === "exams" && (
<>
Welcome to the heart of success on your English language
journey! Our exams are crafted with precision to assess and
enhance your language skills. Each test is a passport to your
linguistic prowess, designed to challenge and elevate your
abilities. Whether you&apos;re a beginner or a seasoned learner,
our exams cater to all levels, providing a comprehensive
evaluation of your reading, writing, speaking, and listening
skills. Prepare to embark on a journey of self-discovery and
language mastery as you navigate through our thoughtfully
curated exams. Your success is not just a destination; it&apos;s
a testament to your dedication and our commitment to empowering
you with the English language.
Welcome to the heart of success on your English language journey! Our exams are crafted with precision to assess and
enhance your language skills. Each test is a passport to your linguistic prowess, designed to challenge and elevate
your abilities. Whether you&apos;re a beginner or a seasoned learner, our exams cater to all levels, providing a
comprehensive evaluation of your reading, writing, speaking, and listening skills. Prepare to embark on a journey of
self-discovery and language mastery as you navigate through our thoughtfully curated exams. Your success is not just a
destination; it&apos;s a testament to your dedication and our commitment to empowering you with the English language.
</>
)}
</span>
@@ -171,26 +128,16 @@ export default function Selection({
<div className="flex items-center gap-4">
<div
onClick={reload}
className="text-mti-purple-light hover:text-mti-purple-dark flex cursor-pointer items-center gap-2 transition duration-300 ease-in-out"
>
<span className="text-mti-black text-lg font-bold">
Unfinished Sessions
</span>
<BsArrowRepeat
className={clsx("text-xl", isLoading && "animate-spin")}
/>
className="text-mti-purple-light hover:text-mti-purple-dark flex cursor-pointer items-center gap-2 transition duration-300 ease-in-out">
<span className="text-mti-black text-lg font-bold">Unfinished Sessions</span>
<BsArrowRepeat className={clsx("text-xl", isLoading && "animate-spin")} />
</div>
</div>
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll">
{sessions
.sort((a, b) => moment(b.date).diff(moment(a.date)))
.map((session) => (
<SessionCard
session={session}
key={session.sessionId}
reload={reload}
loadSession={loadSession}
/>
<SessionCard session={session} key={session.sessionId} reload={reload} loadSession={loadSession} />
))}
</span>
</section>
@@ -198,170 +145,108 @@ export default function Selection({
<section className="-lg:flex-col -lg:items-center -lg:gap-12 mt-4 flex w-full justify-between gap-8">
<div
onClick={
!disableSelection && !selectedModules.includes("level")
? () => toggleModule("reading")
: undefined
}
onClick={!disableSelection && !selectedModules.includes("level") ? () => toggleModule("reading") : undefined}
className={clsx(
"bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out",
selectedModules.includes("reading") || disableSelection
? "border-mti-purple-light"
: "border-mti-gray-platinum",
)}
>
selectedModules.includes("reading") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum",
)}>
<div className="bg-ielts-reading absolute top-0 flex h-16 w-16 -translate-y-1/2 items-center justify-center rounded-full">
<BsBook className="h-7 w-7 text-white" />
</div>
<span className="font-semibold">Reading:</span>
<p className="text-left text-xs">
Expand your vocabulary, improve your reading comprehension and
improve your ability to interpret texts in English.
Expand your vocabulary, improve your reading comprehension and improve your ability to interpret texts in English.
</p>
{!selectedModules.includes("reading") &&
!selectedModules.includes("level") &&
!disableSelection && (
{!selectedModules.includes("reading") && !selectedModules.includes("level") && !disableSelection && (
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
)}
{(selectedModules.includes("reading") || disableSelection) && (
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
)}
{selectedModules.includes("level") && (
<BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />
)}
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />}
</div>
<div
onClick={
!disableSelection && !selectedModules.includes("level")
? () => toggleModule("listening")
: undefined
}
onClick={!disableSelection && !selectedModules.includes("level") ? () => toggleModule("listening") : undefined}
className={clsx(
"bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out",
selectedModules.includes("listening") || disableSelection
? "border-mti-purple-light"
: "border-mti-gray-platinum",
)}
>
selectedModules.includes("listening") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum",
)}>
<div className="bg-ielts-listening absolute top-0 flex h-16 w-16 -translate-y-1/2 items-center justify-center rounded-full">
<BsHeadphones className="h-7 w-7 text-white" />
</div>
<span className="font-semibold">Listening:</span>
<p className="text-left text-xs">
Improve your ability to follow conversations in English and your
ability to understand different accents and intonations.
Improve your ability to follow conversations in English and your ability to understand different accents and intonations.
</p>
{!selectedModules.includes("listening") &&
!selectedModules.includes("level") &&
!disableSelection && (
{!selectedModules.includes("listening") && !selectedModules.includes("level") && !disableSelection && (
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
)}
{(selectedModules.includes("listening") || disableSelection) && (
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
)}
{selectedModules.includes("level") && (
<BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />
)}
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />}
</div>
<div
onClick={
!disableSelection && !selectedModules.includes("level")
? () => toggleModule("writing")
: undefined
}
onClick={!disableSelection && !selectedModules.includes("level") ? () => toggleModule("writing") : undefined}
className={clsx(
"bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out",
selectedModules.includes("writing") || disableSelection
? "border-mti-purple-light"
: "border-mti-gray-platinum",
)}
>
selectedModules.includes("writing") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum",
)}>
<div className="bg-ielts-writing absolute top-0 flex h-16 w-16 -translate-y-1/2 items-center justify-center rounded-full">
<BsPen className="h-7 w-7 text-white" />
</div>
<span className="font-semibold">Writing:</span>
<p className="text-left text-xs">
Allow you to practice writing in a variety of formats, from simple
paragraphs to complex essays.
Allow you to practice writing in a variety of formats, from simple paragraphs to complex essays.
</p>
{!selectedModules.includes("writing") &&
!selectedModules.includes("level") &&
!disableSelection && (
{!selectedModules.includes("writing") && !selectedModules.includes("level") && !disableSelection && (
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
)}
{(selectedModules.includes("writing") || disableSelection) && (
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
)}
{selectedModules.includes("level") && (
<BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />
)}
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />}
</div>
<div
onClick={
!disableSelection && !selectedModules.includes("level")
? () => toggleModule("speaking")
: undefined
}
onClick={!disableSelection && !selectedModules.includes("level") ? () => toggleModule("speaking") : undefined}
className={clsx(
"bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out",
selectedModules.includes("speaking") || disableSelection
? "border-mti-purple-light"
: "border-mti-gray-platinum",
)}
>
selectedModules.includes("speaking") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum",
)}>
<div className="bg-ielts-speaking absolute top-0 flex h-16 w-16 -translate-y-1/2 items-center justify-center rounded-full">
<BsMegaphone className="h-7 w-7 text-white" />
</div>
<span className="font-semibold">Speaking:</span>
<p className="text-left text-xs">
You&apos;ll have access to interactive dialogs, pronunciation
exercises and speech recordings.
You&apos;ll have access to interactive dialogs, pronunciation exercises and speech recordings.
</p>
{!selectedModules.includes("speaking") &&
!selectedModules.includes("level") &&
!disableSelection && (
{!selectedModules.includes("speaking") && !selectedModules.includes("level") && !disableSelection && (
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
)}
{(selectedModules.includes("speaking") || disableSelection) && (
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
)}
{selectedModules.includes("level") && (
<BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />
)}
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />}
</div>
{!disableSelection && (
<div
onClick={
selectedModules.length === 0 ||
selectedModules.includes("level")
? () => toggleModule("level")
: undefined
}
onClick={selectedModules.length === 0 || selectedModules.includes("level") ? () => toggleModule("level") : undefined}
className={clsx(
"bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out",
selectedModules.includes("level") || disableSelection
? "border-mti-purple-light"
: "border-mti-gray-platinum",
)}
>
selectedModules.includes("level") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum",
)}>
<div className="bg-ielts-level absolute top-0 flex h-16 w-16 -translate-y-1/2 items-center justify-center rounded-full">
<BsClipboard className="h-7 w-7 text-white" />
</div>
<span className="font-semibold">Level:</span>
<p className="text-left text-xs">
You&apos;ll be able to test your english level with multiple
choice questions.
</p>
{!selectedModules.includes("level") &&
selectedModules.length === 0 &&
!disableSelection && (
<p className="text-left text-xs">You&apos;ll be able to test your english level with multiple choice questions.</p>
{!selectedModules.includes("level") && selectedModules.length === 0 && !disableSelection && (
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
)}
{(selectedModules.includes("level") || disableSelection) && (
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
)}
{!selectedModules.includes("level") &&
selectedModules.length > 0 && (
{!selectedModules.includes("level") && selectedModules.length > 0 && (
<BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />
)}
</div>
@@ -371,68 +256,51 @@ export default function Selection({
<div className="flex w-full flex-col items-center gap-3">
<div
className="text-mti-gray-dim -md:justify-center flex w-full cursor-pointer items-center gap-3 text-sm"
onClick={() => setAvoidRepeatedExams((prev) => !prev)}
>
onClick={() => setAvoidRepeatedExams((prev) => !prev)}>
<input type="checkbox" className="hidden" />
<div
className={clsx(
"border-mti-purple-light flex h-6 w-6 items-center justify-center rounded-md border bg-white",
"transition duration-300 ease-in-out",
avoidRepeatedExams && "!bg-mti-purple-light ",
)}
>
)}>
<BsCheck color="white" className="h-full w-full" />
</div>
<span
className="tooltip"
data-tip="If possible, the platform will choose exams not yet done."
>
<span className="tooltip" data-tip="If possible, the platform will choose exams not yet done.">
Avoid Repeated Questions
</span>
</div>
<div
className="text-mti-gray-dim -md:justify-center flex w-full cursor-pointer items-center gap-3 text-sm"
// onClick={() => setVariant((prev) => (prev === "full" ? "partial" : "full"))}>
>
<input type="checkbox" className="hidden" disabled />
onClick={() => setVariant((prev) => (prev === "full" ? "partial" : "full"))}>
<input type="checkbox" className="hidden" />
<div
className={clsx(
"border-mti-purple-light flex h-6 w-6 items-center justify-center rounded-md border bg-white",
"transition duration-300 ease-in-out",
variant === "full" && "!bg-mti-purple-light ",
)}
>
)}>
<BsCheck color="white" className="h-full w-full" />
</div>
<span>Full length exams</span>
</div>
</div>
<div
className="tooltip w-full"
data-tip={`Your screen size is too small to do ${page}`}
>
<Button
color="purple"
className="w-full max-w-xs px-12 md:hidden"
disabled
>
<div className="tooltip w-full" data-tip={`Your screen size is too small to do ${page}`}>
<Button color="purple" className="w-full max-w-xs px-12 md:hidden" disabled>
Start Exam
</Button>
</div>
<Button
onClick={() =>
onStart(
!disableSelection
? selectedModules.sort(sortByModuleName)
: ["reading", "listening", "writing", "speaking"],
!disableSelection ? selectedModules.sort(sortByModuleName) : ["reading", "listening", "writing", "speaking"],
avoidRepeatedExams,
variant,
)
}
color="purple"
className="-md:hidden w-full max-w-xs px-12 md:self-end"
disabled={selectedModules.length === 0 && !disableSelection}
>
disabled={selectedModules.length === 0 && !disableSelection}>
Start Exam
</Button>
</div>

View File

@@ -2,32 +2,40 @@ import {Group, User} from "@/interfaces/user";
import axios from "axios";
import {useEffect, useState} from "react";
export default function useGroups(admin?: string, userType?: string) {
interface Props {
admin?: string;
userType?: string;
adminAdmins?: string;
}
export default function useGroups({admin, userType, adminAdmins}: Props) {
const [groups, setGroups] = useState<Group[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const isMasterType = userType?.startsWith('master');
const isMasterType = userType?.startsWith("master");
const getData = () => {
setIsLoading(true);
const url = admin ? `/api/groups?admin=${admin}` : "/api/groups";
const url = admin && !adminAdmins ? `/api/groups?admin=${admin}` : "/api/groups";
axios
.get<Group[]>(url)
.then((response) => {
if(isMasterType) {
return setGroups(response.data);
}
const filter = (g: Group) => g.admin === admin || g.participants.includes(admin || "");
if (isMasterType) return setGroups(response.data);
const filteredGroups = admin ? response.data.filter(filter) : response.data;
const filterByAdmins = !!adminAdmins
? [adminAdmins, ...response.data.filter((g) => g.participants.includes(adminAdmins)).flatMap((g) => g.admin)]
: [admin];
const adminFilter = (g: Group) => filterByAdmins.includes(g.admin) || g.participants.includes(admin || "");
const filteredGroups = !!admin || !!adminAdmins ? response.data.filter(adminFilter) : response.data;
return setGroups(admin ? filteredGroups.map((g) => ({...g, disableEditing: g.disableEditing || g.admin !== admin})) : filteredGroups);
})
.finally(() => setIsLoading(false));
};
useEffect(getData, [admin, isMasterType]);
useEffect(getData, [admin, adminAdmins, isMasterType]);
return {groups, isLoading, isError, reload: getData};
}

View File

@@ -17,7 +17,6 @@ export default function usePermissions(user: string) {
const permissionTypes = response.data
.filter((x) => !x.users.includes(user))
.reduce((acc, curr) => [...acc, curr.type], [] as PermissionType[]);
console.log(response.data, permissionTypes);
setPermissions(permissionTypes);
})
.finally(() => setIsLoading(false));

View File

@@ -12,6 +12,7 @@ interface ExamBase {
isDiagnostic: boolean;
variant?: Variant;
difficulty?: Difficulty;
shuffle?: boolean;
createdBy?: string; // option as it has been added later
createdAt?: string; // option as it has been added later
}
@@ -36,6 +37,7 @@ export interface LevelExam extends ExamBase {
export interface LevelPart {
context?: string;
showContextLines?: boolean;
exercises: Exercise[];
}
@@ -65,6 +67,7 @@ export interface UserSolution {
};
exercise: string;
isDisabled?: boolean;
shuffleMaps?: ShuffleMap[]
}
export interface WritingExam extends ExamBase {
@@ -96,8 +99,8 @@ export type Exercise =
export interface Evaluation {
comment: string;
overall: number;
task_response: {[key: string]: number | {grade: number; comment: string}};
misspelled_pairs?: {correction: string | null; misspelled: string}[];
task_response: { [key: string]: number | { grade: number; comment: string } };
misspelled_pairs?: { correction: string | null; misspelled: string }[];
}
@@ -110,10 +113,9 @@ type InteractiveTranscriptType = { [key in InteractiveTranscriptKey]?: string };
type InteractiveFixedTextType = { [key in InteractiveFixedTextKey]?: string };
interface InteractiveSpeakingEvaluation extends Evaluation,
InteractivePerfectAnswerType,
InteractiveTranscriptType,
InteractiveFixedTextType
{}
InteractivePerfectAnswerType,
InteractiveTranscriptType,
InteractiveFixedTextType { }
interface SpeakingEvaluation extends CommonEvaluation {
@@ -187,10 +189,10 @@ export interface InteractiveSpeakingExercise {
first_title?: string;
second_title?: string;
text: string;
prompts: {text: string; video_url: string}[];
prompts: { text: string; video_url: string }[];
userSolutions: {
id: string;
solution: {questionIndex: number; question: string; answer: string}[];
solution: { questionIndex: number; question: string; answer: string }[];
evaluation?: InteractiveSpeakingEvaluation;
}[];
topic?: string;
@@ -199,13 +201,23 @@ export interface InteractiveSpeakingExercise {
variant?: "initial" | "final";
}
export interface FillBlanksMCOption {
id: string;
options: {
A: string;
B: string;
C: string;
D: string;
}
}
export interface FillBlanksExercise {
prompt: string; // *EXAMPLE: "Complete the summary below. Click a blank to select the corresponding word for it."
type: "fillBlanks";
id: string;
words: (string | {letter: string; word: string})[]; // *EXAMPLE: ["preserve", "unaware"]
words: (string | { letter: string; word: string } | FillBlanksMCOption)[]; // *EXAMPLE: ["preserve", "unaware"]
text: string; // *EXAMPLE: "They tried to {{1}} burning"
allowRepetition: boolean;
allowRepetition?: boolean;
solutions: {
id: string; // *EXAMPLE: "1"
solution: string; // *EXAMPLE: "preserve"
@@ -214,6 +226,7 @@ export interface FillBlanksExercise {
id: string; // *EXAMPLE: "1"
solution: string; // *EXAMPLE: "preserve"
}[];
variant?: string;
}
export interface TrueFalseExercise {
@@ -221,7 +234,7 @@ export interface TrueFalseExercise {
id: string;
prompt: string; // *EXAMPLE: "Select the appropriate option."
questions: TrueFalseQuestion[];
userSolutions: {id: string; solution: "true" | "false" | "not_given"}[];
userSolutions: { id: string; solution: "true" | "false" | "not_given" }[];
}
export interface TrueFalseQuestion {
@@ -250,7 +263,7 @@ export interface MatchSentencesExercise {
type: "matchSentences";
id: string;
prompt: string;
userSolutions: {question: string; option: string}[];
userSolutions: { question: string; option: string }[];
sentences: MatchSentenceExerciseSentence[];
allowRepetition: boolean;
options: MatchSentenceExerciseOption[];
@@ -273,7 +286,7 @@ export interface MultipleChoiceExercise {
id: string;
prompt: string; // *EXAMPLE: "Select the appropriate option."
questions: MultipleChoiceQuestion[];
userSolutions: {question: string; option: string}[];
userSolutions: { question: string; option: string }[];
}
export interface MultipleChoiceQuestion {
@@ -286,4 +299,12 @@ export interface MultipleChoiceQuestion {
src?: string; // *EXAMPLE: "https://i.imgur.com/rEbrSqA.png" (only used if the variant is "image")
text?: string; // *EXAMPLE: "wallet, pens and novel" (only used if the variant is "text")
}[];
shuffleMap?: Record<string, string>;
}
export interface ShuffleMap {
id: string;
map: {
[key: string]: string;
}
}

View File

@@ -1,15 +1,8 @@
import { Module } from ".";
import { InstructorGender } from "./exam";
import { PermissionType } from "./permissions";
import {Module} from ".";
import {InstructorGender, ShuffleMap} from "./exam";
import {PermissionType} from "./permissions";
export type User =
| StudentUser
| TeacherUser
| CorporateUser
| AgentUser
| AdminUser
| DeveloperUser
| MasterCorporateUser;
export type User = StudentUser | TeacherUser | CorporateUser | AgentUser | AdminUser | DeveloperUser | MasterCorporateUser;
export type UserStatus = "active" | "disabled" | "paymentDue";
export interface BasicUser {
@@ -19,15 +12,16 @@ export interface BasicUser {
id: string;
isFirstLogin: boolean;
focus: "academic" | "general";
levels: { [key in Module]: number };
desiredLevels: { [key in Module]: number };
levels: {[key in Module]: number};
desiredLevels: {[key in Module]: number};
type: Type;
bio: string;
isVerified: boolean;
subscriptionExpirationDate?: null | Date;
registrationDate?: Date;
status: UserStatus;
permissions: PermissionType[],
permissions: PermissionType[];
lastLogin?: Date;
}
export interface StudentUser extends BasicUser {
@@ -112,22 +106,15 @@ export interface DemographicCorporateInformation {
}
export type Gender = "male" | "female" | "other";
export type EmploymentStatus =
| "employed"
| "student"
| "self-employed"
| "unemployed"
| "retired"
| "other";
export const EMPLOYMENT_STATUS: { status: EmploymentStatus; label: string }[] =
[
{ status: "student", label: "Student" },
{ status: "employed", label: "Employed" },
{ status: "unemployed", label: "Unemployed" },
{ status: "self-employed", label: "Self-employed" },
{ status: "retired", label: "Retired" },
{ status: "other", label: "Other" },
];
export type EmploymentStatus = "employed" | "student" | "self-employed" | "unemployed" | "retired" | "other";
export const EMPLOYMENT_STATUS: {status: EmploymentStatus; label: string}[] = [
{status: "student", label: "Student"},
{status: "employed", label: "Employed"},
{status: "unemployed", label: "Unemployed"},
{status: "self-employed", label: "Self-employed"},
{status: "retired", label: "Retired"},
{status: "other", label: "Other"},
];
export interface Stat {
id: string;
@@ -148,6 +135,7 @@ export interface Stat {
missing: number;
};
isDisabled?: boolean;
shuffleMaps?: ShuffleMap[];
}
export interface Group {
@@ -170,20 +158,5 @@ export interface Code {
passport_id?: string;
}
export type Type =
| "student"
| "teacher"
| "corporate"
| "admin"
| "developer"
| "agent"
| "mastercorporate";
export const userTypes: Type[] = [
"student",
"teacher",
"corporate",
"admin",
"developer",
"agent",
"mastercorporate",
];
export type Type = "student" | "teacher" | "corporate" | "admin" | "developer" | "agent" | "mastercorporate";
export const userTypes: Type[] = ["student", "teacher", "corporate", "admin", "developer", "agent", "mastercorporate"];

6
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@@ -111,7 +111,6 @@ export default function BatchCreateUser({user}: {user: User}) {
return clear();
}
console.log(information);
setInfos(information);
} catch {
toast.error(

View File

@@ -154,7 +154,13 @@ const CreatePanel = ({user, users, group, onClose}: CreateDialogProps) => {
label: `${users.find((y) => y.id === x)?.email} - ${users.find((y) => y.id === x)?.name}`,
}))}
options={users
.filter((x) => (user.type === "teacher" ? x.type === "student" : x.type === "student" || x.type === "teacher"))
.filter((x) =>
user.type === "teacher"
? x.type === "student"
: user.type === "corporate"
? x.type === "student" || x.type === "teacher"
: x.type === "student" || x.type === "teacher" || x.type === "corporate",
)
.map((x) => ({value: x.id, label: `${x.email} - ${x.name}`}))}
onChange={(value) => setParticipants(value.map((x) => x.value))}
isMulti
@@ -201,7 +207,11 @@ export default function GroupList({user}: {user: User}) {
const {permissions} = usePermissions(user?.id || "");
const {users} = useUsers();
const {groups, reload} = useGroups(user && filterTypes.includes(user?.type) ? user.id : undefined, user?.type);
const {groups, reload} = useGroups({
admin: user && filterTypes.includes(user?.type) ? user.id : undefined,
userType: user?.type,
adminAdmins: user?.type === "teacher" ? user?.id : undefined,
});
useEffect(() => {
if (user && ["corporate", "teacher", "mastercorporate"].includes(user.type)) {

View File

@@ -58,7 +58,7 @@ export default function UserList({
const {users, reload} = useUsers();
const {permissions} = usePermissions(user?.id || "");
const {groups} = useGroups(user && ["corporate", "teacher", "mastercorporate"].includes(user?.type) ? user.id : undefined);
const {groups} = useGroups({admin: user && ["corporate", "teacher", "mastercorporate"].includes(user?.type) ? user.id : undefined});
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
const router = useRouter();
@@ -262,10 +262,12 @@ export default function UserList({
cell: ({row, getValue}) => (
<div
className={clsx(
PERMISSIONS.updateExpiryDate[row.original.type]?.includes(user.type) &&
checkAccess(user, ["admin", "corporate", "developer", "mastercorporate"]) &&
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer",
)}
onClick={() => (PERMISSIONS.updateExpiryDate[row.original.type]?.includes(user.type) ? setSelectedUser(row.original) : null)}>
onClick={() =>
checkAccess(user, ["admin", "corporate", "developer", "mastercorporate"]) ? setSelectedUser(row.original) : null
}>
{getValue()}
</div>
),
@@ -279,10 +281,10 @@ export default function UserList({
) as any,
cell: (info) =>
info.getValue()
? `${countryCodes.findOne("countryCode" as any, info.getValue()).flag} ${
countries[info.getValue() as unknown as keyof TCountries].name
} (+${countryCodes.findOne("countryCode" as any, info.getValue()).countryCallingCode})`
: "Not available",
? `${countryCodes.findOne("countryCode" as any, info.getValue())?.flag} ${
countries[info.getValue() as unknown as keyof TCountries]?.name
} (+${countryCodes.findOne("countryCode" as any, info.getValue())?.countryCallingCode})`
: "N/A",
}),
columnHelper.accessor("demographicInformation.phone", {
header: (
@@ -291,7 +293,7 @@ export default function UserList({
<SorterArrow name="phone" />
</button>
) as any,
cell: (info) => info.getValue() || "Not available",
cell: (info) => info.getValue() || "N/A",
enableSorting: true,
}),
columnHelper.accessor(
@@ -301,14 +303,23 @@ export default function UserList({
id: "employment",
header: (
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "employment"))}>
<span>Employment/Position</span>
<span>Employment</span>
<SorterArrow name="employment" />
</button>
) as any,
cell: (info) => (info.row.original.type === "corporate" ? info.getValue() : capitalize(info.getValue())) || "Not available",
cell: (info) => (info.row.original.type === "corporate" ? info.getValue() : capitalize(info.getValue())) || "N/A",
enableSorting: true,
},
),
columnHelper.accessor("lastLogin", {
header: (
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "lastLogin"))}>
<span>Last Login</span>
<SorterArrow name="lastLogin" />
</button>
) as any,
cell: (info) => (!!info.getValue() ? moment(info.getValue()).format("YYYY-MM-DD HH:mm") : "N/A"),
}),
columnHelper.accessor("demographicInformation.gender", {
header: (
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "gender"))}>
@@ -316,7 +327,7 @@ export default function UserList({
<SorterArrow name="gender" />
</button>
) as any,
cell: (info) => capitalize(info.getValue()) || "Not available",
cell: (info) => capitalize(info.getValue()) || "N/A",
enableSorting: true,
}),
{
@@ -341,11 +352,13 @@ export default function UserList({
cell: ({row, getValue}) => (
<div
className={clsx(
PERMISSIONS.updateExpiryDate[row.original.type]?.includes(user.type) &&
checkAccess(user, ["admin", "corporate", "developer", "mastercorporate"]) &&
"underline text-mti-purple-light hover:text-mti-purple-dark transition ease-in-out duration-300 cursor-pointer",
)}
onClick={() => (PERMISSIONS.updateExpiryDate[row.original.type]?.includes(user.type) ? setSelectedUser(row.original) : null)}>
{row.original.type === "corporate" ? row.original.corporateInformation?.companyInformation?.name || getValue() : getValue()}
onClick={() =>
checkAccess(user, ["admin", "corporate", "developer", "mastercorporate"]) ? setSelectedUser(row.original) : null
}>
{getValue()}
</div>
),
}),
@@ -379,7 +392,7 @@ export default function UserList({
columnHelper.accessor("corporateInformation.companyInformation.name", {
header: (
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "companyName"))}>
<span>Company Name</span>
<span>Company</span>
<SorterArrow name="companyName" />
</button>
) as any,
@@ -388,7 +401,7 @@ export default function UserList({
columnHelper.accessor("subscriptionExpirationDate", {
header: (
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "expiryDate"))}>
<span>Expiry Date</span>
<span>Expiration</span>
<SorterArrow name="expiryDate" />
</button>
) as any,
@@ -401,7 +414,7 @@ export default function UserList({
columnHelper.accessor("isVerified", {
header: (
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "verification"))}>
<span>Verification</span>
<span>Verified</span>
<SorterArrow name="verification" />
</button>
) as any,
@@ -464,6 +477,15 @@ export default function UserList({
return 0;
}
if (sorter === "lastLogin" || sorter === reverseString("lastLogin")) {
if (!a.lastLogin && b.lastLogin) return sorter === "lastLogin" ? -1 : 1;
if (a.lastLogin && !b.lastLogin) return sorter === "lastLogin" ? 1 : -1;
if (!a.lastLogin && !b.lastLogin) return 0;
if (moment(a.lastLogin).isAfter(b.lastLogin)) return sorter === "lastLogin" ? -1 : 1;
if (moment(b.lastLogin).isAfter(a.lastLogin)) return sorter === "lastLogin" ? 1 : -1;
return 0;
}
if (sorter === "country" || sorter === reverseString("country")) {
if (!a.demographicInformation?.country && b.demographicInformation?.country) return sorter === "country" ? -1 : 1;
if (a.demographicInformation?.country && !b.demographicInformation?.country) return sorter === "country" ? 1 : -1;

View File

@@ -27,7 +27,7 @@ export default function Lists({user}: {user: User}) {
}>
User List
</Tab>
{checkAccess(user, ["developer"]) && (
{checkAccess(user, ["developer", "admin", "corporate", "mastercorporate", "teacher"]) && (
<Tab
className={({selected}) =>
clsx(

View File

@@ -12,7 +12,7 @@ import Selection from "@/exams/Selection";
import Speaking from "@/exams/Speaking";
import Writing from "@/exams/Writing";
import useUser from "@/hooks/useUser";
import {Exam, UserSolution, Variant} from "@/interfaces/exam";
import {Exam, LevelExam, UserSolution, Variant} from "@/interfaces/exam";
import {Stat} from "@/interfaces/user";
import useExamStore from "@/stores/examStore";
import {evaluateSpeakingAnswer, evaluateWritingAnswer} from "@/utils/evaluation";
@@ -257,6 +257,7 @@ export default function ExamPage({page}: Props) {
user: user?.id || "",
date: new Date().getTime(),
isDisabled: solution.isDisabled,
shuffleMaps: solution.shuffleMaps,
...(assignment ? {assignment: assignment.id} : {}),
}));
@@ -459,6 +460,19 @@ export default function ExamPage({page}: Props) {
inactivity: totalInactivity,
}}
onViewResults={(index?: number) => {
if (exams[0].module === "level") {
const levelExam = exams[0] as LevelExam;
const allExercises = levelExam.parts.flatMap(part => part.exercises);
const exerciseOrderMap = new Map(allExercises.map((ex, index) => [ex.id, index]));
const orderedSolutions = userSolutions.slice().sort((a, b) => {
const indexA = exerciseOrderMap.get(a.exercise) ?? Infinity;
const indexB = exerciseOrderMap.get(b.exercise) ?? Infinity;
return indexA - indexB;
});
setUserSolutions(orderedSolutions);
} else {
setUserSolutions(userSolutions);
}
setShowSolutions(true);
setModuleIndex(index || 0);
setExerciseIndex(["reading", "listening"].includes(exams[0].module) ? -1 : 0);

View File

@@ -31,7 +31,7 @@ export default function PaymentDue({user, hasExpired = false, reload}: Props) {
const {packages} = usePackages();
const {discounts} = useDiscounts();
const {users} = useUsers();
const {groups} = useGroups();
const {groups} = useGroups({});
const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user?.id});
useEffect(() => {

View File

@@ -36,5 +36,5 @@ async function GET(req: NextApiRequest, res: NextApiResponse) {
const assigners = await getAllAssignersByCorporate(id);
const assignments = await getAssignmentsByAssigners([...assigners, id]);
res.status(200).json(assignments);
res.status(200).json(uniqBy(assignments, "id"));
}

View File

@@ -1,23 +1,13 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import { app } from "@/firebase";
import {
getFirestore,
setDoc,
doc,
query,
collection,
where,
getDocs,
getDoc,
deleteDoc,
} from "firebase/firestore";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { Code, Type } from "@/interfaces/user";
import { PERMISSIONS } from "@/constants/userPermissions";
import { uuidv4 } from "@firebase/util";
import { prepareMailer, prepareMailOptions } from "@/email";
import type {NextApiRequest, NextApiResponse} from "next";
import {app} from "@/firebase";
import {getFirestore, setDoc, doc, query, collection, where, getDocs, getDoc, deleteDoc} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {Code, Group, Type} from "@/interfaces/user";
import {PERMISSIONS} from "@/constants/userPermissions";
import {uuidv4} from "@firebase/util";
import {prepareMailer, prepareMailOptions} from "@/email";
const db = getFirestore(app);
@@ -28,22 +18,17 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") return post(req, res);
if (req.method === "DELETE") return del(req, res);
return res.status(404).json({ ok: false });
return res.status(404).json({ok: false});
}
async function get(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res
.status(401)
.json({ ok: false, reason: "You must be logged in to generate a code!" });
res.status(401).json({ok: false, reason: "You must be logged in to generate a code!"});
return;
}
const { creator } = req.query as { creator?: string };
const q = query(
collection(db, "codes"),
where("creator", "==", creator || ""),
);
const {creator} = req.query as {creator?: string};
const q = query(collection(db, "codes"), where("creator", "==", creator || ""));
const snapshot = await getDocs(creator ? q : collection(db, "codes"));
res.status(200).json(snapshot.docs.map((doc) => doc.data()));
@@ -51,16 +36,14 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
async function post(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res
.status(401)
.json({ ok: false, reason: "You must be logged in to generate a code!" });
res.status(401).json({ok: false, reason: "You must be logged in to generate a code!"});
return;
}
const { type, codes, infos, expiryDate } = req.body as {
const {type, codes, infos, expiryDate} = req.body as {
type: Type;
codes: string[];
infos?: { email: string; name: string; passport_id?: string }[];
infos?: {email: string; name: string; passport_id?: string}[];
expiryDate: null | Date;
};
const permission = PERMISSIONS.generateCode[type];
@@ -68,23 +51,28 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
if (!permission.includes(req.session.user.type)) {
res.status(403).json({
ok: false,
reason:
"Your account type does not have permissions to generate a code for that type of user!",
reason: "Your account type does not have permissions to generate a code for that type of user!",
});
return;
}
const codesGeneratedByUserSnapshot = await getDocs(
query(collection(db, "codes"), where("creator", "==", req.session.user.id)),
);
const codesGeneratedByUserSnapshot = await getDocs(query(collection(db, "codes"), where("creator", "==", req.session.user.id)));
const creatorGroupsSnapshot = await getDocs(query(collection(db, "groups"), where("admin", "==", req.session.user.id)));
const creatorGroups = (
creatorGroupsSnapshot.docs.map((x) => ({
...x.data(),
})) as Group[]
).filter((x) => x.name === "Students" || x.name === "Teachers" || x.name === "Corporate");
const usersInGroups = creatorGroups.flatMap((x) => x.participants);
const userCodes = codesGeneratedByUserSnapshot.docs.map((x) => ({
...x.data(),
}));
})) as Code[];
if (req.session.user.type === "corporate") {
const totalCodes = codesGeneratedByUserSnapshot.docs.length + codes.length;
const allowedCodes =
req.session.user.corporateInformation?.companyInformation.userAmount || 0;
const totalCodes = userCodes.filter((x) => !x.userId || !usersInGroups.includes(x.userId)).length + usersInGroups.length + codes.length;
const allowedCodes = req.session.user.corporateInformation?.companyInformation.userAmount || 0;
if (totalCodes > allowedCodes) {
res.status(403).json({
@@ -108,7 +96,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
};
if (infos && infos.length > index) {
const { email, name, passport_id } = infos[index];
const {email, name, passport_id} = infos[index];
const previousCode = userCodes.find((x) => x.email === email) as Code;
const transport = prepareMailer();
@@ -133,9 +121,9 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
...codeInformation,
email: email.trim().toLowerCase(),
name: name.trim(),
...(passport_id ? { passport_id: passport_id.trim() } : {}),
...(passport_id ? {passport_id: passport_id.trim()} : {}),
},
{ merge: true },
{merge: true},
);
}
@@ -149,15 +137,13 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
});
Promise.all(codePromises).then((results) => {
res.status(200).json({ ok: true, valid: results.filter((x) => x).length });
res.status(200).json({ok: true, valid: results.filter((x) => x).length});
});
}
async function del(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res
.status(401)
.json({ ok: false, reason: "You must be logged in to generate a code!" });
res.status(401).json({ok: false, reason: "You must be logged in to generate a code!"});
return;
}
@@ -170,5 +156,5 @@ async function del(req: NextApiRequest, res: NextApiResponse) {
await deleteDoc(snapshot.ref);
}
res.status(200).json({ codes });
res.status(200).json({codes});
}

View File

@@ -107,10 +107,12 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
}
const user = docUser.data() as User;
await setDoc(docUser.ref, {lastLogin: new Date().toISOString()}, {merge: true});
req.session.user = {
...user,
id: req.session.user.id,
lastLogin: new Date(),
};
await req.session.save();

119
src/pages/groups.tsx Normal file
View File

@@ -0,0 +1,119 @@
/* eslint-disable @next/next/no-img-element */
import Head from "next/head";
import Navbar from "@/components/Navbar";
import {BsFileEarmarkText, BsPencil, BsStar, BsBook, BsHeadphones, BsPen, BsMegaphone} from "react-icons/bs";
import {withIronSessionSsr} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {useEffect, useState} from "react";
import useStats from "@/hooks/useStats";
import {averageScore, groupBySession, totalExams} from "@/utils/stats";
import useUser from "@/hooks/useUser";
import Diagnostic from "@/components/Diagnostic";
import {ToastContainer} from "react-toastify";
import {capitalize} from "lodash";
import {Module} from "@/interfaces";
import ProgressBar from "@/components/Low/ProgressBar";
import Layout from "@/components/High/Layout";
import {calculateAverageLevel} from "@/utils/score";
import axios from "axios";
import DemographicInformationInput from "@/components/DemographicInformationInput";
import moment from "moment";
import Link from "next/link";
import {MODULE_ARRAY} from "@/utils/moduleUtils";
import ProfileSummary from "@/components/ProfileSummary";
import StudentDashboard from "@/dashboards/Student";
import AdminDashboard from "@/dashboards/Admin";
import CorporateDashboard from "@/dashboards/Corporate";
import TeacherDashboard from "@/dashboards/Teacher";
import AgentDashboard from "@/dashboards/Agent";
import MasterCorporateDashboard from "@/dashboards/MasterCorporate";
import PaymentDue from "./(status)/PaymentDue";
import {useRouter} from "next/router";
import {PayPalScriptProvider} from "@paypal/react-paypal-js";
import {CorporateUser, MasterCorporateUser, Type, User, userTypes} from "@/interfaces/user";
import Select from "react-select";
import {USER_TYPE_LABELS} from "@/resources/user";
import {checkAccess, getTypesOfUser} from "@/utils/permissions";
import {shouldRedirectHome} from "@/utils/navigation.disabled";
import useGroups from "@/hooks/useGroups";
import useUsers from "@/hooks/useUsers";
import {getUserName} from "@/utils/users";
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user;
if (!user || !user.isVerified) {
return {
redirect: {
destination: "/login",
permanent: false,
},
};
}
if (shouldRedirectHome(user)) {
return {
redirect: {
destination: "/",
permanent: false,
},
};
}
return {
props: {user: req.session.user},
};
}, sessionOptions);
interface Props {
user: User;
envVariables: {[key: string]: string};
}
export default function Home(props: Props) {
const {user, mutateUser} = useUser({redirectTo: "/login"});
const {groups} = useGroups({});
const {users} = useUsers();
const router = useRouter();
useEffect(() => {
console.log(groups);
}, [groups]);
return (
<>
<Head>
<title>EnCoach</title>
<meta
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" />
</Head>
<ToastContainer />
{user && (
<Layout user={user}>
<div className="w-full h-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{groups
.filter((x) => x.participants.includes(user.id))
.map((group) => (
<div key={group.id} className="p-4 border rounded-xl flex flex-col gap-2">
<span>
<b>Group: </b>
{group.name}
</span>
<span>
<b>Admin: </b>
{getUserName(users.find((x) => x.id === group.admin))}
</span>
<b>Participants: </b>
<span>{group.participants.map((x) => getUserName(users.find((u) => u.id === x))).join(", ")}</span>
</div>
))}
</div>
</Layout>
)}
</>
);
}

View File

@@ -1,56 +1,41 @@
/* eslint-disable @next/next/no-img-element */
import Head from "next/head";
import { withIronSessionSsr } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import {
ChangeEvent,
Dispatch,
ReactNode,
SetStateAction,
useEffect,
useRef,
useState,
} from "react";
import {withIronSessionSsr} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {ChangeEvent, Dispatch, ReactNode, SetStateAction, useEffect, useRef, useState} from "react";
import useUser from "@/hooks/useUser";
import { toast, ToastContainer } from "react-toastify";
import {toast, ToastContainer} from "react-toastify";
import Layout from "@/components/High/Layout";
import Input from "@/components/Low/Input";
import Button from "@/components/Low/Button";
import Link from "next/link";
import axios from "axios";
import { ErrorMessage } from "@/constants/errors";
import {ErrorMessage} from "@/constants/errors";
import clsx from "clsx";
import {
CorporateUser,
EmploymentStatus,
EMPLOYMENT_STATUS,
Gender,
User,
DemographicInformation,
} from "@/interfaces/user";
import {CorporateUser, EmploymentStatus, EMPLOYMENT_STATUS, Gender, User, DemographicInformation} from "@/interfaces/user";
import CountrySelect from "@/components/Low/CountrySelect";
import { shouldRedirectHome } from "@/utils/navigation.disabled";
import {shouldRedirectHome} from "@/utils/navigation.disabled";
import moment from "moment";
import { BsCamera, BsQuestionCircleFill } from "react-icons/bs";
import { USER_TYPE_LABELS } from "@/resources/user";
import {BsCamera, BsQuestionCircleFill} from "react-icons/bs";
import {USER_TYPE_LABELS} from "@/resources/user";
import useGroups from "@/hooks/useGroups";
import useUsers from "@/hooks/useUsers";
import { convertBase64 } from "@/utils";
import { Divider } from "primereact/divider";
import {convertBase64} from "@/utils";
import {Divider} from "primereact/divider";
import GenderInput from "@/components/High/GenderInput";
import EmploymentStatusInput from "@/components/High/EmploymentStatusInput";
import TimezoneSelect from "@/components/Low/TImezoneSelect";
import Modal from "@/components/Modal";
import { Module } from "@/interfaces";
import {Module} from "@/interfaces";
import ModuleLevelSelector from "@/components/Medium/ModuleLevelSelector";
import Select from "@/components/Low/Select";
import { InstructorGender } from "@/interfaces/exam";
import { capitalize } from "lodash";
import {InstructorGender} from "@/interfaces/exam";
import {capitalize} from "lodash";
import TopicModal from "@/components/Medium/TopicModal";
import { v4 } from "uuid";
import { checkAccess, getTypesOfUser } from "@/utils/permissions";
import {v4} from "uuid";
import {checkAccess, getTypesOfUser} from "@/utils/permissions";
export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user;
if (!user || !user.isVerified) {
@@ -72,7 +57,7 @@ export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
}
return {
props: { user: req.session.user },
props: {user: req.session.user},
};
}, sessionOptions);
@@ -81,11 +66,9 @@ interface Props {
mutateUser: Function;
}
const DoubleColumnRow = ({ children }: { children: ReactNode }) => (
<div className="flex flex-col lg:flex-row gap-8 w-full">{children}</div>
);
const DoubleColumnRow = ({children}: {children: ReactNode}) => <div className="flex flex-col lg:flex-row gap-8 w-full">{children}</div>;
function UserProfile({ user, mutateUser }: Props) {
function UserProfile({user, mutateUser}: Props) {
const [bio, setBio] = useState(user.bio || "");
const [name, setName] = useState(user.name || "");
const [email, setEmail] = useState(user.email || "");
@@ -94,88 +77,51 @@ function UserProfile({ user, mutateUser }: Props) {
const [isLoading, setIsLoading] = useState(false);
const [profilePicture, setProfilePicture] = useState(user.profilePicture);
const [desiredLevels, setDesiredLevels] = useState<
{ [key in Module]: number } | undefined
>(
checkAccess(user, ["developer", "student"]) ? user.desiredLevels : undefined
const [desiredLevels, setDesiredLevels] = useState<{[key in Module]: number} | undefined>(
checkAccess(user, ["developer", "student"]) ? user.desiredLevels : undefined,
);
const [focus, setFocus] = useState<"academic" | "general">(user.focus);
const [country, setCountry] = useState<string>(
user.demographicInformation?.country || ""
);
const [phone, setPhone] = useState<string>(
user.demographicInformation?.phone || ""
);
const [gender, setGender] = useState<Gender | undefined>(
user.demographicInformation?.gender || undefined
);
const [country, setCountry] = useState<string>(user.demographicInformation?.country || "");
const [phone, setPhone] = useState<string>(user.demographicInformation?.phone || "");
const [gender, setGender] = useState<Gender | undefined>(user.demographicInformation?.gender || undefined);
const [employment, setEmployment] = useState<EmploymentStatus | undefined>(
checkAccess(user, ["corporate", "mastercorporate"])
? undefined
: (user.demographicInformation as DemographicInformation)?.employment
checkAccess(user, ["corporate", "mastercorporate"]) ? undefined : (user.demographicInformation as DemographicInformation)?.employment,
);
const [passport_id, setPassportID] = useState<string | undefined>(
checkAccess(user, ["student"])
? (user.demographicInformation as DemographicInformation)?.passport_id
: undefined
checkAccess(user, ["student"]) ? (user.demographicInformation as DemographicInformation)?.passport_id : undefined,
);
const [preferredGender, setPreferredGender] = useState<
InstructorGender | undefined
>(
user.type === "student" || user.type === "developer"
? user.preferredGender || "varied"
: undefined
const [preferredGender, setPreferredGender] = useState<InstructorGender | undefined>(
user.type === "student" || user.type === "developer" ? user.preferredGender || "varied" : undefined,
);
const [preferredTopics, setPreferredTopics] = useState<string[] | undefined>(
user.type === "student" || user.type === "developer"
? user.preferredTopics
: undefined
user.type === "student" || user.type === "developer" ? user.preferredTopics : undefined,
);
const [position, setPosition] = useState<string | undefined>(
user.type === "corporate"
? user.demographicInformation?.position
: undefined
);
const [corporateInformation, setCorporateInformation] = useState(
user.type === "corporate" ? user.corporateInformation : undefined
);
const [companyName, setCompanyName] = useState<string | undefined>(
user.type === "agent" ? user.agentInformation?.companyName : undefined
);
const [commercialRegistration, setCommercialRegistration] = useState<
string | undefined
>(
user.type === "agent"
? user.agentInformation?.commercialRegistration
: undefined
);
const [arabName, setArabName] = useState<string | undefined>(
user.type === "agent" ? user.agentInformation?.companyArabName : undefined
const [position, setPosition] = useState<string | undefined>(user.type === "corporate" ? user.demographicInformation?.position : undefined);
const [corporateInformation, setCorporateInformation] = useState(user.type === "corporate" ? user.corporateInformation : undefined);
const [companyName, setCompanyName] = useState<string | undefined>(user.type === "agent" ? user.agentInformation?.companyName : undefined);
const [commercialRegistration, setCommercialRegistration] = useState<string | undefined>(
user.type === "agent" ? user.agentInformation?.commercialRegistration : undefined,
);
const [arabName, setArabName] = useState<string | undefined>(user.type === "agent" ? user.agentInformation?.companyArabName : undefined);
const [timezone, setTimezone] = useState<string>(
user.demographicInformation?.timezone || moment.tz.guess()
);
const [timezone, setTimezone] = useState<string>(user.demographicInformation?.timezone || moment.tz.guess());
const [isPreferredTopicsOpen, setIsPreferredTopicsOpen] = useState(false);
const { groups } = useGroups();
const { users } = useUsers();
const {groups} = useGroups({});
const {users} = useUsers();
const profilePictureInput = useRef(null);
const expirationDateColor = (date: Date) => {
const momentDate = moment(date);
const today = moment(new Date());
if (today.add(1, "days").isAfter(momentDate))
return "!bg-mti-red-ultralight border-mti-red-light";
if (today.add(3, "days").isAfter(momentDate))
return "!bg-mti-rose-ultralight border-mti-rose-light";
if (today.add(7, "days").isAfter(momentDate))
return "!bg-mti-orange-ultralight border-mti-orange-light";
if (today.add(1, "days").isAfter(momentDate)) return "!bg-mti-red-ultralight border-mti-red-light";
if (today.add(3, "days").isAfter(momentDate)) return "!bg-mti-rose-ultralight border-mti-rose-light";
if (today.add(7, "days").isAfter(momentDate)) return "!bg-mti-orange-ultralight border-mti-orange-light";
};
const uploadProfilePicture = async (event: ChangeEvent<HTMLInputElement>) => {
@@ -195,20 +141,15 @@ function UserProfile({ user, mutateUser }: Props) {
}
if (newPassword && !password) {
toast.error(
"To update your password you need to input your current one!"
);
toast.error("To update your password you need to input your current one!");
setIsLoading(false);
return;
}
if (email !== user?.email) {
const userAdmins = groups
.filter((x) => x.participants.includes(user.id))
.map((x) => x.admin);
const userAdmins = groups.filter((x) => x.participants.includes(user.id)).map((x) => x.admin);
const message =
users.filter((x) => userAdmins.includes(x.id) && x.type === "corporate")
.length > 0
users.filter((x) => userAdmins.includes(x.id) && x.type === "corporate").length > 0
? "If you change your e-mail address, you will lose all benefits from your university/institute. Are you sure you want to continue?"
: "Are you sure you want to update your e-mail address?";
@@ -239,7 +180,7 @@ function UserProfile({ user, mutateUser }: Props) {
passport_id,
timezone,
},
...(user.type === "corporate" ? { corporateInformation } : {}),
...(user.type === "corporate" ? {corporateInformation} : {}),
...(user.type === "agent"
? {
agentInformation: {
@@ -253,7 +194,7 @@ function UserProfile({ user, mutateUser }: Props) {
.then((response) => {
if (response.status === 200) {
toast.success("Your profile has been updated!");
mutateUser((response.data as { user: User }).user);
mutateUser((response.data as {user: User}).user);
setIsLoading(false);
return;
}
@@ -269,9 +210,7 @@ function UserProfile({ user, mutateUser }: Props) {
const ExpirationDate = () => (
<div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim">
Expiry Date (click to purchase)
</label>
<label className="font-normal text-base text-mti-gray-dim">Expiry Date (click to purchase)</label>
<Link
href="/payment"
className={clsx(
@@ -280,30 +219,22 @@ function UserProfile({ user, mutateUser }: Props) {
!user.subscriptionExpirationDate
? "!bg-mti-green-ultralight !border-mti-green-light"
: expirationDateColor(user.subscriptionExpirationDate),
"bg-white border-mti-gray-platinum"
)}
>
"bg-white border-mti-gray-platinum",
)}>
{!user.subscriptionExpirationDate && "Unlimited"}
{user.subscriptionExpirationDate &&
moment(user.subscriptionExpirationDate).format("DD/MM/YYYY")}
{user.subscriptionExpirationDate && moment(user.subscriptionExpirationDate).format("DD/MM/YYYY")}
</Link>
</div>
);
const TimezoneInput = () => (
<div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim">
Timezone
</label>
<label className="font-normal text-base text-mti-gray-dim">Timezone</label>
<TimezoneSelect value={timezone} onChange={setTimezone} />
</div>
);
const manualDownloadLink = ["student", "teacher", "corporate"].includes(
user.type
)
? `/manuals/${user.type}.pdf`
: "";
const manualDownloadLink = ["student", "teacher", "corporate"].includes(user.type) ? `/manuals/${user.type}.pdf` : "";
return (
<Layout user={user}>
@@ -312,10 +243,7 @@ function UserProfile({ user, mutateUser }: Props) {
<div className="flex -md:flex-col-reverse -md:items-center w-full justify-between">
<div className="flex flex-col gap-8 w-full md:w-2/3">
<h1 className="text-4xl font-bold mb-6 -md:hidden">Edit Profile</h1>
<form
className="flex flex-col items-center gap-6 w-full"
onSubmit={(e) => e.preventDefault()}
>
<form className="flex flex-col items-center gap-6 w-full" onSubmit={(e) => e.preventDefault()}>
<DoubleColumnRow>
{user.type !== "corporate" ? (
<Input
@@ -411,9 +339,7 @@ function UserProfile({ user, mutateUser }: Props) {
<DoubleColumnRow>
<div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim">
Country *
</label>
<label className="font-normal text-base text-mti-gray-dim">Country *</label>
<CountrySelect value={country} onChange={setCountry} />
</div>
<Input
@@ -446,37 +372,26 @@ function UserProfile({ user, mutateUser }: Props) {
<Divider />
{desiredLevels &&
["developer", "student"].includes(user.type) && (
{desiredLevels && ["developer", "student"].includes(user.type) && (
<>
<div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim">
Desired Levels
</label>
<label className="font-normal text-base text-mti-gray-dim">Desired Levels</label>
<ModuleLevelSelector
levels={desiredLevels}
setLevels={
setDesiredLevels as Dispatch<
SetStateAction<{ [key in Module]: number }>
>
}
setLevels={setDesiredLevels as Dispatch<SetStateAction<{[key in Module]: number}>>}
/>
</div>
<div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim">
Focus
</label>
<label className="font-normal text-base text-mti-gray-dim">Focus</label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-y-4 gap-x-16 w-full">
<button
onClick={() => setFocus("academic")}
className={clsx(
"w-full border border-mti-gray-platinum rounded-full px-6 py-4 flex justify-center items-center gap-12 bg-white",
"hover:bg-mti-purple-light hover:text-white",
focus === "academic" &&
"!bg-mti-purple-light !text-white",
"transition duration-300 ease-in-out"
)}
>
focus === "academic" && "!bg-mti-purple-light !text-white",
"transition duration-300 ease-in-out",
)}>
Academic
</button>
<button
@@ -484,11 +399,9 @@ function UserProfile({ user, mutateUser }: Props) {
className={clsx(
"w-full border border-mti-gray-platinum rounded-full px-6 py-4 flex justify-center items-center gap-12 bg-white",
"hover:bg-mti-purple-light hover:text-white",
focus === "general" &&
"!bg-mti-purple-light !text-white",
"transition duration-300 ease-in-out"
)}
>
focus === "general" && "!bg-mti-purple-light !text-white",
"transition duration-300 ease-in-out",
)}>
General
</button>
</div>
@@ -496,31 +409,22 @@ function UserProfile({ user, mutateUser }: Props) {
</>
)}
{preferredGender &&
["developer", "student"].includes(user.type) && (
{preferredGender && ["developer", "student"].includes(user.type) && (
<>
<Divider />
<DoubleColumnRow>
<div className="flex flex-col gap-3 w-full">
<label className="font-normal text-base text-mti-gray-dim">
Speaking Instructor&apos;s Gender
</label>
<label className="font-normal text-base text-mti-gray-dim">Speaking Instructor&apos;s Gender</label>
<Select
value={{
value: preferredGender,
label: capitalize(preferredGender),
}}
onChange={(value) =>
value
? setPreferredGender(
value.value as InstructorGender
)
: null
}
onChange={(value) => (value ? setPreferredGender(value.value as InstructorGender) : null)}
options={[
{ value: "male", label: "Male" },
{ value: "female", label: "Female" },
{ value: "varied", label: "Varied" },
{value: "male", label: "Male"},
{value: "female", label: "Female"},
{value: "varied", label: "Varied"},
]}
/>
</div>
@@ -529,18 +433,12 @@ function UserProfile({ user, mutateUser }: Props) {
Preferred Topics{" "}
<span
className="tooltip"
data-tip="These topics will be considered for speaking and writing modules, aiming to include at least one exercise containing of the these in the selected exams."
>
data-tip="These topics will be considered for speaking and writing modules, aiming to include at least one exercise containing of the these in the selected exams.">
<BsQuestionCircleFill />
</span>
</label>
<Button
className="w-full"
variant="outline"
onClick={() => setIsPreferredTopicsOpen(true)}
>
Select Topics ({preferredTopics?.length || "All"}{" "}
selected)
<Button className="w-full" variant="outline" onClick={() => setIsPreferredTopicsOpen(true)}>
Select Topics ({preferredTopics?.length || "All"} selected)
</Button>
</div>
</DoubleColumnRow>
@@ -565,9 +463,7 @@ function UserProfile({ user, mutateUser }: Props) {
name="companyUsers"
onChange={() => null}
label="Number of users"
defaultValue={
user.corporateInformation.companyInformation.userAmount
}
defaultValue={user.corporateInformation.companyInformation.userAmount}
disabled
required
/>
@@ -611,20 +507,14 @@ function UserProfile({ user, mutateUser }: Props) {
</>
)}
{user.type === "corporate" &&
user.corporateInformation.referralAgent && (
{user.type === "corporate" && user.corporateInformation.referralAgent && (
<>
<Divider />
<DoubleColumnRow>
<Input
name="agentName"
onChange={() => null}
defaultValue={
users.find(
(x) =>
x.id === user.corporateInformation.referralAgent
)?.name
}
defaultValue={users.find((x) => x.id === user.corporateInformation.referralAgent)?.name}
type="text"
label="Country Manager's Name"
placeholder="Not available"
@@ -634,12 +524,7 @@ function UserProfile({ user, mutateUser }: Props) {
<Input
name="agentEmail"
onChange={() => null}
defaultValue={
users.find(
(x) =>
x.id === user.corporateInformation.referralAgent
)?.email
}
defaultValue={users.find((x) => x.id === user.corporateInformation.referralAgent)?.email}
type="text"
label="Country Manager's E-mail"
placeholder="Not available"
@@ -649,15 +534,11 @@ function UserProfile({ user, mutateUser }: Props) {
</DoubleColumnRow>
<DoubleColumnRow>
<div className="flex flex-col gap-2 w-full">
<label className="font-normal text-base text-mti-gray-dim">
Country Manager&apos;s Country *
</label>
<label className="font-normal text-base text-mti-gray-dim">Country Manager&apos;s Country *</label>
<CountrySelect
value={
users.find(
(x) =>
x.id === user.corporateInformation.referralAgent
)?.demographicInformation?.country
users.find((x) => x.id === user.corporateInformation.referralAgent)?.demographicInformation
?.country
}
onChange={() => null}
disabled
@@ -671,10 +552,7 @@ function UserProfile({ user, mutateUser }: Props) {
onChange={() => null}
placeholder="Not available"
defaultValue={
users.find(
(x) =>
x.id === user.corporateInformation.referralAgent
)?.demographicInformation?.phone
users.find((x) => x.id === user.corporateInformation.referralAgent)?.demographicInformation?.phone
}
disabled
required
@@ -685,10 +563,7 @@ function UserProfile({ user, mutateUser }: Props) {
{user.type !== "corporate" && (
<DoubleColumnRow>
<EmploymentStatusInput
value={employment}
onChange={setEmployment}
/>
<EmploymentStatusInput value={employment} onChange={setEmployment} />
<div className="flex flex-col gap-8 w-full">
<GenderInput value={gender} onChange={setGender} />
@@ -701,62 +576,37 @@ function UserProfile({ user, mutateUser }: Props) {
<div className="flex flex-col gap-6 w-48">
<div
className="flex flex-col gap-3 items-center h-fit cursor-pointer group"
onClick={() => (profilePictureInput.current as any)?.click()}
>
onClick={() => (profilePictureInput.current as any)?.click()}>
<div className="relative overflow-hidden h-48 w-48 rounded-full">
<div
className={clsx(
"absolute top-0 left-0 bg-mti-purple-light/60 w-full h-full z-20 flex items-center justify-center opacity-0 group-hover:opacity-100",
"transition ease-in-out duration-300"
)}
>
"transition ease-in-out duration-300",
)}>
<BsCamera className="text-6xl text-mti-purple-ultralight/80" />
</div>
<img
src={profilePicture}
alt={user.name}
className="aspect-square drop-shadow-xl self-end object-cover"
/>
<img src={profilePicture} alt={user.name} className="aspect-square drop-shadow-xl self-end object-cover" />
</div>
<input
type="file"
className="hidden"
onChange={uploadProfilePicture}
accept="image/*"
ref={profilePictureInput}
/>
<input type="file" className="hidden" onChange={uploadProfilePicture} accept="image/*" ref={profilePictureInput} />
<span
onClick={() => (profilePictureInput.current as any)?.click()}
className="cursor-pointer text-mti-purple-light text-sm"
>
className="cursor-pointer text-mti-purple-light text-sm">
Change picture
</span>
<h6 className="font-normal text-base text-mti-gray-taupe">
{USER_TYPE_LABELS[user.type]}
</h6>
<h6 className="font-normal text-base text-mti-gray-taupe">{USER_TYPE_LABELS[user.type]}</h6>
</div>
{user.type === "agent" && (
<div className="flag items-center h-fit">
<img
alt={
user.demographicInformation?.country.toLowerCase() + "_flag"
}
alt={user.demographicInformation?.country.toLowerCase() + "_flag"}
src={`https://flagcdn.com/w320/${user.demographicInformation?.country.toLowerCase()}.png`}
width="320"
/>
</div>
)}
{manualDownloadLink && (
<a
href={manualDownloadLink}
className="max-w-[200px] self-end w-full"
download
>
<Button
color="purple"
variant="outline"
className="max-w-[200px] self-end w-full"
>
<a href={manualDownloadLink} className="max-w-[200px] self-end w-full" download>
<Button color="purple" variant="outline" className="max-w-[200px] self-end w-full">
Download Manual
</Button>
</a>
@@ -775,20 +625,11 @@ function UserProfile({ user, mutateUser }: Props) {
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
<Link href="/" className="max-w-[200px] self-end w-full">
<Button
color="purple"
variant="outline"
className="max-w-[200px] self-end w-full"
>
<Button color="purple" variant="outline" className="max-w-[200px] self-end w-full">
Back
</Button>
</Link>
<Button
color="purple"
className="max-w-[200px] self-end w-full"
onClick={updateUser}
disabled={isLoading}
>
<Button color="purple" className="max-w-[200px] self-end w-full" onClick={updateUser} disabled={isLoading}>
Save Changes
</Button>
</div>
@@ -798,7 +639,7 @@ function UserProfile({ user, mutateUser }: Props) {
}
export default function Home() {
const { user, mutateUser } = useUser({ redirectTo: "/login" });
const {user, mutateUser} = useUser({redirectTo: "/login"});
return (
<>

View File

@@ -1,30 +1,29 @@
/* eslint-disable @next/next/no-img-element */
import Head from "next/head";
import { withIronSessionSsr } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { Stat, User } from "@/interfaces/user";
import { useEffect, useRef, useState } from "react";
import {withIronSessionSsr} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {Stat, User} from "@/interfaces/user";
import {useEffect, useRef, useState} from "react";
import useStats from "@/hooks/useStats";
import { groupByDate } from "@/utils/stats";
import {groupByDate} from "@/utils/stats";
import moment from "moment";
import useUsers from "@/hooks/useUsers";
import useExamStore from "@/stores/examStore";
import { ToastContainer } from "react-toastify";
import { useRouter } from "next/router";
import {ToastContainer} from "react-toastify";
import {useRouter} from "next/router";
import Layout from "@/components/High/Layout";
import clsx from "clsx";
import Select from "@/components/Low/Select";
import useGroups from "@/hooks/useGroups";
import { shouldRedirectHome } from "@/utils/navigation.disabled";
import {shouldRedirectHome} from "@/utils/navigation.disabled";
import useAssignments from "@/hooks/useAssignments";
import { uuidv4 } from "@firebase/util";
import { usePDFDownload } from "@/hooks/usePDFDownload";
import {uuidv4} from "@firebase/util";
import {usePDFDownload} from "@/hooks/usePDFDownload";
import useRecordStore from "@/stores/recordStore";
import useTrainingContentStore from "@/stores/trainingContentStore";
import StatsGridItem from "@/components/StatGridItem";
export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user;
if (!user || !user.isVerified) {
@@ -46,7 +45,7 @@ export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
}
return {
props: { user: req.session.user },
props: {user: req.session.user},
};
}, sessionOptions);
@@ -55,16 +54,21 @@ const defaultSelectableCorporate = {
label: "All",
};
export default function History({ user }: { user: User }) {
const [statsUserId, setStatsUserId, training, setTraining] = useRecordStore((state) => [state.selectedUser, state.setSelectedUser, state.training, state.setTraining]);
export default function History({user}: {user: User}) {
const [statsUserId, setStatsUserId, training, setTraining] = useRecordStore((state) => [
state.selectedUser,
state.setSelectedUser,
state.training,
state.setTraining,
]);
// const [statsUserId, setStatsUserId] = useState<string | undefined>(user.id);
const [groupedStats, setGroupedStats] = useState<{ [key: string]: Stat[] }>();
const [groupedStats, setGroupedStats] = useState<{[key: string]: Stat[]}>();
const [filter, setFilter] = useState<"months" | "weeks" | "days" | "assignments">();
const { assignments } = useAssignments({});
const {assignments} = useAssignments({});
const { users } = useUsers();
const { stats, isLoading: isStatsLoading } = useStats(statsUserId);
const { groups: allGroups } = useGroups();
const {users} = useUsers();
const {stats, isLoading: isStatsLoading} = useStats(statsUserId);
const {groups: allGroups} = useGroups({});
const groups = allGroups.filter((x) => x.admin === user.id);
@@ -104,12 +108,12 @@ export default function History({ user }: { user: User }) {
setFilter((prev) => (prev === value ? undefined : value));
};
const filterStatsByDate = (stats: { [key: string]: Stat[] }) => {
const filterStatsByDate = (stats: {[key: string]: Stat[]}) => {
if (filter && filter !== "assignments") {
const filterDate = moment()
.subtract({ [filter as string]: 1 })
.subtract({[filter as string]: 1})
.format("x");
const filteredStats: { [key: string]: Stat[] } = {};
const filteredStats: {[key: string]: Stat[]} = {};
Object.keys(stats).forEach((timestamp) => {
if (timestamp >= filterDate) filteredStats[timestamp] = stats[timestamp];
@@ -118,7 +122,7 @@ export default function History({ user }: { user: User }) {
}
if (filter && filter === "assignments") {
const filteredStats: { [key: string]: Stat[] } = {};
const filteredStats: {[key: string]: Stat[]} = {};
Object.keys(stats).forEach((timestamp) => {
if (stats[timestamp].map((s) => s.assignment === undefined).includes(false))
@@ -137,13 +141,13 @@ export default function History({ user }: { user: User }) {
useEffect(() => {
const handleRouteChange = (url: string) => {
setTraining(false)
}
router.events.on('routeChangeStart', handleRouteChange)
setTraining(false);
};
router.events.on("routeChangeStart", handleRouteChange);
return () => {
router.events.off('routeChangeStart', handleRouteChange)
}
}, [router.events, setTraining])
router.events.off("routeChangeStart", handleRouteChange);
};
}, [router.events, setTraining]);
const handleTrainingContentSubmission = () => {
if (groupedStats) {
@@ -156,11 +160,10 @@ export default function History({ user }: { user: User }) {
}
return accumulator;
}, {});
setTrainingStats(Object.values(selectedStats).flat())
setTrainingStats(Object.values(selectedStats).flat());
router.push("/training");
}
}
};
const customContent = (timestamp: string) => {
if (!groupedStats) return <></>;
@@ -272,7 +275,7 @@ export default function History({ user }: { user: User }) {
value={selectableCorporates.find((x) => x.value === selectedCorporate)}
onChange={(value) => setSelectedCorporate(value?.value || "")}
styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
menuPortal: (base) => ({...base, zIndex: 9999}),
option: (styles, state) => ({
...styles,
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
@@ -289,7 +292,7 @@ export default function History({ user }: { user: User }) {
value={selectedUserSelectValue}
onChange={(value) => setStatsUserId(value?.value!)}
styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
menuPortal: (base) => ({...base, zIndex: 9999}),
option: (styles, state) => ({
...styles,
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
@@ -313,7 +316,7 @@ export default function History({ user }: { user: User }) {
value={selectedUserSelectValue}
onChange={(value) => setStatsUserId(value?.value!)}
styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
menuPortal: (base) => ({...base, zIndex: 9999}),
option: (styles, state) => ({
...styles,
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
@@ -323,10 +326,12 @@ export default function History({ user }: { user: User }) {
/>
</>
)}
{(training && (
{training && (
<div className="flex flex-row">
<div className="font-semibold text-2xl mr-4">Select up to 10 exercises
{`(${selectedTrainingExams.length}/${MAX_TRAINING_EXAMS})`}</div>
<div className="font-semibold text-2xl mr-4">
Select up to 10 exercises
{`(${selectedTrainingExams.length}/${MAX_TRAINING_EXAMS})`}
</div>
<button
className={clsx(
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light ml-4 disabled:cursor-not-allowed",
@@ -337,7 +342,7 @@ export default function History({ user }: { user: User }) {
Submit
</button>
</div>
))}
)}
</div>
<div className="flex gap-4 w-full justify-center xl:justify-end">
<button

View File

@@ -28,7 +28,7 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
};
}
if (shouldRedirectHome(user) || !["developer", "admin", "corporate", "agent", "mastercorporate"].includes(user.type)) {
if (shouldRedirectHome(user) || !checkAccess(user, ["admin", "developer", "corporate", "teacher", "mastercorporate"])) {
return {
redirect: {
destination: "/",

View File

@@ -71,7 +71,7 @@ export default function Stats() {
const {user} = useUser({redirectTo: "/login"});
const {users} = useUsers();
const {groups} = useGroups(user?.id);
const {groups} = useGroups({admin: user?.id});
const {stats} = useStats(statsUserId, !statsUserId);
useEffect(() => {
@@ -202,7 +202,7 @@ export default function Stats() {
}}
/>
)}
{(["corporate", "teacher", "mastercorporate"].includes(user.type) ) && groups.length > 0 && (
{["corporate", "teacher", "mastercorporate"].includes(user.type) && groups.length > 0 && (
<Select
className="w-full"
options={users

View File

@@ -1,14 +1,14 @@
/* eslint-disable @next/next/no-img-element */
import Head from "next/head";
import { withIronSessionSsr } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { Stat, User } from "@/interfaces/user";
import { ToastContainer } from "react-toastify";
import {withIronSessionSsr} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {Stat, User} from "@/interfaces/user";
import {ToastContainer} from "react-toastify";
import Layout from "@/components/High/Layout";
import { shouldRedirectHome } from "@/utils/navigation.disabled";
import { use, useEffect, useState } from "react";
import {shouldRedirectHome} from "@/utils/navigation.disabled";
import {use, useEffect, useState} from "react";
import clsx from "clsx";
import { FaPlus } from "react-icons/fa";
import {FaPlus} from "react-icons/fa";
import useRecordStore from "@/stores/recordStore";
import router from "next/router";
import useTrainingContentStore from "@/stores/trainingContentStore";
@@ -16,13 +16,13 @@ import axios from "axios";
import Select from "@/components/Low/Select";
import useUsers from "@/hooks/useUsers";
import useGroups from "@/hooks/useGroups";
import { ITrainingContent } from "@/training/TrainingInterfaces";
import {ITrainingContent} from "@/training/TrainingInterfaces";
import moment from "moment";
import { uuidv4 } from "@firebase/util";
import {uuidv4} from "@firebase/util";
import TrainingScore from "@/training/TrainingScore";
import ModuleBadge from "@/components/ModuleBadge";
export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user;
if (!user || !user.isVerified) {
@@ -44,7 +44,7 @@ export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
}
return {
props: { user: req.session.user },
props: {user: req.session.user},
};
}, sessionOptions);
@@ -53,12 +53,16 @@ const defaultSelectableCorporate = {
label: "All",
};
const Training: React.FC<{ user: User }> = ({ user }) => {
const Training: React.FC<{user: User}> = ({user}) => {
// Record stuff
const { users } = useUsers();
const {users} = useUsers();
const [selectedCorporate, setSelectedCorporate] = useState<string>(defaultSelectableCorporate.value);
const [statsUserId, setStatsUserId, setRecordTraining] = useRecordStore((state) => [state.selectedUser, state.setSelectedUser, state.setTraining]);
const { groups: allGroups } = useGroups();
const [statsUserId, setStatsUserId, setRecordTraining] = useRecordStore((state) => [
state.selectedUser,
state.setSelectedUser,
state.setTraining,
]);
const {groups: allGroups} = useGroups({});
const groups = allGroups.filter((x) => x.admin === user.id);
const [filter, setFilter] = useState<"months" | "weeks" | "days">();
@@ -70,22 +74,22 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
const [trainingContent, setTrainingContent] = useState<ITrainingContent[]>([]);
const [isNewContentLoading, setIsNewContentLoading] = useState(stats.length != 0);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [groupedByTrainingContent, setGroupedByTrainingContent] = useState<{ [key: string]: ITrainingContent }>();
const [groupedByTrainingContent, setGroupedByTrainingContent] = useState<{[key: string]: ITrainingContent}>();
useEffect(() => {
const handleRouteChange = (url: string) => {
setTrainingStats([])
}
router.events.on('routeChangeStart', handleRouteChange)
setTrainingStats([]);
};
router.events.on("routeChangeStart", handleRouteChange);
return () => {
router.events.off('routeChangeStart', handleRouteChange)
}
}, [router.events, setTrainingStats])
router.events.off("routeChangeStart", handleRouteChange);
};
}, [router.events, setTrainingStats]);
useEffect(() => {
const postStats = async () => {
try {
const response = await axios.post<{ id: string }>(`/api/training`, stats);
const response = await axios.post<{id: string}>(`/api/training`, stats);
return response.data.id;
} catch (error) {
setIsNewContentLoading(false);
@@ -93,19 +97,19 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
};
if (isNewContentLoading) {
postStats().then(id => {
postStats().then((id) => {
setTrainingStats([]);
if (id) {
router.push(`/training/${id}`)
router.push(`/training/${id}`);
}
});
}
}, [isNewContentLoading])
}, [isNewContentLoading]);
useEffect(() => {
const loadTrainingContent = async () => {
try {
const response = await axios.get<ITrainingContent[]>('/api/training');
const response = await axios.get<ITrainingContent[]>("/api/training");
setTrainingContent(response.data);
setIsLoading(false);
} catch (error) {
@@ -118,16 +122,15 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
const handleNewTrainingContent = () => {
setRecordTraining(true);
router.push('/record')
}
router.push("/record");
};
const filterTrainingContentByDate = (trainingContent: { [key: string]: ITrainingContent }) => {
const filterTrainingContentByDate = (trainingContent: {[key: string]: ITrainingContent}) => {
if (filter) {
const filterDate = moment()
.subtract({ [filter as string]: 1 })
.subtract({[filter as string]: 1})
.format("x");
const filteredTrainingContent: { [key: string]: ITrainingContent } = {};
const filteredTrainingContent: {[key: string]: ITrainingContent} = {};
Object.keys(trainingContent).forEach((timestamp) => {
if (timestamp >= filterDate) filteredTrainingContent[timestamp] = trainingContent[timestamp];
@@ -142,12 +145,11 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
const grouped = trainingContent.reduce((acc, content) => {
acc[content.created_at] = content;
return acc;
}, {} as { [key: number]: ITrainingContent });
}, {} as {[key: number]: ITrainingContent});
setGroupedByTrainingContent(grouped);
}
}, [trainingContent])
}, [trainingContent]);
// Record Stuff
const selectableCorporates = [
@@ -213,21 +215,20 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
};
const selectTrainingContent = (trainingContent: ITrainingContent) => {
router.push(`/training/${trainingContent.id}`)
router.push(`/training/${trainingContent.id}`);
};
const trainingContentContainer = (timestamp: string) => {
if (!groupedByTrainingContent) return <></>;
const trainingContent: ITrainingContent = groupedByTrainingContent[timestamp];
const uniqueModules = [...new Set(trainingContent.exams.map(exam => exam.module))];
const uniqueModules = [...new Set(trainingContent.exams.map((exam) => exam.module))];
return (
<>
<div
key={uuidv4()}
className={clsx(
"flex flex-col justify-between gap-4 border border-mti-gray-platinum p-4 cursor-pointer rounded-xl transition ease-in-out duration-300 -md:hidden"
"flex flex-col justify-between gap-4 border border-mti-gray-platinum p-4 cursor-pointer rounded-xl transition ease-in-out duration-300 -md:hidden",
)}
onClick={() => selectTrainingContent(trainingContent)}
role="button">
@@ -243,10 +244,7 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
</div>
</div>
</div>
<TrainingScore
trainingContent={trainingContent}
gridView={true}
/>
<TrainingScore trainingContent={trainingContent} gridView={true} />
</div>
</>
);
@@ -266,12 +264,12 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
<ToastContainer />
<Layout user={user}>
{(isNewContentLoading || isLoading ? (
{isNewContentLoading || isLoading ? (
<div className="absolute left-1/2 top-1/2 flex h-fit w-fit -translate-x-1/2 -translate-y-1/2 animate-pulse flex-col items-center gap-12">
<span className="loading loading-infinity w-32 bg-mti-green-light" />
{isNewContentLoading && (<span className="text-center text-2xl font-bold text-mti-green-light">
Assessing your exams, please be patient...
</span>)}
{isNewContentLoading && (
<span className="text-center text-2xl font-bold text-mti-green-light">Assessing your exams, please be patient...</span>
)}
</div>
) : (
<>
@@ -286,7 +284,7 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
value={selectableCorporates.find((x) => x.value === selectedCorporate)}
onChange={(value) => setSelectedCorporate(value?.value || "")}
styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
menuPortal: (base) => ({...base, zIndex: 9999}),
option: (styles, state) => ({
...styles,
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
@@ -303,7 +301,7 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
value={selectedUserSelectValue}
onChange={(value) => setStatsUserId(value?.value!)}
styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
menuPortal: (base) => ({...base, zIndex: 9999}),
option: (styles, state) => ({
...styles,
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
@@ -327,7 +325,7 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
value={selectedUserSelectValue}
onChange={(value) => setStatsUserId(value?.value!)}
styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
menuPortal: (base) => ({...base, zIndex: 9999}),
option: (styles, state) => ({
...styles,
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
@@ -337,7 +335,7 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
/>
</>
)}
{(user.type === "student" && (
{user.type === "student" && (
<>
<div className="flex items-center">
<div className="font-semibold text-2xl">Generate New Training Material</div>
@@ -351,7 +349,7 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
</button>
</div>
</>
))}
)}
</div>
<div className="flex gap-4 w-full justify-center xl:justify-end">
<button
@@ -396,10 +394,10 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
</div>
)}
</>
))}
)}
</Layout>
</>
);
}
};
export default Training;

View File

@@ -5,17 +5,17 @@ export const USER_TYPE_LABELS: {[key in Type]: string} = {
teacher: "Teacher",
corporate: "Corporate",
agent: "Country Manager",
admin: "Admin",
admin: "Super Admin",
developer: "Developer",
mastercorporate: "Master Corporate"
mastercorporate: "Master Corporate",
};
export function isCorporateUser(user: User): user is CorporateUser {
return (user as CorporateUser).corporateInformation !== undefined;
return (user as CorporateUser)?.corporateInformation !== undefined;
}
export function isAgentUser(user: User): user is AgentUser {
return (user as AgentUser).agentInformation !== undefined;
return (user as AgentUser)?.agentInformation !== undefined;
}
export function getUserCompanyName(user: User, users: User[], groups: Group[]) {
@@ -30,3 +30,15 @@ export function getUserCompanyName(user: User, users: User[], groups: Group[]) {
const admin = belongingGroupsAdmins[0] as CorporateUser;
return admin.corporateInformation?.companyInformation.name || admin.name;
}
export function getCorporateUser(user: User, users: User[], groups: Group[]) {
if (isCorporateUser(user)) return user;
const belongingGroups = groups.filter((x) => x.participants.includes(user.id));
const belongingGroupsAdmins = belongingGroups.map((x) => users.find((u) => u.id === x.admin)).filter((x) => !!x && isCorporateUser(x));
if (belongingGroupsAdmins.length === 0) return undefined;
const admin = belongingGroupsAdmins[0] as CorporateUser;
return admin;
}

View File

@@ -1,5 +1,5 @@
import {Module} from "@/interfaces";
import {Exam, UserSolution} from "@/interfaces/exam";
import {Exam, ShuffleMap, UserSolution} from "@/interfaces/exam";
import {Assignment} from "@/interfaces/results";
import {create} from "zustand";
@@ -18,6 +18,7 @@ export interface ExamState {
exerciseIndex: number;
questionIndex: number;
inactivity: number;
shuffleMaps: ShuffleMap[];
}
export interface ExamFunctions {
@@ -35,6 +36,7 @@ export interface ExamFunctions {
setExerciseIndex: (exerciseIndex: number) => void;
setQuestionIndex: (questionIndex: number) => void;
setInactivity: (inactivity: number) => void;
setShuffleMaps: (shuffleMaps: ShuffleMap[]) => void;
reset: () => void;
}
@@ -53,6 +55,7 @@ export const initialState: ExamState = {
exerciseIndex: -1,
questionIndex: 0,
inactivity: 0,
shuffleMaps: []
};
const useExamStore = create<ExamState & ExamFunctions>((set) => ({
@@ -72,6 +75,7 @@ const useExamStore = create<ExamState & ExamFunctions>((set) => ({
setExerciseIndex: (exerciseIndex: number) => set(() => ({exerciseIndex})),
setQuestionIndex: (questionIndex: number) => set(() => ({questionIndex})),
setInactivity: (inactivity: number) => set(() => ({inactivity})),
setShuffleMaps: (shuffleMaps) => set(() => ({shuffleMaps})),
reset: () => set(() => initialState),
}));

View File

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

@@ -1,5 +1,6 @@
import {Module} from "@/interfaces";
import {LevelScore} from "@/constants/ielts";
import {Stat, User} from "@/interfaces/user";
type Type = "academic" | "general";
@@ -178,3 +179,28 @@ export const getLevelLabel = (level: number) => {
return ["Proficiency", "C2"];
};
export const averageLevelCalculator = (users: User[], studentStats: Stat[]) => {
const formattedStats = studentStats
.map((s) => ({
focus: users.find((u) => u.id === s.user)?.focus,
score: s.score,
module: s.module,
}))
.filter((f) => !!f.focus);
const bandScores = formattedStats.map((s) => ({
module: s.module,
level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!),
}));
const levels: {[key in Module]: number} = {
reading: 0,
listening: 0,
writing: 0,
speaking: 0,
level: 0,
};
bandScores.forEach((b) => (levels[b.module] += b.level));
return calculateAverageLevel(levels);
};

View File

@@ -137,5 +137,6 @@ export const convertToUserSolutions = (stats: Stat[]): UserSolution[] => {
solutions: stat.solutions,
type: stat.type,
module: stat.module,
shuffleMaps: stat.shuffleMaps
}));
};

View File

@@ -4,12 +4,36 @@ module.exports = {
safelist: [
{
pattern: /bg-ai-detection-result-(ai|mixed|human)/,
}
},
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
keyframes: {
"accordion-down": {
from: {height: "0"},
to: {height: "var(--radix-accordion-content-height)"},
},
"accordion-up": {
from: {height: "var(--radix-accordion-content-height)"},
to: {height: "0"},
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
scale: {
102: "1.02",
},
boxShadow: {
'training-inset': 'inset 0px 2px 18px 0px #00000029',
"training-inset": "inset 0px 2px 18px 0px #00000029",
},
colors: {
mti: {
@@ -42,18 +66,18 @@ module.exports = {
"ai-detection": {
result: {
ai: {DEFAULT: "#f4bf4f", text: "#f0bc4f", bg: "#fff8e8"},
mixed: {DEFAULT:"#93aafb", bg: "rgba(147, 170, 251, 0.3)"},
human: {DEFAULT:"#50c08a", bg: "#e9f9ed"}
mixed: {DEFAULT: "#93aafb", bg: "rgba(147, 170, 251, 0.3)"},
human: {DEFAULT: "#50c08a", bg: "#e9f9ed"},
},
confidence: {
high: {DEFAULT: "#84d1ac", transparent: "#daf0e3"},
medium: {DEFAULT: "#f7ec88", transparent: "#fcf8d8"},
low: {DEFAULT: "#ffc1c1", transparent: "#ffebe9"},
border: "#888888"
border: "#888888",
},
highlight: "#ffefb7",
text: "#8992B1"
}
text: "#8992B1",
},
},
screens: {
"-sm": {max: "639px"},
@@ -65,5 +89,5 @@ module.exports = {
},
},
},
plugins: [require("daisyui"), require("tailwind-scrollbar-hide"),],
plugins: [require("daisyui"), require("tailwind-scrollbar-hide"), require("tailwindcss-animate")],
};

594
yarn.lock
View File

@@ -149,6 +149,14 @@
"@dnd-kit/utilities" "^3.2.2"
tslib "^2.0.0"
"@dnd-kit/sortable@^8.0.0":
version "8.0.0"
resolved "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-8.0.0.tgz"
integrity sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==
dependencies:
"@dnd-kit/utilities" "^3.2.2"
tslib "^2.0.0"
"@dnd-kit/utilities@^3.2.2":
version "3.2.2"
resolved "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz"
@@ -740,6 +748,14 @@
dependencies:
"@floating-ui/utils" "^0.2.5"
"@floating-ui/dom@^1.0.0":
version "1.6.10"
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.10.tgz#b74c32f34a50336c86dcf1f1c845cf3a39e26d6f"
integrity sha512-fskgCFv8J8OamCmyun8MfjB1Olfn+uZKjOKZ0vhYF3gRmEUXcGOjxWL8bBr7i4kIuPZ2KD2S3EUIOxnjC8kl2A==
dependencies:
"@floating-ui/core" "^1.6.0"
"@floating-ui/utils" "^0.2.7"
"@floating-ui/dom@^1.0.1", "@floating-ui/dom@^1.6.1":
version "1.6.8"
resolved "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.8.tgz"
@@ -748,11 +764,32 @@
"@floating-ui/core" "^1.6.0"
"@floating-ui/utils" "^0.2.5"
"@floating-ui/react-dom@^2.0.0", "@floating-ui/react-dom@^2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.1.tgz#cca58b6b04fc92b4c39288252e285e0422291fb0"
integrity sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==
dependencies:
"@floating-ui/dom" "^1.0.0"
"@floating-ui/react@^0.26.16":
version "0.26.22"
resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.26.22.tgz#b46f645f9cd19a591da706aed24608c23cdb89a2"
integrity sha512-LNv4azPt8SpT4WW7Kku5JNVjLk2GcS0bGGjFTAgqOONRFo9r/aaGHHPpdiIuQbB1t8shmWyWqTTUDmZ9fcNshg==
dependencies:
"@floating-ui/react-dom" "^2.1.1"
"@floating-ui/utils" "^0.2.7"
tabbable "^6.0.0"
"@floating-ui/utils@^0.2.5":
version "0.2.5"
resolved "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.5.tgz"
integrity sha512-sTcG+QZ6fdEUObICavU+aB3Mp8HY4n14wYHdxK4fXjPmv3PXZZeY5RaguJmGyeH/CJQhX3fqKUtS4qc1LoHwhQ==
"@floating-ui/utils@^0.2.7":
version "0.2.7"
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.7.tgz#d0ece53ce99ab5a8e37ebdfe5e32452a2bfc073e"
integrity sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA==
"@google-cloud/firestore@^6.8.0":
version "6.8.0"
resolved "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-6.8.0.tgz"
@@ -843,12 +880,15 @@
protobufjs "^7.0.0"
yargs "^16.2.0"
"@headlessui/react@^1.7.13":
version "1.7.14"
resolved "https://registry.npmjs.org/@headlessui/react/-/react-1.7.14.tgz"
integrity sha512-znzdq9PG8rkwcu9oQ2FwIy0ZFtP9Z7ycS+BAqJ3R5EIqC/0bJGvhT7193rFf+45i9nnPsYvCQVW4V/bB9Xc+gA==
"@headlessui/react@^2.1.2":
version "2.1.2"
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-2.1.2.tgz#3ca9378d7d0db6aefdb135f957815790786214ef"
integrity sha512-Kb3hgk9gRNRcTZktBrKdHhF3xFhYkca1Rk6e1/im2ENf83dgN54orMW0uSKTXFnUpZOUFZ+wcY05LlipwgZIFQ==
dependencies:
client-only "^0.0.1"
"@floating-ui/react" "^0.26.16"
"@react-aria/focus" "^3.17.1"
"@react-aria/interactions" "^3.21.3"
"@tanstack/react-virtual" "^3.8.1"
"@humanwhocodes/config-array@^0.11.8":
version "0.11.8"
@@ -952,10 +992,10 @@
dependencies:
prop-types "^15.7.2"
"@next/env@13.1.6":
version "13.1.6"
resolved "https://registry.npmjs.org/@next/env/-/env-13.1.6.tgz"
integrity sha512-s+W9Fdqh5MFk6ECrbnVmmAOwxKQuhGMT7xXHrkYIBMBcTiOqNWhv5KbJIboKR5STXxNXl32hllnvKaffzFaWQg==
"@next/env@14.2.5":
version "14.2.5"
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.5.tgz#1d9328ab828711d3517d0a1d505acb55e5ef7ad0"
integrity sha512-/zZGkrTOsraVfYjGP8uM0p6r0BDT6xWpkjdVbcz66PJVSpwXX3yNiRycxAuDfBKGWBrZBXRuK/YVlkNgxHGwmA==
"@next/eslint-plugin-next@13.1.6":
version "13.1.6"
@@ -964,75 +1004,50 @@
dependencies:
glob "7.1.7"
"@next/font@13.1.6":
version "13.1.6"
resolved "https://registry.npmjs.org/@next/font/-/font-13.1.6.tgz"
integrity sha512-AITjmeb1RgX1HKMCiA39ztx2mxeAyxl4ljv2UoSBUGAbFFMg8MO7YAvjHCgFhD39hL7YTbFjol04e/BPBH5RzQ==
"@next/swc-darwin-arm64@14.2.5":
version "14.2.5"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.5.tgz#d0a160cf78c18731c51cc0bff131c706b3e9bb05"
integrity sha512-/9zVxJ+K9lrzSGli1///ujyRfon/ZneeZ+v4ptpiPoOU+GKZnm8Wj8ELWU1Pm7GHltYRBklmXMTUqM/DqQ99FQ==
"@next/swc-android-arm-eabi@13.1.6":
version "13.1.6"
resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.1.6.tgz#d766dfc10e27814d947b20f052067c239913dbcc"
integrity sha512-F3/6Z8LH/pGlPzR1AcjPFxx35mPqjE5xZcf+IL+KgbW9tMkp7CYi1y7qKrEWU7W4AumxX/8OINnDQWLiwLasLQ==
"@next/swc-darwin-x64@14.2.5":
version "14.2.5"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.5.tgz#eb832a992407f6e6352eed05a073379f1ce0589c"
integrity sha512-vXHOPCwfDe9qLDuq7U1OYM2wUY+KQ4Ex6ozwsKxp26BlJ6XXbHleOUldenM67JRyBfVjv371oneEvYd3H2gNSA==
"@next/swc-android-arm64@13.1.6":
version "13.1.6"
resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-13.1.6.tgz#f37a98d5f18927d8c9970d750d516ac779465176"
integrity sha512-cMwQjnB8vrYkWyK/H0Rf2c2pKIH4RGjpKUDvbjVAit6SbwPDpmaijLio0LWFV3/tOnY6kvzbL62lndVA0mkYpw==
"@next/swc-linux-arm64-gnu@14.2.5":
version "14.2.5"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.5.tgz#098fdab57a4664969bc905f5801ef5a89582c689"
integrity sha512-vlhB8wI+lj8q1ExFW8lbWutA4M2ZazQNvMWuEDqZcuJJc78iUnLdPPunBPX8rC4IgT6lIx/adB+Cwrl99MzNaA==
"@next/swc-darwin-arm64@13.1.6":
version "13.1.6"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.1.6.tgz#ec1b90fd9bf809d8b81004c5182e254dced4ad96"
integrity sha512-KKRQH4DDE4kONXCvFMNBZGDb499Hs+xcFAwvj+rfSUssIDrZOlyfJNy55rH5t2Qxed1e4K80KEJgsxKQN1/fyw==
"@next/swc-linux-arm64-musl@14.2.5":
version "14.2.5"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.5.tgz#243a1cc1087fb75481726dd289c7b219fa01f2b5"
integrity sha512-NpDB9NUR2t0hXzJJwQSGu1IAOYybsfeB+LxpGsXrRIb7QOrYmidJz3shzY8cM6+rO4Aojuef0N/PEaX18pi9OA==
"@next/swc-darwin-x64@13.1.6":
version "13.1.6"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.1.6.tgz#e869ac75d16995eee733a7d1550322d9051c1eb4"
integrity sha512-/uOky5PaZDoaU99ohjtNcDTJ6ks/gZ5ykTQDvNZDjIoCxFe3+t06bxsTPY6tAO6uEAw5f6vVFX5H5KLwhrkZCA==
"@next/swc-linux-x64-gnu@14.2.5":
version "14.2.5"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.5.tgz#b8a2e436387ee4a52aa9719b718992e0330c4953"
integrity sha512-8XFikMSxWleYNryWIjiCX+gU201YS+erTUidKdyOVYi5qUQo/gRxv/3N1oZFCgqpesN6FPeqGM72Zve+nReVXQ==
"@next/swc-freebsd-x64@13.1.6":
version "13.1.6"
resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.1.6.tgz#84a7b2e423a2904afc2edca21c2f1ba6b53fa4c1"
integrity sha512-qaEALZeV7to6weSXk3Br80wtFQ7cFTpos/q+m9XVRFggu+8Ib895XhMWdJBzew6aaOcMvYR6KQ6JmHA2/eMzWw==
"@next/swc-linux-x64-musl@14.2.5":
version "14.2.5"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.5.tgz#cb8a9adad5fb8df86112cfbd363aab5c6d32757b"
integrity sha512-6QLwi7RaYiQDcRDSU/os40r5o06b5ue7Jsk5JgdRBGGp8l37RZEh9JsLSM8QF0YDsgcosSeHjglgqi25+m04IQ==
"@next/swc-linux-arm-gnueabihf@13.1.6":
version "13.1.6"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.1.6.tgz#980eed1f655ff8a72187d8a6ef9e73ac39d20d23"
integrity sha512-OybkbC58A1wJ+JrJSOjGDvZzrVEQA4sprJejGqMwiZyLqhr9Eo8FXF0y6HL+m1CPCpPhXEHz/2xKoYsl16kNqw==
"@next/swc-win32-arm64-msvc@14.2.5":
version "14.2.5"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.5.tgz#81f996c1c38ea0900d4e7719cc8814be8a835da0"
integrity sha512-1GpG2VhbspO+aYoMOQPQiqc/tG3LzmsdBH0LhnDS3JrtDx2QmzXe0B6mSZZiN3Bq7IOMXxv1nlsjzoS1+9mzZw==
"@next/swc-linux-arm64-gnu@13.1.6":
version "13.1.6"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.1.6.tgz#87a71db21cded3f7c63d1d19079845c59813c53d"
integrity sha512-yCH+yDr7/4FDuWv6+GiYrPI9kcTAO3y48UmaIbrKy8ZJpi7RehJe3vIBRUmLrLaNDH3rY1rwoHi471NvR5J5NQ==
"@next/swc-win32-ia32-msvc@14.2.5":
version "14.2.5"
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.5.tgz#f61c74ce823e10b2bc150e648fc192a7056422e0"
integrity sha512-Igh9ZlxwvCDsu6438FXlQTHlRno4gFpJzqPjSIBZooD22tKeI4fE/YMRoHVJHmrQ2P5YL1DoZ0qaOKkbeFWeMg==
"@next/swc-linux-arm64-musl@13.1.6":
version "13.1.6"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.1.6.tgz#c5aac8619331b9fd030603bbe2b36052011e11de"
integrity sha512-ECagB8LGX25P9Mrmlc7Q/TQBb9rGScxHbv/kLqqIWs2fIXy6Y/EiBBiM72NTwuXUFCNrWR4sjUPSooVBJJ3ESQ==
"@next/swc-linux-x64-gnu@13.1.6":
version "13.1.6"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.1.6.tgz#9513d36d540bbfea575576746736054c31aacdea"
integrity sha512-GT5w2mruk90V/I5g6ScuueE7fqj/d8Bui2qxdw6lFxmuTgMeol5rnzAv4uAoVQgClOUO/MULilzlODg9Ib3Y4Q==
"@next/swc-linux-x64-musl@13.1.6":
version "13.1.6"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.1.6.tgz#d61fc6884899f5957251f4ce3f522e34a2c479b7"
integrity sha512-keFD6KvwOPzmat4TCnlnuxJCQepPN+8j3Nw876FtULxo8005Y9Ghcl7ACcR8GoiKoddAq8gxNBrpjoxjQRHeAQ==
"@next/swc-win32-arm64-msvc@13.1.6":
version "13.1.6"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.1.6.tgz#fac2077a8ae9768e31444c9ae90807e64117cda7"
integrity sha512-OwertslIiGQluFvHyRDzBCIB07qJjqabAmINlXUYt7/sY7Q7QPE8xVi5beBxX/rxTGPIbtyIe3faBE6Z2KywhQ==
"@next/swc-win32-ia32-msvc@13.1.6":
version "13.1.6"
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.1.6.tgz#498bc11c91b4c482a625bf4b978f98ae91111e46"
integrity sha512-g8zowiuP8FxUR9zslPmlju7qYbs2XBtTLVSxVikPtUDQedhcls39uKYLvOOd1JZg0ehyhopobRoH1q+MHlIN/w==
"@next/swc-win32-x64-msvc@13.1.6":
version "13.1.6"
resolved "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.1.6.tgz"
integrity sha512-Ls2OL9hi3YlJKGNdKv8k3X/lLgc3VmLG3a/DeTkAd+lAituJp8ZHmRmm9f9SL84fT3CotlzcgbdaCDfFwFA6bA==
"@next/swc-win32-x64-msvc@14.2.5":
version "14.2.5"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.5.tgz#ed199a920efb510cfe941cd75ed38a7be21e756f"
integrity sha512-tEQ7oinq1/CjSG9uSTerca3v4AZ+dFa+4Yu6ihaG8Ud8ddqLQgFGcnwYls13H5X5CPDPZJdYxyeMui6muOLd4g==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
@@ -1186,6 +1201,214 @@
resolved "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz"
integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
"@radix-ui/primitive@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.0.tgz#42ef83b3b56dccad5d703ae8c42919a68798bbe2"
integrity sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==
"@radix-ui/react-arrow@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz#744f388182d360b86285217e43b6c63633f39e7a"
integrity sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==
dependencies:
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-compose-refs@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz#656432461fc8283d7b591dcf0d79152fae9ecc74"
integrity sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==
"@radix-ui/react-context@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.0.tgz#6df8d983546cfd1999c8512f3a8ad85a6e7fcee8"
integrity sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==
"@radix-ui/react-dismissable-layer@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz#2cd0a49a732372513733754e6032d3fb7988834e"
integrity sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==
dependencies:
"@radix-ui/primitive" "1.1.0"
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-escape-keydown" "1.1.0"
"@radix-ui/react-focus-guards@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz#8e9abb472a9a394f59a1b45f3dd26cfe3fc6da13"
integrity sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==
"@radix-ui/react-focus-scope@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz#ebe2891a298e0a33ad34daab2aad8dea31caf0b2"
integrity sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==
dependencies:
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-icons@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-icons/-/react-icons-1.3.0.tgz#c61af8f323d87682c5ca76b856d60c2312dbcb69"
integrity sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==
"@radix-ui/react-id@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.0.tgz#de47339656594ad722eb87f94a6b25f9cffae0ed"
integrity sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==
dependencies:
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-popover@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.1.1.tgz#604b783cdb3494ed4f16a58c17f0e81e61ab7775"
integrity sha512-3y1A3isulwnWhvTTwmIreiB8CF4L+qRjZnK1wYLO7pplddzXKby/GnZ2M7OZY3qgnl6p9AodUIHRYGXNah8Y7g==
dependencies:
"@radix-ui/primitive" "1.1.0"
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-context" "1.1.0"
"@radix-ui/react-dismissable-layer" "1.1.0"
"@radix-ui/react-focus-guards" "1.1.0"
"@radix-ui/react-focus-scope" "1.1.0"
"@radix-ui/react-id" "1.1.0"
"@radix-ui/react-popper" "1.2.0"
"@radix-ui/react-portal" "1.1.1"
"@radix-ui/react-presence" "1.1.0"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-slot" "1.1.0"
"@radix-ui/react-use-controllable-state" "1.1.0"
aria-hidden "^1.1.1"
react-remove-scroll "2.5.7"
"@radix-ui/react-popper@1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.0.tgz#a3e500193d144fe2d8f5d5e60e393d64111f2a7a"
integrity sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==
dependencies:
"@floating-ui/react-dom" "^2.0.0"
"@radix-ui/react-arrow" "1.1.0"
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-context" "1.1.0"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-use-rect" "1.1.0"
"@radix-ui/react-use-size" "1.1.0"
"@radix-ui/rect" "1.1.0"
"@radix-ui/react-portal@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.1.tgz#1957f1eb2e1aedfb4a5475bd6867d67b50b1d15f"
integrity sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==
dependencies:
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-presence@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.0.tgz#227d84d20ca6bfe7da97104b1a8b48a833bfb478"
integrity sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==
dependencies:
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-primitive@2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz#fe05715faa9203a223ccc0be15dc44b9f9822884"
integrity sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==
dependencies:
"@radix-ui/react-slot" "1.1.0"
"@radix-ui/react-slot@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.0.tgz#7c5e48c36ef5496d97b08f1357bb26ed7c714b84"
integrity sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==
dependencies:
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-use-callback-ref@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz#bce938ca413675bc937944b0d01ef6f4a6dc5bf1"
integrity sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==
"@radix-ui/react-use-controllable-state@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz#1321446857bb786917df54c0d4d084877aab04b0"
integrity sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==
dependencies:
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-escape-keydown@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz#31a5b87c3b726504b74e05dac1edce7437b98754"
integrity sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==
dependencies:
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-layout-effect@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz#3c2c8ce04827b26a39e442ff4888d9212268bd27"
integrity sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==
"@radix-ui/react-use-rect@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz#13b25b913bd3e3987cc9b073a1a164bb1cf47b88"
integrity sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==
dependencies:
"@radix-ui/rect" "1.1.0"
"@radix-ui/react-use-size@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz#b4dba7fbd3882ee09e8d2a44a3eed3a7e555246b"
integrity sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==
dependencies:
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/rect@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.0.tgz#f817d1d3265ac5415dadc67edab30ae196696438"
integrity sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==
"@react-aria/focus@^3.17.1":
version "3.18.1"
resolved "https://registry.yarnpkg.com/@react-aria/focus/-/focus-3.18.1.tgz#b54b88e78662549ddae917e3143723c8dd7a4e90"
integrity sha512-N0Cy61WCIv+57mbqC7hiZAsB+3rF5n4JKabxUmg/2RTJL6lq7hJ5N4gx75ymKxkN8GnVDwt4pKZah48Wopa5jw==
dependencies:
"@react-aria/interactions" "^3.22.1"
"@react-aria/utils" "^3.25.1"
"@react-types/shared" "^3.24.1"
"@swc/helpers" "^0.5.0"
clsx "^2.0.0"
"@react-aria/interactions@^3.21.3", "@react-aria/interactions@^3.22.1":
version "3.22.1"
resolved "https://registry.yarnpkg.com/@react-aria/interactions/-/interactions-3.22.1.tgz#f2219a100c886cee383da7be9ae05e9dd940d39a"
integrity sha512-5TLzQaDAQQ5C70yG8GInbO4wIylKY67RfTIIwQPGR/4n5OIjbUD8BOj3NuSsuZ/frUPaBXo1VEBBmSO23fxkjw==
dependencies:
"@react-aria/ssr" "^3.9.5"
"@react-aria/utils" "^3.25.1"
"@react-types/shared" "^3.24.1"
"@swc/helpers" "^0.5.0"
"@react-aria/ssr@^3.9.5":
version "3.9.5"
resolved "https://registry.yarnpkg.com/@react-aria/ssr/-/ssr-3.9.5.tgz#775d84f51f90934ff51ae74eeba3728daac1a381"
integrity sha512-xEwGKoysu+oXulibNUSkXf8itW0npHHTa6c4AyYeZIJyRoegeteYuFpZUBPtIDE8RfHdNsSmE1ssOkxRnwbkuQ==
dependencies:
"@swc/helpers" "^0.5.0"
"@react-aria/utils@^3.25.1":
version "3.25.1"
resolved "https://registry.yarnpkg.com/@react-aria/utils/-/utils-3.25.1.tgz#f6530ce47aa28617924cc6868b4cf1c113a909c5"
integrity sha512-5Uj864e7T5+yj78ZfLnfHqmypLiqW2mN+nsdslog2z5ssunTqjolVeM15ootXskjISlZ7MojLpq97kIC4nlnAw==
dependencies:
"@react-aria/ssr" "^3.9.5"
"@react-stately/utils" "^3.10.2"
"@react-types/shared" "^3.24.1"
"@swc/helpers" "^0.5.0"
clsx "^2.0.0"
"@react-pdf/fns@2.2.1":
version "2.2.1"
resolved "https://registry.npmjs.org/@react-pdf/fns/-/fns-2.2.1.tgz"
@@ -1365,18 +1588,50 @@
"@react-spring/shared" "~9.7.4"
"@react-spring/types" "~9.7.4"
"@react-stately/utils@^3.10.2":
version "3.10.2"
resolved "https://registry.yarnpkg.com/@react-stately/utils/-/utils-3.10.2.tgz#09377f771592ff537c901aa64178cb3a004a916f"
integrity sha512-fh6OTQtbeQC0ywp6LJuuKs6tKIgFvt/DlIZEcIpGho6/oZG229UnIk6TUekwxnDbumuYyan6D9EgUtEMmT8UIg==
dependencies:
"@swc/helpers" "^0.5.0"
"@react-types/shared@^3.24.1":
version "3.24.1"
resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.24.1.tgz#fa06cb681d144fce9c515d8bd296d81440a45d25"
integrity sha512-AUQeGYEm/zDTN6zLzdXolDxz3Jk5dDL7f506F07U8tBwxNNI3WRdhU84G0/AaFikOZzDXhOZDr3MhQMzyE7Ydw==
"@rushstack/eslint-patch@^1.1.3":
version "1.2.0"
resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz"
integrity sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==
"@swc/helpers@0.4.14", "@swc/helpers@^0.4.2":
"@swc/counter@^0.1.3":
version "0.1.3"
resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9"
integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==
"@swc/helpers@0.5.5":
version "0.5.5"
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.5.tgz#12689df71bfc9b21c4f4ca00ae55f2f16c8b77c0"
integrity sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==
dependencies:
"@swc/counter" "^0.1.3"
tslib "^2.4.0"
"@swc/helpers@^0.4.2":
version "0.4.14"
resolved "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.14.tgz"
integrity sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==
dependencies:
tslib "^2.4.0"
"@swc/helpers@^0.5.0":
version "0.5.12"
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.12.tgz#37aaca95284019eb5d2207101249435659709f4b"
integrity sha512-KMZNXiGibsW9kvZAO1Pam2JPTDBm+KSHMMHWdsyI/1DbIZjT2A6Gy3hblVXUMEDvUAKq+e0vL0X0o54owWji7g==
dependencies:
tslib "^2.4.0"
"@tanstack/react-table@^8.10.1":
version "8.19.3"
resolved "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.19.3.tgz"
@@ -1384,11 +1639,23 @@
dependencies:
"@tanstack/table-core" "8.19.3"
"@tanstack/react-virtual@^3.8.1":
version "3.9.0"
resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.9.0.tgz#728e3a1917cb98fb67a17f4190e75f531f1eb0de"
integrity sha512-5TeTSQBMV1PIFzBP9cduIX5klRaTvbOw+CxRx3LaUhwqiZLEZBZqz8anEIqG4eHNhDAe+BLarRDeNE9cNM1/EA==
dependencies:
"@tanstack/virtual-core" "3.9.0"
"@tanstack/table-core@8.19.3":
version "8.19.3"
resolved "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.19.3.tgz"
integrity sha512-IqREj9ADoml9zCAouIG/5kCGoyIxPFdqdyoxis9FisXFi5vT+iYfEfLosq4xkU/iDbMcEuAj+X8dWRLvKYDNoQ==
"@tanstack/virtual-core@3.9.0":
version "3.9.0"
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.9.0.tgz#60db41fe8a19bb1a21873d86d8731416391e838a"
integrity sha512-Saga7/QRGej/IDCVP5BgJ1oDqlDT2d9rQyoflS3fgMS8ntJ8JGw/LBqK2GorHa06+VrNFc0tGz65XQHJQJetFQ==
"@tootallnate/once@2":
version "2.0.0"
resolved "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz"
@@ -1935,6 +2202,13 @@ argparse@^2.0.1:
resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
aria-hidden@^1.1.1:
version "1.2.4"
resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.4.tgz#b78e383fdbc04d05762c78b4a25a501e736c4522"
integrity sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==
dependencies:
tslib "^2.0.0"
aria-query@^5.1.3:
version "5.1.3"
resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz"
@@ -2251,10 +2525,19 @@ buffer@^6:
base64-js "^1.3.1"
ieee754 "^1.2.1"
<<<<<<< HEAD
buffers@~0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb"
integrity sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==
=======
busboy@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==
dependencies:
streamsearch "^1.1.0"
>>>>>>> develop
call-bind@^1.0.2, call-bind@^1.0.7:
version "1.0.7"
@@ -2282,11 +2565,16 @@ camelcase@^5.0.0:
resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz"
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001464:
caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001464:
version "1.0.30001480"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001480.tgz"
integrity sha512-q7cpoPPvZYgtyC4VaBSN0Bt+PJ4c4EYRf0DrduInOz2SkFpHD5p3LnvEpqBp7UnJn+8x1Ogl1s38saUxe+ihQQ==
caniuse-lite@^1.0.30001579:
version "1.0.30001651"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz#52de59529e8b02b1aedcaaf5c05d9e23c0c28138"
integrity sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==
catharsis@^0.9.0:
version "0.9.0"
resolved "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz"
@@ -2345,12 +2633,19 @@ chownr@^2.0.0:
resolved "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz"
integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
class-variance-authority@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/class-variance-authority/-/class-variance-authority-0.7.0.tgz#1c3134d634d80271b1837452b06d821915954522"
integrity sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==
dependencies:
clsx "2.0.0"
classnames@^2.2.6, classnames@^2.3.0, classnames@^2.5.1:
version "2.5.1"
resolved "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz"
integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
client-only@0.0.1, client-only@^0.0.1:
client-only@0.0.1:
version "0.0.1"
resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz"
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
@@ -2378,11 +2673,21 @@ clone@^2.1.2:
resolved "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz"
integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==
clsx@^1.1.1, clsx@^1.2.1:
clsx@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b"
integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==
clsx@^1.1.1:
version "1.2.1"
resolved "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
clsx@^2.0.0, clsx@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz"
@@ -2716,6 +3021,11 @@ detect-libc@^2.0.0:
resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz"
integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==
detect-node-es@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493"
integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==
dezalgo@^1.0.4:
version "1.0.4"
resolved "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz"
@@ -3640,6 +3950,11 @@ get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.4:
has-symbols "^1.0.3"
hasown "^2.0.0"
get-nonce@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3"
integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==
get-symbol-description@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz"
@@ -4045,6 +4360,13 @@ internal-slot@^1.0.3, internal-slot@^1.0.4:
has "^1.0.3"
side-channel "^1.0.4"
invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
dependencies:
loose-envify "^1.0.0"
iron-session@^6.3.1:
version "6.3.1"
resolved "https://registry.npmjs.org/iron-session/-/iron-session-6.3.1.tgz"
@@ -4893,7 +5215,7 @@ mz@^2.7.0:
object-assign "^4.0.1"
thenify-all "^1.0.0"
nanoid@^3.3.4, nanoid@^3.3.6:
nanoid@^3.3.6:
version "3.3.6"
resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz"
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
@@ -4908,30 +5230,28 @@ neo-async@^2.6.2:
resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
next@13.1.6:
version "13.1.6"
resolved "https://registry.npmjs.org/next/-/next-13.1.6.tgz"
integrity sha512-hHlbhKPj9pW+Cymvfzc15lvhaOZ54l+8sXDXJWm3OBNBzgrVj6hwGPmqqsXg40xO1Leq+kXpllzRPuncpC0Phw==
next@^14.2.5:
version "14.2.5"
resolved "https://registry.yarnpkg.com/next/-/next-14.2.5.tgz#afe4022bb0b752962e2205836587a289270efbea"
integrity sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==
dependencies:
"@next/env" "13.1.6"
"@swc/helpers" "0.4.14"
caniuse-lite "^1.0.30001406"
postcss "8.4.14"
"@next/env" "14.2.5"
"@swc/helpers" "0.5.5"
busboy "1.6.0"
caniuse-lite "^1.0.30001579"
graceful-fs "^4.2.11"
postcss "8.4.31"
styled-jsx "5.1.1"
optionalDependencies:
"@next/swc-android-arm-eabi" "13.1.6"
"@next/swc-android-arm64" "13.1.6"
"@next/swc-darwin-arm64" "13.1.6"
"@next/swc-darwin-x64" "13.1.6"
"@next/swc-freebsd-x64" "13.1.6"
"@next/swc-linux-arm-gnueabihf" "13.1.6"
"@next/swc-linux-arm64-gnu" "13.1.6"
"@next/swc-linux-arm64-musl" "13.1.6"
"@next/swc-linux-x64-gnu" "13.1.6"
"@next/swc-linux-x64-musl" "13.1.6"
"@next/swc-win32-arm64-msvc" "13.1.6"
"@next/swc-win32-ia32-msvc" "13.1.6"
"@next/swc-win32-x64-msvc" "13.1.6"
"@next/swc-darwin-arm64" "14.2.5"
"@next/swc-darwin-x64" "14.2.5"
"@next/swc-linux-arm64-gnu" "14.2.5"
"@next/swc-linux-arm64-musl" "14.2.5"
"@next/swc-linux-x64-gnu" "14.2.5"
"@next/swc-linux-x64-musl" "14.2.5"
"@next/swc-win32-arm64-msvc" "14.2.5"
"@next/swc-win32-ia32-msvc" "14.2.5"
"@next/swc-win32-x64-msvc" "14.2.5"
nice-try@^1.0.4:
version "1.0.5"
@@ -5306,12 +5626,12 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^
resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
postcss@8.4.14:
version "8.4.14"
resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz"
integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==
postcss@8.4.31:
version "8.4.31"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
dependencies:
nanoid "^3.3.4"
nanoid "^3.3.6"
picocolors "^1.0.0"
source-map-js "^1.0.2"
@@ -5659,6 +5979,25 @@ react-popper@^2.2.5, react-popper@^2.3.0:
react-fast-compare "^3.0.1"
warning "^4.0.2"
react-remove-scroll-bar@^2.3.4:
version "2.3.6"
resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz#3e585e9d163be84a010180b18721e851ac81a29c"
integrity sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==
dependencies:
react-style-singleton "^2.2.1"
tslib "^2.0.0"
react-remove-scroll@2.5.7:
version "2.5.7"
resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz#15a1fd038e8497f65a695bf26a4a57970cac1ccb"
integrity sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==
dependencies:
react-remove-scroll-bar "^2.3.4"
react-style-singleton "^2.2.1"
tslib "^2.1.0"
use-callback-ref "^1.3.0"
use-sidecar "^1.1.2"
react-select@^5.7.5:
version "5.8.0"
resolved "https://registry.npmjs.org/react-select/-/react-select-5.8.0.tgz"
@@ -5679,6 +6018,15 @@ react-string-replace@^1.1.0:
resolved "https://registry.npmjs.org/react-string-replace/-/react-string-replace-1.1.0.tgz"
integrity sha512-N6RalSDFGbOHs0IJi1H611WbZsvk3ZT47Jl2JEXFbiS3kTwsdCYij70Keo/tWtLy7sfhDsYm7CwNM/WmjXIaMw==
react-style-singleton@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4"
integrity sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==
dependencies:
get-nonce "^1.0.0"
invariant "^2.2.4"
tslib "^2.0.0"
react-toastify@^9.1.2:
version "9.1.2"
resolved "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.2.tgz"
@@ -6083,6 +6431,7 @@ stream-shift@^1.0.2:
resolved "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz"
integrity sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==
<<<<<<< HEAD
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
@@ -6093,6 +6442,14 @@ stream-shift@^1.0.2:
strip-ansi "^6.0.1"
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
=======
streamsearch@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
>>>>>>> develop
version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -6269,11 +6626,26 @@ synckit@^0.8.4:
"@pkgr/utils" "^2.3.1"
tslib "^2.5.0"
tabbable@^6.0.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97"
integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==
tailwind-merge@^2.5.2:
version "2.5.2"
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.5.2.tgz#000f05a703058f9f9f3829c644235f81d4c08a1f"
integrity sha512-kjEBm+pvD+6eAwzJL2Bi+02/9LFLal1Gs61+QB7HvTfQQ0aXwC5LGT8PEt1gS0CWKktKe6ysPTAy3cBC5MeiIg==
tailwind-scrollbar-hide@^1.1.7:
version "1.1.7"
resolved "https://registry.npmjs.org/tailwind-scrollbar-hide/-/tailwind-scrollbar-hide-1.1.7.tgz"
integrity sha512-X324n9OtpTmOMqEgDUEA/RgLrNfBF/jwJdctaPZDzB3mppxJk7TLIDmOreEDm1Bq4R9LSPu4Epf8VSdovNU+iA==
tailwindcss-animate@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz#318b692c4c42676cc9e67b19b78775742388bef4"
integrity sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==
tailwindcss@^3, tailwindcss@^3.2.4:
version "3.3.1"
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.1.tgz"
@@ -6560,6 +6932,13 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
use-callback-ref@^1.3.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.2.tgz#6134c7f6ff76e2be0b56c809b17a650c942b1693"
integrity sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==
dependencies:
tslib "^2.0.0"
use-file-picker@^2.1.0:
version "2.1.2"
resolved "https://registry.npmjs.org/use-file-picker/-/use-file-picker-2.1.2.tgz"
@@ -6572,6 +6951,14 @@ use-isomorphic-layout-effect@^1.1.2:
resolved "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz"
integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==
use-sidecar@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2"
integrity sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==
dependencies:
detect-node-es "^1.1.0"
tslib "^2.0.0"
use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz"
@@ -6720,7 +7107,12 @@ wordwrap@^1.0.0:
resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz"
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
<<<<<<< HEAD
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
=======
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
name wrap-ansi-cjs
>>>>>>> develop
version "7.0.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==