Merge branch 'develop' into ENCOA-83_MasterStatistical
This commit is contained in:
17
components.json
Normal file
17
components.json
Normal 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
23
package-lock.json
generated
@@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@beam-australia/react-env": "^3.1.1",
|
"@beam-australia/react-env": "^3.1.1",
|
||||||
"@dnd-kit/core": "^6.1.0",
|
"@dnd-kit/core": "^6.1.0",
|
||||||
|
"@dnd-kit/sortable": "^8.0.0",
|
||||||
"@firebase/util": "^1.9.7",
|
"@firebase/util": "^1.9.7",
|
||||||
"@headlessui/react": "^1.7.13",
|
"@headlessui/react": "^1.7.13",
|
||||||
"@mdi/js": "^7.1.96",
|
"@mdi/js": "^7.1.96",
|
||||||
@@ -453,6 +454,19 @@
|
|||||||
"react-dom": ">=16.8.0"
|
"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": {
|
"node_modules/@dnd-kit/utilities": {
|
||||||
"version": "3.2.2",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
||||||
@@ -10430,6 +10444,15 @@
|
|||||||
"tslib": "^2.0.0"
|
"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": {
|
"@dnd-kit/utilities": {
|
||||||
"version": "3.2.2",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
||||||
|
|||||||
13
package.json
13
package.json
@@ -12,13 +12,15 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@beam-australia/react-env": "^3.1.1",
|
"@beam-australia/react-env": "^3.1.1",
|
||||||
"@dnd-kit/core": "^6.1.0",
|
"@dnd-kit/core": "^6.1.0",
|
||||||
|
"@dnd-kit/sortable": "^8.0.0",
|
||||||
"@firebase/util": "^1.9.7",
|
"@firebase/util": "^1.9.7",
|
||||||
"@headlessui/react": "^1.7.13",
|
"@headlessui/react": "^2.1.2",
|
||||||
"@mdi/js": "^7.1.96",
|
"@mdi/js": "^7.1.96",
|
||||||
"@mdi/react": "^1.6.1",
|
"@mdi/react": "^1.6.1",
|
||||||
"@next/font": "13.1.6",
|
|
||||||
"@paypal/paypal-js": "^7.1.0",
|
"@paypal/paypal-js": "^7.1.0",
|
||||||
"@paypal/react-paypal-js": "^8.1.3",
|
"@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-pdf/renderer": "^3.1.14",
|
||||||
"@react-spring/web": "^9.7.4",
|
"@react-spring/web": "^9.7.4",
|
||||||
"@tanstack/react-table": "^8.10.1",
|
"@tanstack/react-table": "^8.10.1",
|
||||||
@@ -29,7 +31,8 @@
|
|||||||
"axios": "^1.3.5",
|
"axios": "^1.3.5",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"chart.js": "^4.2.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",
|
"countries-list": "^3.0.1",
|
||||||
"country-codes-list": "^1.6.11",
|
"country-codes-list": "^1.6.11",
|
||||||
"currency-symbol-map": "^5.1.0",
|
"currency-symbol-map": "^5.1.0",
|
||||||
@@ -48,7 +51,7 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"moment-timezone": "^0.5.44",
|
"moment-timezone": "^0.5.44",
|
||||||
"next": "13.1.6",
|
"next": "^14.2.5",
|
||||||
"nodemailer": "^6.9.5",
|
"nodemailer": "^6.9.5",
|
||||||
"nodemailer-express-handlebars": "^6.1.0",
|
"nodemailer-express-handlebars": "^6.1.0",
|
||||||
"primeicons": "^6.0.1",
|
"primeicons": "^6.0.1",
|
||||||
@@ -77,7 +80,9 @@
|
|||||||
"short-unique-id": "5.0.2",
|
"short-unique-id": "5.0.2",
|
||||||
"stripe": "^13.10.0",
|
"stripe": "^13.10.0",
|
||||||
"swr": "^2.1.3",
|
"swr": "^2.1.3",
|
||||||
|
"tailwind-merge": "^2.5.2",
|
||||||
"tailwind-scrollbar-hide": "^1.1.7",
|
"tailwind-scrollbar-hide": "^1.1.7",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"typescript": "4.9.5",
|
"typescript": "4.9.5",
|
||||||
"use-file-picker": "^2.1.0",
|
"use-file-picker": "^2.1.0",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
|
|||||||
@@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
64
src/components/Exercises/FillBlanks/WordsDrawer.tsx
Normal file
64
src/components/Exercises/FillBlanks/WordsDrawer.tsx
Normal 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;
|
||||||
232
src/components/Exercises/FillBlanks/index.tsx
Normal file
232
src/components/Exercises/FillBlanks/index.tsx
Normal 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;
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* 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 useExamStore from "@/stores/examStore";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {useEffect, useState} from "react";
|
import { useEffect, useState } from "react";
|
||||||
import reactStringReplace from "react-string-replace";
|
import reactStringReplace from "react-string-replace";
|
||||||
import {CommonProps} from ".";
|
import { CommonProps } from ".";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
|
|
||||||
function Question({
|
function Question({
|
||||||
@@ -14,22 +14,30 @@ function Question({
|
|||||||
options,
|
options,
|
||||||
userSolution,
|
userSolution,
|
||||||
onSelectOption,
|
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) => {
|
const renderPrompt = (prompt: string) => {
|
||||||
return reactStringReplace(prompt, /((<u>)[\w\s']+(<\/u>))/g, (match) => {
|
return reactStringReplace(prompt, /((<u>)[\w\s']+(<\/u>))/g, (match) => {
|
||||||
const word = match.replaceAll("<u>", "").replaceAll("</u>", "");
|
const word = match.replaceAll("<u>", "").replaceAll("</u>", "");
|
||||||
return word.length > 0 ? <u>{word}</u> : null;
|
return word.length > 0 ? <u>{word}</u> : null;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
// {renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")}
|
||||||
<div className="flex flex-col gap-10">
|
<div className="flex flex-col gap-10">
|
||||||
{isNaN(Number(id)) ? (
|
{isNaN(Number(id)) ? (
|
||||||
<span>{renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")} </span>
|
<span dangerouslySetInnerHTML={{ __html: prompt }} />
|
||||||
) : (
|
) : (
|
||||||
<span className="">
|
<span className="">
|
||||||
<>
|
<>
|
||||||
{id} - <span>{renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")} </span>
|
{id} - <span dangerouslySetInnerHTML={{ __html: prompt }} />
|
||||||
</>
|
</>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -65,53 +73,79 @@ function Question({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MultipleChoice({id, prompt, type, questions, userSolutions, onNext, onBack}: MultipleChoiceExercise & CommonProps) {
|
export default function MultipleChoice({ id, prompt, type, questions, userSolutions, onNext, onBack }: MultipleChoiceExercise & CommonProps) {
|
||||||
const [answers, setAnswers] = useState<{question: string; option: string}[]>(userSolutions);
|
const [answers, setAnswers] = useState<{ question: string; option: string }[]>(userSolutions);
|
||||||
|
|
||||||
const {questionIndex, setQuestionIndex} = useExamStore((state) => state);
|
//const { shuffleMaps } = useExamStore((state) => state);
|
||||||
const {userSolutions: storeUserSolutions, setUserSolutions} = useExamStore((state) => state);
|
const { questionIndex, setQuestionIndex } = useExamStore((state) => state);
|
||||||
|
const { userSolutions: storeUserSolutions, setUserSolutions } = useExamStore((state) => state);
|
||||||
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
||||||
|
|
||||||
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
|
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
|
||||||
|
|
||||||
useEffect(() => {
|
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
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [answers]);
|
}, [answers]);
|
||||||
|
|
||||||
useEffect(() => {
|
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
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [hasExamEnded]);
|
}, [hasExamEnded]);
|
||||||
|
|
||||||
const onSelectOption = (option: string) => {
|
const onSelectOption = (option: string) => {
|
||||||
const question = questions[questionIndex];
|
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 calculateScore = () => {
|
||||||
const total = questions.length;
|
const total = questions.length;
|
||||||
const correct = answers.filter(
|
const correct = answers.filter((x) => {
|
||||||
(x) => questions.find((y) => y.id.toString() === x.question.toString())?.solution === x.option || false,
|
const matchingQuestion = questions.find((y) => {
|
||||||
).length;
|
return y.id.toString() === x.question.toString();
|
||||||
const missing = total - answers.filter((x) => questions.find((y) => y.id.toString() === x.question.toString())).length;
|
});
|
||||||
|
|
||||||
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 = () => {
|
const next = () => {
|
||||||
if (questionIndex === questions.length - 1) {
|
if (questionIndex === questions.length - 1) {
|
||||||
onNext({exercise: id, solutions: answers, score: calculateScore(), type});
|
onNext({ exercise: id, solutions: answers, score: calculateScore(), type, });//...getShuffles() });
|
||||||
} else {
|
} else {
|
||||||
setQuestionIndex(questionIndex + 1);
|
setQuestionIndex(questionIndex + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToTop();
|
scrollToTop();
|
||||||
};
|
};
|
||||||
|
|
||||||
const back = () => {
|
const back = () => {
|
||||||
if (questionIndex === 0) {
|
if (questionIndex === 0) {
|
||||||
onBack({exercise: id, solutions: answers, score: calculateScore(), type});
|
onBack({ exercise: id, solutions: answers, score: calculateScore(), type, });// ...getShuffles() });
|
||||||
} else {
|
} else {
|
||||||
setQuestionIndex(questionIndex - 1);
|
setQuestionIndex(questionIndex - 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ const FillBlanksEdit = (props: Props) => {
|
|||||||
label={`Word ${index + 1}`}
|
label={`Word ${index + 1}`}
|
||||||
name="word"
|
name="word"
|
||||||
required
|
required
|
||||||
value={typeof word === "string" ? word : word.word}
|
value={typeof word === "string" ? word : ("word" in word ? word.word : "")}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
updateExercise({
|
updateExercise({
|
||||||
words: exercise.words.map((sol, idx) =>
|
words: exercise.words.map((sol, idx) =>
|
||||||
|
|||||||
39
src/components/HighlightContent.tsx
Normal file
39
src/components/HighlightContent.tsx
Normal 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
51
src/components/List.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
BsCurrencyDollar,
|
BsCurrencyDollar,
|
||||||
BsClipboardData,
|
BsClipboardData,
|
||||||
BsFileLock,
|
BsFileLock,
|
||||||
|
BsPeople,
|
||||||
} from "react-icons/bs";
|
} from "react-icons/bs";
|
||||||
import {CiDumbbell} from "react-icons/ci";
|
import {CiDumbbell} from "react-icons/ci";
|
||||||
import {RiLogoutBoxFill} from "react-icons/ri";
|
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") && (
|
{checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewStats") && (
|
||||||
<Nav disabled={disableNavigation} Icon={BsGraphUp} label="Stats" path={path} keyPath="/stats" isMinimized={isMinimized} />
|
<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") && (
|
{checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewRecords") && (
|
||||||
<Nav disabled={disableNavigation} Icon={BsClockHistory} label="Record" path={path} keyPath="/record" isMinimized={isMinimized} />
|
<Nav disabled={disableNavigation} Icon={BsClockHistory} label="Record" path={path} keyPath="/record" isMinimized={isMinimized} />
|
||||||
)}
|
)}
|
||||||
@@ -146,25 +150,25 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, u
|
|||||||
badge={totalAssignedTickets}
|
badge={totalAssignedTickets}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{checkAccess(user, ["developer", "admin"]) && (
|
{checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"]) && (
|
||||||
<>
|
<Nav
|
||||||
<Nav
|
disabled={disableNavigation}
|
||||||
disabled={disableNavigation}
|
Icon={BsCloudFill}
|
||||||
Icon={BsCloudFill}
|
label="Generation"
|
||||||
label="Generation"
|
path={path}
|
||||||
path={path}
|
keyPath="/generation"
|
||||||
keyPath="/generation"
|
isMinimized={isMinimized}
|
||||||
isMinimized={isMinimized}
|
/>
|
||||||
/>
|
)}
|
||||||
<Nav
|
{checkAccess(user, ["developer", "admin", "corporate", "mastercorporate", "agent"]) && (
|
||||||
disabled={disableNavigation}
|
<Nav
|
||||||
Icon={BsFileLock}
|
disabled={disableNavigation}
|
||||||
label="Permissions"
|
Icon={BsFileLock}
|
||||||
path={path}
|
label="Permissions"
|
||||||
keyPath="/permissions"
|
path={path}
|
||||||
isMinimized={isMinimized}
|
keyPath="/permissions"
|
||||||
/>
|
isMinimized={isMinimized}
|
||||||
</>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="-xl:flex flex-col gap-3 xl:hidden">
|
<div className="-xl:flex flex-col gap-3 xl:hidden">
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {FillBlanksExercise} from "@/interfaces/exam";
|
import { FillBlanksExercise, FillBlanksMCOption } from "@/interfaces/exam";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import reactStringReplace from "react-string-replace";
|
import reactStringReplace from "react-string-replace";
|
||||||
import {CommonProps} from ".";
|
import { CommonProps } from ".";
|
||||||
import {Fragment} from "react";
|
import { Fragment } from "react";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
|
|
||||||
export default function FillBlanksSolutions({
|
export default function FillBlanksSolutions({
|
||||||
@@ -19,21 +19,42 @@ export default function FillBlanksSolutions({
|
|||||||
const calculateScore = () => {
|
const calculateScore = () => {
|
||||||
const total = text.match(/({{\d+}})/g)?.length || 0;
|
const total = text.match(/({{\d+}})/g)?.length || 0;
|
||||||
const correct = userSolutions.filter((x) => {
|
const correct = userSolutions.filter((x) => {
|
||||||
const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution.toLowerCase();
|
const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution;
|
||||||
if (!solution) return false;
|
if (!solution) return false;
|
||||||
|
|
||||||
const option = words.find((w) =>
|
const option = words.find((w) => {
|
||||||
typeof w === "string" ? w.toLowerCase() === x.solution.toLowerCase() : w.letter.toLowerCase() === x.solution.toLowerCase(),
|
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 (!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;
|
}).length;
|
||||||
|
|
||||||
const missing = total - userSolutions.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length;
|
const missing = total - userSolutions.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length;
|
||||||
|
|
||||||
return {total, correct, missing};
|
return { total, correct, missing };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const 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) => {
|
const renderLines = (line: string) => {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
@@ -56,23 +77,53 @@ export default function FillBlanksSolutions({
|
|||||||
const userSolutionWord = words.find((w) =>
|
const userSolutionWord = words.find((w) =>
|
||||||
typeof w === "string"
|
typeof w === "string"
|
||||||
? w.toLowerCase() === userSolution.solution.toLowerCase()
|
? 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 (
|
return (
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"rounded-full hover:text-white hover:bg-mti-purple transition duration-300 ease-in-out my-1",
|
"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",
|
userSolution && "px-5 py-2 text-center text-white bg-mti-purple-light",
|
||||||
)}>
|
)}>
|
||||||
{solution.solution}
|
{solutionText}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
} else {
|
||||||
|
|
||||||
if (userSolutionText !== solution.solution) {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<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",
|
"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",
|
userSolution && "px-5 py-2 text-center text-white bg-mti-purple-light",
|
||||||
)}>
|
)}>
|
||||||
{solution.solution}
|
{solutionText}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -138,14 +189,14 @@ export default function FillBlanksSolutions({
|
|||||||
<Button
|
<Button
|
||||||
color="purple"
|
color="purple"
|
||||||
variant="outline"
|
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">
|
className="max-w-[200px] w-full">
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
color="purple"
|
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">
|
className="max-w-[200px] self-end w-full">
|
||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* 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 useExamStore from "@/stores/examStore";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {useEffect, useState} from "react";
|
import { useEffect, useState } from "react";
|
||||||
import reactStringReplace from "react-string-replace";
|
import reactStringReplace from "react-string-replace";
|
||||||
import {CommonProps} from ".";
|
import { CommonProps } from ".";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
|
|
||||||
function Question({
|
function Question({
|
||||||
@@ -14,7 +14,40 @@ function Question({
|
|||||||
solution,
|
solution,
|
||||||
options,
|
options,
|
||||||
userSolution,
|
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) => {
|
const renderPrompt = (prompt: string) => {
|
||||||
return reactStringReplace(prompt, /((<u>)[\w\s']+(<\/u>))/g, (match) => {
|
return reactStringReplace(prompt, /((<u>)[\w\s']+(<\/u>))/g, (match) => {
|
||||||
const word = match.replaceAll("<u>", "").replaceAll("</u>", "");
|
const word = match.replaceAll("<u>", "").replaceAll("</u>", "");
|
||||||
@@ -23,11 +56,11 @@ function Question({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const optionColor = (option: string) => {
|
const optionColor = (option: string) => {
|
||||||
if (option === solution && !userSolution) {
|
if (option === newSolution && !userSolution) {
|
||||||
return "!border-mti-gray-davy !text-mti-gray-davy";
|
return "!border-mti-gray-davy !text-mti-gray-davy";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (option === solution) {
|
if (option === newSolution) {
|
||||||
return "!border-mti-purple-light !text-mti-purple-light";
|
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">
|
<div className="grid grid-cols-4 gap-4 place-items-center">
|
||||||
{variant === "image" &&
|
{variant === "image" &&
|
||||||
options.map((option) => (
|
questionOptions.map((option) => (
|
||||||
<div
|
<div
|
||||||
key={option.id}
|
key={option?.id}
|
||||||
className={clsx(
|
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",
|
"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>
|
<span className={clsx("text-sm", newSolution !== option?.id && userSolution !== option?.id && "opacity-50")}>{option?.id}</span>
|
||||||
<img src={option.src!} alt={`Option ${option.id}`} />
|
{"src" in option && <img src={option?.src!} alt={`Option ${option?.id}`} />}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{variant === "text" &&
|
{variant === "text" &&
|
||||||
options.map((option) => (
|
questionOptions.map((option) => (
|
||||||
<div
|
<div
|
||||||
key={option.id}
|
key={option?.id}
|
||||||
className={clsx("flex border p-4 rounded-xl gap-2 cursor-pointer bg-white text-sm", optionColor(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 className="font-semibold">{option?.id}.</span>
|
||||||
<span>{option.text}</span>
|
<span>{option?.text}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -72,8 +105,8 @@ function Question({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MultipleChoice({id, type, prompt, questions, userSolutions, onNext, onBack}: MultipleChoiceExercise & CommonProps) {
|
export default function MultipleChoice({ id, type, prompt, questions, userSolutions, onNext, onBack }: MultipleChoiceExercise & CommonProps) {
|
||||||
const {questionIndex, setQuestionIndex} = useExamStore((state) => state);
|
const { questionIndex, setQuestionIndex } = useExamStore((state) => state);
|
||||||
|
|
||||||
const calculateScore = () => {
|
const calculateScore = () => {
|
||||||
const total = questions.length;
|
const total = questions.length;
|
||||||
@@ -82,12 +115,12 @@ export default function MultipleChoice({id, type, prompt, questions, userSolutio
|
|||||||
).length;
|
).length;
|
||||||
const missing = total - userSolutions.filter((x) => questions.find((y) => y.id.toString() === x.question.toString())).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 = () => {
|
const next = () => {
|
||||||
if (questionIndex === questions.length - 1) {
|
if (questionIndex === questions.length - 1) {
|
||||||
onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type});
|
onNext({ exercise: id, solutions: userSolutions, score: calculateScore(), type });
|
||||||
} else {
|
} else {
|
||||||
setQuestionIndex(questionIndex + 1);
|
setQuestionIndex(questionIndex + 1);
|
||||||
}
|
}
|
||||||
@@ -95,7 +128,7 @@ export default function MultipleChoice({id, type, prompt, questions, userSolutio
|
|||||||
|
|
||||||
const back = () => {
|
const back = () => {
|
||||||
if (questionIndex === 0) {
|
if (questionIndex === 0) {
|
||||||
onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type});
|
onBack({ exercise: id, solutions: userSolutions, score: calculateScore(), type });
|
||||||
} else {
|
} else {
|
||||||
setQuestionIndex(questionIndex - 1);
|
setQuestionIndex(questionIndex - 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { animated } from '@react-spring/web';
|
import { animated } from '@react-spring/web';
|
||||||
import { FaRegCirclePlay, FaRegCircleStop } from "react-icons/fa6";
|
import { FaRegCirclePlay, FaRegCircleStop } from "react-icons/fa6";
|
||||||
import HighlightedContent from './AnimatedHighlight';
|
import HighlightContent from '../HighlightContent';
|
||||||
import { ITrainingTip, SegmentRef, TimelineEvent } from './TrainingInterfaces';
|
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'>
|
<div className='flex-1 bg-white p-6 rounded-lg shadow'>
|
||||||
{/*<h2 className="text-xl font-bold mb-4">Question</h2>*/}
|
{/*<h2 className="text-xl font-bold mb-4">Question</h2>*/}
|
||||||
<div className="mb-4" dangerouslySetInnerHTML={{ __html: tip.exercise.question }} />
|
<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>
|
||||||
<div className='flex-1'>
|
<div className='flex-1'>
|
||||||
<div className='bg-gray-50 rounded-lg shadow'>
|
<div className='bg-gray-50 rounded-lg shadow'>
|
||||||
|
|||||||
@@ -525,7 +525,9 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
isChecked={!!expiryDate}
|
isChecked={!!expiryDate}
|
||||||
onChange={(checked) => setExpiryDate(checked ? user.subscriptionExpirationDate || new Date() : null)}
|
onChange={(checked) => setExpiryDate(checked ? user.subscriptionExpirationDate || new Date() : null)}
|
||||||
disabled={disabled}>
|
disabled={
|
||||||
|
disabled || (!["admin", "developer"].includes(loggedInUser.type) && !!loggedInUser.subscriptionExpirationDate)
|
||||||
|
}>
|
||||||
Enabled
|
Enabled
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
@@ -564,7 +566,12 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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" />
|
<Divider className="w-full !m-0" />
|
||||||
<div className="flex flex-col md:flex-row gap-8 w-full">
|
<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>
|
<label className="font-normal text-base text-mti-gray-dim">Status</label>
|
||||||
<Select
|
<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"
|
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}
|
menuPortalTarget={document?.body}
|
||||||
value={USER_STATUS_OPTIONS.find((o) => o.value === status)}
|
value={USER_STATUS_OPTIONS.find((o) => o.value === status)}
|
||||||
onChange={(value) => setStatus(value?.value as typeof user.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>
|
<label className="font-normal text-base text-mti-gray-dim">Type</label>
|
||||||
<Select
|
<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"
|
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}
|
menuPortalTarget={document?.body}
|
||||||
value={USER_TYPE_OPTIONS.find((o) => o.value === type)}
|
value={USER_TYPE_OPTIONS.find((o) => o.value === type)}
|
||||||
onChange={(value) => setType(value?.value as typeof user.type)}
|
onChange={(value) => setType(value?.value as typeof user.type)}
|
||||||
|
|||||||
31
src/components/ui/popover.tsx
Normal file
31
src/components/ui/popover.tsx
Normal 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 }
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -2,17 +2,12 @@
|
|||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import useStats from "@/hooks/useStats";
|
import useStats from "@/hooks/useStats";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import { User } from "@/interfaces/user";
|
import {User} from "@/interfaces/user";
|
||||||
import UserList from "@/pages/(admin)/Lists/UserList";
|
import UserList from "@/pages/(admin)/Lists/UserList";
|
||||||
import { dateSorter } from "@/utils";
|
import {dateSorter} from "@/utils";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { useEffect, useState } from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {
|
import {BsArrowLeft, BsPersonFill, BsBank, BsCurrencyDollar} from "react-icons/bs";
|
||||||
BsArrowLeft,
|
|
||||||
BsPersonFill,
|
|
||||||
BsBank,
|
|
||||||
BsCurrencyDollar,
|
|
||||||
} from "react-icons/bs";
|
|
||||||
import UserCard from "@/components/UserCard";
|
import UserCard from "@/components/UserCard";
|
||||||
import useGroups from "@/hooks/useGroups";
|
import useGroups from "@/hooks/useGroups";
|
||||||
|
|
||||||
@@ -20,276 +15,235 @@ import IconCard from "./IconCard";
|
|||||||
import usePaymentStatusUsers from "@/hooks/usePaymentStatusUsers";
|
import usePaymentStatusUsers from "@/hooks/usePaymentStatusUsers";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AgentDashboard({ user }: Props) {
|
export default function AgentDashboard({user}: Props) {
|
||||||
const [page, setPage] = useState("");
|
const [page, setPage] = useState("");
|
||||||
const [selectedUser, setSelectedUser] = useState<User>();
|
const [selectedUser, setSelectedUser] = useState<User>();
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
|
||||||
const { stats } = useStats();
|
const {stats} = useStats();
|
||||||
const { users, reload } = useUsers();
|
const {users, reload} = useUsers();
|
||||||
const { groups } = useGroups(user.id);
|
const {pending, done} = usePaymentStatusUsers();
|
||||||
const { pending, done } = usePaymentStatusUsers();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShowModal(!!selectedUser && page === "");
|
setShowModal(!!selectedUser && page === "");
|
||||||
}, [selectedUser, page]);
|
}, [selectedUser, page]);
|
||||||
|
|
||||||
const corporateFilter = (user: User) => user.type === "corporate";
|
const corporateFilter = (user: User) => user.type === "corporate";
|
||||||
const referredCorporateFilter = (x: User) =>
|
const referredCorporateFilter = (x: User) =>
|
||||||
x.type === "corporate" &&
|
x.type === "corporate" && !!x.corporateInformation && x.corporateInformation.referralAgent === user.id;
|
||||||
!!x.corporateInformation &&
|
const inactiveReferredCorporateFilter = (x: User) =>
|
||||||
x.corporateInformation.referralAgent === user.id;
|
referredCorporateFilter(x) && (x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
|
||||||
const inactiveReferredCorporateFilter = (x: User) =>
|
|
||||||
referredCorporateFilter(x) &&
|
|
||||||
(x.status === "disabled" || moment().isAfter(x.subscriptionExpirationDate));
|
|
||||||
|
|
||||||
const UserDisplay = ({
|
const UserDisplay = ({displayUser, allowClick = true}: {displayUser: User; allowClick?: boolean}) => (
|
||||||
displayUser,
|
<div
|
||||||
allowClick = true,
|
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">
|
||||||
displayUser: User;
|
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
|
||||||
allowClick?: boolean;
|
<div className="flex flex-col gap-1 items-start">
|
||||||
}) => (
|
<span>
|
||||||
<div
|
{displayUser.type === "corporate"
|
||||||
onClick={() => allowClick && setSelectedUser(displayUser)}
|
? displayUser.corporateInformation?.companyInformation?.name || displayUser.name
|
||||||
className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300"
|
: displayUser.name}
|
||||||
>
|
</span>
|
||||||
<img
|
<span className="text-sm opacity-75">{displayUser.email}</span>
|
||||||
src={displayUser.profilePicture}
|
</div>
|
||||||
alt={displayUser.name}
|
</div>
|
||||||
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.name}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm opacity-75">{displayUser.email}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const ReferredCorporateList = () => {
|
const ReferredCorporateList = () => {
|
||||||
return (
|
return (
|
||||||
<UserList
|
<UserList
|
||||||
user={user}
|
user={user}
|
||||||
filters={[referredCorporateFilter]}
|
filters={[referredCorporateFilter]}
|
||||||
renderHeader={(total) => (
|
renderHeader={(total) => (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
onClick={() => setPage("")}
|
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" />
|
||||||
<BsArrowLeft className="text-xl" />
|
<span>Back</span>
|
||||||
<span>Back</span>
|
</div>
|
||||||
</div>
|
<h2 className="text-2xl font-semibold">Referred Corporate ({total})</h2>
|
||||||
<h2 className="text-2xl font-semibold">
|
</div>
|
||||||
Referred Corporate ({total})
|
)}
|
||||||
</h2>
|
/>
|
||||||
</div>
|
);
|
||||||
)}
|
};
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const InactiveReferredCorporateList = () => {
|
const InactiveReferredCorporateList = () => {
|
||||||
return (
|
return (
|
||||||
<UserList
|
<UserList
|
||||||
user={user}
|
user={user}
|
||||||
filters={[inactiveReferredCorporateFilter]}
|
filters={[inactiveReferredCorporateFilter]}
|
||||||
renderHeader={(total) => (
|
renderHeader={(total) => (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
onClick={() => setPage("")}
|
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" />
|
||||||
<BsArrowLeft className="text-xl" />
|
<span>Back</span>
|
||||||
<span>Back</span>
|
</div>
|
||||||
</div>
|
<h2 className="text-2xl font-semibold">Inactive Referred Corporate ({total})</h2>
|
||||||
<h2 className="text-2xl font-semibold">
|
</div>
|
||||||
Inactive Referred Corporate ({total})
|
)}
|
||||||
</h2>
|
/>
|
||||||
</div>
|
);
|
||||||
)}
|
};
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const CorporateList = () => {
|
const CorporateList = () => {
|
||||||
const filter = (x: User) => x.type === "corporate";
|
const filter = (x: User) => x.type === "corporate";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserList
|
<UserList
|
||||||
user={user}
|
user={user}
|
||||||
filters={[filter]}
|
filters={[filter]}
|
||||||
renderHeader={(total) => (
|
renderHeader={(total) => (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
onClick={() => setPage("")}
|
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" />
|
||||||
<BsArrowLeft className="text-xl" />
|
<span>Back</span>
|
||||||
<span>Back</span>
|
</div>
|
||||||
</div>
|
<h2 className="text-2xl font-semibold">Corporate ({total})</h2>
|
||||||
<h2 className="text-2xl font-semibold">Corporate ({total})</h2>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
};
|
||||||
};
|
|
||||||
|
|
||||||
const CorporatePaidStatusList = ({ paid }: { paid: Boolean }) => {
|
const CorporatePaidStatusList = ({paid}: {paid: Boolean}) => {
|
||||||
const list = paid ? done : pending;
|
const list = paid ? done : pending;
|
||||||
const filter = (x: User) => x.type === "corporate" && list.includes(x.id);
|
const filter = (x: User) => x.type === "corporate" && list.includes(x.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserList
|
<UserList
|
||||||
user={user}
|
user={user}
|
||||||
filters={[filter]}
|
filters={[filter]}
|
||||||
renderHeader={(total) => (
|
renderHeader={(total) => (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
onClick={() => setPage("")}
|
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" />
|
||||||
<BsArrowLeft className="text-xl" />
|
<span>Back</span>
|
||||||
<span>Back</span>
|
</div>
|
||||||
</div>
|
<h2 className="text-2xl font-semibold">
|
||||||
<h2 className="text-2xl font-semibold">
|
{paid ? "Payment Done" : "Pending Payment"} ({total})
|
||||||
{paid ? "Payment Done" : "Pending Payment"} ({total})
|
</h2>
|
||||||
</h2>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
};
|
||||||
};
|
|
||||||
|
|
||||||
const DefaultDashboard = () => (
|
const DefaultDashboard = () => (
|
||||||
<>
|
<>
|
||||||
<section className="flex flex-wrap gap-2 items-center -lg:justify-center lg:gap-4 text-center">
|
<section className="flex flex-wrap gap-2 items-center -lg:justify-center lg:gap-4 text-center">
|
||||||
<IconCard
|
<IconCard
|
||||||
onClick={() => setPage("referredCorporate")}
|
onClick={() => setPage("referredCorporate")}
|
||||||
Icon={BsBank}
|
Icon={BsBank}
|
||||||
label="Referred Corporate"
|
label="Referred Corporate"
|
||||||
value={users.filter(referredCorporateFilter).length}
|
value={users.filter(referredCorporateFilter).length}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
onClick={() => setPage("inactiveReferredCorporate")}
|
onClick={() => setPage("inactiveReferredCorporate")}
|
||||||
Icon={BsBank}
|
Icon={BsBank}
|
||||||
label="Inactive Referred Corporate"
|
label="Inactive Referred Corporate"
|
||||||
value={users.filter(inactiveReferredCorporateFilter).length}
|
value={users.filter(inactiveReferredCorporateFilter).length}
|
||||||
color="rose"
|
color="rose"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
onClick={() => setPage("corporate")}
|
onClick={() => setPage("corporate")}
|
||||||
Icon={BsBank}
|
Icon={BsBank}
|
||||||
label="Corporate"
|
label="Corporate"
|
||||||
value={users.filter(corporateFilter).length}
|
value={users.filter(corporateFilter).length}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard onClick={() => setPage("paymentdone")} Icon={BsCurrencyDollar} label="Payment Done" value={done.length} color="purple" />
|
||||||
onClick={() => setPage("paymentdone")}
|
<IconCard
|
||||||
Icon={BsCurrencyDollar}
|
onClick={() => setPage("paymentpending")}
|
||||||
label="Payment Done"
|
Icon={BsCurrencyDollar}
|
||||||
value={done.length}
|
label="Pending Payment"
|
||||||
color="purple"
|
value={pending.length}
|
||||||
/>
|
color="rose"
|
||||||
<IconCard
|
/>
|
||||||
onClick={() => setPage("paymentpending")}
|
</section>
|
||||||
Icon={BsCurrencyDollar}
|
|
||||||
label="Pending Payment"
|
|
||||||
value={pending.length}
|
|
||||||
color="rose"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
||||||
<div className="bg-white shadow flex flex-col rounded-xl w-full">
|
<div className="bg-white shadow flex flex-col rounded-xl w-full">
|
||||||
<span className="p-4">Latest Referred Corporate</span>
|
<span className="p-4">Latest Referred Corporate</span>
|
||||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||||
{users
|
{users
|
||||||
.filter(referredCorporateFilter)
|
.filter(referredCorporateFilter)
|
||||||
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
|
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} displayUser={x} />
|
<UserDisplay key={x.id} displayUser={x} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white shadow flex flex-col rounded-xl w-full">
|
<div className="bg-white shadow flex flex-col rounded-xl w-full">
|
||||||
<span className="p-4">Latest corporate</span>
|
<span className="p-4">Latest corporate</span>
|
||||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||||
{users
|
{users
|
||||||
.filter(corporateFilter)
|
.filter(corporateFilter)
|
||||||
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
|
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} displayUser={x} allowClick={false} />
|
<UserDisplay key={x.id} displayUser={x} allowClick={false} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white shadow flex flex-col rounded-xl w-full">
|
<div className="bg-white shadow flex flex-col rounded-xl w-full">
|
||||||
<span className="p-4">Referenced corporate expiring in 1 month</span>
|
<span className="p-4">Referenced corporate expiring in 1 month</span>
|
||||||
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
|
||||||
{users
|
{users
|
||||||
.filter(
|
.filter(
|
||||||
(x) =>
|
(x) =>
|
||||||
referredCorporateFilter(x) &&
|
referredCorporateFilter(x) &&
|
||||||
moment().isAfter(
|
moment().isAfter(moment(x.subscriptionExpirationDate).subtract(30, "days")) &&
|
||||||
moment(x.subscriptionExpirationDate).subtract(30, "days")
|
moment().isBefore(moment(x.subscriptionExpirationDate)),
|
||||||
) &&
|
)
|
||||||
moment().isBefore(moment(x.subscriptionExpirationDate))
|
.map((x) => (
|
||||||
)
|
<UserDisplay key={x.id} displayUser={x} />
|
||||||
.map((x) => (
|
))}
|
||||||
<UserDisplay key={x.id} displayUser={x} />
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
</>
|
||||||
</section>
|
);
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal isOpen={showModal} onClose={() => setSelectedUser(undefined)}>
|
<Modal isOpen={showModal} onClose={() => setSelectedUser(undefined)}>
|
||||||
<>
|
<>
|
||||||
{selectedUser && (
|
{selectedUser && (
|
||||||
<div className="w-full flex flex-col gap-8">
|
<div className="w-full flex flex-col gap-8">
|
||||||
<UserCard
|
<UserCard
|
||||||
loggedInUser={user}
|
loggedInUser={user}
|
||||||
onClose={(shouldReload) => {
|
onClose={(shouldReload) => {
|
||||||
setSelectedUser(undefined);
|
setSelectedUser(undefined);
|
||||||
if (shouldReload) reload();
|
if (shouldReload) reload();
|
||||||
}}
|
}}
|
||||||
onViewStudents={
|
onViewStudents={
|
||||||
selectedUser.type === "corporate" ||
|
selectedUser.type === "corporate" || selectedUser.type === "teacher" ? () => setPage("students") : undefined
|
||||||
selectedUser.type === "teacher"
|
}
|
||||||
? () => setPage("students")
|
onViewTeachers={selectedUser.type === "corporate" ? () => setPage("teachers") : undefined}
|
||||||
: undefined
|
user={selectedUser}
|
||||||
}
|
/>
|
||||||
onViewTeachers={
|
</div>
|
||||||
selectedUser.type === "corporate"
|
)}
|
||||||
? () => setPage("teachers")
|
</>
|
||||||
: undefined
|
</Modal>
|
||||||
}
|
{page === "referredCorporate" && <ReferredCorporateList />}
|
||||||
user={selectedUser}
|
{page === "corporate" && <CorporateList />}
|
||||||
/>
|
{page === "inactiveReferredCorporate" && <InactiveReferredCorporateList />}
|
||||||
</div>
|
{page === "paymentdone" && <CorporatePaidStatusList paid={true} />}
|
||||||
)}
|
{page === "paymentpending" && <CorporatePaidStatusList paid={false} />}
|
||||||
</>
|
{page === "" && <DefaultDashboard />}
|
||||||
</Modal>
|
</>
|
||||||
{page === "referredCorporate" && <ReferredCorporateList />}
|
);
|
||||||
{page === "corporate" && <CorporateList />}
|
|
||||||
{page === "inactiveReferredCorporate" && (
|
|
||||||
<InactiveReferredCorporateList />
|
|
||||||
)}
|
|
||||||
{page === "paymentdone" && <CorporatePaidStatusList paid={true} />}
|
|
||||||
{page === "paymentpending" && <CorporatePaidStatusList paid={false} />}
|
|
||||||
{page === "" && <DefaultDashboard />}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ import {useAssignmentArchive} from "@/hooks/useAssignmentArchive";
|
|||||||
import {uniqBy} from "lodash";
|
import {uniqBy} from "lodash";
|
||||||
import {useAssignmentUnarchive} from "@/hooks/useAssignmentUnarchive";
|
import {useAssignmentUnarchive} from "@/hooks/useAssignmentUnarchive";
|
||||||
import {getUserName} from "@/utils/users";
|
import {getUserName} from "@/utils/users";
|
||||||
|
import {User} from "@/interfaces/user";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
users: User[];
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
allowDownload?: boolean;
|
allowDownload?: boolean;
|
||||||
reload?: Function;
|
reload?: Function;
|
||||||
@@ -37,9 +39,8 @@ export default function AssignmentCard({
|
|||||||
allowArchive,
|
allowArchive,
|
||||||
allowUnarchive,
|
allowUnarchive,
|
||||||
allowExcelDownload,
|
allowExcelDownload,
|
||||||
|
users,
|
||||||
}: Assignment & Props) {
|
}: Assignment & Props) {
|
||||||
const {users} = useUsers();
|
|
||||||
|
|
||||||
const renderPdfIcon = usePDFDownload("assignments");
|
const renderPdfIcon = usePDFDownload("assignments");
|
||||||
const renderExcelIcon = usePDFDownload("assignments", "excel");
|
const renderExcelIcon = usePDFDownload("assignments", "excel");
|
||||||
const renderArchiveIcon = useAssignmentArchive(id, reload);
|
const renderArchiveIcon = useAssignmentArchive(id, reload);
|
||||||
|
|||||||
@@ -371,7 +371,7 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
|
|||||||
!startDate ||
|
!startDate ||
|
||||||
!endDate ||
|
!endDate ||
|
||||||
assignees.length === 0 ||
|
assignees.length === 0 ||
|
||||||
(!!examIDs && examIDs.length < selectedModules.length)
|
(!useRandomExams && examIDs.length < selectedModules.length)
|
||||||
}
|
}
|
||||||
className="w-full max-w-[200px]"
|
className="w-full max-w-[200px]"
|
||||||
onClick={createAssignment}
|
onClick={createAssignment}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import {
|
|||||||
} from "react-icons/bs";
|
} from "react-icons/bs";
|
||||||
import UserCard from "@/components/UserCard";
|
import UserCard from "@/components/UserCard";
|
||||||
import useGroups from "@/hooks/useGroups";
|
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_ARRAY} from "@/utils/moduleUtils";
|
||||||
import {Module} from "@/interfaces";
|
import {Module} from "@/interfaces";
|
||||||
import {groupByExam} from "@/utils/stats";
|
import {groupByExam} from "@/utils/stats";
|
||||||
@@ -45,11 +45,113 @@ import AssignmentView from "./AssignmentView";
|
|||||||
import AssignmentCreator from "./AssignmentCreator";
|
import AssignmentCreator from "./AssignmentCreator";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import AssignmentCard from "./AssignmentCard";
|
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 {
|
interface Props {
|
||||||
user: CorporateUser;
|
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) {
|
export default function CorporateDashboard({user}: Props) {
|
||||||
const [page, setPage] = useState("");
|
const [page, setPage] = useState("");
|
||||||
const [selectedUser, setSelectedUser] = useState<User>();
|
const [selectedUser, setSelectedUser] = useState<User>();
|
||||||
@@ -57,11 +159,12 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
const [corporateUserToShow, setCorporateUserToShow] = useState<CorporateUser>();
|
const [corporateUserToShow, setCorporateUserToShow] = useState<CorporateUser>();
|
||||||
const [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
|
const [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
|
||||||
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
|
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
|
||||||
|
const [userBalance, setUserBalance] = useState(0);
|
||||||
|
|
||||||
const {stats} = useStats();
|
const {stats} = useStats();
|
||||||
const {users, reload} = useUsers();
|
const {users, reload, isLoading} = useUsers();
|
||||||
const {codes} = useCodes(user.id);
|
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 {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({corporate: user.id});
|
||||||
|
|
||||||
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
|
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
|
||||||
@@ -71,6 +174,14 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
setShowModal(!!selectedUser && page === "");
|
setShowModal(!!selectedUser && page === "");
|
||||||
}, [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(() => {
|
useEffect(() => {
|
||||||
// in this case it fetches the master corporate account
|
// in this case it fetches the master corporate account
|
||||||
getUserCorporate(user.id).then(setCorporateUserToShow);
|
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>
|
<h2 className="text-2xl font-semibold">Active Assignments ({assignments.filter(activeFilter).length})</h2>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{assignments.filter(activeFilter).map((a) => (
|
{assignments.filter(activeFilter).map((a) => (
|
||||||
<AssignmentCard {...a} onClick={() => setSelectedAssignment(a)} key={a.id} />
|
<AssignmentCard {...a} users={users} onClick={() => setSelectedAssignment(a)} key={a.id} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -244,6 +355,7 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
{assignments.filter(futureFilter).map((a) => (
|
{assignments.filter(futureFilter).map((a) => (
|
||||||
<AssignmentCard
|
<AssignmentCard
|
||||||
{...a}
|
{...a}
|
||||||
|
users={users}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedAssignment(a);
|
setSelectedAssignment(a);
|
||||||
setIsCreatingAssignment(true);
|
setIsCreatingAssignment(true);
|
||||||
@@ -259,6 +371,7 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
{assignments.filter(pastFilter).map((a) => (
|
{assignments.filter(pastFilter).map((a) => (
|
||||||
<AssignmentCard
|
<AssignmentCard
|
||||||
{...a}
|
{...a}
|
||||||
|
users={users}
|
||||||
onClick={() => setSelectedAssignment(a)}
|
onClick={() => setSelectedAssignment(a)}
|
||||||
key={a.id}
|
key={a.id}
|
||||||
allowDownload
|
allowDownload
|
||||||
@@ -275,6 +388,7 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
{assignments.filter(archivedFilter).map((a) => (
|
{assignments.filter(archivedFilter).map((a) => (
|
||||||
<AssignmentCard
|
<AssignmentCard
|
||||||
{...a}
|
{...a}
|
||||||
|
users={users}
|
||||||
onClick={() => setSelectedAssignment(a)}
|
onClick={() => setSelectedAssignment(a)}
|
||||||
key={a.id}
|
key={a.id}
|
||||||
allowDownload
|
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 averageLevelCalculator = (studentStats: Stat[]) => {
|
||||||
const formattedStats = studentStats
|
const formattedStats = studentStats
|
||||||
.map((s) => ({
|
.map((s) => ({
|
||||||
@@ -352,7 +496,7 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsPersonCheck}
|
Icon={BsPersonCheck}
|
||||||
label="User Balance"
|
label="User Balance"
|
||||||
value={`${codes.length}/${user.corporateInformation?.companyInformation?.userAmount || 0}`}
|
value={`${userBalance}/${user.corporateInformation?.companyInformation?.userAmount || 0}`}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
@@ -361,6 +505,13 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
value={user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy") : "Unlimited"}
|
value={user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy") : "Unlimited"}
|
||||||
color="rose"
|
color="rose"
|
||||||
/>
|
/>
|
||||||
|
<IconCard
|
||||||
|
Icon={BsPersonFillGear}
|
||||||
|
label="Student Performance"
|
||||||
|
value={users.filter(studentFilter).length}
|
||||||
|
color="purple"
|
||||||
|
onClick={() => setPage("studentsPerformance")}
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
disabled={isAssignmentsLoading}
|
disabled={isAssignmentsLoading}
|
||||||
onClick={() => setPage("assignments")}
|
onClick={() => setPage("assignments")}
|
||||||
@@ -489,6 +640,7 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
{page === "teachers" && <TeachersList />}
|
{page === "teachers" && <TeachersList />}
|
||||||
{page === "groups" && <GroupsList />}
|
{page === "groups" && <GroupsList />}
|
||||||
{page === "assignments" && <AssignmentsPage />}
|
{page === "assignments" && <AssignmentsPage />}
|
||||||
|
{page === "studentsPerformance" && <StudentPerformancePage />}
|
||||||
{page === "" && <DefaultDashboard />}
|
{page === "" && <DefaultDashboard />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,140 +1,108 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import useGroups from "@/hooks/useGroups";
|
import useGroups from "@/hooks/useGroups";
|
||||||
import { User } from "@/interfaces/user";
|
import {User} from "@/interfaces/user";
|
||||||
import Select from "@/components/Low/Select";
|
import Select from "@/components/Low/Select";
|
||||||
import ProgressBar from "@/components/Low/ProgressBar";
|
import ProgressBar from "@/components/Low/ProgressBar";
|
||||||
import {
|
import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs";
|
||||||
BsBook,
|
import {MODULE_ARRAY} from "@/utils/moduleUtils";
|
||||||
BsClipboard,
|
import {capitalize} from "lodash";
|
||||||
BsHeadphones,
|
import {getLevelLabel} from "@/utils/score";
|
||||||
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 (
|
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">
|
<div className="flex flex-col gap-3">
|
||||||
<h3 className="text-xl font-semibold">{user.name}</h3>
|
<h3 className="text-xl font-semibold">{user.name}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full gap-3 flex-wrap">
|
<div className="flex w-full gap-3 flex-wrap">
|
||||||
{MODULE_ARRAY.map((module) => {
|
{MODULE_ARRAY.map((module) => {
|
||||||
const desiredLevel = user.desiredLevels[module] || 9;
|
const desiredLevel = user.desiredLevels[module] || 9;
|
||||||
const level = user.levels[module] || 0;
|
const level = user.levels[module] || 0;
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="border-mti-gray-anti-flash flex flex-col gap-2 rounded-xl border p-4 min-w-[250px]" key={module}>
|
||||||
className="border-mti-gray-anti-flash flex flex-col gap-2 rounded-xl border p-4 min-w-[250px]"
|
<div className="flex items-center gap-2 md:gap-3">
|
||||||
key={module}
|
<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" />}
|
||||||
<div className="flex items-center gap-2 md:gap-3">
|
{module === "listening" && <BsHeadphones className="text-ielts-listening h-4 w-4 md:h-5 md:w-5" />}
|
||||||
<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 === "writing" && <BsPen className="text-ielts-writing h-4 w-4 md:h-5 md:w-5" />}
|
||||||
{module === "reading" && (
|
{module === "speaking" && <BsMegaphone className="text-ielts-speaking h-4 w-4 md:h-5 md:w-5" />}
|
||||||
<BsBook className="text-ielts-reading 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>
|
||||||
{module === "listening" && (
|
<div className="flex w-full flex-col">
|
||||||
<BsHeadphones className="text-ielts-listening h-4 w-4 md:h-5 md:w-5" />
|
<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 === "writing" && (
|
{module === "level" && <span>English Level: {getLevelLabel(level).join(" / ")}</span>}
|
||||||
<BsPen className="text-ielts-writing h-4 w-4 md:h-5 md:w-5" />
|
{module !== "level" && (
|
||||||
)}
|
<div className="flex flex-col">
|
||||||
{module === "speaking" && (
|
<span>Level {level} / Level 9</span>
|
||||||
<BsMegaphone className="text-ielts-speaking h-4 w-4 md:h-5 md:w-5" />
|
<span>Desired Level: {desiredLevel}</span>
|
||||||
)}
|
</div>
|
||||||
{module === "level" && (
|
)}
|
||||||
<BsClipboard className="text-ielts-level h-4 w-4 md:h-5 md:w-5" />
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-col">
|
<div className="md:pl-14">
|
||||||
<span className="text-sm font-bold md:font-extrabold w-full">
|
<ProgressBar
|
||||||
{capitalize(module)}
|
color={module}
|
||||||
</span>
|
label=""
|
||||||
<div className="text-mti-gray-dim text-sm font-normal">
|
mark={Math.round((desiredLevel * 100) / 9)}
|
||||||
{module === "level" && (
|
markLabel={`Desired Level: ${desiredLevel}`}
|
||||||
<span>
|
percentage={Math.round((level * 100) / 9)}
|
||||||
English Level: {getLevelLabel(level).join(" / ")}
|
className="h-2 w-full"
|
||||||
</span>
|
/>
|
||||||
)}
|
</div>
|
||||||
{module !== "level" && (
|
</div>
|
||||||
<div className="flex flex-col">
|
);
|
||||||
<span>Level {level} / Level 9</span>
|
})}
|
||||||
<span>Desired Level: {desiredLevel}</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="md:pl-14">
|
|
||||||
<ProgressBar
|
|
||||||
color={module}
|
|
||||||
label=""
|
|
||||||
mark={Math.round((desiredLevel * 100) / 9)}
|
|
||||||
markLabel={`Desired Level: ${desiredLevel}`}
|
|
||||||
percentage={Math.round((level * 100) / 9)}
|
|
||||||
className="h-2 w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const CorporateStudentsLevels = () => {
|
const CorporateStudentsLevels = () => {
|
||||||
const { users } = useUsers();
|
const {users} = useUsers();
|
||||||
const { groups } = useGroups();
|
const {groups} = useGroups({});
|
||||||
|
|
||||||
const corporateUsers = users.filter((u) => u.type === "corporate") as User[];
|
const corporateUsers = users.filter((u) => u.type === "corporate") as User[];
|
||||||
const [corporateId, setCorporateId] = React.useState<string>("");
|
const [corporateId, setCorporateId] = React.useState<string>("");
|
||||||
const corporate =
|
const corporate = corporateUsers.find((u) => u.id === corporateId) || corporateUsers[0];
|
||||||
corporateUsers.find((u) => u.id === corporateId) || corporateUsers[0];
|
|
||||||
|
|
||||||
const groupsFromCorporate = corporate
|
const groupsFromCorporate = corporate ? groups.filter((g) => g.admin === corporate.id) : [];
|
||||||
? groups.filter((g) => g.admin === corporate.id)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const groupsParticipants = groupsFromCorporate
|
const groupsParticipants = groupsFromCorporate
|
||||||
.flatMap((g) => g.participants)
|
.flatMap((g) => g.participants)
|
||||||
.reduce((accm: User[], p) => {
|
.reduce((accm: User[], p) => {
|
||||||
const user = users.find((u) => u.id === p) as User;
|
const user = users.find((u) => u.id === p) as User;
|
||||||
if (user) {
|
if (user) {
|
||||||
return [...accm, user];
|
return [...accm, user];
|
||||||
}
|
}
|
||||||
return accm;
|
return accm;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Select
|
<Select
|
||||||
options={corporateUsers.map((x: User) => ({
|
options={corporateUsers.map((x: User) => ({
|
||||||
value: x.id,
|
value: x.id,
|
||||||
label: `${x.name} - ${x.email}`,
|
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!)}
|
onChange={(value) => setCorporateId(value?.value!)}
|
||||||
styles={{
|
styles={{
|
||||||
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
option: (styles, state) => ({
|
option: (styles, state) => ({
|
||||||
...styles,
|
...styles,
|
||||||
backgroundColor: state.isFocused
|
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||||
? "#D5D9F0"
|
color: state.isFocused ? "black" : styles.color,
|
||||||
: state.isSelected
|
}),
|
||||||
? "#7872BF"
|
}}
|
||||||
: "white",
|
/>
|
||||||
color: state.isFocused ? "black" : styles.color,
|
{groupsParticipants.map((u) => (
|
||||||
}),
|
<Card user={u} key={u.id} />
|
||||||
}}
|
))}
|
||||||
/>
|
</>
|
||||||
{groupsParticipants.map((u) => (
|
);
|
||||||
<Card user={u} key={u.id} />
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CorporateStudentsLevels;
|
export default CorporateStudentsLevels;
|
||||||
|
|||||||
@@ -2,39 +2,35 @@
|
|||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import useStats from "@/hooks/useStats";
|
import useStats from "@/hooks/useStats";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import {
|
import {CorporateUser, Group, MasterCorporateUser, Stat, User} from "@/interfaces/user";
|
||||||
Group,
|
|
||||||
MasterCorporateUser,
|
|
||||||
Stat,
|
|
||||||
User,
|
|
||||||
CorporateUser,
|
|
||||||
} from "@/interfaces/user";
|
|
||||||
import UserList from "@/pages/(admin)/Lists/UserList";
|
import UserList from "@/pages/(admin)/Lists/UserList";
|
||||||
import { dateSorter } from "@/utils";
|
import { dateSorter } from "@/utils";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
BsArrowLeft,
|
BsArrowLeft,
|
||||||
BsClipboard2Data,
|
BsClipboard2Data,
|
||||||
BsClock,
|
BsClock,
|
||||||
BsPaperclip,
|
BsPaperclip,
|
||||||
BsPersonFill,
|
BsPersonFill,
|
||||||
BsPencilSquare,
|
BsPencilSquare,
|
||||||
BsPersonCheck,
|
BsPersonCheck,
|
||||||
BsPeople,
|
BsPeople,
|
||||||
BsBank,
|
BsBank,
|
||||||
BsEnvelopePaper,
|
BsEnvelopePaper,
|
||||||
BsArrowRepeat,
|
BsArrowRepeat,
|
||||||
BsPlus,
|
BsPlus,
|
||||||
BsDatabase,
|
BsPersonFillGear,
|
||||||
|
BsFilter,
|
||||||
|
BsDatabase
|
||||||
} from "react-icons/bs";
|
} from "react-icons/bs";
|
||||||
import UserCard from "@/components/UserCard";
|
import UserCard from "@/components/UserCard";
|
||||||
import useGroups from "@/hooks/useGroups";
|
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_ARRAY} from "@/utils/moduleUtils";
|
||||||
import { Module } from "@/interfaces";
|
import {Module} from "@/interfaces";
|
||||||
import { groupByExam } from "@/utils/stats";
|
import {groupByExam} from "@/utils/stats";
|
||||||
import IconCard from "./IconCard";
|
import IconCard from "./IconCard";
|
||||||
import GroupList from "@/pages/(admin)/Lists/GroupList";
|
import GroupList from "@/pages/(admin)/Lists/GroupList";
|
||||||
import useFilterStore from "@/stores/listFilterStore";
|
import useFilterStore from "@/stores/listFilterStore";
|
||||||
@@ -46,28 +42,279 @@ import AssignmentView from "./AssignmentView";
|
|||||||
import AssignmentCreator from "./AssignmentCreator";
|
import AssignmentCreator from "./AssignmentCreator";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import AssignmentCard from "./AssignmentCard";
|
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";
|
import MasterStatistical from "./MasterStatistical";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: MasterCorporateUser;
|
user: MasterCorporateUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MasterCorporateDashboard({ user }: Props) {
|
const activeFilter = (a: Assignment) =>
|
||||||
const [page, setPage] = useState("");
|
moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment()) && a.assignees.length > a.results.length;
|
||||||
const [selectedUser, setSelectedUser] = useState<User>();
|
const pastFilter = (a: Assignment) => (moment(a.endDate).isBefore(moment()) || a.assignees.length === a.results.length) && !a.archived;
|
||||||
const [showModal, setShowModal] = useState(false);
|
const archivedFilter = (a: Assignment) => a.archived;
|
||||||
const [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
|
const futureFilter = (a: Assignment) => moment(a.startDate).isAfter(moment());
|
||||||
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
|
|
||||||
|
|
||||||
const { stats } = useStats();
|
type StudentPerformanceItem = User & {corporate?: CorporateUser; group?: Group};
|
||||||
const { users, reload } = useUsers();
|
const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPerformanceItem[]; stats: Stat[]; users: User[]; groups: Group[]}) => {
|
||||||
const { codes } = useCodes(user.id);
|
const [isShowingAmount, setIsShowingAmount] = useState(false);
|
||||||
const { groups } = useGroups(user.id, user.type);
|
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({admin: user.id, userType: user.type});
|
||||||
|
|
||||||
const masterCorporateUserGroups = [
|
const masterCorporateUserGroups = [
|
||||||
...new Set(
|
...new Set(
|
||||||
groups.filter((u) => u.admin === user.id).flatMap((g) => g.participants)
|
groups.filter((u) => u.admin === user.id).flatMap((g) => g.participants)
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
const corporateUserGroups = [
|
const corporateUserGroups = [
|
||||||
...new Set(groups.flatMap((g) => g.participants)),
|
...new Set(groups.flatMap((g) => g.participants)),
|
||||||
];
|
];
|
||||||
@@ -85,13 +332,20 @@ export default function MasterCorporateDashboard({ user }: Props) {
|
|||||||
setShowModal(!!selectedUser && page === "");
|
setShowModal(!!selectedUser && page === "");
|
||||||
}, [selectedUser, page]);
|
}, [selectedUser, page]);
|
||||||
|
|
||||||
const studentFilter = (user: User) =>
|
useEffect(() => {
|
||||||
user.type === "student" && corporateUserGroups.includes(user.id);
|
setCorporateAssignments(
|
||||||
const teacherFilter = (user: User) =>
|
assignments.filter(activeFilter).map((a) => ({
|
||||||
user.type === "teacher" && corporateUserGroups.includes(user.id);
|
...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) =>
|
const studentFilter = (user: User) => user.type === "student" && corporateUserGroups.includes(user.id);
|
||||||
stats.filter((s) => s.user === 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) => (
|
const UserDisplay = (displayUser: User) => (
|
||||||
<div
|
<div
|
||||||
@@ -210,149 +464,179 @@ export default function MasterCorporateDashboard({ user }: Props) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AssignmentsPage = () => {
|
// const AssignmentsPage = () => {
|
||||||
const activeFilter = (a: Assignment) =>
|
// const activeFilter = (a: Assignment) =>
|
||||||
moment(a.endDate).isAfter(moment()) &&
|
// moment(a.endDate).isAfter(moment()) &&
|
||||||
moment(a.startDate).isBefore(moment()) &&
|
// moment(a.startDate).isBefore(moment()) &&
|
||||||
a.assignees.length > a.results.length;
|
// a.assignees.length > a.results.length;
|
||||||
const pastFilter = (a: Assignment) =>
|
// const pastFilter = (a: Assignment) =>
|
||||||
(moment(a.endDate).isBefore(moment()) ||
|
// (moment(a.endDate).isBefore(moment()) ||
|
||||||
a.assignees.length === a.results.length) &&
|
// a.assignees.length === a.results.length) &&
|
||||||
!a.archived;
|
// !a.archived;
|
||||||
const archivedFilter = (a: Assignment) => a.archived;
|
// const archivedFilter = (a: Assignment) => a.archived;
|
||||||
const futureFilter = (a: Assignment) =>
|
// const futureFilter = (a: Assignment) =>
|
||||||
moment(a.startDate).isAfter(moment());
|
// moment(a.startDate).isAfter(moment());
|
||||||
|
|
||||||
return (
|
const StudentPerformancePage = () => {
|
||||||
<>
|
const students = users
|
||||||
<AssignmentView
|
.filter((x) => x.type === "student" && groups.flatMap((g) => g.participants).includes(x.id))
|
||||||
isOpen={!!selectedAssignment && !isCreatingAssignment}
|
.map((u) => ({
|
||||||
onClose={() => {
|
...u,
|
||||||
setSelectedAssignment(undefined);
|
group: groups.find((x) => x.participants.includes(u.id)),
|
||||||
setIsCreatingAssignment(false);
|
corporate: getCorporateUser(u, users, groups),
|
||||||
reloadAssignments();
|
}));
|
||||||
}}
|
|
||||||
assignment={selectedAssignment}
|
return (
|
||||||
/>
|
<>
|
||||||
<AssignmentCreator
|
<div className="w-full flex justify-between items-center">
|
||||||
assignment={selectedAssignment}
|
<div
|
||||||
groups={groups.filter(
|
onClick={() => setPage("")}
|
||||||
(x) => x.admin === user.id || x.participants.includes(user.id)
|
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" />
|
||||||
users={users.filter(
|
<span>Back</span>
|
||||||
(x) =>
|
</div>
|
||||||
x.type === "student" &&
|
<div
|
||||||
(!!selectedUser
|
onClick={reloadAssignments}
|
||||||
? groups
|
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
|
||||||
.filter((g) => g.admin === selectedUser.id)
|
<span>Reload</span>
|
||||||
.flatMap((g) => g.participants)
|
<BsArrowRepeat className={clsx("text-xl", isAssignmentsLoading && "animate-spin")} />
|
||||||
.includes(x.id) || false
|
</div>
|
||||||
: groups.flatMap((g) => g.participants).includes(x.id))
|
</div>
|
||||||
)}
|
<StudentPerformanceList items={students} stats={stats} users={users} groups={groups} />
|
||||||
assigner={user.id}
|
</>
|
||||||
isCreating={isCreatingAssignment}
|
);
|
||||||
cancelCreation={() => {
|
};
|
||||||
setIsCreatingAssignment(false);
|
|
||||||
setSelectedAssignment(undefined);
|
const AssignmentsPage = () => {
|
||||||
reloadAssignments();
|
return (
|
||||||
}}
|
<>
|
||||||
/>
|
<AssignmentView
|
||||||
<div className="w-full flex justify-between items-center">
|
isOpen={!!selectedAssignment && !isCreatingAssignment}
|
||||||
<div
|
onClose={() => {
|
||||||
onClick={() => setPage("")}
|
setSelectedAssignment(undefined);
|
||||||
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
|
setIsCreatingAssignment(false);
|
||||||
>
|
reloadAssignments();
|
||||||
<BsArrowLeft className="text-xl" />
|
}}
|
||||||
<span>Back</span>
|
assignment={selectedAssignment}
|
||||||
</div>
|
/>
|
||||||
<div
|
<AssignmentCreator
|
||||||
onClick={reloadAssignments}
|
assignment={selectedAssignment}
|
||||||
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
|
groups={groups.filter((x) => x.admin === user.id || x.participants.includes(user.id))}
|
||||||
>
|
users={users.filter(
|
||||||
<span>Reload</span>
|
(x) =>
|
||||||
<BsArrowRepeat
|
x.type === "student" &&
|
||||||
className={clsx(
|
(!!selectedUser
|
||||||
"text-xl",
|
? groups
|
||||||
isAssignmentsLoading && "animate-spin"
|
.filter((g) => g.admin === selectedUser.id)
|
||||||
)}
|
.flatMap((g) => g.participants)
|
||||||
/>
|
.includes(x.id) || false
|
||||||
</div>
|
: groups.flatMap((g) => g.participants).includes(x.id)),
|
||||||
</div>
|
)}
|
||||||
<section className="flex flex-col gap-4">
|
assigner={user.id}
|
||||||
<h2 className="text-2xl font-semibold">
|
isCreating={isCreatingAssignment}
|
||||||
Active Assignments ({assignments.filter(activeFilter).length})
|
cancelCreation={() => {
|
||||||
</h2>
|
setIsCreatingAssignment(false);
|
||||||
<div className="flex flex-wrap gap-2">
|
setSelectedAssignment(undefined);
|
||||||
{assignments.filter(activeFilter).map((a) => (
|
reloadAssignments();
|
||||||
<AssignmentCard
|
}}
|
||||||
{...a}
|
/>
|
||||||
onClick={() => setSelectedAssignment(a)}
|
<div className="w-full flex justify-between items-center">
|
||||||
key={a.id}
|
<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">
|
||||||
</div>
|
<BsArrowLeft className="text-xl" />
|
||||||
</section>
|
<span>Back</span>
|
||||||
<section className="flex flex-col gap-4">
|
</div>
|
||||||
<h2 className="text-2xl font-semibold">
|
<div
|
||||||
Planned Assignments ({assignments.filter(futureFilter).length})
|
onClick={reloadAssignments}
|
||||||
</h2>
|
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
|
||||||
<div className="flex flex-wrap gap-2">
|
<span>Reload</span>
|
||||||
<div
|
<BsArrowRepeat className={clsx("text-xl", isAssignmentsLoading && "animate-spin")} />
|
||||||
onClick={() => setIsCreatingAssignment(true)}
|
</div>
|
||||||
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"
|
</div>
|
||||||
>
|
<div className="flex flex-col gap-2">
|
||||||
<BsPlus className="text-6xl" />
|
<span className="text-lg font-bold">Active Assignments Status</span>
|
||||||
<span className="text-lg">New Assignment</span>
|
<div className="flex items-center gap-4">
|
||||||
</div>
|
<span>
|
||||||
{assignments.filter(futureFilter).map((a) => (
|
<b>Total:</b> {assignments.filter(activeFilter).reduce((acc, curr) => acc + curr.results.length, 0)}/
|
||||||
<AssignmentCard
|
{assignments.filter(activeFilter).reduce((acc, curr) => curr.exams.length + acc, 0)}
|
||||||
{...a}
|
</span>
|
||||||
onClick={() => {
|
{Object.keys(groupBy(corporateAssignments, (x) => x.corporate?.id)).map((x) => (
|
||||||
setSelectedAssignment(a);
|
<div key={x}>
|
||||||
setIsCreatingAssignment(true);
|
<span className="font-semibold">{getUserCompanyName(users.find((u) => u.id === x)!, users, groups)}: </span>
|
||||||
}}
|
<span>
|
||||||
key={a.id}
|
{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>
|
||||||
</section>
|
))}
|
||||||
<section className="flex flex-col gap-4">
|
</div>
|
||||||
<h2 className="text-2xl font-semibold">
|
</div>
|
||||||
Past Assignments ({assignments.filter(pastFilter).length})
|
<section className="flex flex-col gap-4">
|
||||||
</h2>
|
<h2 className="text-2xl font-semibold">Active Assignments ({assignments.filter(activeFilter).length})</h2>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{assignments.filter(pastFilter).map((a) => (
|
{assignments.filter(activeFilter).map((a) => (
|
||||||
<AssignmentCard
|
<AssignmentCard {...a} users={users} onClick={() => setSelectedAssignment(a)} key={a.id} />
|
||||||
{...a}
|
))}
|
||||||
onClick={() => setSelectedAssignment(a)}
|
</div>
|
||||||
key={a.id}
|
</section>
|
||||||
allowDownload
|
<section className="flex flex-col gap-4">
|
||||||
reload={reloadAssignments}
|
<h2 className="text-2xl font-semibold">Planned Assignments ({assignments.filter(futureFilter).length})</h2>
|
||||||
allowArchive
|
<div className="flex flex-wrap gap-2">
|
||||||
/>
|
<div
|
||||||
))}
|
onClick={() => setIsCreatingAssignment(true)}
|
||||||
</div>
|
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">
|
||||||
</section>
|
<BsPlus className="text-6xl" />
|
||||||
<section className="flex flex-col gap-4">
|
<span className="text-lg">New Assignment</span>
|
||||||
<h2 className="text-2xl font-semibold">
|
</div>
|
||||||
Archived Assignments ({assignments.filter(archivedFilter).length})
|
{assignments.filter(futureFilter).map((a) => (
|
||||||
</h2>
|
<AssignmentCard
|
||||||
<div className="flex flex-wrap gap-2">
|
{...a}
|
||||||
{assignments.filter(archivedFilter).map((a) => (
|
users={users}
|
||||||
<AssignmentCard
|
onClick={() => {
|
||||||
{...a}
|
setSelectedAssignment(a);
|
||||||
onClick={() => setSelectedAssignment(a)}
|
setIsCreatingAssignment(true);
|
||||||
key={a.id}
|
}}
|
||||||
allowDownload
|
key={a.id}
|
||||||
reload={reloadAssignments}
|
/>
|
||||||
allowUnarchive
|
))}
|
||||||
/>
|
</div>
|
||||||
))}
|
</section>
|
||||||
</div>
|
<section className="flex flex-col gap-4">
|
||||||
</section>
|
<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
|
||||||
|
reload={reloadAssignments}
|
||||||
|
allowArchive
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section className="flex flex-col gap-4">
|
||||||
|
<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
|
||||||
|
reload={reloadAssignments}
|
||||||
|
allowUnarchive
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const MasterStatisticalPage = () => {
|
const MasterStatisticalPage = () => {
|
||||||
return (
|
return (
|
||||||
@@ -381,128 +665,85 @@ export default function MasterCorporateDashboard({ user }: Props) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const averageLevelCalculator = (studentStats: Stat[]) => {
|
const DefaultDashboard = () => (
|
||||||
const formattedStats = studentStats
|
<>
|
||||||
.map((s) => ({
|
<section className="flex flex-wrap gap-2 items-center -lg:justify-center lg:justify-between text-center">
|
||||||
focus: users.find((u) => u.id === s.user)?.focus,
|
<IconCard
|
||||||
score: s.score,
|
onClick={() => setPage("students")}
|
||||||
module: s.module,
|
Icon={BsPersonFill}
|
||||||
}))
|
label="Students"
|
||||||
.filter((f) => !!f.focus);
|
value={users.filter(studentFilter).length}
|
||||||
const bandScores = formattedStats.map((s) => ({
|
color="purple"
|
||||||
module: s.module,
|
/>
|
||||||
level: calculateBandScore(
|
<IconCard
|
||||||
s.score.correct,
|
onClick={() => setPage("teachers")}
|
||||||
s.score.total,
|
Icon={BsPencilSquare}
|
||||||
s.module,
|
label="Teachers"
|
||||||
s.focus!
|
value={users.filter(teacherFilter).length}
|
||||||
),
|
color="purple"
|
||||||
}));
|
/>
|
||||||
|
<IconCard
|
||||||
const levels: { [key in Module]: number } = {
|
Icon={BsClipboard2Data}
|
||||||
reading: 0,
|
label="Exams Performed"
|
||||||
listening: 0,
|
value={stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)).length}
|
||||||
writing: 0,
|
color="purple"
|
||||||
speaking: 0,
|
/>
|
||||||
level: 0,
|
<IconCard
|
||||||
};
|
Icon={BsPaperclip}
|
||||||
bandScores.forEach((b) => (levels[b.module] += b.level));
|
label="Average Level"
|
||||||
|
value={averageLevelCalculator(
|
||||||
return calculateAverageLevel(levels);
|
users,
|
||||||
};
|
stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)),
|
||||||
|
).toFixed(1)}
|
||||||
const DefaultDashboard = () => (
|
color="purple"
|
||||||
<>
|
/>
|
||||||
<section className="flex flex-wrap gap-2 items-center -lg:justify-center lg:justify-between text-center">
|
<IconCard onClick={() => setPage("groups")} Icon={BsPeople} label="Groups" value={groups.length} color="purple" />
|
||||||
<IconCard
|
<IconCard
|
||||||
onClick={() => setPage("students")}
|
Icon={BsPersonCheck}
|
||||||
Icon={BsPersonFill}
|
label="User Balance"
|
||||||
label="Students"
|
value={`${codes.length}/${user.corporateInformation?.companyInformation?.userAmount || 0}`}
|
||||||
value={users.filter(studentFilter).length}
|
color="purple"
|
||||||
color="purple"
|
/>
|
||||||
/>
|
<IconCard
|
||||||
<IconCard
|
Icon={BsClock}
|
||||||
onClick={() => setPage("teachers")}
|
label="Expiration Date"
|
||||||
Icon={BsPencilSquare}
|
value={user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy") : "Unlimited"}
|
||||||
label="Teachers"
|
color="rose"
|
||||||
value={users.filter(teacherFilter).length}
|
/>
|
||||||
color="purple"
|
<IconCard
|
||||||
/>
|
Icon={BsBank}
|
||||||
<IconCard
|
label="Corporate"
|
||||||
Icon={BsClipboard2Data}
|
value={masterCorporateUserGroups.length}
|
||||||
label="Exams Performed"
|
color="purple"
|
||||||
value={
|
onClick={() => setPage("corporate")}
|
||||||
stats.filter((s) =>
|
/>
|
||||||
groups.flatMap((g) => g.participants).includes(s.user)
|
<IconCard
|
||||||
).length
|
Icon={BsPersonFillGear}
|
||||||
}
|
label="Student Performance"
|
||||||
color="purple"
|
value={users.filter(studentFilter).length}
|
||||||
/>
|
color="purple"
|
||||||
<IconCard
|
onClick={() => setPage("studentsPerformance")}
|
||||||
Icon={BsPaperclip}
|
/>
|
||||||
label="Average Level"
|
<IconCard
|
||||||
value={averageLevelCalculator(
|
|
||||||
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
|
|
||||||
Icon={BsPersonCheck}
|
|
||||||
label="User Balance"
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
color="rose"
|
|
||||||
/>
|
|
||||||
<IconCard
|
|
||||||
Icon={BsBank}
|
|
||||||
label="Corporate"
|
|
||||||
value={masterCorporateUserGroups.length}
|
|
||||||
color="purple"
|
|
||||||
onClick={() => setPage("corporate")}
|
|
||||||
/>
|
|
||||||
<IconCard
|
|
||||||
Icon={BsDatabase}
|
Icon={BsDatabase}
|
||||||
label="Master Statistical"
|
label="Master Statistical"
|
||||||
// value={masterCorporateUserGroups.length}
|
// value={masterCorporateUserGroups.length}
|
||||||
color="purple"
|
color="purple"
|
||||||
onClick={() => setPage("statistical")}
|
onClick={() => setPage("statistical")}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
disabled={isAssignmentsLoading}
|
disabled={isAssignmentsLoading}
|
||||||
onClick={() => setPage("assignments")}
|
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" />
|
||||||
<BsEnvelopePaper className="text-6xl text-mti-purple-light" />
|
<span className="flex flex-col gap-1 items-center text-xl">
|
||||||
<span className="flex flex-col gap-1 items-center text-xl">
|
<span className="text-lg">Assignments</span>
|
||||||
<span className="text-lg">Assignments</span>
|
<span className="font-semibold text-mti-purple-light">
|
||||||
<span className="font-semibold text-mti-purple-light">
|
{isAssignmentsLoading ? "Loading..." : assignments.filter((a) => !a.archived).length}
|
||||||
{isAssignmentsLoading
|
</span>
|
||||||
? "Loading..."
|
</span>
|
||||||
: assignments.filter((a) => !a.archived).length}
|
</button>
|
||||||
</span>
|
</section>
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
||||||
<div className="bg-white shadow flex flex-col rounded-xl w-full">
|
<div className="bg-white shadow flex flex-col rounded-xl w-full">
|
||||||
@@ -619,23 +860,24 @@ export default function MasterCorporateDashboard({ user }: Props) {
|
|||||||
.includes(x.id),
|
.includes(x.id),
|
||||||
});
|
});
|
||||||
|
|
||||||
router.push("/list/users");
|
router.push("/list/users");
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
user={selectedUser}
|
user={selectedUser}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
</Modal>
|
</Modal>
|
||||||
{page === "students" && <StudentsList />}
|
{page === "students" && <StudentsList />}
|
||||||
{page === "teachers" && <TeachersList />}
|
{page === "teachers" && <TeachersList />}
|
||||||
{page === "groups" && <GroupsList />}
|
{page === "groups" && <GroupsList />}
|
||||||
{page === "corporate" && <CorporateList />}
|
{page === "corporate" && <CorporateList />}
|
||||||
{page === "assignments" && <AssignmentsPage />}
|
{page === "assignments" && <AssignmentsPage />}
|
||||||
|
{page === "studentsPerformance" && <StudentPerformancePage />}
|
||||||
{page === "statistical" && <MasterStatisticalPage />}
|
{page === "statistical" && <MasterStatisticalPage />}
|
||||||
{page === "" && <DefaultDashboard />}
|
{page === "" && <DefaultDashboard />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -63,7 +63,7 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
|
|
||||||
const {stats} = useStats();
|
const {stats} = useStats();
|
||||||
const {users, reload} = useUsers();
|
const {users, reload} = useUsers();
|
||||||
const {groups} = useGroups(user.id);
|
const {groups} = useGroups({adminAdmins: user.id});
|
||||||
const {permissions} = usePermissions(user.id);
|
const {permissions} = usePermissions(user.id);
|
||||||
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assigner: user.id});
|
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assigner: user.id});
|
||||||
|
|
||||||
@@ -193,7 +193,7 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
? groups
|
? groups
|
||||||
.filter((g) => g.admin === selectedUser.id)
|
.filter((g) => g.admin === selectedUser.id)
|
||||||
.flatMap((g) => g.participants)
|
.flatMap((g) => g.participants)
|
||||||
.includes(x.id) || false
|
.includes(x.id)
|
||||||
: groups.flatMap((g) => g.participants).includes(x.id)),
|
: groups.flatMap((g) => g.participants).includes(x.id)),
|
||||||
)}
|
)}
|
||||||
assigner={user.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>
|
<h2 className="text-2xl font-semibold">Active Assignments ({assignments.filter(activeFilter).length})</h2>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{assignments.filter(activeFilter).map((a) => (
|
{assignments.filter(activeFilter).map((a) => (
|
||||||
<AssignmentCard {...a} onClick={() => setSelectedAssignment(a)} key={a.id} />
|
<AssignmentCard {...a} users={users} onClick={() => setSelectedAssignment(a)} key={a.id} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -238,6 +238,7 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
{assignments.filter(futureFilter).map((a) => (
|
{assignments.filter(futureFilter).map((a) => (
|
||||||
<AssignmentCard
|
<AssignmentCard
|
||||||
{...a}
|
{...a}
|
||||||
|
users={users}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedAssignment(a);
|
setSelectedAssignment(a);
|
||||||
setIsCreatingAssignment(true);
|
setIsCreatingAssignment(true);
|
||||||
@@ -253,6 +254,7 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
{assignments.filter(pastFilter).map((a) => (
|
{assignments.filter(pastFilter).map((a) => (
|
||||||
<AssignmentCard
|
<AssignmentCard
|
||||||
{...a}
|
{...a}
|
||||||
|
users={users}
|
||||||
onClick={() => setSelectedAssignment(a)}
|
onClick={() => setSelectedAssignment(a)}
|
||||||
key={a.id}
|
key={a.id}
|
||||||
allowDownload
|
allowDownload
|
||||||
@@ -268,6 +270,7 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
{assignments.filter(archivedFilter).map((a) => (
|
{assignments.filter(archivedFilter).map((a) => (
|
||||||
<AssignmentCard
|
<AssignmentCard
|
||||||
{...a}
|
{...a}
|
||||||
|
users={users}
|
||||||
onClick={() => setSelectedAssignment(a)}
|
onClick={() => setSelectedAssignment(a)}
|
||||||
key={a.id}
|
key={a.id}
|
||||||
allowDownload
|
allowDownload
|
||||||
|
|||||||
@@ -1,55 +1,191 @@
|
|||||||
import BlankQuestionsModal from "@/components/BlankQuestionsModal";
|
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 Button from "@/components/Low/Button";
|
||||||
import ModuleTitle from "@/components/Medium/ModuleTitle";
|
import ModuleTitle from "@/components/Medium/ModuleTitle";
|
||||||
import {renderSolution} from "@/components/Solutions";
|
import { renderSolution } from "@/components/Solutions";
|
||||||
import {infoButtonStyle} from "@/constants/buttonStyles";
|
import { infoButtonStyle } from "@/constants/buttonStyles";
|
||||||
import {LevelExam, LevelPart, UserSolution, WritingExam} from "@/interfaces/exam";
|
import { Module } from "@/interfaces";
|
||||||
|
import { Exercise, FillBlanksExercise, FillBlanksMCOption, LevelExam, LevelPart, MultipleChoiceExercise, ShuffleMap, UserSolution, WritingExam } from "@/interfaces/exam";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import {defaultUserSolutions} from "@/utils/exams";
|
import { defaultUserSolutions } from "@/utils/exams";
|
||||||
import {countExercises} from "@/utils/moduleUtils";
|
import { countExercises } from "@/utils/moduleUtils";
|
||||||
import {mdiArrowRight} from "@mdi/js";
|
import { mdiArrowRight } from "@mdi/js";
|
||||||
import Icon from "@mdi/react";
|
import Icon from "@mdi/react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {Fragment, useEffect, useState} from "react";
|
import { Dispatch, Fragment, SetStateAction, use, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {BsChevronDown, BsChevronUp} from "react-icons/bs";
|
import { BsChevronDown, BsChevronUp } from "react-icons/bs";
|
||||||
import {toast} from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
exam: LevelExam;
|
exam: LevelExam;
|
||||||
showSolutions?: boolean;
|
showSolutions?: boolean;
|
||||||
onFinish: (userSolutions: UserSolution[]) => void;
|
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" />
|
||||||
|
{!!part.context &&
|
||||||
|
part.context
|
||||||
|
.split(/\n|(\\n)/g)
|
||||||
|
.filter((x) => x && x.length > 0 && x !== "\\n")
|
||||||
|
.map((line, index) => (
|
||||||
|
<Fragment key={index}>
|
||||||
|
<p key={index}>{line}</p>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 w-full">
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<div className="border border-mti-gray-dim w-full rounded-full opacity-10" />
|
<div className="border border-mti-gray-dim w-full rounded-full opacity-10" />
|
||||||
{!!part.context &&
|
<div className="flex mt-2">
|
||||||
part.context
|
<div className="flex-shrink-0 w-8 pr-2">
|
||||||
.split(/\n|(\\n)/g)
|
{lineNumbers.map(num => (
|
||||||
.filter((x) => x && x.length > 0 && x !== "\\n")
|
<div key={num} className="text-gray-400 flex justify-end" style={{ lineHeight: `${lineHeight}px` }}>
|
||||||
.map((line, index) => (
|
{num}
|
||||||
<Fragment key={index}>
|
</div>
|
||||||
<p key={index}>{line}</p>
|
|
||||||
</Fragment>
|
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
|
<div ref={textRef} className="h-fit whitespace-pre-wrap ml-2">
|
||||||
|
<HighlightContent html={part.context!} highlightPhrases={highlightPhrases} firstOccurence={true} />
|
||||||
|
</div>
|
||||||
|
</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 [showBlankModal, setShowBlankModal] = useState(false);
|
||||||
|
|
||||||
const {userSolutions, setUserSolutions} = useExamStore((state) => state);
|
const { userSolutions, setUserSolutions } = useExamStore((state) => state);
|
||||||
const {hasExamEnded, setHasExamEnded} = useExamStore((state) => state);
|
const { hasExamEnded, setHasExamEnded } = useExamStore((state) => state);
|
||||||
const {partIndex, setPartIndex} = useExamStore((state) => state);
|
const { partIndex, setPartIndex } = useExamStore((state) => state);
|
||||||
const {exerciseIndex, setExerciseIndex} = useExamStore((state) => state);
|
const { exerciseIndex, setExerciseIndex } = useExamStore((state) => state);
|
||||||
const [storeQuestionIndex, setStoreQuestionIndex] = useExamStore((state) => [state.questionIndex, state.setQuestionIndex]);
|
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 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(() => {
|
useEffect(() => {
|
||||||
if (hasExamEnded && exerciseIndex === -1) {
|
if (hasExamEnded && exerciseIndex === -1) {
|
||||||
setExerciseIndex(exerciseIndex + 1);
|
setExerciseIndex(exerciseIndex + 1);
|
||||||
@@ -65,15 +201,136 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) {
|
|||||||
onFinish(userSolutions);
|
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) => {
|
const nextExercise = (solution?: UserSolution) => {
|
||||||
scrollToTop();
|
scrollToTop();
|
||||||
if (solution) {
|
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) {
|
if (storeQuestionIndex > 0) {
|
||||||
const exercise = getExercise();
|
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: storeQuestionIndex }]);
|
||||||
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== exercise.id), {id: exercise.id, amount: storeQuestionIndex}]);
|
|
||||||
}
|
}
|
||||||
setStoreQuestionIndex(0);
|
setStoreQuestionIndex(0);
|
||||||
|
|
||||||
@@ -94,6 +351,7 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) {
|
|||||||
(x) => x === 0,
|
(x) => x === 0,
|
||||||
) &&
|
) &&
|
||||||
!showSolutions &&
|
!showSolutions &&
|
||||||
|
!editing &&
|
||||||
!hasExamEnded
|
!hasExamEnded
|
||||||
) {
|
) {
|
||||||
setShowBlankModal(true);
|
setShowBlankModal(true);
|
||||||
@@ -103,7 +361,11 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) {
|
|||||||
setHasExamEnded(false);
|
setHasExamEnded(false);
|
||||||
|
|
||||||
if (solution) {
|
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 {
|
} else {
|
||||||
onFinish(userSolutions);
|
onFinish(userSolutions);
|
||||||
}
|
}
|
||||||
@@ -112,26 +374,17 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) {
|
|||||||
const previousExercise = (solution?: UserSolution) => {
|
const previousExercise = (solution?: UserSolution) => {
|
||||||
scrollToTop();
|
scrollToTop();
|
||||||
if (solution) {
|
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) {
|
if (storeQuestionIndex > 0) {
|
||||||
const exercise = getExercise();
|
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: storeQuestionIndex }]);
|
||||||
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== exercise.id), {id: exercise.id, amount: storeQuestionIndex}]);
|
|
||||||
}
|
}
|
||||||
setStoreQuestionIndex(0);
|
setStoreQuestionIndex(0);
|
||||||
|
|
||||||
setExerciseIndex(exerciseIndex - 1);
|
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 = () => {
|
const calculateExerciseIndex = () => {
|
||||||
if (partIndex === 0)
|
if (partIndex === 0)
|
||||||
return (
|
return (
|
||||||
@@ -157,7 +410,12 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) {
|
|||||||
</h4>
|
</h4>
|
||||||
<span className="text-base">You will be allowed to read the text while doing the exercises</span>
|
<span className="text-base">You will be allowed to read the text while doing the exercises</span>
|
||||||
</div>
|
</div>
|
||||||
<TextComponent part={exam.parts[partIndex]} />
|
<TextComponent
|
||||||
|
part={exam.parts[partIndex]}
|
||||||
|
highlightPhrases={highlightPhrases}
|
||||||
|
contextWord={contextWord}
|
||||||
|
setContextWordLine={setContextWordLine}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -171,7 +429,7 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) {
|
|||||||
exerciseIndex={calculateExerciseIndex()}
|
exerciseIndex={calculateExerciseIndex()}
|
||||||
module="level"
|
module="level"
|
||||||
totalExercises={countExercises(exam.parts.flatMap((x) => x.exercises))}
|
totalExercises={countExercises(exam.parts.flatMap((x) => x.exercises))}
|
||||||
disableTimer={showSolutions}
|
disableTimer={showSolutions || editing}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@@ -184,12 +442,14 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) {
|
|||||||
partIndex > -1 &&
|
partIndex > -1 &&
|
||||||
exerciseIndex < exam.parts[partIndex].exercises.length &&
|
exerciseIndex < exam.parts[partIndex].exercises.length &&
|
||||||
!showSolutions &&
|
!showSolutions &&
|
||||||
renderExercise(getExercise(), exam.id, nextExercise, previousExercise)}
|
!editing &&
|
||||||
|
currentExercise &&
|
||||||
|
renderExercise(currentExercise, exam.id, nextExercise, previousExercise)}
|
||||||
|
|
||||||
{exerciseIndex > -1 &&
|
{exerciseIndex > -1 &&
|
||||||
partIndex > -1 &&
|
partIndex > -1 &&
|
||||||
exerciseIndex < exam.parts[partIndex].exercises.length &&
|
exerciseIndex < exam.parts[partIndex].exercises.length &&
|
||||||
showSolutions &&
|
(showSolutions || editing) &&
|
||||||
renderSolution(exam.parts[partIndex].exercises[exerciseIndex], nextExercise, previousExercise)}
|
renderSolution(exam.parts[partIndex].exercises[exerciseIndex], nextExercise, previousExercise)}
|
||||||
</div>
|
</div>
|
||||||
{exerciseIndex === -1 && partIndex > 0 && (
|
{exerciseIndex === -1 && partIndex > 0 && (
|
||||||
|
|||||||
@@ -1,442 +1,310 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import { useState } from "react";
|
import {useState} from "react";
|
||||||
import { Module } from "@/interfaces";
|
import {Module} from "@/interfaces";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { User } from "@/interfaces/user";
|
import {User} from "@/interfaces/user";
|
||||||
import ProgressBar from "@/components/Low/ProgressBar";
|
import ProgressBar from "@/components/Low/ProgressBar";
|
||||||
import {
|
import {BsArrowRepeat, BsBook, BsCheck, BsCheckCircle, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle} from "react-icons/bs";
|
||||||
BsArrowRepeat,
|
import {totalExamsByModule} from "@/utils/stats";
|
||||||
BsBook,
|
|
||||||
BsCheck,
|
|
||||||
BsCheckCircle,
|
|
||||||
BsClipboard,
|
|
||||||
BsHeadphones,
|
|
||||||
BsMegaphone,
|
|
||||||
BsPen,
|
|
||||||
BsXCircle,
|
|
||||||
} from "react-icons/bs";
|
|
||||||
import { totalExamsByModule } from "@/utils/stats";
|
|
||||||
import useStats from "@/hooks/useStats";
|
import useStats from "@/hooks/useStats";
|
||||||
import Button from "@/components/Low/Button";
|
import Button from "@/components/Low/Button";
|
||||||
import { calculateAverageLevel } from "@/utils/score";
|
import {calculateAverageLevel} from "@/utils/score";
|
||||||
import { sortByModuleName } from "@/utils/moduleUtils";
|
import {sortByModuleName} from "@/utils/moduleUtils";
|
||||||
import { capitalize } from "lodash";
|
import {capitalize} from "lodash";
|
||||||
import ProfileSummary from "@/components/ProfileSummary";
|
import ProfileSummary from "@/components/ProfileSummary";
|
||||||
import { Variant } from "@/interfaces/exam";
|
import {Variant} from "@/interfaces/exam";
|
||||||
import useSessions, { Session } from "@/hooks/useSessions";
|
import useSessions, {Session} from "@/hooks/useSessions";
|
||||||
import SessionCard from "@/components/Medium/SessionCard";
|
import SessionCard from "@/components/Medium/SessionCard";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
page: "exercises" | "exams";
|
page: "exercises" | "exams";
|
||||||
onStart: (
|
onStart: (modules: Module[], avoidRepeated: boolean, variant: Variant) => void;
|
||||||
modules: Module[],
|
disableSelection?: boolean;
|
||||||
avoidRepeated: boolean,
|
|
||||||
variant: Variant,
|
|
||||||
) => void;
|
|
||||||
disableSelection?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Selection({
|
export default function Selection({user, page, onStart, disableSelection = false}: Props) {
|
||||||
user,
|
const [selectedModules, setSelectedModules] = useState<Module[]>([]);
|
||||||
page,
|
const [avoidRepeatedExams, setAvoidRepeatedExams] = useState(true);
|
||||||
onStart,
|
const [variant, setVariant] = useState<Variant>("full");
|
||||||
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 {stats} = useStats(user?.id);
|
||||||
const { sessions, isLoading, reload } = useSessions(user.id);
|
const {sessions, isLoading, reload} = useSessions(user.id);
|
||||||
|
|
||||||
const state = useExamStore((state) => state);
|
const state = useExamStore((state) => state);
|
||||||
|
|
||||||
const toggleModule = (module: Module) => {
|
const toggleModule = (module: Module) => {
|
||||||
const modules = selectedModules.filter((x) => x !== module);
|
const modules = selectedModules.filter((x) => x !== module);
|
||||||
setSelectedModules((prev) =>
|
setSelectedModules((prev) => (prev.includes(module) ? modules : [...modules, module]));
|
||||||
prev.includes(module) ? modules : [...modules, module],
|
};
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadSession = async (session: Session) => {
|
const loadSession = async (session: Session) => {
|
||||||
state.setSelectedModules(session.selectedModules);
|
state.setSelectedModules(session.selectedModules);
|
||||||
state.setExam(session.exam);
|
state.setExam(session.exam);
|
||||||
state.setExams(session.exams);
|
state.setExams(session.exams);
|
||||||
state.setSessionId(session.sessionId);
|
state.setSessionId(session.sessionId);
|
||||||
state.setAssignment(session.assignment);
|
state.setAssignment(session.assignment);
|
||||||
state.setExerciseIndex(session.exerciseIndex);
|
state.setExerciseIndex(session.exerciseIndex);
|
||||||
state.setPartIndex(session.partIndex);
|
state.setPartIndex(session.partIndex);
|
||||||
state.setModuleIndex(session.moduleIndex);
|
state.setModuleIndex(session.moduleIndex);
|
||||||
state.setTimeSpent(session.timeSpent);
|
state.setTimeSpent(session.timeSpent);
|
||||||
state.setUserSolutions(session.userSolutions);
|
state.setUserSolutions(session.userSolutions);
|
||||||
state.setShowSolutions(false);
|
state.setShowSolutions(false);
|
||||||
state.setQuestionIndex(session.questionIndex);
|
state.setQuestionIndex(session.questionIndex);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="relative flex h-full w-full flex-col gap-8 md:gap-16">
|
<div className="relative flex h-full w-full flex-col gap-8 md:gap-16">
|
||||||
{user && (
|
{user && (
|
||||||
<ProfileSummary
|
<ProfileSummary
|
||||||
user={user}
|
user={user}
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
icon: (
|
icon: <BsBook className="text-ielts-reading h-6 w-6 md:h-8 md:w-8" />,
|
||||||
<BsBook className="text-ielts-reading h-6 w-6 md:h-8 md:w-8" />
|
label: "Reading",
|
||||||
),
|
value: totalExamsByModule(stats, "reading"),
|
||||||
label: "Reading",
|
tooltip: "The amount of reading exams performed.",
|
||||||
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" />,
|
||||||
{
|
label: "Listening",
|
||||||
icon: (
|
value: totalExamsByModule(stats, "listening"),
|
||||||
<BsHeadphones className="text-ielts-listening h-6 w-6 md:h-8 md:w-8" />
|
tooltip: "The amount of listening exams performed.",
|
||||||
),
|
},
|
||||||
label: "Listening",
|
{
|
||||||
value: totalExamsByModule(stats, "listening"),
|
icon: <BsPen className="text-ielts-writing h-6 w-6 md:h-8 md:w-8" />,
|
||||||
tooltip: "The amount of listening exams performed.",
|
label: "Writing",
|
||||||
},
|
value: totalExamsByModule(stats, "writing"),
|
||||||
{
|
tooltip: "The amount of writing exams performed.",
|
||||||
icon: (
|
},
|
||||||
<BsPen className="text-ielts-writing 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: "Writing",
|
label: "Speaking",
|
||||||
value: totalExamsByModule(stats, "writing"),
|
value: totalExamsByModule(stats, "speaking"),
|
||||||
tooltip: "The amount of writing exams performed.",
|
tooltip: "The amount of speaking exams performed.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: (
|
icon: <BsClipboard className="text-ielts-level h-6 w-6 md:h-8 md:w-8" />,
|
||||||
<BsMegaphone className="text-ielts-speaking h-6 w-6 md:h-8 md:w-8" />
|
label: "Level",
|
||||||
),
|
value: totalExamsByModule(stats, "level"),
|
||||||
label: "Speaking",
|
tooltip: "The amount of level exams performed.",
|
||||||
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" />
|
|
||||||
),
|
|
||||||
label: "Level",
|
|
||||||
value: totalExamsByModule(stats, "level"),
|
|
||||||
tooltip: "The amount of level exams performed.",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<section className="flex flex-col gap-3">
|
<section className="flex flex-col gap-3">
|
||||||
<span className="text-lg font-bold">About {capitalize(page)}</span>
|
<span className="text-lg font-bold">About {capitalize(page)}</span>
|
||||||
<span className="text-mti-gray-taupe">
|
<span className="text-mti-gray-taupe">
|
||||||
{page === "exercises" && (
|
{page === "exercises" && (
|
||||||
<>
|
<>
|
||||||
In the realm of language acquisition, practice makes perfect,
|
In the realm of language acquisition, practice makes perfect, and our exercises are the key to unlocking your full
|
||||||
and our exercises are the key to unlocking your full potential.
|
potential. Dive into a world of interactive and engaging exercises that cater to diverse learning styles. From grammar
|
||||||
Dive into a world of interactive and engaging exercises that
|
drills that build a strong foundation to vocabulary challenges that broaden your lexicon, our exercises are carefully
|
||||||
cater to diverse learning styles. From grammar drills that build
|
designed to make learning English both enjoyable and effective. Whether you're looking to reinforce specific
|
||||||
a strong foundation to vocabulary challenges that broaden your
|
skills or embark on a holistic language journey, our exercises are your companions in the pursuit of excellence.
|
||||||
lexicon, our exercises are carefully designed to make learning
|
Embrace the joy of learning as you navigate through a variety of activities that cater to every facet of language
|
||||||
English both enjoyable and effective. Whether you're
|
acquisition. Your linguistic adventure starts here!
|
||||||
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
|
{page === "exams" && (
|
||||||
navigate through a variety of activities that cater to every
|
<>
|
||||||
facet of language acquisition. Your linguistic adventure starts
|
Welcome to the heart of success on your English language journey! Our exams are crafted with precision to assess and
|
||||||
here!
|
enhance your language skills. Each test is a passport to your linguistic prowess, designed to challenge and elevate
|
||||||
</>
|
your abilities. Whether you'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
|
||||||
{page === "exams" && (
|
self-discovery and language mastery as you navigate through our thoughtfully curated exams. Your success is not just a
|
||||||
<>
|
destination; it'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
|
</span>
|
||||||
linguistic prowess, designed to challenge and elevate your
|
</section>
|
||||||
abilities. Whether you'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's
|
|
||||||
a testament to your dedication and our commitment to empowering
|
|
||||||
you with the English language.
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{sessions.length > 0 && (
|
{sessions.length > 0 && (
|
||||||
<section className="flex flex-col gap-3 md:gap-3">
|
<section className="flex flex-col gap-3 md:gap-3">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div
|
<div
|
||||||
onClick={reload}
|
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"
|
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>
|
||||||
<span className="text-mti-black text-lg font-bold">
|
<BsArrowRepeat className={clsx("text-xl", isLoading && "animate-spin")} />
|
||||||
Unfinished Sessions
|
</div>
|
||||||
</span>
|
</div>
|
||||||
<BsArrowRepeat
|
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll">
|
||||||
className={clsx("text-xl", isLoading && "animate-spin")}
|
{sessions
|
||||||
/>
|
.sort((a, b) => moment(b.date).diff(moment(a.date)))
|
||||||
</div>
|
.map((session) => (
|
||||||
</div>
|
<SessionCard session={session} key={session.sessionId} reload={reload} loadSession={loadSession} />
|
||||||
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll">
|
))}
|
||||||
{sessions
|
</span>
|
||||||
.sort((a, b) => moment(b.date).diff(moment(a.date)))
|
</section>
|
||||||
.map((session) => (
|
)}
|
||||||
<SessionCard
|
|
||||||
session={session}
|
|
||||||
key={session.sessionId}
|
|
||||||
reload={reload}
|
|
||||||
loadSession={loadSession}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</span>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<section className="-lg:flex-col -lg:items-center -lg:gap-12 mt-4 flex w-full justify-between gap-8">
|
<section className="-lg:flex-col -lg:items-center -lg:gap-12 mt-4 flex w-full justify-between gap-8">
|
||||||
<div
|
<div
|
||||||
onClick={
|
onClick={!disableSelection && !selectedModules.includes("level") ? () => toggleModule("reading") : undefined}
|
||||||
!disableSelection && !selectedModules.includes("level")
|
className={clsx(
|
||||||
? () => toggleModule("reading")
|
"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",
|
||||||
: undefined
|
selectedModules.includes("reading") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
||||||
}
|
)}>
|
||||||
className={clsx(
|
<div className="bg-ielts-reading absolute top-0 flex h-16 w-16 -translate-y-1/2 items-center justify-center rounded-full">
|
||||||
"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",
|
<BsBook className="h-7 w-7 text-white" />
|
||||||
selectedModules.includes("reading") || disableSelection
|
</div>
|
||||||
? "border-mti-purple-light"
|
<span className="font-semibold">Reading:</span>
|
||||||
: "border-mti-gray-platinum",
|
<p className="text-left text-xs">
|
||||||
)}
|
Expand your vocabulary, improve your reading comprehension and improve your ability to interpret texts in English.
|
||||||
>
|
</p>
|
||||||
<div className="bg-ielts-reading absolute top-0 flex h-16 w-16 -translate-y-1/2 items-center justify-center rounded-full">
|
{!selectedModules.includes("reading") && !selectedModules.includes("level") && !disableSelection && (
|
||||||
<BsBook className="h-7 w-7 text-white" />
|
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
|
||||||
</div>
|
)}
|
||||||
<span className="font-semibold">Reading:</span>
|
{(selectedModules.includes("reading") || disableSelection) && (
|
||||||
<p className="text-left text-xs">
|
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
|
||||||
Expand your vocabulary, improve your reading comprehension and
|
)}
|
||||||
improve your ability to interpret texts in English.
|
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />}
|
||||||
</p>
|
</div>
|
||||||
{!selectedModules.includes("reading") &&
|
<div
|
||||||
!selectedModules.includes("level") &&
|
onClick={!disableSelection && !selectedModules.includes("level") ? () => toggleModule("listening") : undefined}
|
||||||
!disableSelection && (
|
className={clsx(
|
||||||
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
|
"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("reading") || disableSelection) && (
|
)}>
|
||||||
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
|
<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" />
|
||||||
{selectedModules.includes("level") && (
|
</div>
|
||||||
<BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />
|
<span className="font-semibold">Listening:</span>
|
||||||
)}
|
<p className="text-left text-xs">
|
||||||
</div>
|
Improve your ability to follow conversations in English and your ability to understand different accents and intonations.
|
||||||
<div
|
</p>
|
||||||
onClick={
|
{!selectedModules.includes("listening") && !selectedModules.includes("level") && !disableSelection && (
|
||||||
!disableSelection && !selectedModules.includes("level")
|
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
|
||||||
? () => toggleModule("listening")
|
)}
|
||||||
: undefined
|
{(selectedModules.includes("listening") || disableSelection) && (
|
||||||
}
|
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
|
||||||
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") && <BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />}
|
||||||
selectedModules.includes("listening") || disableSelection
|
</div>
|
||||||
? "border-mti-purple-light"
|
<div
|
||||||
: "border-mti-gray-platinum",
|
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",
|
||||||
<div className="bg-ielts-listening absolute top-0 flex h-16 w-16 -translate-y-1/2 items-center justify-center rounded-full">
|
selectedModules.includes("writing") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
||||||
<BsHeadphones className="h-7 w-7 text-white" />
|
)}>
|
||||||
</div>
|
<div className="bg-ielts-writing absolute top-0 flex h-16 w-16 -translate-y-1/2 items-center justify-center rounded-full">
|
||||||
<span className="font-semibold">Listening:</span>
|
<BsPen className="h-7 w-7 text-white" />
|
||||||
<p className="text-left text-xs">
|
</div>
|
||||||
Improve your ability to follow conversations in English and your
|
<span className="font-semibold">Writing:</span>
|
||||||
ability to understand different accents and intonations.
|
<p className="text-left text-xs">
|
||||||
</p>
|
Allow you to practice writing in a variety of formats, from simple paragraphs to complex essays.
|
||||||
{!selectedModules.includes("listening") &&
|
</p>
|
||||||
!selectedModules.includes("level") &&
|
{!selectedModules.includes("writing") && !selectedModules.includes("level") && !disableSelection && (
|
||||||
!disableSelection && (
|
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
|
||||||
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
|
)}
|
||||||
)}
|
{(selectedModules.includes("writing") || disableSelection) && (
|
||||||
{(selectedModules.includes("listening") || disableSelection) && (
|
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
|
||||||
<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") && (
|
</div>
|
||||||
<BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />
|
<div
|
||||||
)}
|
onClick={!disableSelection && !selectedModules.includes("level") ? () => toggleModule("speaking") : undefined}
|
||||||
</div>
|
className={clsx(
|
||||||
<div
|
"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",
|
||||||
onClick={
|
selectedModules.includes("speaking") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
||||||
!disableSelection && !selectedModules.includes("level")
|
)}>
|
||||||
? () => toggleModule("writing")
|
<div className="bg-ielts-speaking absolute top-0 flex h-16 w-16 -translate-y-1/2 items-center justify-center rounded-full">
|
||||||
: undefined
|
<BsMegaphone className="h-7 w-7 text-white" />
|
||||||
}
|
</div>
|
||||||
className={clsx(
|
<span className="font-semibold">Speaking:</span>
|
||||||
"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",
|
<p className="text-left text-xs">
|
||||||
selectedModules.includes("writing") || disableSelection
|
You'll have access to interactive dialogs, pronunciation exercises and speech recordings.
|
||||||
? "border-mti-purple-light"
|
</p>
|
||||||
: "border-mti-gray-platinum",
|
{!selectedModules.includes("speaking") && !selectedModules.includes("level") && !disableSelection && (
|
||||||
)}
|
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
|
||||||
>
|
)}
|
||||||
<div className="bg-ielts-writing absolute top-0 flex h-16 w-16 -translate-y-1/2 items-center justify-center rounded-full">
|
{(selectedModules.includes("speaking") || disableSelection) && (
|
||||||
<BsPen className="h-7 w-7 text-white" />
|
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
|
||||||
</div>
|
)}
|
||||||
<span className="font-semibold">Writing:</span>
|
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />}
|
||||||
<p className="text-left text-xs">
|
</div>
|
||||||
Allow you to practice writing in a variety of formats, from simple
|
{!disableSelection && (
|
||||||
paragraphs to complex essays.
|
<div
|
||||||
</p>
|
onClick={selectedModules.length === 0 || selectedModules.includes("level") ? () => toggleModule("level") : undefined}
|
||||||
{!selectedModules.includes("writing") &&
|
className={clsx(
|
||||||
!selectedModules.includes("level") &&
|
"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",
|
||||||
!disableSelection && (
|
selectedModules.includes("level") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
||||||
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
|
)}>
|
||||||
)}
|
<div className="bg-ielts-level absolute top-0 flex h-16 w-16 -translate-y-1/2 items-center justify-center rounded-full">
|
||||||
{(selectedModules.includes("writing") || disableSelection) && (
|
<BsClipboard className="h-7 w-7 text-white" />
|
||||||
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
|
</div>
|
||||||
)}
|
<span className="font-semibold">Level:</span>
|
||||||
{selectedModules.includes("level") && (
|
<p className="text-left text-xs">You'll be able to test your english level with multiple choice questions.</p>
|
||||||
<BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />
|
{!selectedModules.includes("level") && selectedModules.length === 0 && !disableSelection && (
|
||||||
)}
|
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
|
||||||
</div>
|
)}
|
||||||
<div
|
{(selectedModules.includes("level") || disableSelection) && (
|
||||||
onClick={
|
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
|
||||||
!disableSelection && !selectedModules.includes("level")
|
)}
|
||||||
? () => toggleModule("speaking")
|
{!selectedModules.includes("level") && selectedModules.length > 0 && (
|
||||||
: undefined
|
<BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />
|
||||||
}
|
)}
|
||||||
className={clsx(
|
</div>
|
||||||
"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
|
</section>
|
||||||
? "border-mti-purple-light"
|
<div className="-md:flex-col -md:gap-4 -md:justify-center flex w-full items-center md:justify-between">
|
||||||
: "border-mti-gray-platinum",
|
<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"
|
||||||
<div className="bg-ielts-speaking absolute top-0 flex h-16 w-16 -translate-y-1/2 items-center justify-center rounded-full">
|
onClick={() => setAvoidRepeatedExams((prev) => !prev)}>
|
||||||
<BsMegaphone className="h-7 w-7 text-white" />
|
<input type="checkbox" className="hidden" />
|
||||||
</div>
|
<div
|
||||||
<span className="font-semibold">Speaking:</span>
|
className={clsx(
|
||||||
<p className="text-left text-xs">
|
"border-mti-purple-light flex h-6 w-6 items-center justify-center rounded-md border bg-white",
|
||||||
You'll have access to interactive dialogs, pronunciation
|
"transition duration-300 ease-in-out",
|
||||||
exercises and speech recordings.
|
avoidRepeatedExams && "!bg-mti-purple-light ",
|
||||||
</p>
|
)}>
|
||||||
{!selectedModules.includes("speaking") &&
|
<BsCheck color="white" className="h-full w-full" />
|
||||||
!selectedModules.includes("level") &&
|
</div>
|
||||||
!disableSelection && (
|
<span className="tooltip" data-tip="If possible, the platform will choose exams not yet done.">
|
||||||
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
|
Avoid Repeated Questions
|
||||||
)}
|
</span>
|
||||||
{(selectedModules.includes("speaking") || disableSelection) && (
|
</div>
|
||||||
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
|
<div
|
||||||
)}
|
className="text-mti-gray-dim -md:justify-center flex w-full cursor-pointer items-center gap-3 text-sm"
|
||||||
{selectedModules.includes("level") && (
|
onClick={() => setVariant((prev) => (prev === "full" ? "partial" : "full"))}>
|
||||||
<BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />
|
<input type="checkbox" className="hidden" />
|
||||||
)}
|
<div
|
||||||
</div>
|
className={clsx(
|
||||||
{!disableSelection && (
|
"border-mti-purple-light flex h-6 w-6 items-center justify-center rounded-md border bg-white",
|
||||||
<div
|
"transition duration-300 ease-in-out",
|
||||||
onClick={
|
variant === "full" && "!bg-mti-purple-light ",
|
||||||
selectedModules.length === 0 ||
|
)}>
|
||||||
selectedModules.includes("level")
|
<BsCheck color="white" className="h-full w-full" />
|
||||||
? () => toggleModule("level")
|
</div>
|
||||||
: undefined
|
<span>Full length exams</span>
|
||||||
}
|
</div>
|
||||||
className={clsx(
|
</div>
|
||||||
"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",
|
<div className="tooltip w-full" data-tip={`Your screen size is too small to do ${page}`}>
|
||||||
selectedModules.includes("level") || disableSelection
|
<Button color="purple" className="w-full max-w-xs px-12 md:hidden" disabled>
|
||||||
? "border-mti-purple-light"
|
Start Exam
|
||||||
: "border-mti-gray-platinum",
|
</Button>
|
||||||
)}
|
</div>
|
||||||
>
|
<Button
|
||||||
<div className="bg-ielts-level absolute top-0 flex h-16 w-16 -translate-y-1/2 items-center justify-center rounded-full">
|
onClick={() =>
|
||||||
<BsClipboard className="h-7 w-7 text-white" />
|
onStart(
|
||||||
</div>
|
!disableSelection ? selectedModules.sort(sortByModuleName) : ["reading", "listening", "writing", "speaking"],
|
||||||
<span className="font-semibold">Level:</span>
|
avoidRepeatedExams,
|
||||||
<p className="text-left text-xs">
|
variant,
|
||||||
You'll be able to test your english level with multiple
|
)
|
||||||
choice questions.
|
}
|
||||||
</p>
|
color="purple"
|
||||||
{!selectedModules.includes("level") &&
|
className="-md:hidden w-full max-w-xs px-12 md:self-end"
|
||||||
selectedModules.length === 0 &&
|
disabled={selectedModules.length === 0 && !disableSelection}>
|
||||||
!disableSelection && (
|
Start Exam
|
||||||
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
|
</Button>
|
||||||
)}
|
</div>
|
||||||
{(selectedModules.includes("level") || disableSelection) && (
|
</div>
|
||||||
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
|
</>
|
||||||
)}
|
);
|
||||||
{!selectedModules.includes("level") &&
|
|
||||||
selectedModules.length > 0 && (
|
|
||||||
<BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
<div className="-md:flex-col -md:gap-4 -md:justify-center flex w-full items-center md:justify-between">
|
|
||||||
<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)}
|
|
||||||
>
|
|
||||||
<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."
|
|
||||||
>
|
|
||||||
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 />
|
|
||||||
<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
|
|
||||||
>
|
|
||||||
Start Exam
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
onClick={() =>
|
|
||||||
onStart(
|
|
||||||
!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}
|
|
||||||
>
|
|
||||||
Start Exam
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,32 +2,40 @@ import {Group, User} from "@/interfaces/user";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {useEffect, useState} from "react";
|
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 [groups, setGroups] = useState<Group[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isError, setIsError] = useState(false);
|
const [isError, setIsError] = useState(false);
|
||||||
|
|
||||||
const isMasterType = userType?.startsWith('master');
|
const isMasterType = userType?.startsWith("master");
|
||||||
|
|
||||||
const getData = () => {
|
const getData = () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const url = admin ? `/api/groups?admin=${admin}` : "/api/groups";
|
const url = admin && !adminAdmins ? `/api/groups?admin=${admin}` : "/api/groups";
|
||||||
axios
|
axios
|
||||||
.get<Group[]>(url)
|
.get<Group[]>(url)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if(isMasterType) {
|
if (isMasterType) return setGroups(response.data);
|
||||||
return setGroups(response.data);
|
|
||||||
}
|
|
||||||
const filter = (g: Group) => g.admin === admin || g.participants.includes(admin || "");
|
|
||||||
|
|
||||||
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);
|
return setGroups(admin ? filteredGroups.map((g) => ({...g, disableEditing: g.disableEditing || g.admin !== admin})) : filteredGroups);
|
||||||
})
|
})
|
||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(getData, [admin, isMasterType]);
|
useEffect(getData, [admin, adminAdmins, isMasterType]);
|
||||||
|
|
||||||
return {groups, isLoading, isError, reload: getData};
|
return {groups, isLoading, isError, reload: getData};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export default function usePermissions(user: string) {
|
|||||||
const permissionTypes = response.data
|
const permissionTypes = response.data
|
||||||
.filter((x) => !x.users.includes(user))
|
.filter((x) => !x.users.includes(user))
|
||||||
.reduce((acc, curr) => [...acc, curr.type], [] as PermissionType[]);
|
.reduce((acc, curr) => [...acc, curr.type], [] as PermissionType[]);
|
||||||
console.log(response.data, permissionTypes);
|
|
||||||
setPermissions(permissionTypes);
|
setPermissions(permissionTypes);
|
||||||
})
|
})
|
||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ interface ExamBase {
|
|||||||
isDiagnostic: boolean;
|
isDiagnostic: boolean;
|
||||||
variant?: Variant;
|
variant?: Variant;
|
||||||
difficulty?: Difficulty;
|
difficulty?: Difficulty;
|
||||||
|
shuffle?: boolean;
|
||||||
createdBy?: string; // option as it has been added later
|
createdBy?: string; // option as it has been added later
|
||||||
createdAt?: 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 {
|
export interface LevelPart {
|
||||||
context?: string;
|
context?: string;
|
||||||
|
showContextLines?: boolean;
|
||||||
exercises: Exercise[];
|
exercises: Exercise[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +67,7 @@ export interface UserSolution {
|
|||||||
};
|
};
|
||||||
exercise: string;
|
exercise: string;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
|
shuffleMaps?: ShuffleMap[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WritingExam extends ExamBase {
|
export interface WritingExam extends ExamBase {
|
||||||
@@ -77,7 +80,7 @@ interface WordCounter {
|
|||||||
limit: number;
|
limit: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SpeakingExam extends ExamBase {
|
export interface SpeakingExam extends ExamBase {
|
||||||
module: "speaking";
|
module: "speaking";
|
||||||
exercises: (SpeakingExercise | InteractiveSpeakingExercise)[];
|
exercises: (SpeakingExercise | InteractiveSpeakingExercise)[];
|
||||||
instructorGender: InstructorGender;
|
instructorGender: InstructorGender;
|
||||||
@@ -96,8 +99,8 @@ export type Exercise =
|
|||||||
export interface Evaluation {
|
export interface Evaluation {
|
||||||
comment: string;
|
comment: string;
|
||||||
overall: number;
|
overall: number;
|
||||||
task_response: {[key: string]: number | {grade: number; comment: string}};
|
task_response: { [key: string]: number | { grade: number; comment: string } };
|
||||||
misspelled_pairs?: {correction: string | null; misspelled: string}[];
|
misspelled_pairs?: { correction: string | null; misspelled: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -110,10 +113,9 @@ type InteractiveTranscriptType = { [key in InteractiveTranscriptKey]?: string };
|
|||||||
type InteractiveFixedTextType = { [key in InteractiveFixedTextKey]?: string };
|
type InteractiveFixedTextType = { [key in InteractiveFixedTextKey]?: string };
|
||||||
|
|
||||||
interface InteractiveSpeakingEvaluation extends Evaluation,
|
interface InteractiveSpeakingEvaluation extends Evaluation,
|
||||||
InteractivePerfectAnswerType,
|
InteractivePerfectAnswerType,
|
||||||
InteractiveTranscriptType,
|
InteractiveTranscriptType,
|
||||||
InteractiveFixedTextType
|
InteractiveFixedTextType { }
|
||||||
{}
|
|
||||||
|
|
||||||
|
|
||||||
interface SpeakingEvaluation extends CommonEvaluation {
|
interface SpeakingEvaluation extends CommonEvaluation {
|
||||||
@@ -187,10 +189,10 @@ export interface InteractiveSpeakingExercise {
|
|||||||
first_title?: string;
|
first_title?: string;
|
||||||
second_title?: string;
|
second_title?: string;
|
||||||
text: string;
|
text: string;
|
||||||
prompts: {text: string; video_url: string}[];
|
prompts: { text: string; video_url: string }[];
|
||||||
userSolutions: {
|
userSolutions: {
|
||||||
id: string;
|
id: string;
|
||||||
solution: {questionIndex: number; question: string; answer: string}[];
|
solution: { questionIndex: number; question: string; answer: string }[];
|
||||||
evaluation?: InteractiveSpeakingEvaluation;
|
evaluation?: InteractiveSpeakingEvaluation;
|
||||||
}[];
|
}[];
|
||||||
topic?: string;
|
topic?: string;
|
||||||
@@ -199,13 +201,23 @@ export interface InteractiveSpeakingExercise {
|
|||||||
variant?: "initial" | "final";
|
variant?: "initial" | "final";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FillBlanksMCOption {
|
||||||
|
id: string;
|
||||||
|
options: {
|
||||||
|
A: string;
|
||||||
|
B: string;
|
||||||
|
C: string;
|
||||||
|
D: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface FillBlanksExercise {
|
export interface FillBlanksExercise {
|
||||||
prompt: string; // *EXAMPLE: "Complete the summary below. Click a blank to select the corresponding word for it."
|
prompt: string; // *EXAMPLE: "Complete the summary below. Click a blank to select the corresponding word for it."
|
||||||
type: "fillBlanks";
|
type: "fillBlanks";
|
||||||
id: string;
|
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"
|
text: string; // *EXAMPLE: "They tried to {{1}} burning"
|
||||||
allowRepetition: boolean;
|
allowRepetition?: boolean;
|
||||||
solutions: {
|
solutions: {
|
||||||
id: string; // *EXAMPLE: "1"
|
id: string; // *EXAMPLE: "1"
|
||||||
solution: string; // *EXAMPLE: "preserve"
|
solution: string; // *EXAMPLE: "preserve"
|
||||||
@@ -214,6 +226,7 @@ export interface FillBlanksExercise {
|
|||||||
id: string; // *EXAMPLE: "1"
|
id: string; // *EXAMPLE: "1"
|
||||||
solution: string; // *EXAMPLE: "preserve"
|
solution: string; // *EXAMPLE: "preserve"
|
||||||
}[];
|
}[];
|
||||||
|
variant?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TrueFalseExercise {
|
export interface TrueFalseExercise {
|
||||||
@@ -221,7 +234,7 @@ export interface TrueFalseExercise {
|
|||||||
id: string;
|
id: string;
|
||||||
prompt: string; // *EXAMPLE: "Select the appropriate option."
|
prompt: string; // *EXAMPLE: "Select the appropriate option."
|
||||||
questions: TrueFalseQuestion[];
|
questions: TrueFalseQuestion[];
|
||||||
userSolutions: {id: string; solution: "true" | "false" | "not_given"}[];
|
userSolutions: { id: string; solution: "true" | "false" | "not_given" }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TrueFalseQuestion {
|
export interface TrueFalseQuestion {
|
||||||
@@ -250,7 +263,7 @@ export interface MatchSentencesExercise {
|
|||||||
type: "matchSentences";
|
type: "matchSentences";
|
||||||
id: string;
|
id: string;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
userSolutions: {question: string; option: string}[];
|
userSolutions: { question: string; option: string }[];
|
||||||
sentences: MatchSentenceExerciseSentence[];
|
sentences: MatchSentenceExerciseSentence[];
|
||||||
allowRepetition: boolean;
|
allowRepetition: boolean;
|
||||||
options: MatchSentenceExerciseOption[];
|
options: MatchSentenceExerciseOption[];
|
||||||
@@ -273,7 +286,7 @@ export interface MultipleChoiceExercise {
|
|||||||
id: string;
|
id: string;
|
||||||
prompt: string; // *EXAMPLE: "Select the appropriate option."
|
prompt: string; // *EXAMPLE: "Select the appropriate option."
|
||||||
questions: MultipleChoiceQuestion[];
|
questions: MultipleChoiceQuestion[];
|
||||||
userSolutions: {question: string; option: string}[];
|
userSolutions: { question: string; option: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MultipleChoiceQuestion {
|
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")
|
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")
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,189 +1,162 @@
|
|||||||
import { Module } from ".";
|
import {Module} from ".";
|
||||||
import { InstructorGender } from "./exam";
|
import {InstructorGender, ShuffleMap} from "./exam";
|
||||||
import { PermissionType } from "./permissions";
|
import {PermissionType} from "./permissions";
|
||||||
|
|
||||||
export type User =
|
export type User = StudentUser | TeacherUser | CorporateUser | AgentUser | AdminUser | DeveloperUser | MasterCorporateUser;
|
||||||
| StudentUser
|
|
||||||
| TeacherUser
|
|
||||||
| CorporateUser
|
|
||||||
| AgentUser
|
|
||||||
| AdminUser
|
|
||||||
| DeveloperUser
|
|
||||||
| MasterCorporateUser;
|
|
||||||
export type UserStatus = "active" | "disabled" | "paymentDue";
|
export type UserStatus = "active" | "disabled" | "paymentDue";
|
||||||
|
|
||||||
export interface BasicUser {
|
export interface BasicUser {
|
||||||
email: string;
|
email: string;
|
||||||
name: string;
|
name: string;
|
||||||
profilePicture: string;
|
profilePicture: string;
|
||||||
id: string;
|
id: string;
|
||||||
isFirstLogin: boolean;
|
isFirstLogin: boolean;
|
||||||
focus: "academic" | "general";
|
focus: "academic" | "general";
|
||||||
levels: { [key in Module]: number };
|
levels: {[key in Module]: number};
|
||||||
desiredLevels: { [key in Module]: number };
|
desiredLevels: {[key in Module]: number};
|
||||||
type: Type;
|
type: Type;
|
||||||
bio: string;
|
bio: string;
|
||||||
isVerified: boolean;
|
isVerified: boolean;
|
||||||
subscriptionExpirationDate?: null | Date;
|
subscriptionExpirationDate?: null | Date;
|
||||||
registrationDate?: Date;
|
registrationDate?: Date;
|
||||||
status: UserStatus;
|
status: UserStatus;
|
||||||
permissions: PermissionType[],
|
permissions: PermissionType[];
|
||||||
|
lastLogin?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StudentUser extends BasicUser {
|
export interface StudentUser extends BasicUser {
|
||||||
type: "student";
|
type: "student";
|
||||||
preferredGender?: InstructorGender;
|
preferredGender?: InstructorGender;
|
||||||
demographicInformation?: DemographicInformation;
|
demographicInformation?: DemographicInformation;
|
||||||
preferredTopics?: string[];
|
preferredTopics?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TeacherUser extends BasicUser {
|
export interface TeacherUser extends BasicUser {
|
||||||
type: "teacher";
|
type: "teacher";
|
||||||
demographicInformation?: DemographicInformation;
|
demographicInformation?: DemographicInformation;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CorporateUser extends BasicUser {
|
export interface CorporateUser extends BasicUser {
|
||||||
type: "corporate";
|
type: "corporate";
|
||||||
corporateInformation: CorporateInformation;
|
corporateInformation: CorporateInformation;
|
||||||
demographicInformation?: DemographicCorporateInformation;
|
demographicInformation?: DemographicCorporateInformation;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MasterCorporateUser extends BasicUser {
|
export interface MasterCorporateUser extends BasicUser {
|
||||||
type: "mastercorporate";
|
type: "mastercorporate";
|
||||||
corporateInformation: CorporateInformation;
|
corporateInformation: CorporateInformation;
|
||||||
demographicInformation?: DemographicCorporateInformation;
|
demographicInformation?: DemographicCorporateInformation;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentUser extends BasicUser {
|
export interface AgentUser extends BasicUser {
|
||||||
type: "agent";
|
type: "agent";
|
||||||
agentInformation: AgentInformation;
|
agentInformation: AgentInformation;
|
||||||
demographicInformation?: DemographicInformation;
|
demographicInformation?: DemographicInformation;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminUser extends BasicUser {
|
export interface AdminUser extends BasicUser {
|
||||||
type: "admin";
|
type: "admin";
|
||||||
demographicInformation?: DemographicInformation;
|
demographicInformation?: DemographicInformation;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeveloperUser extends BasicUser {
|
export interface DeveloperUser extends BasicUser {
|
||||||
type: "developer";
|
type: "developer";
|
||||||
preferredGender?: InstructorGender;
|
preferredGender?: InstructorGender;
|
||||||
demographicInformation?: DemographicInformation;
|
demographicInformation?: DemographicInformation;
|
||||||
preferredTopics?: string[];
|
preferredTopics?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CorporateInformation {
|
export interface CorporateInformation {
|
||||||
companyInformation: CompanyInformation;
|
companyInformation: CompanyInformation;
|
||||||
monthlyDuration: number;
|
monthlyDuration: number;
|
||||||
payment?: {
|
payment?: {
|
||||||
value: number;
|
value: number;
|
||||||
currency: string;
|
currency: string;
|
||||||
commission: number;
|
commission: number;
|
||||||
};
|
};
|
||||||
referralAgent?: string;
|
referralAgent?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentInformation {
|
export interface AgentInformation {
|
||||||
companyName: string;
|
companyName: string;
|
||||||
commercialRegistration: string;
|
commercialRegistration: string;
|
||||||
companyArabName?: string;
|
companyArabName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CompanyInformation {
|
export interface CompanyInformation {
|
||||||
name: string;
|
name: string;
|
||||||
userAmount: number;
|
userAmount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DemographicInformation {
|
export interface DemographicInformation {
|
||||||
country: string;
|
country: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
gender: Gender;
|
gender: Gender;
|
||||||
employment: EmploymentStatus;
|
employment: EmploymentStatus;
|
||||||
passport_id?: string;
|
passport_id?: string;
|
||||||
timezone?: string;
|
timezone?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DemographicCorporateInformation {
|
export interface DemographicCorporateInformation {
|
||||||
country: string;
|
country: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
gender: Gender;
|
gender: Gender;
|
||||||
position: string;
|
position: string;
|
||||||
timezone?: string;
|
timezone?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Gender = "male" | "female" | "other";
|
export type Gender = "male" | "female" | "other";
|
||||||
export type EmploymentStatus =
|
export type EmploymentStatus = "employed" | "student" | "self-employed" | "unemployed" | "retired" | "other";
|
||||||
| "employed"
|
export const EMPLOYMENT_STATUS: {status: EmploymentStatus; label: string}[] = [
|
||||||
| "student"
|
{status: "student", label: "Student"},
|
||||||
| "self-employed"
|
{status: "employed", label: "Employed"},
|
||||||
| "unemployed"
|
{status: "unemployed", label: "Unemployed"},
|
||||||
| "retired"
|
{status: "self-employed", label: "Self-employed"},
|
||||||
| "other";
|
{status: "retired", label: "Retired"},
|
||||||
export const EMPLOYMENT_STATUS: { status: EmploymentStatus; label: string }[] =
|
{status: "other", label: "Other"},
|
||||||
[
|
];
|
||||||
{ 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 {
|
export interface Stat {
|
||||||
id: string;
|
id: string;
|
||||||
user: string;
|
user: string;
|
||||||
exam: string;
|
exam: string;
|
||||||
exercise: string;
|
exercise: string;
|
||||||
session: string;
|
session: string;
|
||||||
date: number;
|
date: number;
|
||||||
module: Module;
|
module: Module;
|
||||||
solutions: any[];
|
solutions: any[];
|
||||||
type: string;
|
type: string;
|
||||||
timeSpent?: number;
|
timeSpent?: number;
|
||||||
inactivity?: number;
|
inactivity?: number;
|
||||||
assignment?: string;
|
assignment?: string;
|
||||||
score: {
|
score: {
|
||||||
correct: number;
|
correct: number;
|
||||||
total: number;
|
total: number;
|
||||||
missing: number;
|
missing: number;
|
||||||
};
|
};
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
|
shuffleMaps?: ShuffleMap[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Group {
|
export interface Group {
|
||||||
admin: string;
|
admin: string;
|
||||||
name: string;
|
name: string;
|
||||||
participants: string[];
|
participants: string[];
|
||||||
id: string;
|
id: string;
|
||||||
disableEditing?: boolean;
|
disableEditing?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Code {
|
export interface Code {
|
||||||
code: string;
|
code: string;
|
||||||
creator: string;
|
creator: string;
|
||||||
expiryDate: Date;
|
expiryDate: Date;
|
||||||
type: Type;
|
type: Type;
|
||||||
creationDate?: string;
|
creationDate?: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
passport_id?: string;
|
passport_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Type =
|
export type Type = "student" | "teacher" | "corporate" | "admin" | "developer" | "agent" | "mastercorporate";
|
||||||
| "student"
|
export const userTypes: Type[] = ["student", "teacher", "corporate", "admin", "developer", "agent", "mastercorporate"];
|
||||||
| "teacher"
|
|
||||||
| "corporate"
|
|
||||||
| "admin"
|
|
||||||
| "developer"
|
|
||||||
| "agent"
|
|
||||||
| "mastercorporate";
|
|
||||||
export const userTypes: Type[] = [
|
|
||||||
"student",
|
|
||||||
"teacher",
|
|
||||||
"corporate",
|
|
||||||
"admin",
|
|
||||||
"developer",
|
|
||||||
"agent",
|
|
||||||
"mastercorporate",
|
|
||||||
];
|
|
||||||
|
|||||||
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal 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))
|
||||||
|
}
|
||||||
@@ -111,7 +111,6 @@ export default function BatchCreateUser({user}: {user: User}) {
|
|||||||
return clear();
|
return clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(information);
|
|
||||||
setInfos(information);
|
setInfos(information);
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(
|
toast.error(
|
||||||
|
|||||||
@@ -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}`,
|
label: `${users.find((y) => y.id === x)?.email} - ${users.find((y) => y.id === x)?.name}`,
|
||||||
}))}
|
}))}
|
||||||
options={users
|
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}`}))}
|
.map((x) => ({value: x.id, label: `${x.email} - ${x.name}`}))}
|
||||||
onChange={(value) => setParticipants(value.map((x) => x.value))}
|
onChange={(value) => setParticipants(value.map((x) => x.value))}
|
||||||
isMulti
|
isMulti
|
||||||
@@ -201,7 +207,11 @@ export default function GroupList({user}: {user: User}) {
|
|||||||
const {permissions} = usePermissions(user?.id || "");
|
const {permissions} = usePermissions(user?.id || "");
|
||||||
|
|
||||||
const {users} = useUsers();
|
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(() => {
|
useEffect(() => {
|
||||||
if (user && ["corporate", "teacher", "mastercorporate"].includes(user.type)) {
|
if (user && ["corporate", "teacher", "mastercorporate"].includes(user.type)) {
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export default function UserList({
|
|||||||
|
|
||||||
const {users, reload} = useUsers();
|
const {users, reload} = useUsers();
|
||||||
const {permissions} = usePermissions(user?.id || "");
|
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 appendUserFilters = useFilterStore((state) => state.appendUserFilter);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -262,10 +262,12 @@ export default function UserList({
|
|||||||
cell: ({row, getValue}) => (
|
cell: ({row, getValue}) => (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
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",
|
"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()}
|
{getValue()}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -279,10 +281,10 @@ export default function UserList({
|
|||||||
) as any,
|
) as any,
|
||||||
cell: (info) =>
|
cell: (info) =>
|
||||||
info.getValue()
|
info.getValue()
|
||||||
? `${countryCodes.findOne("countryCode" as any, info.getValue()).flag} ${
|
? `${countryCodes.findOne("countryCode" as any, info.getValue())?.flag} ${
|
||||||
countries[info.getValue() as unknown as keyof TCountries].name
|
countries[info.getValue() as unknown as keyof TCountries]?.name
|
||||||
} (+${countryCodes.findOne("countryCode" as any, info.getValue()).countryCallingCode})`
|
} (+${countryCodes.findOne("countryCode" as any, info.getValue())?.countryCallingCode})`
|
||||||
: "Not available",
|
: "N/A",
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("demographicInformation.phone", {
|
columnHelper.accessor("demographicInformation.phone", {
|
||||||
header: (
|
header: (
|
||||||
@@ -291,7 +293,7 @@ export default function UserList({
|
|||||||
<SorterArrow name="phone" />
|
<SorterArrow name="phone" />
|
||||||
</button>
|
</button>
|
||||||
) as any,
|
) as any,
|
||||||
cell: (info) => info.getValue() || "Not available",
|
cell: (info) => info.getValue() || "N/A",
|
||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor(
|
columnHelper.accessor(
|
||||||
@@ -301,14 +303,23 @@ export default function UserList({
|
|||||||
id: "employment",
|
id: "employment",
|
||||||
header: (
|
header: (
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "employment"))}>
|
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "employment"))}>
|
||||||
<span>Employment/Position</span>
|
<span>Employment</span>
|
||||||
<SorterArrow name="employment" />
|
<SorterArrow name="employment" />
|
||||||
</button>
|
</button>
|
||||||
) as any,
|
) 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,
|
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", {
|
columnHelper.accessor("demographicInformation.gender", {
|
||||||
header: (
|
header: (
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "gender"))}>
|
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "gender"))}>
|
||||||
@@ -316,7 +327,7 @@ export default function UserList({
|
|||||||
<SorterArrow name="gender" />
|
<SorterArrow name="gender" />
|
||||||
</button>
|
</button>
|
||||||
) as any,
|
) as any,
|
||||||
cell: (info) => capitalize(info.getValue()) || "Not available",
|
cell: (info) => capitalize(info.getValue()) || "N/A",
|
||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
@@ -341,11 +352,13 @@ export default function UserList({
|
|||||||
cell: ({row, getValue}) => (
|
cell: ({row, getValue}) => (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
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",
|
"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={() =>
|
||||||
{row.original.type === "corporate" ? row.original.corporateInformation?.companyInformation?.name || getValue() : getValue()}
|
checkAccess(user, ["admin", "corporate", "developer", "mastercorporate"]) ? setSelectedUser(row.original) : null
|
||||||
|
}>
|
||||||
|
{getValue()}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
@@ -379,7 +392,7 @@ export default function UserList({
|
|||||||
columnHelper.accessor("corporateInformation.companyInformation.name", {
|
columnHelper.accessor("corporateInformation.companyInformation.name", {
|
||||||
header: (
|
header: (
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "companyName"))}>
|
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "companyName"))}>
|
||||||
<span>Company Name</span>
|
<span>Company</span>
|
||||||
<SorterArrow name="companyName" />
|
<SorterArrow name="companyName" />
|
||||||
</button>
|
</button>
|
||||||
) as any,
|
) as any,
|
||||||
@@ -388,7 +401,7 @@ export default function UserList({
|
|||||||
columnHelper.accessor("subscriptionExpirationDate", {
|
columnHelper.accessor("subscriptionExpirationDate", {
|
||||||
header: (
|
header: (
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "expiryDate"))}>
|
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "expiryDate"))}>
|
||||||
<span>Expiry Date</span>
|
<span>Expiration</span>
|
||||||
<SorterArrow name="expiryDate" />
|
<SorterArrow name="expiryDate" />
|
||||||
</button>
|
</button>
|
||||||
) as any,
|
) as any,
|
||||||
@@ -401,7 +414,7 @@ export default function UserList({
|
|||||||
columnHelper.accessor("isVerified", {
|
columnHelper.accessor("isVerified", {
|
||||||
header: (
|
header: (
|
||||||
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "verification"))}>
|
<button className="flex gap-2 items-center" onClick={() => setSorter((prev) => selectSorter(prev, "verification"))}>
|
||||||
<span>Verification</span>
|
<span>Verified</span>
|
||||||
<SorterArrow name="verification" />
|
<SorterArrow name="verification" />
|
||||||
</button>
|
</button>
|
||||||
) as any,
|
) as any,
|
||||||
@@ -464,6 +477,15 @@ export default function UserList({
|
|||||||
return 0;
|
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 (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;
|
||||||
if (a.demographicInformation?.country && !b.demographicInformation?.country) return sorter === "country" ? 1 : -1;
|
if (a.demographicInformation?.country && !b.demographicInformation?.country) return sorter === "country" ? 1 : -1;
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default function Lists({user}: {user: User}) {
|
|||||||
}>
|
}>
|
||||||
User List
|
User List
|
||||||
</Tab>
|
</Tab>
|
||||||
{checkAccess(user, ["developer"]) && (
|
{checkAccess(user, ["developer", "admin", "corporate", "mastercorporate", "teacher"]) && (
|
||||||
<Tab
|
<Tab
|
||||||
className={({selected}) =>
|
className={({selected}) =>
|
||||||
clsx(
|
clsx(
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import Selection from "@/exams/Selection";
|
|||||||
import Speaking from "@/exams/Speaking";
|
import Speaking from "@/exams/Speaking";
|
||||||
import Writing from "@/exams/Writing";
|
import Writing from "@/exams/Writing";
|
||||||
import useUser from "@/hooks/useUser";
|
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 {Stat} from "@/interfaces/user";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import {evaluateSpeakingAnswer, evaluateWritingAnswer} from "@/utils/evaluation";
|
import {evaluateSpeakingAnswer, evaluateWritingAnswer} from "@/utils/evaluation";
|
||||||
@@ -257,6 +257,7 @@ export default function ExamPage({page}: Props) {
|
|||||||
user: user?.id || "",
|
user: user?.id || "",
|
||||||
date: new Date().getTime(),
|
date: new Date().getTime(),
|
||||||
isDisabled: solution.isDisabled,
|
isDisabled: solution.isDisabled,
|
||||||
|
shuffleMaps: solution.shuffleMaps,
|
||||||
...(assignment ? {assignment: assignment.id} : {}),
|
...(assignment ? {assignment: assignment.id} : {}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -459,6 +460,19 @@ export default function ExamPage({page}: Props) {
|
|||||||
inactivity: totalInactivity,
|
inactivity: totalInactivity,
|
||||||
}}
|
}}
|
||||||
onViewResults={(index?: number) => {
|
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);
|
setShowSolutions(true);
|
||||||
setModuleIndex(index || 0);
|
setModuleIndex(index || 0);
|
||||||
setExerciseIndex(["reading", "listening"].includes(exams[0].module) ? -1 : 0);
|
setExerciseIndex(["reading", "listening"].includes(exams[0].module) ? -1 : 0);
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export default function PaymentDue({user, hasExpired = false, reload}: Props) {
|
|||||||
const {packages} = usePackages();
|
const {packages} = usePackages();
|
||||||
const {discounts} = useDiscounts();
|
const {discounts} = useDiscounts();
|
||||||
const {users} = useUsers();
|
const {users} = useUsers();
|
||||||
const {groups} = useGroups();
|
const {groups} = useGroups({});
|
||||||
const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user?.id});
|
const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user?.id});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -36,5 +36,5 @@ async function GET(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const assigners = await getAllAssignersByCorporate(id);
|
const assigners = await getAllAssignersByCorporate(id);
|
||||||
const assignments = await getAssignmentsByAssigners([...assigners, id]);
|
const assignments = await getAssignmentsByAssigners([...assigners, id]);
|
||||||
|
|
||||||
res.status(200).json(assignments);
|
res.status(200).json(uniqBy(assignments, "id"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,174 +1,160 @@
|
|||||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type {NextApiRequest, NextApiResponse} from "next";
|
||||||
import { app } from "@/firebase";
|
import {app} from "@/firebase";
|
||||||
import {
|
import {getFirestore, setDoc, doc, query, collection, where, getDocs, getDoc, deleteDoc} from "firebase/firestore";
|
||||||
getFirestore,
|
import {withIronSessionApiRoute} from "iron-session/next";
|
||||||
setDoc,
|
import {sessionOptions} from "@/lib/session";
|
||||||
doc,
|
import {Code, Group, Type} from "@/interfaces/user";
|
||||||
query,
|
import {PERMISSIONS} from "@/constants/userPermissions";
|
||||||
collection,
|
import {uuidv4} from "@firebase/util";
|
||||||
where,
|
import {prepareMailer, prepareMailOptions} from "@/email";
|
||||||
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";
|
|
||||||
|
|
||||||
const db = getFirestore(app);
|
const db = getFirestore(app);
|
||||||
|
|
||||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (req.method === "GET") return get(req, res);
|
if (req.method === "GET") return get(req, res);
|
||||||
if (req.method === "POST") return post(req, res);
|
if (req.method === "POST") return post(req, res);
|
||||||
if (req.method === "DELETE") return del(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) {
|
async function get(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (!req.session.user) {
|
if (!req.session.user) {
|
||||||
res
|
res.status(401).json({ok: false, reason: "You must be logged in to generate a code!"});
|
||||||
.status(401)
|
return;
|
||||||
.json({ ok: false, reason: "You must be logged in to generate a code!" });
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { creator } = req.query as { creator?: string };
|
const {creator} = req.query as {creator?: string};
|
||||||
const q = query(
|
const q = query(collection(db, "codes"), where("creator", "==", creator || ""));
|
||||||
collection(db, "codes"),
|
const snapshot = await getDocs(creator ? q : 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()));
|
res.status(200).json(snapshot.docs.map((doc) => doc.data()));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function post(req: NextApiRequest, res: NextApiResponse) {
|
async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (!req.session.user) {
|
if (!req.session.user) {
|
||||||
res
|
res.status(401).json({ok: false, reason: "You must be logged in to generate a code!"});
|
||||||
.status(401)
|
return;
|
||||||
.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;
|
type: Type;
|
||||||
codes: string[];
|
codes: string[];
|
||||||
infos?: { email: string; name: string; passport_id?: string }[];
|
infos?: {email: string; name: string; passport_id?: string}[];
|
||||||
expiryDate: null | Date;
|
expiryDate: null | Date;
|
||||||
};
|
};
|
||||||
const permission = PERMISSIONS.generateCode[type];
|
const permission = PERMISSIONS.generateCode[type];
|
||||||
|
|
||||||
if (!permission.includes(req.session.user.type)) {
|
if (!permission.includes(req.session.user.type)) {
|
||||||
res.status(403).json({
|
res.status(403).json({
|
||||||
ok: false,
|
ok: false,
|
||||||
reason:
|
reason: "Your account type does not have permissions to generate a code for that type of user!",
|
||||||
"Your account type does not have permissions to generate a code for that type of user!",
|
});
|
||||||
});
|
return;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const codesGeneratedByUserSnapshot = await getDocs(
|
const codesGeneratedByUserSnapshot = await getDocs(query(collection(db, "codes"), where("creator", "==", req.session.user.id)));
|
||||||
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 userCodes = codesGeneratedByUserSnapshot.docs.map((x) => ({
|
|
||||||
...x.data(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (req.session.user.type === "corporate") {
|
const creatorGroups = (
|
||||||
const totalCodes = codesGeneratedByUserSnapshot.docs.length + codes.length;
|
creatorGroupsSnapshot.docs.map((x) => ({
|
||||||
const allowedCodes =
|
...x.data(),
|
||||||
req.session.user.corporateInformation?.companyInformation.userAmount || 0;
|
})) as Group[]
|
||||||
|
).filter((x) => x.name === "Students" || x.name === "Teachers" || x.name === "Corporate");
|
||||||
|
|
||||||
if (totalCodes > allowedCodes) {
|
const usersInGroups = creatorGroups.flatMap((x) => x.participants);
|
||||||
res.status(403).json({
|
const userCodes = codesGeneratedByUserSnapshot.docs.map((x) => ({
|
||||||
ok: false,
|
...x.data(),
|
||||||
reason: `You have or would have exceeded your amount of allowed codes, you currently are allowed to generate ${
|
})) as Code[];
|
||||||
allowedCodes - codesGeneratedByUserSnapshot.docs.length
|
|
||||||
} codes.`,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const codePromises = codes.map(async (code, index) => {
|
if (req.session.user.type === "corporate") {
|
||||||
const codeRef = doc(db, "codes", code);
|
const totalCodes = userCodes.filter((x) => !x.userId || !usersInGroups.includes(x.userId)).length + usersInGroups.length + codes.length;
|
||||||
let codeInformation = {
|
const allowedCodes = req.session.user.corporateInformation?.companyInformation.userAmount || 0;
|
||||||
type,
|
|
||||||
code,
|
|
||||||
creator: req.session.user!.id,
|
|
||||||
creationDate: new Date().toISOString(),
|
|
||||||
expiryDate,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (infos && infos.length > index) {
|
if (totalCodes > allowedCodes) {
|
||||||
const { email, name, passport_id } = infos[index];
|
res.status(403).json({
|
||||||
const previousCode = userCodes.find((x) => x.email === email) as Code;
|
ok: false,
|
||||||
|
reason: `You have or would have exceeded your amount of allowed codes, you currently are allowed to generate ${
|
||||||
|
allowedCodes - codesGeneratedByUserSnapshot.docs.length
|
||||||
|
} codes.`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const transport = prepareMailer();
|
const codePromises = codes.map(async (code, index) => {
|
||||||
const mailOptions = prepareMailOptions(
|
const codeRef = doc(db, "codes", code);
|
||||||
{
|
let codeInformation = {
|
||||||
type,
|
type,
|
||||||
code: previousCode ? previousCode.code : code,
|
code,
|
||||||
environment: process.env.ENVIRONMENT,
|
creator: req.session.user!.id,
|
||||||
},
|
creationDate: new Date().toISOString(),
|
||||||
[email.toLowerCase().trim()],
|
expiryDate,
|
||||||
"EnCoach Registration",
|
};
|
||||||
"main",
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
if (infos && infos.length > index) {
|
||||||
await transport.sendMail(mailOptions);
|
const {email, name, passport_id} = infos[index];
|
||||||
|
const previousCode = userCodes.find((x) => x.email === email) as Code;
|
||||||
|
|
||||||
if (!previousCode) {
|
const transport = prepareMailer();
|
||||||
await setDoc(
|
const mailOptions = prepareMailOptions(
|
||||||
codeRef,
|
{
|
||||||
{
|
type,
|
||||||
...codeInformation,
|
code: previousCode ? previousCode.code : code,
|
||||||
email: email.trim().toLowerCase(),
|
environment: process.env.ENVIRONMENT,
|
||||||
name: name.trim(),
|
},
|
||||||
...(passport_id ? { passport_id: passport_id.trim() } : {}),
|
[email.toLowerCase().trim()],
|
||||||
},
|
"EnCoach Registration",
|
||||||
{ merge: true },
|
"main",
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
try {
|
||||||
} catch (e) {
|
await transport.sendMail(mailOptions);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await setDoc(codeRef, codeInformation);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Promise.all(codePromises).then((results) => {
|
if (!previousCode) {
|
||||||
res.status(200).json({ ok: true, valid: results.filter((x) => x).length });
|
await setDoc(
|
||||||
});
|
codeRef,
|
||||||
|
{
|
||||||
|
...codeInformation,
|
||||||
|
email: email.trim().toLowerCase(),
|
||||||
|
name: name.trim(),
|
||||||
|
...(passport_id ? {passport_id: passport_id.trim()} : {}),
|
||||||
|
},
|
||||||
|
{merge: true},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await setDoc(codeRef, codeInformation);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Promise.all(codePromises).then((results) => {
|
||||||
|
res.status(200).json({ok: true, valid: results.filter((x) => x).length});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function del(req: NextApiRequest, res: NextApiResponse) {
|
async function del(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (!req.session.user) {
|
if (!req.session.user) {
|
||||||
res
|
res.status(401).json({ok: false, reason: "You must be logged in to generate a code!"});
|
||||||
.status(401)
|
return;
|
||||||
.json({ ok: false, reason: "You must be logged in to generate a code!" });
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const codes = req.query.code as string[];
|
const codes = req.query.code as string[];
|
||||||
|
|
||||||
for (const code of codes) {
|
for (const code of codes) {
|
||||||
const snapshot = await getDoc(doc(db, "codes", code as string));
|
const snapshot = await getDoc(doc(db, "codes", code as string));
|
||||||
if (!snapshot.exists()) continue;
|
if (!snapshot.exists()) continue;
|
||||||
|
|
||||||
await deleteDoc(snapshot.ref);
|
await deleteDoc(snapshot.ref);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).json({ codes });
|
res.status(200).json({codes});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,10 +107,12 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const user = docUser.data() as User;
|
const user = docUser.data() as User;
|
||||||
|
await setDoc(docUser.ref, {lastLogin: new Date().toISOString()}, {merge: true});
|
||||||
|
|
||||||
req.session.user = {
|
req.session.user = {
|
||||||
...user,
|
...user,
|
||||||
id: req.session.user.id,
|
id: req.session.user.id,
|
||||||
|
lastLogin: new Date(),
|
||||||
};
|
};
|
||||||
await req.session.save();
|
await req.session.save();
|
||||||
|
|
||||||
|
|||||||
119
src/pages/groups.tsx
Normal file
119
src/pages/groups.tsx
Normal 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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,30 +1,29 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { withIronSessionSsr } from "iron-session/next";
|
import {withIronSessionSsr} from "iron-session/next";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import { Stat, User } from "@/interfaces/user";
|
import {Stat, User} from "@/interfaces/user";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import {useEffect, useRef, useState} from "react";
|
||||||
import useStats from "@/hooks/useStats";
|
import useStats from "@/hooks/useStats";
|
||||||
import { groupByDate } from "@/utils/stats";
|
import {groupByDate} from "@/utils/stats";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import { ToastContainer } from "react-toastify";
|
import {ToastContainer} from "react-toastify";
|
||||||
import { useRouter } from "next/router";
|
import {useRouter} from "next/router";
|
||||||
import Layout from "@/components/High/Layout";
|
import Layout from "@/components/High/Layout";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import Select from "@/components/Low/Select";
|
import Select from "@/components/Low/Select";
|
||||||
import useGroups from "@/hooks/useGroups";
|
import useGroups from "@/hooks/useGroups";
|
||||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
||||||
import useAssignments from "@/hooks/useAssignments";
|
import useAssignments from "@/hooks/useAssignments";
|
||||||
import { uuidv4 } from "@firebase/util";
|
import {uuidv4} from "@firebase/util";
|
||||||
import { usePDFDownload } from "@/hooks/usePDFDownload";
|
import {usePDFDownload} from "@/hooks/usePDFDownload";
|
||||||
import useRecordStore from "@/stores/recordStore";
|
import useRecordStore from "@/stores/recordStore";
|
||||||
import useTrainingContentStore from "@/stores/trainingContentStore";
|
import useTrainingContentStore from "@/stores/trainingContentStore";
|
||||||
import StatsGridItem from "@/components/StatGridItem";
|
import StatsGridItem from "@/components/StatGridItem";
|
||||||
|
|
||||||
|
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||||
export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
|
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
|
|
||||||
if (!user || !user.isVerified) {
|
if (!user || !user.isVerified) {
|
||||||
@@ -46,7 +45,7 @@ export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: { user: req.session.user },
|
props: {user: req.session.user},
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
@@ -55,16 +54,21 @@ const defaultSelectableCorporate = {
|
|||||||
label: "All",
|
label: "All",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function History({ user }: { user: User }) {
|
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, training, setTraining] = useRecordStore((state) => [
|
||||||
|
state.selectedUser,
|
||||||
|
state.setSelectedUser,
|
||||||
|
state.training,
|
||||||
|
state.setTraining,
|
||||||
|
]);
|
||||||
// const [statsUserId, setStatsUserId] = useState<string | undefined>(user.id);
|
// 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 [filter, setFilter] = useState<"months" | "weeks" | "days" | "assignments">();
|
||||||
const { assignments } = useAssignments({});
|
const {assignments} = useAssignments({});
|
||||||
|
|
||||||
const { users } = useUsers();
|
const {users} = useUsers();
|
||||||
const { stats, isLoading: isStatsLoading } = useStats(statsUserId);
|
const {stats, isLoading: isStatsLoading} = useStats(statsUserId);
|
||||||
const { groups: allGroups } = useGroups();
|
const {groups: allGroups} = useGroups({});
|
||||||
|
|
||||||
const groups = allGroups.filter((x) => x.admin === user.id);
|
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));
|
setFilter((prev) => (prev === value ? undefined : value));
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterStatsByDate = (stats: { [key: string]: Stat[] }) => {
|
const filterStatsByDate = (stats: {[key: string]: Stat[]}) => {
|
||||||
if (filter && filter !== "assignments") {
|
if (filter && filter !== "assignments") {
|
||||||
const filterDate = moment()
|
const filterDate = moment()
|
||||||
.subtract({ [filter as string]: 1 })
|
.subtract({[filter as string]: 1})
|
||||||
.format("x");
|
.format("x");
|
||||||
const filteredStats: { [key: string]: Stat[] } = {};
|
const filteredStats: {[key: string]: Stat[]} = {};
|
||||||
|
|
||||||
Object.keys(stats).forEach((timestamp) => {
|
Object.keys(stats).forEach((timestamp) => {
|
||||||
if (timestamp >= filterDate) filteredStats[timestamp] = stats[timestamp];
|
if (timestamp >= filterDate) filteredStats[timestamp] = stats[timestamp];
|
||||||
@@ -118,7 +122,7 @@ export default function History({ user }: { user: User }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (filter && filter === "assignments") {
|
if (filter && filter === "assignments") {
|
||||||
const filteredStats: { [key: string]: Stat[] } = {};
|
const filteredStats: {[key: string]: Stat[]} = {};
|
||||||
|
|
||||||
Object.keys(stats).forEach((timestamp) => {
|
Object.keys(stats).forEach((timestamp) => {
|
||||||
if (stats[timestamp].map((s) => s.assignment === undefined).includes(false))
|
if (stats[timestamp].map((s) => s.assignment === undefined).includes(false))
|
||||||
@@ -137,13 +141,13 @@ export default function History({ user }: { user: User }) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleRouteChange = (url: string) => {
|
const handleRouteChange = (url: string) => {
|
||||||
setTraining(false)
|
setTraining(false);
|
||||||
}
|
};
|
||||||
router.events.on('routeChangeStart', handleRouteChange)
|
router.events.on("routeChangeStart", handleRouteChange);
|
||||||
return () => {
|
return () => {
|
||||||
router.events.off('routeChangeStart', handleRouteChange)
|
router.events.off("routeChangeStart", handleRouteChange);
|
||||||
}
|
};
|
||||||
}, [router.events, setTraining])
|
}, [router.events, setTraining]);
|
||||||
|
|
||||||
const handleTrainingContentSubmission = () => {
|
const handleTrainingContentSubmission = () => {
|
||||||
if (groupedStats) {
|
if (groupedStats) {
|
||||||
@@ -156,11 +160,10 @@ export default function History({ user }: { user: User }) {
|
|||||||
}
|
}
|
||||||
return accumulator;
|
return accumulator;
|
||||||
}, {});
|
}, {});
|
||||||
setTrainingStats(Object.values(selectedStats).flat())
|
setTrainingStats(Object.values(selectedStats).flat());
|
||||||
router.push("/training");
|
router.push("/training");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
const customContent = (timestamp: string) => {
|
const customContent = (timestamp: string) => {
|
||||||
if (!groupedStats) return <></>;
|
if (!groupedStats) return <></>;
|
||||||
@@ -240,13 +243,13 @@ export default function History({ user }: { user: User }) {
|
|||||||
const selectedUser = getSelectedUser();
|
const selectedUser = getSelectedUser();
|
||||||
const selectedUserSelectValue = selectedUser
|
const selectedUserSelectValue = selectedUser
|
||||||
? {
|
? {
|
||||||
value: selectedUser.id,
|
value: selectedUser.id,
|
||||||
label: `${selectedUser.name} - ${selectedUser.email}`,
|
label: `${selectedUser.name} - ${selectedUser.email}`,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
value: "",
|
value: "",
|
||||||
label: "",
|
label: "",
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@@ -272,7 +275,7 @@ export default function History({ user }: { user: User }) {
|
|||||||
value={selectableCorporates.find((x) => x.value === selectedCorporate)}
|
value={selectableCorporates.find((x) => x.value === selectedCorporate)}
|
||||||
onChange={(value) => setSelectedCorporate(value?.value || "")}
|
onChange={(value) => setSelectedCorporate(value?.value || "")}
|
||||||
styles={{
|
styles={{
|
||||||
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
option: (styles, state) => ({
|
option: (styles, state) => ({
|
||||||
...styles,
|
...styles,
|
||||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||||
@@ -289,7 +292,7 @@ export default function History({ user }: { user: User }) {
|
|||||||
value={selectedUserSelectValue}
|
value={selectedUserSelectValue}
|
||||||
onChange={(value) => setStatsUserId(value?.value!)}
|
onChange={(value) => setStatsUserId(value?.value!)}
|
||||||
styles={{
|
styles={{
|
||||||
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
option: (styles, state) => ({
|
option: (styles, state) => ({
|
||||||
...styles,
|
...styles,
|
||||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||||
@@ -313,7 +316,7 @@ export default function History({ user }: { user: User }) {
|
|||||||
value={selectedUserSelectValue}
|
value={selectedUserSelectValue}
|
||||||
onChange={(value) => setStatsUserId(value?.value!)}
|
onChange={(value) => setStatsUserId(value?.value!)}
|
||||||
styles={{
|
styles={{
|
||||||
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
option: (styles, state) => ({
|
option: (styles, state) => ({
|
||||||
...styles,
|
...styles,
|
||||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
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="flex flex-row">
|
||||||
<div className="font-semibold text-2xl mr-4">Select up to 10 exercises
|
<div className="font-semibold text-2xl mr-4">
|
||||||
{`(${selectedTrainingExams.length}/${MAX_TRAINING_EXAMS})`}</div>
|
Select up to 10 exercises
|
||||||
|
{`(${selectedTrainingExams.length}/${MAX_TRAINING_EXAMS})`}
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
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",
|
"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
|
Submit
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4 w-full justify-center xl:justify-end">
|
<div className="flex gap-4 w-full justify-center xl:justify-end">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -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 {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
destination: "/",
|
destination: "/",
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export default function Stats() {
|
|||||||
|
|
||||||
const {user} = useUser({redirectTo: "/login"});
|
const {user} = useUser({redirectTo: "/login"});
|
||||||
const {users} = useUsers();
|
const {users} = useUsers();
|
||||||
const {groups} = useGroups(user?.id);
|
const {groups} = useGroups({admin: user?.id});
|
||||||
const {stats} = useStats(statsUserId, !statsUserId);
|
const {stats} = useStats(statsUserId, !statsUserId);
|
||||||
|
|
||||||
useEffect(() => {
|
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
|
<Select
|
||||||
className="w-full"
|
className="w-full"
|
||||||
options={users
|
options={users
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { withIronSessionSsr } from "iron-session/next";
|
import {withIronSessionSsr} from "iron-session/next";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import { Stat, User } from "@/interfaces/user";
|
import {Stat, User} from "@/interfaces/user";
|
||||||
import { ToastContainer } from "react-toastify";
|
import {ToastContainer} from "react-toastify";
|
||||||
import Layout from "@/components/High/Layout";
|
import Layout from "@/components/High/Layout";
|
||||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
||||||
import { use, useEffect, useState } from "react";
|
import {use, useEffect, useState} from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { FaPlus } from "react-icons/fa";
|
import {FaPlus} from "react-icons/fa";
|
||||||
import useRecordStore from "@/stores/recordStore";
|
import useRecordStore from "@/stores/recordStore";
|
||||||
import router from "next/router";
|
import router from "next/router";
|
||||||
import useTrainingContentStore from "@/stores/trainingContentStore";
|
import useTrainingContentStore from "@/stores/trainingContentStore";
|
||||||
@@ -16,390 +16,388 @@ import axios from "axios";
|
|||||||
import Select from "@/components/Low/Select";
|
import Select from "@/components/Low/Select";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import useGroups from "@/hooks/useGroups";
|
import useGroups from "@/hooks/useGroups";
|
||||||
import { ITrainingContent } from "@/training/TrainingInterfaces";
|
import {ITrainingContent} from "@/training/TrainingInterfaces";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { uuidv4 } from "@firebase/util";
|
import {uuidv4} from "@firebase/util";
|
||||||
import TrainingScore from "@/training/TrainingScore";
|
import TrainingScore from "@/training/TrainingScore";
|
||||||
import ModuleBadge from "@/components/ModuleBadge";
|
import ModuleBadge from "@/components/ModuleBadge";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
|
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
|
|
||||||
if (!user || !user.isVerified) {
|
if (!user || !user.isVerified) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
destination: "/login",
|
destination: "/login",
|
||||||
permanent: false,
|
permanent: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldRedirectHome(user)) {
|
if (shouldRedirectHome(user)) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
destination: "/",
|
destination: "/",
|
||||||
permanent: false,
|
permanent: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: { user: req.session.user },
|
props: {user: req.session.user},
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
const defaultSelectableCorporate = {
|
const defaultSelectableCorporate = {
|
||||||
value: "",
|
value: "",
|
||||||
label: "All",
|
label: "All",
|
||||||
};
|
};
|
||||||
|
|
||||||
const Training: React.FC<{ user: User }> = ({ user }) => {
|
const Training: React.FC<{user: User}> = ({user}) => {
|
||||||
// Record stuff
|
// Record stuff
|
||||||
const { users } = useUsers();
|
const {users} = useUsers();
|
||||||
const [selectedCorporate, setSelectedCorporate] = useState<string>(defaultSelectableCorporate.value);
|
const [selectedCorporate, setSelectedCorporate] = useState<string>(defaultSelectableCorporate.value);
|
||||||
const [statsUserId, setStatsUserId, setRecordTraining] = useRecordStore((state) => [state.selectedUser, state.setSelectedUser, state.setTraining]);
|
const [statsUserId, setStatsUserId, setRecordTraining] = useRecordStore((state) => [
|
||||||
const { groups: allGroups } = useGroups();
|
state.selectedUser,
|
||||||
const groups = allGroups.filter((x) => x.admin === user.id);
|
state.setSelectedUser,
|
||||||
const [filter, setFilter] = useState<"months" | "weeks" | "days">();
|
state.setTraining,
|
||||||
|
]);
|
||||||
|
const {groups: allGroups} = useGroups({});
|
||||||
|
const groups = allGroups.filter((x) => x.admin === user.id);
|
||||||
|
const [filter, setFilter] = useState<"months" | "weeks" | "days">();
|
||||||
|
|
||||||
const toggleFilter = (value: "months" | "weeks" | "days") => {
|
const toggleFilter = (value: "months" | "weeks" | "days") => {
|
||||||
setFilter((prev) => (prev === value ? undefined : value));
|
setFilter((prev) => (prev === value ? undefined : value));
|
||||||
};
|
};
|
||||||
|
|
||||||
const [stats, setTrainingStats] = useTrainingContentStore((state) => [state.stats, state.setStats]);
|
const [stats, setTrainingStats] = useTrainingContentStore((state) => [state.stats, state.setStats]);
|
||||||
const [trainingContent, setTrainingContent] = useState<ITrainingContent[]>([]);
|
const [trainingContent, setTrainingContent] = useState<ITrainingContent[]>([]);
|
||||||
const [isNewContentLoading, setIsNewContentLoading] = useState(stats.length != 0);
|
const [isNewContentLoading, setIsNewContentLoading] = useState(stats.length != 0);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
const [groupedByTrainingContent, setGroupedByTrainingContent] = useState<{ [key: string]: ITrainingContent }>();
|
const [groupedByTrainingContent, setGroupedByTrainingContent] = useState<{[key: string]: ITrainingContent}>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleRouteChange = (url: string) => {
|
const handleRouteChange = (url: string) => {
|
||||||
setTrainingStats([])
|
setTrainingStats([]);
|
||||||
}
|
};
|
||||||
router.events.on('routeChangeStart', handleRouteChange)
|
router.events.on("routeChangeStart", handleRouteChange);
|
||||||
return () => {
|
return () => {
|
||||||
router.events.off('routeChangeStart', handleRouteChange)
|
router.events.off("routeChangeStart", handleRouteChange);
|
||||||
}
|
};
|
||||||
}, [router.events, setTrainingStats])
|
}, [router.events, setTrainingStats]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const postStats = async () => {
|
const postStats = async () => {
|
||||||
try {
|
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;
|
return response.data.id;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setIsNewContentLoading(false);
|
setIsNewContentLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isNewContentLoading) {
|
if (isNewContentLoading) {
|
||||||
postStats().then(id => {
|
postStats().then((id) => {
|
||||||
setTrainingStats([]);
|
setTrainingStats([]);
|
||||||
if (id) {
|
if (id) {
|
||||||
router.push(`/training/${id}`)
|
router.push(`/training/${id}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [isNewContentLoading])
|
}, [isNewContentLoading]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadTrainingContent = async () => {
|
const loadTrainingContent = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get<ITrainingContent[]>('/api/training');
|
const response = await axios.get<ITrainingContent[]>("/api/training");
|
||||||
setTrainingContent(response.data);
|
setTrainingContent(response.data);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setTrainingContent([]);
|
setTrainingContent([]);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
loadTrainingContent();
|
loadTrainingContent();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleNewTrainingContent = () => {
|
const handleNewTrainingContent = () => {
|
||||||
setRecordTraining(true);
|
setRecordTraining(true);
|
||||||
router.push('/record')
|
router.push("/record");
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const filterTrainingContentByDate = (trainingContent: {[key: string]: ITrainingContent}) => {
|
||||||
|
if (filter) {
|
||||||
|
const filterDate = moment()
|
||||||
|
.subtract({[filter as string]: 1})
|
||||||
|
.format("x");
|
||||||
|
const filteredTrainingContent: {[key: string]: ITrainingContent} = {};
|
||||||
|
|
||||||
const filterTrainingContentByDate = (trainingContent: { [key: string]: ITrainingContent }) => {
|
Object.keys(trainingContent).forEach((timestamp) => {
|
||||||
if (filter) {
|
if (timestamp >= filterDate) filteredTrainingContent[timestamp] = trainingContent[timestamp];
|
||||||
const filterDate = moment()
|
});
|
||||||
.subtract({ [filter as string]: 1 })
|
return filteredTrainingContent;
|
||||||
.format("x");
|
}
|
||||||
const filteredTrainingContent: { [key: string]: ITrainingContent } = {};
|
return trainingContent;
|
||||||
|
};
|
||||||
|
|
||||||
Object.keys(trainingContent).forEach((timestamp) => {
|
useEffect(() => {
|
||||||
if (timestamp >= filterDate) filteredTrainingContent[timestamp] = trainingContent[timestamp];
|
if (trainingContent.length > 0) {
|
||||||
});
|
const grouped = trainingContent.reduce((acc, content) => {
|
||||||
return filteredTrainingContent;
|
acc[content.created_at] = content;
|
||||||
}
|
return acc;
|
||||||
return trainingContent;
|
}, {} as {[key: number]: ITrainingContent});
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
setGroupedByTrainingContent(grouped);
|
||||||
if (trainingContent.length > 0) {
|
}
|
||||||
const grouped = trainingContent.reduce((acc, content) => {
|
}, [trainingContent]);
|
||||||
acc[content.created_at] = content;
|
|
||||||
return acc;
|
|
||||||
}, {} as { [key: number]: ITrainingContent });
|
|
||||||
|
|
||||||
setGroupedByTrainingContent(grouped);
|
// Record Stuff
|
||||||
}
|
const selectableCorporates = [
|
||||||
}, [trainingContent])
|
defaultSelectableCorporate,
|
||||||
|
...users
|
||||||
|
.filter((x) => x.type === "corporate")
|
||||||
|
.map((x) => ({
|
||||||
|
value: x.id,
|
||||||
|
label: `${x.name} - ${x.email}`,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
const getUsersList = (): User[] => {
|
||||||
|
if (selectedCorporate) {
|
||||||
|
// get groups for that corporate
|
||||||
|
const selectedCorporateGroups = allGroups.filter((x) => x.admin === selectedCorporate);
|
||||||
|
|
||||||
// Record Stuff
|
// get the teacher ids for that group
|
||||||
const selectableCorporates = [
|
const selectedCorporateGroupsParticipants = selectedCorporateGroups.flatMap((x) => x.participants);
|
||||||
defaultSelectableCorporate,
|
|
||||||
...users
|
|
||||||
.filter((x) => x.type === "corporate")
|
|
||||||
.map((x) => ({
|
|
||||||
value: x.id,
|
|
||||||
label: `${x.name} - ${x.email}`,
|
|
||||||
})),
|
|
||||||
];
|
|
||||||
|
|
||||||
const getUsersList = (): User[] => {
|
// // search for groups for these teachers
|
||||||
if (selectedCorporate) {
|
// const teacherGroups = allGroups.filter((x) => {
|
||||||
// get groups for that corporate
|
// return selectedCorporateGroupsParticipants.includes(x.admin);
|
||||||
const selectedCorporateGroups = allGroups.filter((x) => x.admin === selectedCorporate);
|
// });
|
||||||
|
|
||||||
// get the teacher ids for that group
|
// const usersList = [
|
||||||
const selectedCorporateGroupsParticipants = selectedCorporateGroups.flatMap((x) => x.participants);
|
// ...selectedCorporateGroupsParticipants,
|
||||||
|
// ...teacherGroups.flatMap((x) => x.participants),
|
||||||
|
// ];
|
||||||
|
const userListWithUsers = selectedCorporateGroupsParticipants.map((x) => users.find((y) => y.id === x)) as User[];
|
||||||
|
return userListWithUsers.filter((x) => x);
|
||||||
|
}
|
||||||
|
|
||||||
// // search for groups for these teachers
|
return users || [];
|
||||||
// const teacherGroups = allGroups.filter((x) => {
|
};
|
||||||
// return selectedCorporateGroupsParticipants.includes(x.admin);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const usersList = [
|
const corporateFilteredUserList = getUsersList();
|
||||||
// ...selectedCorporateGroupsParticipants,
|
const getSelectedUser = () => {
|
||||||
// ...teacherGroups.flatMap((x) => x.participants),
|
if (selectedCorporate) {
|
||||||
// ];
|
const userInCorporate = corporateFilteredUserList.find((x) => x.id === statsUserId);
|
||||||
const userListWithUsers = selectedCorporateGroupsParticipants.map((x) => users.find((y) => y.id === x)) as User[];
|
return userInCorporate || corporateFilteredUserList[0];
|
||||||
return userListWithUsers.filter((x) => x);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return users || [];
|
return users.find((x) => x.id === statsUserId) || user;
|
||||||
};
|
};
|
||||||
|
|
||||||
const corporateFilteredUserList = getUsersList();
|
const selectedUser = getSelectedUser();
|
||||||
const getSelectedUser = () => {
|
const selectedUserSelectValue = selectedUser
|
||||||
if (selectedCorporate) {
|
? {
|
||||||
const userInCorporate = corporateFilteredUserList.find((x) => x.id === statsUserId);
|
value: selectedUser.id,
|
||||||
return userInCorporate || corporateFilteredUserList[0];
|
label: `${selectedUser.name} - ${selectedUser.email}`,
|
||||||
}
|
}
|
||||||
|
: {
|
||||||
|
value: "",
|
||||||
|
label: "",
|
||||||
|
};
|
||||||
|
|
||||||
return users.find((x) => x.id === statsUserId) || user;
|
const formatTimestamp = (timestamp: string) => {
|
||||||
};
|
const date = moment(parseInt(timestamp));
|
||||||
|
const formatter = "YYYY/MM/DD - HH:mm";
|
||||||
|
|
||||||
const selectedUser = getSelectedUser();
|
return date.format(formatter);
|
||||||
const selectedUserSelectValue = selectedUser
|
};
|
||||||
? {
|
|
||||||
value: selectedUser.id,
|
|
||||||
label: `${selectedUser.name} - ${selectedUser.email}`,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
value: "",
|
|
||||||
label: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatTimestamp = (timestamp: string) => {
|
const selectTrainingContent = (trainingContent: ITrainingContent) => {
|
||||||
const date = moment(parseInt(timestamp));
|
router.push(`/training/${trainingContent.id}`);
|
||||||
const formatter = "YYYY/MM/DD - HH:mm";
|
};
|
||||||
|
|
||||||
return date.format(formatter);
|
const trainingContentContainer = (timestamp: string) => {
|
||||||
};
|
if (!groupedByTrainingContent) return <></>;
|
||||||
|
const trainingContent: ITrainingContent = groupedByTrainingContent[timestamp];
|
||||||
|
const uniqueModules = [...new Set(trainingContent.exams.map((exam) => exam.module))];
|
||||||
|
|
||||||
const selectTrainingContent = (trainingContent: ITrainingContent) => {
|
return (
|
||||||
router.push(`/training/${trainingContent.id}`)
|
<>
|
||||||
};
|
<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",
|
||||||
|
)}
|
||||||
|
onClick={() => selectTrainingContent(trainingContent)}
|
||||||
|
role="button">
|
||||||
|
<div className="w-full flex justify-between -md:items-center 2xl:items-center">
|
||||||
|
<div className="flex flex-col md:gap-1 -md:gap-2 2xl:gap-2">
|
||||||
|
<span className="font-medium">{formatTimestamp(timestamp)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="w-full flex flex-row gap-1">
|
||||||
|
{uniqueModules.map((module) => (
|
||||||
|
<ModuleBadge key={module} module={module} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<TrainingScore trainingContent={trainingContent} gridView={true} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>Training | 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 />
|
||||||
|
|
||||||
const trainingContentContainer = (timestamp: string) => {
|
<Layout user={user}>
|
||||||
if (!groupedByTrainingContent) return <></>;
|
{isNewContentLoading || isLoading ? (
|
||||||
const trainingContent: ITrainingContent = groupedByTrainingContent[timestamp];
|
<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">
|
||||||
const uniqueModules = [...new Set(trainingContent.exams.map(exam => exam.module))];
|
<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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="w-full flex -xl:flex-col -xl:gap-4 justify-between items-center">
|
||||||
|
<div className="xl:w-3/4">
|
||||||
|
{(user.type === "developer" || user.type === "admin") && (
|
||||||
|
<>
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">Corporate</label>
|
||||||
|
|
||||||
return (
|
<Select
|
||||||
<>
|
options={selectableCorporates}
|
||||||
<div
|
value={selectableCorporates.find((x) => x.value === selectedCorporate)}
|
||||||
key={uuidv4()}
|
onChange={(value) => setSelectedCorporate(value?.value || "")}
|
||||||
className={clsx(
|
styles={{
|
||||||
"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"
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
)}
|
option: (styles, state) => ({
|
||||||
onClick={() => selectTrainingContent(trainingContent)}
|
...styles,
|
||||||
role="button">
|
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||||
<div className="w-full flex justify-between -md:items-center 2xl:items-center">
|
color: state.isFocused ? "black" : styles.color,
|
||||||
<div className="flex flex-col md:gap-1 -md:gap-2 2xl:gap-2">
|
}),
|
||||||
<span className="font-medium">{formatTimestamp(timestamp)}</span>
|
}}></Select>
|
||||||
</div>
|
<label className="font-normal text-base text-mti-gray-dim">User</label>
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="w-full flex flex-row gap-1">
|
|
||||||
{uniqueModules.map((module) => (
|
|
||||||
<ModuleBadge key={module} module={module} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<TrainingScore
|
|
||||||
trainingContent={trainingContent}
|
|
||||||
gridView={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
<Select
|
||||||
<>
|
options={corporateFilteredUserList.map((x) => ({
|
||||||
<Head>
|
value: x.id,
|
||||||
<title>Training | EnCoach</title>
|
label: `${x.name} - ${x.email}`,
|
||||||
<meta
|
}))}
|
||||||
name="description"
|
value={selectedUserSelectValue}
|
||||||
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
onChange={(value) => setStatsUserId(value?.value!)}
|
||||||
/>
|
styles={{
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
<link rel="icon" href="/favicon.ico" />
|
option: (styles, state) => ({
|
||||||
</Head>
|
...styles,
|
||||||
<ToastContainer />
|
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||||
|
color: state.isFocused ? "black" : styles.color,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{(user.type === "corporate" || user.type === "teacher") && groups.length > 0 && (
|
||||||
|
<>
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">User</label>
|
||||||
|
|
||||||
<Layout user={user}>
|
<Select
|
||||||
{(isNewContentLoading || isLoading ? (
|
options={users
|
||||||
<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">
|
.filter((x) => groups.flatMap((y) => y.participants).includes(x.id))
|
||||||
<span className="loading loading-infinity w-32 bg-mti-green-light" />
|
.map((x) => ({
|
||||||
{isNewContentLoading && (<span className="text-center text-2xl font-bold text-mti-green-light">
|
value: x.id,
|
||||||
Assessing your exams, please be patient...
|
label: `${x.name} - ${x.email}`,
|
||||||
</span>)}
|
}))}
|
||||||
</div>
|
value={selectedUserSelectValue}
|
||||||
) : (
|
onChange={(value) => setStatsUserId(value?.value!)}
|
||||||
<>
|
styles={{
|
||||||
<div className="w-full flex -xl:flex-col -xl:gap-4 justify-between items-center">
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
<div className="xl:w-3/4">
|
option: (styles, state) => ({
|
||||||
{(user.type === "developer" || user.type === "admin") && (
|
...styles,
|
||||||
<>
|
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Corporate</label>
|
color: state.isFocused ? "black" : styles.color,
|
||||||
|
}),
|
||||||
<Select
|
}}
|
||||||
options={selectableCorporates}
|
/>
|
||||||
value={selectableCorporates.find((x) => x.value === selectedCorporate)}
|
</>
|
||||||
onChange={(value) => setSelectedCorporate(value?.value || "")}
|
)}
|
||||||
styles={{
|
{user.type === "student" && (
|
||||||
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
<>
|
||||||
option: (styles, state) => ({
|
<div className="flex items-center">
|
||||||
...styles,
|
<div className="font-semibold text-2xl">Generate New Training Material</div>
|
||||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
<button
|
||||||
color: state.isFocused ? "black" : styles.color,
|
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",
|
||||||
}}></Select>
|
"transition duration-300 ease-in-out",
|
||||||
<label className="font-normal text-base text-mti-gray-dim">User</label>
|
)}
|
||||||
|
onClick={handleNewTrainingContent}>
|
||||||
<Select
|
<FaPlus />
|
||||||
options={corporateFilteredUserList.map((x) => ({
|
</button>
|
||||||
value: x.id,
|
</div>
|
||||||
label: `${x.name} - ${x.email}`,
|
</>
|
||||||
}))}
|
)}
|
||||||
value={selectedUserSelectValue}
|
</div>
|
||||||
onChange={(value) => setStatsUserId(value?.value!)}
|
<div className="flex gap-4 w-full justify-center xl:justify-end">
|
||||||
styles={{
|
<button
|
||||||
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
className={clsx(
|
||||||
option: (styles, state) => ({
|
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
||||||
...styles,
|
"transition duration-300 ease-in-out",
|
||||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
filter === "months" && "!bg-mti-purple-light !text-white",
|
||||||
color: state.isFocused ? "black" : styles.color,
|
)}
|
||||||
}),
|
onClick={() => toggleFilter("months")}>
|
||||||
}}
|
Last month
|
||||||
/>
|
</button>
|
||||||
</>
|
<button
|
||||||
)}
|
className={clsx(
|
||||||
{(user.type === "corporate" || user.type === "teacher") && groups.length > 0 && (
|
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
||||||
<>
|
"transition duration-300 ease-in-out",
|
||||||
<label className="font-normal text-base text-mti-gray-dim">User</label>
|
filter === "weeks" && "!bg-mti-purple-light !text-white",
|
||||||
|
)}
|
||||||
<Select
|
onClick={() => toggleFilter("weeks")}>
|
||||||
options={users
|
Last week
|
||||||
.filter((x) => groups.flatMap((y) => y.participants).includes(x.id))
|
</button>
|
||||||
.map((x) => ({
|
<button
|
||||||
value: x.id,
|
className={clsx(
|
||||||
label: `${x.name} - ${x.email}`,
|
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
||||||
}))}
|
"transition duration-300 ease-in-out",
|
||||||
value={selectedUserSelectValue}
|
filter === "days" && "!bg-mti-purple-light !text-white",
|
||||||
onChange={(value) => setStatsUserId(value?.value!)}
|
)}
|
||||||
styles={{
|
onClick={() => toggleFilter("days")}>
|
||||||
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
Last day
|
||||||
option: (styles, state) => ({
|
</button>
|
||||||
...styles,
|
</div>
|
||||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
</div>
|
||||||
color: state.isFocused ? "black" : styles.color,
|
{trainingContent.length == 0 && (
|
||||||
}),
|
<div className="flex flex-grow justify-center items-center">
|
||||||
}}
|
<span className="font-semibold ml-1">No training content to display...</span>
|
||||||
/>
|
</div>
|
||||||
</>
|
)}
|
||||||
)}
|
{groupedByTrainingContent && Object.keys(groupedByTrainingContent).length > 0 && (
|
||||||
{(user.type === "student" && (
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-2 2xl:grid-cols-3 w-full gap-4 xl:gap-6">
|
||||||
<>
|
{Object.keys(filterTrainingContentByDate(groupedByTrainingContent))
|
||||||
<div className="flex items-center">
|
.sort((a, b) => parseInt(b) - parseInt(a))
|
||||||
<div className="font-semibold text-2xl">Generate New Training Material</div>
|
.map(trainingContentContainer)}
|
||||||
<button
|
</div>
|
||||||
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",
|
</>
|
||||||
"transition duration-300 ease-in-out",
|
)}
|
||||||
)}
|
</Layout>
|
||||||
onClick={handleNewTrainingContent}>
|
</>
|
||||||
<FaPlus />
|
);
|
||||||
</button>
|
};
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-4 w-full justify-center xl:justify-end">
|
|
||||||
<button
|
|
||||||
className={clsx(
|
|
||||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
|
||||||
"transition duration-300 ease-in-out",
|
|
||||||
filter === "months" && "!bg-mti-purple-light !text-white",
|
|
||||||
)}
|
|
||||||
onClick={() => toggleFilter("months")}>
|
|
||||||
Last month
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={clsx(
|
|
||||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
|
||||||
"transition duration-300 ease-in-out",
|
|
||||||
filter === "weeks" && "!bg-mti-purple-light !text-white",
|
|
||||||
)}
|
|
||||||
onClick={() => toggleFilter("weeks")}>
|
|
||||||
Last week
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={clsx(
|
|
||||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
|
||||||
"transition duration-300 ease-in-out",
|
|
||||||
filter === "days" && "!bg-mti-purple-light !text-white",
|
|
||||||
)}
|
|
||||||
onClick={() => toggleFilter("days")}>
|
|
||||||
Last day
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{trainingContent.length == 0 && (
|
|
||||||
<div className="flex flex-grow justify-center items-center">
|
|
||||||
<span className="font-semibold ml-1">No training content to display...</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{groupedByTrainingContent && Object.keys(groupedByTrainingContent).length > 0 && (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-2 2xl:grid-cols-3 w-full gap-4 xl:gap-6">
|
|
||||||
{Object.keys(filterTrainingContentByDate(groupedByTrainingContent))
|
|
||||||
.sort((a, b) => parseInt(b) - parseInt(a))
|
|
||||||
.map(trainingContentContainer)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
))}
|
|
||||||
</Layout>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Training;
|
export default Training;
|
||||||
|
|||||||
@@ -5,17 +5,17 @@ export const USER_TYPE_LABELS: {[key in Type]: string} = {
|
|||||||
teacher: "Teacher",
|
teacher: "Teacher",
|
||||||
corporate: "Corporate",
|
corporate: "Corporate",
|
||||||
agent: "Country Manager",
|
agent: "Country Manager",
|
||||||
admin: "Admin",
|
admin: "Super Admin",
|
||||||
developer: "Developer",
|
developer: "Developer",
|
||||||
mastercorporate: "Master Corporate"
|
mastercorporate: "Master Corporate",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function isCorporateUser(user: User): user is CorporateUser {
|
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 {
|
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[]) {
|
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;
|
const admin = belongingGroupsAdmins[0] as CorporateUser;
|
||||||
return admin.corporateInformation?.companyInformation.name || admin.name;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {Module} from "@/interfaces";
|
import {Module} from "@/interfaces";
|
||||||
import {Exam, UserSolution} from "@/interfaces/exam";
|
import {Exam, ShuffleMap, UserSolution} from "@/interfaces/exam";
|
||||||
import {Assignment} from "@/interfaces/results";
|
import {Assignment} from "@/interfaces/results";
|
||||||
import {create} from "zustand";
|
import {create} from "zustand";
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ export interface ExamState {
|
|||||||
exerciseIndex: number;
|
exerciseIndex: number;
|
||||||
questionIndex: number;
|
questionIndex: number;
|
||||||
inactivity: number;
|
inactivity: number;
|
||||||
|
shuffleMaps: ShuffleMap[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExamFunctions {
|
export interface ExamFunctions {
|
||||||
@@ -35,6 +36,7 @@ export interface ExamFunctions {
|
|||||||
setExerciseIndex: (exerciseIndex: number) => void;
|
setExerciseIndex: (exerciseIndex: number) => void;
|
||||||
setQuestionIndex: (questionIndex: number) => void;
|
setQuestionIndex: (questionIndex: number) => void;
|
||||||
setInactivity: (inactivity: number) => void;
|
setInactivity: (inactivity: number) => void;
|
||||||
|
setShuffleMaps: (shuffleMaps: ShuffleMap[]) => void;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +55,7 @@ export const initialState: ExamState = {
|
|||||||
exerciseIndex: -1,
|
exerciseIndex: -1,
|
||||||
questionIndex: 0,
|
questionIndex: 0,
|
||||||
inactivity: 0,
|
inactivity: 0,
|
||||||
|
shuffleMaps: []
|
||||||
};
|
};
|
||||||
|
|
||||||
const useExamStore = create<ExamState & ExamFunctions>((set) => ({
|
const useExamStore = create<ExamState & ExamFunctions>((set) => ({
|
||||||
@@ -72,6 +75,7 @@ const useExamStore = create<ExamState & ExamFunctions>((set) => ({
|
|||||||
setExerciseIndex: (exerciseIndex: number) => set(() => ({exerciseIndex})),
|
setExerciseIndex: (exerciseIndex: number) => set(() => ({exerciseIndex})),
|
||||||
setQuestionIndex: (questionIndex: number) => set(() => ({questionIndex})),
|
setQuestionIndex: (questionIndex: number) => set(() => ({questionIndex})),
|
||||||
setInactivity: (inactivity: number) => set(() => ({inactivity})),
|
setInactivity: (inactivity: number) => set(() => ({inactivity})),
|
||||||
|
setShuffleMaps: (shuffleMaps) => set(() => ({shuffleMaps})),
|
||||||
|
|
||||||
reset: () => set(() => initialState),
|
reset: () => set(() => initialState),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -2,89 +2,3 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@layer utilities {
|
|
||||||
.scrollbar-hide {
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
/* IE and Edge */
|
|
||||||
scrollbar-width: none;
|
|
||||||
/* Firefox */
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollbar-hide::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
/* Chrome, Safari and Opera */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.training-scrollbar::-webkit-scrollbar {
|
|
||||||
@apply w-1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.training-scrollbar::-webkit-scrollbar-track {
|
|
||||||
@apply bg-transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.training-scrollbar::-webkit-scrollbar-thumb {
|
|
||||||
@apply bg-gray-400 hover:bg-gray-500 rounded-full transition-colors opacity-50 hover:opacity-75;
|
|
||||||
}
|
|
||||||
|
|
||||||
.training-scrollbar {
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--max-width: 1100px;
|
|
||||||
--border-radius: 12px;
|
|
||||||
--font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro",
|
|
||||||
"Fira Mono", "Droid Sans Mono", "Courier New", monospace;
|
|
||||||
|
|
||||||
--foreground-rgb: 53, 51, 56;
|
|
||||||
--background-start-rgb: 245, 245, 245;
|
|
||||||
--background-end-rgb: 245, 245, 245;
|
|
||||||
|
|
||||||
--primary-glow: conic-gradient(from 180deg at 50% 50%, #16abff33 0deg, #0885ff33 55deg, #54d6ff33 120deg, #0071ff33 160deg, transparent 360deg);
|
|
||||||
--secondary-glow: radial-gradient(rgba(255, 255, 255, 1), rgba(255, 255, 255, 0));
|
|
||||||
|
|
||||||
--tile-start-rgb: 239, 245, 249;
|
|
||||||
--tile-end-rgb: 228, 232, 233;
|
|
||||||
--tile-border: conic-gradient(#00000080, #00000040, #00000030, #00000020, #00000010, #00000010, #00000080);
|
|
||||||
|
|
||||||
--callout-rgb: 238, 240, 241;
|
|
||||||
--callout-border-rgb: 172, 175, 176;
|
|
||||||
--card-rgb: 180, 185, 188;
|
|
||||||
--card-border-rgb: 131, 134, 135;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
min-height: 100vh !important;
|
|
||||||
height: 100%;
|
|
||||||
max-width: 100vw;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
font-family: "Open Sans", system-ui, -apple-system, "Helvetica Neue", sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
min-height: 100vh !important;
|
|
||||||
height: 100%;
|
|
||||||
max-width: 100vw;
|
|
||||||
overflow-x: hidden;
|
|
||||||
font-family: "Open Sans", system-ui, -apple-system, "Helvetica Neue", sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
color: rgb(var(--foreground-rgb));
|
|
||||||
background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb));
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import {Module} from "@/interfaces";
|
import {Module} from "@/interfaces";
|
||||||
import {LevelScore} from "@/constants/ielts";
|
import {LevelScore} from "@/constants/ielts";
|
||||||
|
import {Stat, User} from "@/interfaces/user";
|
||||||
|
|
||||||
type Type = "academic" | "general";
|
type Type = "academic" | "general";
|
||||||
|
|
||||||
@@ -178,3 +179,28 @@ export const getLevelLabel = (level: number) => {
|
|||||||
|
|
||||||
return ["Proficiency", "C2"];
|
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);
|
||||||
|
};
|
||||||
|
|||||||
@@ -137,5 +137,6 @@ export const convertToUserSolutions = (stats: Stat[]): UserSolution[] => {
|
|||||||
solutions: stat.solutions,
|
solutions: stat.solutions,
|
||||||
type: stat.type,
|
type: stat.type,
|
||||||
module: stat.module,
|
module: stat.module,
|
||||||
|
shuffleMaps: stat.shuffleMaps
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,13 +3,37 @@ module.exports = {
|
|||||||
content: ["./src/**/*.{html,tsx,ts,js,jsx}"],
|
content: ["./src/**/*.{html,tsx,ts,js,jsx}"],
|
||||||
safelist: [
|
safelist: [
|
||||||
{
|
{
|
||||||
pattern: /bg-ai-detection-result-(ai|mixed|human)/,
|
pattern: /bg-ai-detection-result-(ai|mixed|human)/,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: "2rem",
|
||||||
|
screens: {
|
||||||
|
"2xl": "1400px",
|
||||||
|
},
|
||||||
|
},
|
||||||
extend: {
|
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: {
|
boxShadow: {
|
||||||
'training-inset': 'inset 0px 2px 18px 0px #00000029',
|
"training-inset": "inset 0px 2px 18px 0px #00000029",
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
mti: {
|
mti: {
|
||||||
@@ -42,18 +66,18 @@ module.exports = {
|
|||||||
"ai-detection": {
|
"ai-detection": {
|
||||||
result: {
|
result: {
|
||||||
ai: {DEFAULT: "#f4bf4f", text: "#f0bc4f", bg: "#fff8e8"},
|
ai: {DEFAULT: "#f4bf4f", text: "#f0bc4f", bg: "#fff8e8"},
|
||||||
mixed: {DEFAULT:"#93aafb", bg: "rgba(147, 170, 251, 0.3)"},
|
mixed: {DEFAULT: "#93aafb", bg: "rgba(147, 170, 251, 0.3)"},
|
||||||
human: {DEFAULT:"#50c08a", bg: "#e9f9ed"}
|
human: {DEFAULT: "#50c08a", bg: "#e9f9ed"},
|
||||||
},
|
},
|
||||||
confidence: {
|
confidence: {
|
||||||
high: {DEFAULT: "#84d1ac", transparent: "#daf0e3"},
|
high: {DEFAULT: "#84d1ac", transparent: "#daf0e3"},
|
||||||
medium: {DEFAULT: "#f7ec88", transparent: "#fcf8d8"},
|
medium: {DEFAULT: "#f7ec88", transparent: "#fcf8d8"},
|
||||||
low: {DEFAULT: "#ffc1c1", transparent: "#ffebe9"},
|
low: {DEFAULT: "#ffc1c1", transparent: "#ffebe9"},
|
||||||
border: "#888888"
|
border: "#888888",
|
||||||
},
|
},
|
||||||
highlight: "#ffefb7",
|
highlight: "#ffefb7",
|
||||||
text: "#8992B1"
|
text: "#8992B1",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
screens: {
|
screens: {
|
||||||
"-sm": {max: "639px"},
|
"-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
594
yarn.lock
@@ -149,6 +149,14 @@
|
|||||||
"@dnd-kit/utilities" "^3.2.2"
|
"@dnd-kit/utilities" "^3.2.2"
|
||||||
tslib "^2.0.0"
|
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":
|
"@dnd-kit/utilities@^3.2.2":
|
||||||
version "3.2.2"
|
version "3.2.2"
|
||||||
resolved "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz"
|
resolved "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz"
|
||||||
@@ -740,6 +748,14 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@floating-ui/utils" "^0.2.5"
|
"@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":
|
"@floating-ui/dom@^1.0.1", "@floating-ui/dom@^1.6.1":
|
||||||
version "1.6.8"
|
version "1.6.8"
|
||||||
resolved "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.8.tgz"
|
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/core" "^1.6.0"
|
||||||
"@floating-ui/utils" "^0.2.5"
|
"@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":
|
"@floating-ui/utils@^0.2.5":
|
||||||
version "0.2.5"
|
version "0.2.5"
|
||||||
resolved "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.5.tgz"
|
resolved "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.5.tgz"
|
||||||
integrity sha512-sTcG+QZ6fdEUObICavU+aB3Mp8HY4n14wYHdxK4fXjPmv3PXZZeY5RaguJmGyeH/CJQhX3fqKUtS4qc1LoHwhQ==
|
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":
|
"@google-cloud/firestore@^6.8.0":
|
||||||
version "6.8.0"
|
version "6.8.0"
|
||||||
resolved "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-6.8.0.tgz"
|
resolved "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-6.8.0.tgz"
|
||||||
@@ -843,12 +880,15 @@
|
|||||||
protobufjs "^7.0.0"
|
protobufjs "^7.0.0"
|
||||||
yargs "^16.2.0"
|
yargs "^16.2.0"
|
||||||
|
|
||||||
"@headlessui/react@^1.7.13":
|
"@headlessui/react@^2.1.2":
|
||||||
version "1.7.14"
|
version "2.1.2"
|
||||||
resolved "https://registry.npmjs.org/@headlessui/react/-/react-1.7.14.tgz"
|
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-2.1.2.tgz#3ca9378d7d0db6aefdb135f957815790786214ef"
|
||||||
integrity sha512-znzdq9PG8rkwcu9oQ2FwIy0ZFtP9Z7ycS+BAqJ3R5EIqC/0bJGvhT7193rFf+45i9nnPsYvCQVW4V/bB9Xc+gA==
|
integrity sha512-Kb3hgk9gRNRcTZktBrKdHhF3xFhYkca1Rk6e1/im2ENf83dgN54orMW0uSKTXFnUpZOUFZ+wcY05LlipwgZIFQ==
|
||||||
dependencies:
|
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":
|
"@humanwhocodes/config-array@^0.11.8":
|
||||||
version "0.11.8"
|
version "0.11.8"
|
||||||
@@ -952,10 +992,10 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
prop-types "^15.7.2"
|
prop-types "^15.7.2"
|
||||||
|
|
||||||
"@next/env@13.1.6":
|
"@next/env@14.2.5":
|
||||||
version "13.1.6"
|
version "14.2.5"
|
||||||
resolved "https://registry.npmjs.org/@next/env/-/env-13.1.6.tgz"
|
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.5.tgz#1d9328ab828711d3517d0a1d505acb55e5ef7ad0"
|
||||||
integrity sha512-s+W9Fdqh5MFk6ECrbnVmmAOwxKQuhGMT7xXHrkYIBMBcTiOqNWhv5KbJIboKR5STXxNXl32hllnvKaffzFaWQg==
|
integrity sha512-/zZGkrTOsraVfYjGP8uM0p6r0BDT6xWpkjdVbcz66PJVSpwXX3yNiRycxAuDfBKGWBrZBXRuK/YVlkNgxHGwmA==
|
||||||
|
|
||||||
"@next/eslint-plugin-next@13.1.6":
|
"@next/eslint-plugin-next@13.1.6":
|
||||||
version "13.1.6"
|
version "13.1.6"
|
||||||
@@ -964,75 +1004,50 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
glob "7.1.7"
|
glob "7.1.7"
|
||||||
|
|
||||||
"@next/font@13.1.6":
|
"@next/swc-darwin-arm64@14.2.5":
|
||||||
version "13.1.6"
|
version "14.2.5"
|
||||||
resolved "https://registry.npmjs.org/@next/font/-/font-13.1.6.tgz"
|
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.5.tgz#d0a160cf78c18731c51cc0bff131c706b3e9bb05"
|
||||||
integrity sha512-AITjmeb1RgX1HKMCiA39ztx2mxeAyxl4ljv2UoSBUGAbFFMg8MO7YAvjHCgFhD39hL7YTbFjol04e/BPBH5RzQ==
|
integrity sha512-/9zVxJ+K9lrzSGli1///ujyRfon/ZneeZ+v4ptpiPoOU+GKZnm8Wj8ELWU1Pm7GHltYRBklmXMTUqM/DqQ99FQ==
|
||||||
|
|
||||||
"@next/swc-android-arm-eabi@13.1.6":
|
"@next/swc-darwin-x64@14.2.5":
|
||||||
version "13.1.6"
|
version "14.2.5"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.1.6.tgz#d766dfc10e27814d947b20f052067c239913dbcc"
|
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.5.tgz#eb832a992407f6e6352eed05a073379f1ce0589c"
|
||||||
integrity sha512-F3/6Z8LH/pGlPzR1AcjPFxx35mPqjE5xZcf+IL+KgbW9tMkp7CYi1y7qKrEWU7W4AumxX/8OINnDQWLiwLasLQ==
|
integrity sha512-vXHOPCwfDe9qLDuq7U1OYM2wUY+KQ4Ex6ozwsKxp26BlJ6XXbHleOUldenM67JRyBfVjv371oneEvYd3H2gNSA==
|
||||||
|
|
||||||
"@next/swc-android-arm64@13.1.6":
|
"@next/swc-linux-arm64-gnu@14.2.5":
|
||||||
version "13.1.6"
|
version "14.2.5"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-13.1.6.tgz#f37a98d5f18927d8c9970d750d516ac779465176"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.5.tgz#098fdab57a4664969bc905f5801ef5a89582c689"
|
||||||
integrity sha512-cMwQjnB8vrYkWyK/H0Rf2c2pKIH4RGjpKUDvbjVAit6SbwPDpmaijLio0LWFV3/tOnY6kvzbL62lndVA0mkYpw==
|
integrity sha512-vlhB8wI+lj8q1ExFW8lbWutA4M2ZazQNvMWuEDqZcuJJc78iUnLdPPunBPX8rC4IgT6lIx/adB+Cwrl99MzNaA==
|
||||||
|
|
||||||
"@next/swc-darwin-arm64@13.1.6":
|
"@next/swc-linux-arm64-musl@14.2.5":
|
||||||
version "13.1.6"
|
version "14.2.5"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.1.6.tgz#ec1b90fd9bf809d8b81004c5182e254dced4ad96"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.5.tgz#243a1cc1087fb75481726dd289c7b219fa01f2b5"
|
||||||
integrity sha512-KKRQH4DDE4kONXCvFMNBZGDb499Hs+xcFAwvj+rfSUssIDrZOlyfJNy55rH5t2Qxed1e4K80KEJgsxKQN1/fyw==
|
integrity sha512-NpDB9NUR2t0hXzJJwQSGu1IAOYybsfeB+LxpGsXrRIb7QOrYmidJz3shzY8cM6+rO4Aojuef0N/PEaX18pi9OA==
|
||||||
|
|
||||||
"@next/swc-darwin-x64@13.1.6":
|
"@next/swc-linux-x64-gnu@14.2.5":
|
||||||
version "13.1.6"
|
version "14.2.5"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.1.6.tgz#e869ac75d16995eee733a7d1550322d9051c1eb4"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.5.tgz#b8a2e436387ee4a52aa9719b718992e0330c4953"
|
||||||
integrity sha512-/uOky5PaZDoaU99ohjtNcDTJ6ks/gZ5ykTQDvNZDjIoCxFe3+t06bxsTPY6tAO6uEAw5f6vVFX5H5KLwhrkZCA==
|
integrity sha512-8XFikMSxWleYNryWIjiCX+gU201YS+erTUidKdyOVYi5qUQo/gRxv/3N1oZFCgqpesN6FPeqGM72Zve+nReVXQ==
|
||||||
|
|
||||||
"@next/swc-freebsd-x64@13.1.6":
|
"@next/swc-linux-x64-musl@14.2.5":
|
||||||
version "13.1.6"
|
version "14.2.5"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.1.6.tgz#84a7b2e423a2904afc2edca21c2f1ba6b53fa4c1"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.5.tgz#cb8a9adad5fb8df86112cfbd363aab5c6d32757b"
|
||||||
integrity sha512-qaEALZeV7to6weSXk3Br80wtFQ7cFTpos/q+m9XVRFggu+8Ib895XhMWdJBzew6aaOcMvYR6KQ6JmHA2/eMzWw==
|
integrity sha512-6QLwi7RaYiQDcRDSU/os40r5o06b5ue7Jsk5JgdRBGGp8l37RZEh9JsLSM8QF0YDsgcosSeHjglgqi25+m04IQ==
|
||||||
|
|
||||||
"@next/swc-linux-arm-gnueabihf@13.1.6":
|
"@next/swc-win32-arm64-msvc@14.2.5":
|
||||||
version "13.1.6"
|
version "14.2.5"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.1.6.tgz#980eed1f655ff8a72187d8a6ef9e73ac39d20d23"
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.5.tgz#81f996c1c38ea0900d4e7719cc8814be8a835da0"
|
||||||
integrity sha512-OybkbC58A1wJ+JrJSOjGDvZzrVEQA4sprJejGqMwiZyLqhr9Eo8FXF0y6HL+m1CPCpPhXEHz/2xKoYsl16kNqw==
|
integrity sha512-1GpG2VhbspO+aYoMOQPQiqc/tG3LzmsdBH0LhnDS3JrtDx2QmzXe0B6mSZZiN3Bq7IOMXxv1nlsjzoS1+9mzZw==
|
||||||
|
|
||||||
"@next/swc-linux-arm64-gnu@13.1.6":
|
"@next/swc-win32-ia32-msvc@14.2.5":
|
||||||
version "13.1.6"
|
version "14.2.5"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.1.6.tgz#87a71db21cded3f7c63d1d19079845c59813c53d"
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.5.tgz#f61c74ce823e10b2bc150e648fc192a7056422e0"
|
||||||
integrity sha512-yCH+yDr7/4FDuWv6+GiYrPI9kcTAO3y48UmaIbrKy8ZJpi7RehJe3vIBRUmLrLaNDH3rY1rwoHi471NvR5J5NQ==
|
integrity sha512-Igh9ZlxwvCDsu6438FXlQTHlRno4gFpJzqPjSIBZooD22tKeI4fE/YMRoHVJHmrQ2P5YL1DoZ0qaOKkbeFWeMg==
|
||||||
|
|
||||||
"@next/swc-linux-arm64-musl@13.1.6":
|
"@next/swc-win32-x64-msvc@14.2.5":
|
||||||
version "13.1.6"
|
version "14.2.5"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.1.6.tgz#c5aac8619331b9fd030603bbe2b36052011e11de"
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.5.tgz#ed199a920efb510cfe941cd75ed38a7be21e756f"
|
||||||
integrity sha512-ECagB8LGX25P9Mrmlc7Q/TQBb9rGScxHbv/kLqqIWs2fIXy6Y/EiBBiM72NTwuXUFCNrWR4sjUPSooVBJJ3ESQ==
|
integrity sha512-tEQ7oinq1/CjSG9uSTerca3v4AZ+dFa+4Yu6ihaG8Ud8ddqLQgFGcnwYls13H5X5CPDPZJdYxyeMui6muOLd4g==
|
||||||
|
|
||||||
"@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==
|
|
||||||
|
|
||||||
"@nodelib/fs.scandir@2.1.5":
|
"@nodelib/fs.scandir@2.1.5":
|
||||||
version "2.1.5"
|
version "2.1.5"
|
||||||
@@ -1186,6 +1201,214 @@
|
|||||||
resolved "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz"
|
resolved "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz"
|
||||||
integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
|
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":
|
"@react-pdf/fns@2.2.1":
|
||||||
version "2.2.1"
|
version "2.2.1"
|
||||||
resolved "https://registry.npmjs.org/@react-pdf/fns/-/fns-2.2.1.tgz"
|
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/shared" "~9.7.4"
|
||||||
"@react-spring/types" "~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":
|
"@rushstack/eslint-patch@^1.1.3":
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz"
|
resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz"
|
||||||
integrity sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==
|
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"
|
version "0.4.14"
|
||||||
resolved "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.14.tgz"
|
resolved "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.14.tgz"
|
||||||
integrity sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==
|
integrity sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib "^2.4.0"
|
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":
|
"@tanstack/react-table@^8.10.1":
|
||||||
version "8.19.3"
|
version "8.19.3"
|
||||||
resolved "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.19.3.tgz"
|
resolved "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.19.3.tgz"
|
||||||
@@ -1384,11 +1639,23 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@tanstack/table-core" "8.19.3"
|
"@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":
|
"@tanstack/table-core@8.19.3":
|
||||||
version "8.19.3"
|
version "8.19.3"
|
||||||
resolved "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.19.3.tgz"
|
resolved "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.19.3.tgz"
|
||||||
integrity sha512-IqREj9ADoml9zCAouIG/5kCGoyIxPFdqdyoxis9FisXFi5vT+iYfEfLosq4xkU/iDbMcEuAj+X8dWRLvKYDNoQ==
|
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":
|
"@tootallnate/once@2":
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz"
|
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"
|
resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz"
|
||||||
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
|
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:
|
aria-query@^5.1.3:
|
||||||
version "5.1.3"
|
version "5.1.3"
|
||||||
resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz"
|
resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz"
|
||||||
@@ -2251,10 +2525,19 @@ buffer@^6:
|
|||||||
base64-js "^1.3.1"
|
base64-js "^1.3.1"
|
||||||
ieee754 "^1.2.1"
|
ieee754 "^1.2.1"
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
buffers@~0.1.1:
|
buffers@~0.1.1:
|
||||||
version "0.1.1"
|
version "0.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb"
|
resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb"
|
||||||
integrity sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==
|
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:
|
call-bind@^1.0.2, call-bind@^1.0.7:
|
||||||
version "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"
|
resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz"
|
||||||
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
|
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"
|
version "1.0.30001480"
|
||||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001480.tgz"
|
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001480.tgz"
|
||||||
integrity sha512-q7cpoPPvZYgtyC4VaBSN0Bt+PJ4c4EYRf0DrduInOz2SkFpHD5p3LnvEpqBp7UnJn+8x1Ogl1s38saUxe+ihQQ==
|
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:
|
catharsis@^0.9.0:
|
||||||
version "0.9.0"
|
version "0.9.0"
|
||||||
resolved "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz"
|
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"
|
resolved "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz"
|
||||||
integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
|
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:
|
classnames@^2.2.6, classnames@^2.3.0, classnames@^2.5.1:
|
||||||
version "2.5.1"
|
version "2.5.1"
|
||||||
resolved "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz"
|
resolved "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz"
|
||||||
integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
|
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"
|
version "0.0.1"
|
||||||
resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz"
|
resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz"
|
||||||
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
||||||
@@ -2378,11 +2673,21 @@ clone@^2.1.2:
|
|||||||
resolved "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz"
|
resolved "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz"
|
||||||
integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==
|
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"
|
version "1.2.1"
|
||||||
resolved "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz"
|
resolved "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz"
|
||||||
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
|
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:
|
color-convert@^1.9.0:
|
||||||
version "1.9.3"
|
version "1.9.3"
|
||||||
resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz"
|
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"
|
resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz"
|
||||||
integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==
|
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:
|
dezalgo@^1.0.4:
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz"
|
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"
|
has-symbols "^1.0.3"
|
||||||
hasown "^2.0.0"
|
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:
|
get-symbol-description@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz"
|
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"
|
has "^1.0.3"
|
||||||
side-channel "^1.0.4"
|
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:
|
iron-session@^6.3.1:
|
||||||
version "6.3.1"
|
version "6.3.1"
|
||||||
resolved "https://registry.npmjs.org/iron-session/-/iron-session-6.3.1.tgz"
|
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"
|
object-assign "^4.0.1"
|
||||||
thenify-all "^1.0.0"
|
thenify-all "^1.0.0"
|
||||||
|
|
||||||
nanoid@^3.3.4, nanoid@^3.3.6:
|
nanoid@^3.3.6:
|
||||||
version "3.3.6"
|
version "3.3.6"
|
||||||
resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz"
|
resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz"
|
||||||
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
|
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"
|
resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz"
|
||||||
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
||||||
|
|
||||||
next@13.1.6:
|
next@^14.2.5:
|
||||||
version "13.1.6"
|
version "14.2.5"
|
||||||
resolved "https://registry.npmjs.org/next/-/next-13.1.6.tgz"
|
resolved "https://registry.yarnpkg.com/next/-/next-14.2.5.tgz#afe4022bb0b752962e2205836587a289270efbea"
|
||||||
integrity sha512-hHlbhKPj9pW+Cymvfzc15lvhaOZ54l+8sXDXJWm3OBNBzgrVj6hwGPmqqsXg40xO1Leq+kXpllzRPuncpC0Phw==
|
integrity sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@next/env" "13.1.6"
|
"@next/env" "14.2.5"
|
||||||
"@swc/helpers" "0.4.14"
|
"@swc/helpers" "0.5.5"
|
||||||
caniuse-lite "^1.0.30001406"
|
busboy "1.6.0"
|
||||||
postcss "8.4.14"
|
caniuse-lite "^1.0.30001579"
|
||||||
|
graceful-fs "^4.2.11"
|
||||||
|
postcss "8.4.31"
|
||||||
styled-jsx "5.1.1"
|
styled-jsx "5.1.1"
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
"@next/swc-android-arm-eabi" "13.1.6"
|
"@next/swc-darwin-arm64" "14.2.5"
|
||||||
"@next/swc-android-arm64" "13.1.6"
|
"@next/swc-darwin-x64" "14.2.5"
|
||||||
"@next/swc-darwin-arm64" "13.1.6"
|
"@next/swc-linux-arm64-gnu" "14.2.5"
|
||||||
"@next/swc-darwin-x64" "13.1.6"
|
"@next/swc-linux-arm64-musl" "14.2.5"
|
||||||
"@next/swc-freebsd-x64" "13.1.6"
|
"@next/swc-linux-x64-gnu" "14.2.5"
|
||||||
"@next/swc-linux-arm-gnueabihf" "13.1.6"
|
"@next/swc-linux-x64-musl" "14.2.5"
|
||||||
"@next/swc-linux-arm64-gnu" "13.1.6"
|
"@next/swc-win32-arm64-msvc" "14.2.5"
|
||||||
"@next/swc-linux-arm64-musl" "13.1.6"
|
"@next/swc-win32-ia32-msvc" "14.2.5"
|
||||||
"@next/swc-linux-x64-gnu" "13.1.6"
|
"@next/swc-win32-x64-msvc" "14.2.5"
|
||||||
"@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"
|
|
||||||
|
|
||||||
nice-try@^1.0.4:
|
nice-try@^1.0.4:
|
||||||
version "1.0.5"
|
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"
|
resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz"
|
||||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||||
|
|
||||||
postcss@8.4.14:
|
postcss@8.4.31:
|
||||||
version "8.4.14"
|
version "8.4.31"
|
||||||
resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz"
|
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
|
||||||
integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==
|
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
nanoid "^3.3.4"
|
nanoid "^3.3.6"
|
||||||
picocolors "^1.0.0"
|
picocolors "^1.0.0"
|
||||||
source-map-js "^1.0.2"
|
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"
|
react-fast-compare "^3.0.1"
|
||||||
warning "^4.0.2"
|
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:
|
react-select@^5.7.5:
|
||||||
version "5.8.0"
|
version "5.8.0"
|
||||||
resolved "https://registry.npmjs.org/react-select/-/react-select-5.8.0.tgz"
|
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"
|
resolved "https://registry.npmjs.org/react-string-replace/-/react-string-replace-1.1.0.tgz"
|
||||||
integrity sha512-N6RalSDFGbOHs0IJi1H611WbZsvk3ZT47Jl2JEXFbiS3kTwsdCYij70Keo/tWtLy7sfhDsYm7CwNM/WmjXIaMw==
|
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:
|
react-toastify@^9.1.2:
|
||||||
version "9.1.2"
|
version "9.1.2"
|
||||||
resolved "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.2.tgz"
|
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"
|
resolved "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz"
|
||||||
integrity sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==
|
integrity sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
"string-width-cjs@npm:string-width@^4.2.0":
|
"string-width-cjs@npm:string-width@^4.2.0":
|
||||||
version "4.2.3"
|
version "4.2.3"
|
||||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
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"
|
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:
|
"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"
|
version "4.2.3"
|
||||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
@@ -6269,11 +6626,26 @@ synckit@^0.8.4:
|
|||||||
"@pkgr/utils" "^2.3.1"
|
"@pkgr/utils" "^2.3.1"
|
||||||
tslib "^2.5.0"
|
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:
|
tailwind-scrollbar-hide@^1.1.7:
|
||||||
version "1.1.7"
|
version "1.1.7"
|
||||||
resolved "https://registry.npmjs.org/tailwind-scrollbar-hide/-/tailwind-scrollbar-hide-1.1.7.tgz"
|
resolved "https://registry.npmjs.org/tailwind-scrollbar-hide/-/tailwind-scrollbar-hide-1.1.7.tgz"
|
||||||
integrity sha512-X324n9OtpTmOMqEgDUEA/RgLrNfBF/jwJdctaPZDzB3mppxJk7TLIDmOreEDm1Bq4R9LSPu4Epf8VSdovNU+iA==
|
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:
|
tailwindcss@^3, tailwindcss@^3.2.4:
|
||||||
version "3.3.1"
|
version "3.3.1"
|
||||||
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.1.tgz"
|
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.1.tgz"
|
||||||
@@ -6560,6 +6932,13 @@ uri-js@^4.2.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode "^2.1.0"
|
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:
|
use-file-picker@^2.1.0:
|
||||||
version "2.1.2"
|
version "2.1.2"
|
||||||
resolved "https://registry.npmjs.org/use-file-picker/-/use-file-picker-2.1.2.tgz"
|
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"
|
resolved "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz"
|
||||||
integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==
|
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:
|
use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz"
|
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"
|
resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz"
|
||||||
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
|
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-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||||
|
name wrap-ansi-cjs
|
||||||
|
>>>>>>> develop
|
||||||
version "7.0.0"
|
version "7.0.0"
|
||||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
|
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
|
||||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||||
|
|||||||
Reference in New Issue
Block a user