Merged in feature/training-content (pull request #58)
Feature/training content Approved-by: Tiago Ribeiro
This commit is contained in:
116
package-lock.json
generated
116
package-lock.json
generated
@@ -17,6 +17,7 @@
|
||||
"@paypal/paypal-js": "^7.1.0",
|
||||
"@paypal/react-paypal-js": "^8.1.3",
|
||||
"@react-pdf/renderer": "^3.1.14",
|
||||
"@react-spring/web": "^9.7.4",
|
||||
"@tanstack/react-table": "^8.10.1",
|
||||
"@types/node": "18.13.0",
|
||||
"@types/react": "18.0.27",
|
||||
@@ -2219,6 +2220,72 @@
|
||||
"resolved": "https://registry.npmjs.org/@react-pdf/types/-/types-2.5.0.tgz",
|
||||
"integrity": "sha512-XsVRkt0hQ60I4e3leAVt+aZR3KJCaJd179BfJHAv4F4x6Vq3yqkry8lcbUWKGKDw1j3/8sW4FsgGR41SFvsG9A=="
|
||||
},
|
||||
"node_modules/@react-spring/animated": {
|
||||
"version": "9.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.4.tgz",
|
||||
"integrity": "sha512-7As+8Pty2QlemJ9O5ecsuPKjmO0NKvmVkRR1n6mEotFgWar8FKuQt2xgxz3RTgxcccghpx1YdS1FCdElQNexmQ==",
|
||||
"dependencies": {
|
||||
"@react-spring/shared": "~9.7.4",
|
||||
"@react-spring/types": "~9.7.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-spring/core": {
|
||||
"version": "9.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.4.tgz",
|
||||
"integrity": "sha512-GzjA44niEJBFUe9jN3zubRDDDP2E4tBlhNlSIkTChiNf9p4ZQlgXBg50qbXfSXHQPHak/ExYxwhipKVsQ/sUTw==",
|
||||
"dependencies": {
|
||||
"@react-spring/animated": "~9.7.4",
|
||||
"@react-spring/shared": "~9.7.4",
|
||||
"@react-spring/types": "~9.7.4"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/react-spring/donate"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-spring/rafz": {
|
||||
"version": "9.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.4.tgz",
|
||||
"integrity": "sha512-mqDI6rW0Ca8IdryOMiXRhMtVGiEGLIO89vIOyFQXRIwwIMX30HLya24g9z4olDvFyeDW3+kibiKwtZnA4xhldA=="
|
||||
},
|
||||
"node_modules/@react-spring/shared": {
|
||||
"version": "9.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.4.tgz",
|
||||
"integrity": "sha512-bEPI7cQp94dOtCFSEYpxvLxj0+xQfB5r9Ru1h8OMycsIq7zFZon1G0sHrBLaLQIWeMCllc4tVDYRTLIRv70C8w==",
|
||||
"dependencies": {
|
||||
"@react-spring/rafz": "~9.7.4",
|
||||
"@react-spring/types": "~9.7.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-spring/types": {
|
||||
"version": "9.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.4.tgz",
|
||||
"integrity": "sha512-iQVztO09ZVfsletMiY+DpT/JRiBntdsdJ4uqk3UJFhrhS8mIC9ZOZbmfGSRs/kdbNPQkVyzucceDicQ/3Mlj9g=="
|
||||
},
|
||||
"node_modules/@react-spring/web": {
|
||||
"version": "9.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.4.tgz",
|
||||
"integrity": "sha512-UMvCZp7I5HCVIleSa4BwbNxynqvj+mJjG2m20VO2yPoi2pnCYANy58flvz9v/YcXTAvsmL655FV3pm5fbr6akA==",
|
||||
"dependencies": {
|
||||
"@react-spring/animated": "~9.7.4",
|
||||
"@react-spring/core": "~9.7.4",
|
||||
"@react-spring/shared": "~9.7.4",
|
||||
"@react-spring/types": "~9.7.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rushstack/eslint-patch": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz",
|
||||
@@ -11551,6 +11618,55 @@
|
||||
"resolved": "https://registry.npmjs.org/@react-pdf/types/-/types-2.5.0.tgz",
|
||||
"integrity": "sha512-XsVRkt0hQ60I4e3leAVt+aZR3KJCaJd179BfJHAv4F4x6Vq3yqkry8lcbUWKGKDw1j3/8sW4FsgGR41SFvsG9A=="
|
||||
},
|
||||
"@react-spring/animated": {
|
||||
"version": "9.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.4.tgz",
|
||||
"integrity": "sha512-7As+8Pty2QlemJ9O5ecsuPKjmO0NKvmVkRR1n6mEotFgWar8FKuQt2xgxz3RTgxcccghpx1YdS1FCdElQNexmQ==",
|
||||
"requires": {
|
||||
"@react-spring/shared": "~9.7.4",
|
||||
"@react-spring/types": "~9.7.4"
|
||||
}
|
||||
},
|
||||
"@react-spring/core": {
|
||||
"version": "9.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.4.tgz",
|
||||
"integrity": "sha512-GzjA44niEJBFUe9jN3zubRDDDP2E4tBlhNlSIkTChiNf9p4ZQlgXBg50qbXfSXHQPHak/ExYxwhipKVsQ/sUTw==",
|
||||
"requires": {
|
||||
"@react-spring/animated": "~9.7.4",
|
||||
"@react-spring/shared": "~9.7.4",
|
||||
"@react-spring/types": "~9.7.4"
|
||||
}
|
||||
},
|
||||
"@react-spring/rafz": {
|
||||
"version": "9.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.4.tgz",
|
||||
"integrity": "sha512-mqDI6rW0Ca8IdryOMiXRhMtVGiEGLIO89vIOyFQXRIwwIMX30HLya24g9z4olDvFyeDW3+kibiKwtZnA4xhldA=="
|
||||
},
|
||||
"@react-spring/shared": {
|
||||
"version": "9.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.4.tgz",
|
||||
"integrity": "sha512-bEPI7cQp94dOtCFSEYpxvLxj0+xQfB5r9Ru1h8OMycsIq7zFZon1G0sHrBLaLQIWeMCllc4tVDYRTLIRv70C8w==",
|
||||
"requires": {
|
||||
"@react-spring/rafz": "~9.7.4",
|
||||
"@react-spring/types": "~9.7.4"
|
||||
}
|
||||
},
|
||||
"@react-spring/types": {
|
||||
"version": "9.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.4.tgz",
|
||||
"integrity": "sha512-iQVztO09ZVfsletMiY+DpT/JRiBntdsdJ4uqk3UJFhrhS8mIC9ZOZbmfGSRs/kdbNPQkVyzucceDicQ/3Mlj9g=="
|
||||
},
|
||||
"@react-spring/web": {
|
||||
"version": "9.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.4.tgz",
|
||||
"integrity": "sha512-UMvCZp7I5HCVIleSa4BwbNxynqvj+mJjG2m20VO2yPoi2pnCYANy58flvz9v/YcXTAvsmL655FV3pm5fbr6akA==",
|
||||
"requires": {
|
||||
"@react-spring/animated": "~9.7.4",
|
||||
"@react-spring/core": "~9.7.4",
|
||||
"@react-spring/shared": "~9.7.4",
|
||||
"@react-spring/types": "~9.7.4"
|
||||
}
|
||||
},
|
||||
"@rushstack/eslint-patch": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz",
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"@paypal/paypal-js": "^7.1.0",
|
||||
"@paypal/react-paypal-js": "^8.1.3",
|
||||
"@react-pdf/renderer": "^3.1.14",
|
||||
"@react-spring/web": "^9.7.4",
|
||||
"@tanstack/react-table": "^8.10.1",
|
||||
"@types/node": "18.13.0",
|
||||
"@types/react": "18.0.27",
|
||||
|
||||
@@ -9,7 +9,7 @@ import SegmentedProgressBar from "./SegmentedProgressBar";
|
||||
// Colors and texts scrapped from gpt's zero react bundle
|
||||
const AIDetection: React.FC<AIDetectionAttributes> = ({ predicted_class, confidence_category, class_probabilities, sentences }) => {
|
||||
const probabilityTooltipContent = `
|
||||
GTP's Zero deep learning model predicts the <br/>
|
||||
Encoach's deep learning model predicts the <br/>
|
||||
probability this text has been entirely <br/>
|
||||
generated by AI. For instance, a 40% AI <br/>
|
||||
probability does not indicate that the text<br/>
|
||||
@@ -19,7 +19,7 @@ const AIDetection: React.FC<AIDetectionAttributes> = ({ predicted_class, confide
|
||||
`;
|
||||
const confidenceTooltipContent = `
|
||||
Confidence scores are a safeguard to better<br/>
|
||||
understand AI identification results. GTP Zero<br/>
|
||||
understand AI identification results. Encoach<br/>
|
||||
trained it's deep learning model on a diverse<br/>
|
||||
dataset of millions of human and AI-written<br/>
|
||||
documents. Green scores indicate that you can scan<br/>
|
||||
@@ -32,19 +32,19 @@ const AIDetection: React.FC<AIDetectionAttributes> = ({ predicted_class, confide
|
||||
const confidenceKeywords = ["moderately", "highly", "confident", "uncertain"];
|
||||
var confidence = {
|
||||
low: {
|
||||
ai: "GPT Zero is uncertain about this text. If GPT Zero had to classify it, it would be considered",
|
||||
human: "GPT Zero is uncertain about this text. If GPT Zero had to classify it, it would likely be considered",
|
||||
mixed: "GPT Zero is uncertain about this text. If GPT Zero had to classify it, it would likely be a"
|
||||
ai: "Encoach is uncertain about this text. If Encoach had to classify it, it would be considered",
|
||||
human: "Encoach is uncertain about this text. If Encoach had to classify it, it would likely be considered",
|
||||
mixed: "Encoach is uncertain about this text. If Encoach had to classify it, it would likely be a"
|
||||
},
|
||||
medium: {
|
||||
ai: "GPT Zero is moderately confident this text was",
|
||||
human: "GPT Zero is moderately confident this text is entirely",
|
||||
mixed: "GPT Zero is moderately confident this text is a"
|
||||
ai: "Encoach is moderately confident this text was",
|
||||
human: "Encoach is moderately confident this text is entirely",
|
||||
mixed: "Encoach is moderately confident this text is a"
|
||||
},
|
||||
high: {
|
||||
ai: "GPT Zero is highly confident this text was",
|
||||
human: "GPT Zero is highly confident this text is entirely",
|
||||
mixed: "GPT Zero is highly confident this text is a"
|
||||
ai: "Encoach is highly confident this text was",
|
||||
human: "Encoach is highly confident this text is entirely",
|
||||
mixed: "Encoach is highly confident this text is a"
|
||||
}
|
||||
}
|
||||
var classPrediction = {
|
||||
@@ -107,7 +107,7 @@ const AIDetection: React.FC<AIDetectionAttributes> = ({ predicted_class, confide
|
||||
<Tooltip id="probability-tooltip" className="z-50 bg-white shadow-md rounded-sm" style={tooltipStyle} />
|
||||
<Tooltip id="confidence-tooltip" className="z-50 bg-white shadow-md rounded-sm" style={tooltipStyle} />
|
||||
<div className="flex flex-col bg-white p-6 rounded-lg shadow-lg gap-16">
|
||||
<h1 className="text-lg font-semibold">GPT Zero AI Detection Results</h1>
|
||||
<h1 className="text-lg font-semibold">Encoach Detection Results</h1>
|
||||
<div className="flex flex-row -md:flex-col -lg:gap-0 -xl:gap-10 gap-20 items-stretch -md:items-center">
|
||||
<div className="flex -md:w-5/6 w-1/2 justify-center">
|
||||
<div className="flex flex-col border rounded-xl">
|
||||
|
||||
24
src/components/ModuleBadge.tsx
Normal file
24
src/components/ModuleBadge.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import clsx from "clsx";
|
||||
import { BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen } from "react-icons/bs";
|
||||
|
||||
const ModuleBadge: React.FC<{ module: string; level?: number }> = ({ module, level }) => (
|
||||
<div
|
||||
className={clsx(
|
||||
"flex gap-2 items-center w-fit text-white -md:px-4 xl:px-4 md:px-2 py-2 rounded-xl",
|
||||
module === "reading" && "bg-ielts-reading",
|
||||
module === "listening" && "bg-ielts-listening",
|
||||
module === "writing" && "bg-ielts-writing",
|
||||
module === "speaking" && "bg-ielts-speaking",
|
||||
module === "level" && "bg-ielts-level",
|
||||
)}>
|
||||
{module === "reading" && <BsBook className="w-4 h-4" />}
|
||||
{module === "listening" && <BsHeadphones className="w-4 h-4" />}
|
||||
{module === "writing" && <BsPen className="w-4 h-4" />}
|
||||
{module === "speaking" && <BsMegaphone className="w-4 h-4" />}
|
||||
{module === "level" && <BsClipboard className="w-4 h-4" />}
|
||||
{/* do not switch to level && it will convert the 0.0 to 0*/}
|
||||
{level !== undefined && (<span className="text-sm">{level.toFixed(1)}</span>)}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ModuleBadge;
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
BsClipboardData,
|
||||
BsFileLock,
|
||||
} from "react-icons/bs";
|
||||
import { CiDumbbell } from "react-icons/ci";
|
||||
import {RiLogoutBoxFill} from "react-icons/ri";
|
||||
import {SlPencil} from "react-icons/sl";
|
||||
import {FaAward} from "react-icons/fa";
|
||||
@@ -109,6 +110,9 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, u
|
||||
{checkAccess(user, getTypesOfUser(["agent"]), "viewRecords") && (
|
||||
<Nav disabled={disableNavigation} Icon={BsClockHistory} label="Record" path={path} keyPath="/record" isMinimized={isMinimized} />
|
||||
)}
|
||||
{checkAccess(user, getTypesOfUser(["agent"]), "viewRecords") && (
|
||||
<Nav disabled={disableNavigation} Icon={CiDumbbell} label="Training" path={path} keyPath="/training" isMinimized={isMinimized} />
|
||||
)}
|
||||
{checkAccess(user, ["admin", "developer", "agent", "corporate", "mastercorporate"], "viewPaymentRecords") && (
|
||||
<Nav
|
||||
disabled={disableNavigation}
|
||||
|
||||
256
src/components/StatGridItem.tsx
Normal file
256
src/components/StatGridItem.tsx
Normal file
@@ -0,0 +1,256 @@
|
||||
import React from 'react';
|
||||
import { BsClock, BsXCircle } from 'react-icons/bs';
|
||||
import clsx from 'clsx';
|
||||
import { Stat, User } from '@/interfaces/user';
|
||||
import { Module } from "@/interfaces";
|
||||
import ai_usage from "@/utils/ai.detection";
|
||||
import { calculateBandScore } from "@/utils/score";
|
||||
import moment from 'moment';
|
||||
import { Assignment } from '@/interfaces/results';
|
||||
import { uuidv4 } from "@firebase/util";
|
||||
import { useRouter } from "next/router";
|
||||
import { uniqBy } from "lodash";
|
||||
import { sortByModule } from "@/utils/moduleUtils";
|
||||
import { convertToUserSolutions } from "@/utils/stats";
|
||||
import { getExamById } from "@/utils/exams";
|
||||
import { Exam, UserSolution } from '@/interfaces/exam';
|
||||
import ModuleBadge from './ModuleBadge';
|
||||
|
||||
const formatTimestamp = (timestamp: string | number) => {
|
||||
const time = typeof timestamp === "string" ? parseInt(timestamp) : timestamp;
|
||||
const date = moment(time);
|
||||
const formatter = "YYYY/MM/DD - HH:mm";
|
||||
return date.format(formatter);
|
||||
};
|
||||
|
||||
const aggregateScoresByModule = (stats: Stat[]): { module: Module; total: number; missing: number; correct: number }[] => {
|
||||
const scores: {
|
||||
[key in Module]: { total: number; missing: number; correct: number };
|
||||
} = {
|
||||
reading: {
|
||||
total: 0,
|
||||
correct: 0,
|
||||
missing: 0,
|
||||
},
|
||||
listening: {
|
||||
total: 0,
|
||||
correct: 0,
|
||||
missing: 0,
|
||||
},
|
||||
writing: {
|
||||
total: 0,
|
||||
correct: 0,
|
||||
missing: 0,
|
||||
},
|
||||
speaking: {
|
||||
total: 0,
|
||||
correct: 0,
|
||||
missing: 0,
|
||||
},
|
||||
level: {
|
||||
total: 0,
|
||||
correct: 0,
|
||||
missing: 0,
|
||||
},
|
||||
};
|
||||
|
||||
stats.forEach((x) => {
|
||||
scores[x.module!] = {
|
||||
total: scores[x.module!].total + x.score.total,
|
||||
correct: scores[x.module!].correct + x.score.correct,
|
||||
missing: scores[x.module!].missing + x.score.missing,
|
||||
};
|
||||
});
|
||||
|
||||
return Object.keys(scores)
|
||||
.filter((x) => scores[x as Module].total > 0)
|
||||
.map((x) => ({ module: x as Module, ...scores[x as Module] }));
|
||||
};
|
||||
|
||||
interface StatsGridItemProps {
|
||||
stats: Stat[];
|
||||
timestamp: string | number;
|
||||
user: User,
|
||||
assignments: Assignment[];
|
||||
users: User[];
|
||||
training?: boolean,
|
||||
selectedTrainingExams?: string[];
|
||||
setSelectedTrainingExams?: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
setExams: (exams: Exam[]) => void;
|
||||
setShowSolutions: (show: boolean) => void;
|
||||
setUserSolutions: (solutions: UserSolution[]) => void;
|
||||
setSelectedModules: (modules: Module[]) => void;
|
||||
setInactivity: (inactivity: number) => void;
|
||||
setTimeSpent: (time: number) => void;
|
||||
renderPdfIcon: (session: string, color: string, textColor: string) => React.ReactNode;
|
||||
}
|
||||
|
||||
const StatsGridItem: React.FC<StatsGridItemProps> = ({
|
||||
stats,
|
||||
timestamp,
|
||||
user,
|
||||
assignments,
|
||||
users,
|
||||
training,
|
||||
selectedTrainingExams,
|
||||
setSelectedTrainingExams,
|
||||
setExams,
|
||||
setShowSolutions,
|
||||
setUserSolutions,
|
||||
setSelectedModules,
|
||||
setInactivity,
|
||||
setTimeSpent,
|
||||
renderPdfIcon
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const correct = stats.reduce((accumulator, current) => accumulator + current.score.correct, 0);
|
||||
const total = stats.reduce((accumulator, current) => accumulator + current.score.total, 0);
|
||||
const aggregatedScores = aggregateScoresByModule(stats).filter((x) => x.total > 0);
|
||||
const assignmentID = stats.reduce((_, current) => current.assignment as any, "");
|
||||
const assignment = assignments.find((a) => a.id === assignmentID);
|
||||
const isDisabled = stats.some((x) => x.isDisabled);
|
||||
|
||||
const aiUsage = Math.round(ai_usage(stats) * 100);
|
||||
|
||||
const aggregatedLevels = aggregatedScores.map((x) => ({
|
||||
module: x.module,
|
||||
level: calculateBandScore(x.correct, x.total, x.module, user.focus),
|
||||
}));
|
||||
|
||||
const textColor = clsx(
|
||||
correct / total >= 0.7 && "text-mti-purple",
|
||||
correct / total >= 0.3 && correct / total < 0.7 && "text-mti-red",
|
||||
correct / total < 0.3 && "text-mti-rose",
|
||||
);
|
||||
|
||||
const { timeSpent, inactivity, session } = stats[0];
|
||||
|
||||
const selectExam = () => {
|
||||
if (training && !isDisabled && typeof setSelectedTrainingExams !== "undefined" && typeof timestamp == "string") {
|
||||
setSelectedTrainingExams(prevExams => {
|
||||
const index = prevExams.indexOf(timestamp);
|
||||
|
||||
if (index !== -1) {
|
||||
const newExams = [...prevExams];
|
||||
newExams.splice(index, 1);
|
||||
return newExams;
|
||||
} else {
|
||||
return [...prevExams, timestamp];
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const examPromises = uniqBy(stats, "exam").map((stat) => {
|
||||
return getExamById(stat.module, stat.exam);
|
||||
});
|
||||
|
||||
if (isDisabled) return;
|
||||
|
||||
Promise.all(examPromises).then((exams) => {
|
||||
if (exams.every((x) => !!x)) {
|
||||
if (!!timeSpent) setTimeSpent(timeSpent);
|
||||
if (!!inactivity) setInactivity(inactivity);
|
||||
setUserSolutions(convertToUserSolutions(stats));
|
||||
setShowSolutions(true);
|
||||
setExams(exams.map((x) => x!).sort(sortByModule));
|
||||
setSelectedModules(
|
||||
exams
|
||||
.map((x) => x!)
|
||||
.sort(sortByModule)
|
||||
.map((x) => x!.module),
|
||||
);
|
||||
router.push("/exercises");
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<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 className="flex items-center gap-2">
|
||||
{!!timeSpent && (
|
||||
<span className="text-sm flex gap-2 items-center tooltip" data-tip="Time Spent">
|
||||
<BsClock /> {Math.floor(timeSpent / 60)} minutes
|
||||
</span>
|
||||
)}
|
||||
{!!inactivity && (
|
||||
<span className="text-sm flex gap-2 items-center tooltip" data-tip="Inactivity">
|
||||
<BsXCircle /> {Math.floor(inactivity / 60)} minutes
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-row gap-2">
|
||||
<span className={textColor}>
|
||||
Level{" "}
|
||||
{(aggregatedLevels.reduce((accumulator, current) => accumulator + current.level, 0) / aggregatedLevels.length).toFixed(1)}
|
||||
</span>
|
||||
{renderPdfIcon(session, textColor, textColor)}
|
||||
</div>
|
||||
{aiUsage >= 50 && user.type !== "student" && (
|
||||
<div className={clsx(
|
||||
"ml-auto border px-1 rounded w-fit mr-1",
|
||||
{
|
||||
'bg-orange-100 border-orange-400 text-orange-700': aiUsage < 80,
|
||||
'bg-red-100 border-red-400 text-red-700': aiUsage >= 80,
|
||||
}
|
||||
)}>
|
||||
<span className="text-xs">AI Usage</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full flex flex-col gap-1">
|
||||
<div className="grid grid-cols-4 gap-2 place-items-start w-full -md:mt-2">
|
||||
{aggregatedLevels.map(({ module, level }) => (
|
||||
<ModuleBadge key={module} module={module} level={level} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{assignment && (
|
||||
<span className="font-light text-sm">
|
||||
Assignment: {assignment.name}, Teacher: {users.find((u) => u.id === assignment.assigner)?.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
key={uuidv4()}
|
||||
className={clsx(
|
||||
"flex flex-col justify-between gap-4 border border-mti-gray-platinum p-4 cursor-pointer rounded-xl transition ease-in-out duration-300 -md:hidden",
|
||||
isDisabled && "grayscale tooltip",
|
||||
correct / total >= 0.7 && "hover:border-mti-purple",
|
||||
correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red",
|
||||
correct / total < 0.3 && "hover:border-mti-rose",
|
||||
typeof selectedTrainingExams !== "undefined" && typeof timestamp === "string" && selectedTrainingExams.includes(timestamp) && "border-2 border-slate-600",
|
||||
)}
|
||||
onClick={selectExam}
|
||||
data-tip="This exam is still being evaluated..."
|
||||
role="button">
|
||||
{content}
|
||||
</div>
|
||||
<div
|
||||
key={uuidv4()}
|
||||
className={clsx(
|
||||
"flex flex-col gap-4 border border-mti-gray-platinum p-4 cursor-pointer rounded-xl transition ease-in-out duration-300 -md:tooltip md:hidden",
|
||||
correct / total >= 0.7 && "hover:border-mti-purple",
|
||||
correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red",
|
||||
correct / total < 0.3 && "hover:border-mti-rose",
|
||||
)}
|
||||
data-tip="Your screen size is too small to view previous exams."
|
||||
role="button">
|
||||
{content}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatsGridItem;
|
||||
23
src/components/TrainingContent/AnimatedHighlight.tsx
Normal file
23
src/components/TrainingContent/AnimatedHighlight.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
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;
|
||||
91
src/components/TrainingContent/Exercise.tsx
Normal file
91
src/components/TrainingContent/Exercise.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import React, { useState, useCallback } from "react";
|
||||
import ExerciseWalkthrough from "@/training/ExerciseWalkthrough";
|
||||
import { ITrainingTip, WalkthroughConfigs } from "./TrainingInterfaces";
|
||||
|
||||
|
||||
// This wrapper is just to test new exercises from the handbook, will be removed when all the tips and exercises are in firestore
|
||||
const TrainingExercise: React.FC<ITrainingTip> = (trainingTip: ITrainingTip) => {
|
||||
const leftText = "<div class=\"container mx-auto px-4 overflow-x-auto\"><table class=\"min-w-full bg-white border border-gray-300\"><thead><tr class=\"bg-gray-100\"><th class=\"py-2 px-4 border-b font-semibold text-left\">Category</th><th class=\"py-2 px-4 border-b font-semibold text-left\">Option A</th><th class=\"py-2 px-4 border-b font-semibold text-left\">Option B</th></tr></thead><tbody><tr><td class=\"py-2 px-4 border-b font-medium\">Self</td><td class=\"py-2 px-4 border-b\">You need to take care of yourself and connect with the people around you.</td><td class=\"py-2 px-4 border-b\">Focus on your interests and talents and meet people who are like you.</td></tr><tr class=\"bg-gray-50\"><td class=\"py-2 px-4 border-b font-medium\">Home</td><td class=\"py-2 px-4 border-b\">It's a good idea to paint your living room yellow.</td><td class=\"py-2 px-4 border-b\">You should arrange your home so that it makes you feel happy.</td></tr><tr><td class=\"py-2 px-4 border-b font-medium\">Financial Life</td><td class=\"py-2 px-4 border-b\">You can be happy if you have enough money, but don't want money too much.</td><td class=\"py-2 px-4 border-b\">If you waste money on things you don't need, you won't have enough money for things that you do need.</td></tr><tr class=\"bg-gray-50\"><td class=\"py-2 px-4 border-b font-medium\">Social Life</td><td class=\"py-2 px-4 border-b\">A good group of friends can increase your happiness.</td><td class=\"py-2 px-4 border-b\">Researchers say that a happy friend can increase our mood by nine percent.</td></tr><tr><td class=\"py-2 px-4 border-b font-medium\">Workplace</td><td class=\"py-2 px-4 border-b\">You spend a lot of time at work, so you should like your workplace.</td><td class=\"py-2 px-4 border-b\">Your boss needs to be someone you enjoy working for.</td></tr><tr class=\"bg-gray-50\"><td class=\"py-2 px-4 border-b font-medium\">Community</td><td class=\"py-2 px-4 border-b\">The place where you live is more important for happiness than anything else.</td><td class=\"py-2 px-4 border-b\">Live around people who have the same amount of money as you do.</td></tr></tbody></table></div>";
|
||||
const tip = {
|
||||
category: "Strategy",
|
||||
body: "<p>Look for <b>clues to the main idea</b> in the first (and sometimes second) sentence of a paragraph.</p>"
|
||||
}
|
||||
const question = "<div class=\"container mx-auto px-4 py-8\"><h2 class=\"text-2xl font-bold mb-4\">Identifying Main Ideas</h2><p class=\"text-lg leading-relaxed mb-6\">Read the statements below. Circle the main idea in each pair of statements (a or b).</p></div>";
|
||||
const rightTextData: WalkthroughConfigs[] = [
|
||||
{
|
||||
"html": "<div class='bg-blue-100 p-4 rounded-lg mb-4'><h2 class='text-xl font-bold mb-2'>Identifying Main Ideas</h2><p class='text-gray-700 leading-relaxed'>Let's analyze each pair of statements to determine which one represents the main idea. We'll focus on which statement is more general and encompasses the overall concept.</p></div>",
|
||||
"wordDelay": 200,
|
||||
"holdDelay": 5000,
|
||||
"highlight": []
|
||||
},
|
||||
{
|
||||
"html": "<div class='bg-green-50 p-4 rounded-lg mb-4'><h3 class='text-lg font-semibold mb-2'>1. Self</h3><p class='text-gray-700 leading-relaxed'>Main idea: <b>A. You need to take care of yourself and connect with the people around you.</b></p><p class='mt-2'>This statement is more comprehensive, covering both self-care and social connections. Option B is more specific and could be considered a subset of A.</p></div>",
|
||||
"wordDelay": 200,
|
||||
"holdDelay": 8000,
|
||||
"highlight": ["You need to take care of yourself and connect with the people around you."]
|
||||
},
|
||||
{
|
||||
"html": "<div class='bg-yellow-50 p-4 rounded-lg mb-4'><h3 class='text-lg font-semibold mb-2'>2. Home</h3><p class='text-gray-700 leading-relaxed'>Main idea: <b>B. You should arrange your home so that it makes you feel happy.</b></p><p class='mt-2'>This statement is more general and applies to the entire home. Option A is a specific example that could fall under this broader concept.</p></div>",
|
||||
"wordDelay": 200,
|
||||
"holdDelay": 8000,
|
||||
"highlight": ["You should arrange your home so that it makes you feel happy."]
|
||||
},
|
||||
{
|
||||
"html": "<div class='bg-pink-50 p-4 rounded-lg mb-4'><h3 class='text-lg font-semibold mb-2'>3. Financial Life</h3><p class='text-gray-700 leading-relaxed'>Main idea: <b>A. You can be happy if you have enough money, but don't want money too much.</b></p><p class='mt-2'>This statement provides a balanced view of money's role in happiness. Option B is more specific and could be seen as a consequence of wanting money too much.</p></div>",
|
||||
"wordDelay": 200,
|
||||
"holdDelay": 8000,
|
||||
"highlight": ["You can be happy if you have enough money, but don't want money too much."]
|
||||
},
|
||||
{
|
||||
"html": "<div class='bg-purple-50 p-4 rounded-lg mb-4'><h3 class='text-lg font-semibold mb-2'>4. Social Life</h3><p class='text-gray-700 leading-relaxed'>Main idea: <b>A. A good group of friends can increase your happiness.</b></p><p class='mt-2'>This statement is more general about the impact of friendships. Option B provides a specific statistic that supports this main idea.</p></div>",
|
||||
"wordDelay": 200,
|
||||
"holdDelay": 8000,
|
||||
"highlight": ["A good group of friends can increase your happiness."]
|
||||
},
|
||||
{
|
||||
"html": "<div class='bg-indigo-50 p-4 rounded-lg mb-4'><h3 class='text-lg font-semibold mb-2'>5. Workplace</h3><p class='text-gray-700 leading-relaxed'>Main idea: <b>A. You spend a lot of time at work, so you should like your workplace.</b></p><p class='mt-2'>This statement covers the overall importance of workplace satisfaction. Option B focuses on one specific aspect (the boss) and is less comprehensive.</p></div>",
|
||||
"wordDelay": 200,
|
||||
"holdDelay": 8000,
|
||||
"highlight": ["You spend a lot of time at work, so you should like your workplace."]
|
||||
},
|
||||
{
|
||||
"html": "<div class='bg-red-50 p-4 rounded-lg mb-4'><h3 class='text-lg font-semibold mb-2'>6. Community</h3><p class='text-gray-700 leading-relaxed'>Main idea: <b>A. The place where you live is more important for happiness than anything else.</b></p><p class='mt-2'>While this statement might be debatable, it's more general and encompasses the overall importance of community. Option B is a specific suggestion about community demographics.</p></div>",
|
||||
"wordDelay": 200,
|
||||
"holdDelay": 8000,
|
||||
"highlight": ["The place where you live is more important for happiness than anything else."]
|
||||
},
|
||||
{
|
||||
"html": "<div class='bg-orange-50 p-4 rounded-lg mb-4'><h3 class='text-lg font-semibold mb-2'>Key Strategy</h3><p class='text-gray-700 leading-relaxed'>When identifying main ideas:</p><ul class='list-disc pl-5 space-y-2'><li>Look for broader, more encompassing statements</li><li>Consider which statement other ideas could fall under</li><li>Identify which statement provides a general principle rather than a specific example</li></ul></div>",
|
||||
"wordDelay": 200,
|
||||
"holdDelay": 8000,
|
||||
"highlight": []
|
||||
},
|
||||
{
|
||||
"html": "<div class='bg-teal-50 p-4 rounded-lg'><h3 class='text-lg font-semibold mb-2'>Helpful Tip</h3><p class='text-gray-700 leading-relaxed'>Remember to look for clues to the main idea in the first (and sometimes second) sentence of a paragraph. In this exercise, we applied this concept to pairs of statements. This approach can help you quickly identify the central theme or main point in various types of text.</p></div>",
|
||||
"wordDelay": 200,
|
||||
"holdDelay": 5000,
|
||||
"highlight": []
|
||||
}
|
||||
]
|
||||
|
||||
const mockTip: ITrainingTip = {
|
||||
id: "some random id",
|
||||
tipCategory: tip.category,
|
||||
tipHtml: tip.body,
|
||||
standalone: false,
|
||||
exercise: {
|
||||
question: question,
|
||||
highlightable: leftText,
|
||||
segments: rightTextData
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col p-10">
|
||||
<ExerciseWalkthrough {...trainingTip}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TrainingExercise;
|
||||
287
src/components/TrainingContent/ExerciseWalkthrough.tsx
Normal file
287
src/components/TrainingContent/ExerciseWalkthrough.tsx
Normal file
@@ -0,0 +1,287 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { animated } from '@react-spring/web';
|
||||
import { FaRegCirclePlay, FaRegCircleStop } from "react-icons/fa6";
|
||||
import HighlightedContent from './AnimatedHighlight';
|
||||
import { ITrainingTip, SegmentRef, TimelineEvent } from './TrainingInterfaces';
|
||||
|
||||
|
||||
const ExerciseWalkthrough: React.FC<ITrainingTip> = (tip: ITrainingTip) => {
|
||||
const [isAutoPlaying, setIsAutoPlaying] = useState<boolean>(false);
|
||||
const [currentTime, setCurrentTime] = useState<number>(0);
|
||||
const [walkthroughHtml, setWalkthroughHtml] = useState<string>('');
|
||||
const [highlightedPhrases, setHighlightedPhrases] = useState<string[]>([]);
|
||||
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||
const timelineRef = useRef<TimelineEvent[]>([]);
|
||||
const animationRef = useRef<number | null>(null);
|
||||
const segmentsRef = useRef<SegmentRef[]>([]);
|
||||
|
||||
const toggleAutoPlay = useCallback(() => {
|
||||
setIsAutoPlaying((prev) => {
|
||||
if (!prev && currentTime === getMaxTime()) {
|
||||
setCurrentTime(0);
|
||||
}
|
||||
return !prev;
|
||||
});
|
||||
}, [currentTime]);
|
||||
|
||||
const handleAnimationComplete = useCallback(() => {
|
||||
setIsAutoPlaying(false);
|
||||
}, []);
|
||||
|
||||
const handleResetAnimation = useCallback((newTime: number) => {
|
||||
setCurrentTime(newTime);
|
||||
}, []);
|
||||
|
||||
const getMaxTime = (): number => {
|
||||
return tip.exercise?.segments.reduce((sum, segment) =>
|
||||
sum + segment.wordDelay * segment.html.split(/\s+/).length + segment.holdDelay, 0
|
||||
) ?? 0;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const timeline: TimelineEvent[] = [];
|
||||
let currentTimePosition = 0;
|
||||
segmentsRef.current = [];
|
||||
|
||||
tip.exercise?.segments.forEach((segment, index) => {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(segment.html, 'text/html');
|
||||
const words: string[] = [];
|
||||
const walkTree = (node: Node) => {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
words.push(...(node.textContent?.split(/\s+/).filter(word => word.length > 0) || []));
|
||||
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
Array.from(node.childNodes).forEach(walkTree);
|
||||
}
|
||||
};
|
||||
walkTree(doc.body);
|
||||
|
||||
const textDuration = words.length * segment.wordDelay;
|
||||
|
||||
segmentsRef.current.push({
|
||||
...segment,
|
||||
words: words,
|
||||
startTime: currentTimePosition,
|
||||
endTime: currentTimePosition + textDuration
|
||||
});
|
||||
|
||||
timeline.push({
|
||||
type: 'text',
|
||||
start: currentTimePosition,
|
||||
end: currentTimePosition + textDuration,
|
||||
segmentIndex: index
|
||||
});
|
||||
|
||||
currentTimePosition += textDuration;
|
||||
|
||||
timeline.push({
|
||||
type: 'highlight',
|
||||
start: currentTimePosition,
|
||||
end: currentTimePosition + segment.holdDelay,
|
||||
content: segment.highlight,
|
||||
segmentIndex: index
|
||||
});
|
||||
|
||||
currentTimePosition += segment.holdDelay;
|
||||
});
|
||||
|
||||
timelineRef.current = timeline;
|
||||
}, [tip.exercise?.segments]);
|
||||
|
||||
const updateText = useCallback(() => {
|
||||
const currentEvent = timelineRef.current.find(
|
||||
event => currentTime >= event.start && currentTime < event.end
|
||||
);
|
||||
|
||||
if (currentEvent) {
|
||||
if (currentEvent.type === 'text') {
|
||||
const segment = segmentsRef.current[currentEvent.segmentIndex];
|
||||
const elapsedTime = currentTime - currentEvent.start;
|
||||
const wordsToShow = Math.min(Math.floor(elapsedTime / segment.wordDelay), segment.words.length);
|
||||
|
||||
const previousSegmentsHtml = segmentsRef.current
|
||||
.slice(0, currentEvent.segmentIndex)
|
||||
.map(seg => seg.html)
|
||||
.join('');
|
||||
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(segment.html, 'text/html');
|
||||
let wordCount = 0;
|
||||
const walkTree = (node: Node, action: (node: Node) => void): boolean => {
|
||||
if (node.nodeType === Node.TEXT_NODE && node.textContent) {
|
||||
const words = node.textContent.split(/(\s+)/).filter(word => word.length > 0);
|
||||
if (wordCount + words.filter(w => !/\s+/.test(w)).length <= wordsToShow) {
|
||||
action(node.cloneNode(true));
|
||||
wordCount += words.filter(w => !/\s+/.test(w)).length;
|
||||
} else {
|
||||
const remainingWords = wordsToShow - wordCount;
|
||||
const newTextContent = words.reduce((acc, word) => {
|
||||
if (!/\s+/.test(word) && acc.nonSpaceWords < remainingWords) {
|
||||
acc.text += word;
|
||||
acc.nonSpaceWords++;
|
||||
} else if (/\s+/.test(word) || acc.nonSpaceWords < remainingWords) {
|
||||
acc.text += word;
|
||||
}
|
||||
return acc;
|
||||
}, { text: '', nonSpaceWords: 0 }).text;
|
||||
const newNode = node.cloneNode(false);
|
||||
newNode.textContent = newTextContent;
|
||||
action(newNode);
|
||||
wordCount = wordsToShow;
|
||||
}
|
||||
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const clone = node.cloneNode(false);
|
||||
action(clone);
|
||||
Array.from(node.childNodes).some(child => {
|
||||
return walkTree(child, childNode => (clone as Node).appendChild(childNode));
|
||||
});
|
||||
}
|
||||
return wordCount >= wordsToShow;
|
||||
};
|
||||
const fragment = document.createDocumentFragment();
|
||||
walkTree(doc.body, node => fragment.appendChild(node));
|
||||
|
||||
const serializer = new XMLSerializer();
|
||||
const currentSegmentHtml = Array.from(fragment.childNodes)
|
||||
.map(node => serializer.serializeToString(node))
|
||||
.join('');
|
||||
const newHtml = previousSegmentsHtml + currentSegmentHtml;
|
||||
|
||||
setWalkthroughHtml(newHtml);
|
||||
setHighlightedPhrases([]);
|
||||
} else if (currentEvent.type === 'highlight') {
|
||||
const newHtml = segmentsRef.current
|
||||
.slice(0, currentEvent.segmentIndex + 1)
|
||||
.map(seg => seg.html)
|
||||
.join('');
|
||||
setWalkthroughHtml(newHtml);
|
||||
setHighlightedPhrases(currentEvent.content || []);
|
||||
}
|
||||
}
|
||||
}, [currentTime]);
|
||||
|
||||
useEffect(() => {
|
||||
updateText();
|
||||
}, [currentTime, updateText]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAutoPlaying) {
|
||||
const lastEvent = timelineRef.current[timelineRef.current.length - 1];
|
||||
if (lastEvent && currentTime >= lastEvent.end) {
|
||||
setCurrentTime(0);
|
||||
}
|
||||
setIsPlaying(true);
|
||||
} else {
|
||||
setIsPlaying(false);
|
||||
}
|
||||
}, [isAutoPlaying, currentTime]);
|
||||
|
||||
useEffect(() => {
|
||||
const animate = () => {
|
||||
if (isPlaying) {
|
||||
setCurrentTime((prevTime) => {
|
||||
const newTime = prevTime + 50;
|
||||
const lastEvent = timelineRef.current[timelineRef.current.length - 1];
|
||||
if (lastEvent && newTime >= lastEvent.end) {
|
||||
setIsPlaying(false);
|
||||
handleAnimationComplete();
|
||||
return lastEvent.end;
|
||||
}
|
||||
return newTime;
|
||||
});
|
||||
}
|
||||
animationRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animationRef.current = requestAnimationFrame(animate);
|
||||
|
||||
return () => {
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current);
|
||||
}
|
||||
};
|
||||
}, [isPlaying, handleAnimationComplete]);
|
||||
|
||||
const handleSliderChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newTime = parseInt(e.target.value, 10);
|
||||
setCurrentTime(newTime);
|
||||
handleResetAnimation(newTime);
|
||||
};
|
||||
|
||||
const handleSliderMouseDown = () => {
|
||||
setIsPlaying(false);
|
||||
};
|
||||
|
||||
const handleSliderMouseUp = () => {
|
||||
if (isAutoPlaying) {
|
||||
setIsPlaying(true);
|
||||
}
|
||||
};
|
||||
|
||||
if (tip.standalone || !tip.exercise) {
|
||||
return (
|
||||
<div className="container mx-auto">
|
||||
<h1 className='text-xl font-bold text-red-600'>The exercise for this tip is not available yet!</h1>
|
||||
<div className="tip-container bg-blue-100 p-4 rounded-lg shadow-md mb-4 mt-10">
|
||||
<h3 className="text-xl font-semibold text-blue-800 mb-2">{tip.tipCategory}</h3>
|
||||
<div className="text-gray-700" dangerouslySetInnerHTML={{ __html: tip.tipHtml }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="container mx-auto">
|
||||
<div className="tip-container bg-blue-100 p-4 rounded-lg shadow-md mb-4">
|
||||
<h3 className="text-xl font-semibold text-blue-800 mb-2">{tip.tipCategory}</h3>
|
||||
<div className="text-gray-700" dangerouslySetInnerHTML={{ __html: tip.tipHtml }} />
|
||||
</div>
|
||||
<div className='flex flex-col space-y-4'>
|
||||
<div className='flex flex-row items-center space-x-4 py-4'>
|
||||
<button
|
||||
onClick={toggleAutoPlay}
|
||||
className="p-2 bg-blue-500 text-white rounded-full transition-colors duration-200 hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50"
|
||||
aria-label={isAutoPlaying ? 'Pause' : 'Play'}
|
||||
>
|
||||
{isAutoPlaying ? (
|
||||
<FaRegCircleStop className="w-6 h-6" />
|
||||
) : (
|
||||
<FaRegCirclePlay className="w-6 h-6" />
|
||||
)}
|
||||
</button>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={timelineRef.current.length > 0 ? timelineRef.current[timelineRef.current.length - 1].end : 0}
|
||||
value={currentTime}
|
||||
onChange={handleSliderChange}
|
||||
onMouseDown={handleSliderMouseDown}
|
||||
onMouseUp={handleSliderMouseUp}
|
||||
onTouchStart={handleSliderMouseDown}
|
||||
onTouchEnd={handleSliderMouseUp}
|
||||
className='flex-grow'
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4'>
|
||||
<div className='flex-1 bg-white p-6 rounded-lg shadow'>
|
||||
{/*<h2 className="text-xl font-bold mb-4">Question</h2>*/}
|
||||
<div className="mb-4" dangerouslySetInnerHTML={{ __html: tip.exercise.question }} />
|
||||
<HighlightedContent html={tip.exercise.highlightable} highlightPhrases={highlightedPhrases} />
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<div className='bg-gray-50 rounded-lg shadow'>
|
||||
<div className='p-6 space-y-4'>
|
||||
<animated.div
|
||||
dangerouslySetInnerHTML={{ __html: walkthroughHtml }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExerciseWalkthrough;
|
||||
56
src/components/TrainingContent/TrainingInterfaces.ts
Normal file
56
src/components/TrainingContent/TrainingInterfaces.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Stat } from "@/interfaces/user";
|
||||
|
||||
export interface ITrainingContent {
|
||||
id: string;
|
||||
created_at: number;
|
||||
exams: {
|
||||
id: string;
|
||||
date: number;
|
||||
detailed_summary: string;
|
||||
performance_comment: string;
|
||||
score: number;
|
||||
module: string;
|
||||
stat_ids: string[];
|
||||
stats?: Stat[];
|
||||
}[];
|
||||
tip_ids: string[];
|
||||
tips?: ITrainingTip[];
|
||||
weak_areas: {
|
||||
area: string;
|
||||
comment: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface ITrainingTip {
|
||||
id: string;
|
||||
tipCategory: string;
|
||||
tipHtml: string;
|
||||
standalone: boolean;
|
||||
exercise?: {
|
||||
question: string;
|
||||
highlightable: string;
|
||||
segments: WalkthroughConfigs[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface WalkthroughConfigs {
|
||||
html: string;
|
||||
wordDelay: number;
|
||||
holdDelay: number;
|
||||
highlight: string[];
|
||||
}
|
||||
|
||||
|
||||
export interface TimelineEvent {
|
||||
type: 'text' | 'highlight';
|
||||
start: number;
|
||||
end: number;
|
||||
segmentIndex: number;
|
||||
content?: string[];
|
||||
}
|
||||
|
||||
export interface SegmentRef extends WalkthroughConfigs {
|
||||
words: string[];
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
}
|
||||
90
src/components/TrainingContent/TrainingScore.tsx
Normal file
90
src/components/TrainingContent/TrainingScore.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import { RiArrowRightUpLine, RiArrowLeftDownLine } from 'react-icons/ri';
|
||||
import { FaChartLine } from 'react-icons/fa';
|
||||
import { GiLightBulb } from 'react-icons/gi';
|
||||
import clsx from 'clsx';
|
||||
import { ITrainingContent } from './TrainingInterfaces';
|
||||
|
||||
interface TrainingScoreProps {
|
||||
trainingContent: ITrainingContent
|
||||
gridView: boolean;
|
||||
}
|
||||
|
||||
const TrainingScore: React.FC<TrainingScoreProps> = ({
|
||||
trainingContent,
|
||||
gridView
|
||||
}) => {
|
||||
const scores = trainingContent.exams.map(exam => exam.score);
|
||||
const highestScore = Math.max(...scores);
|
||||
const lowestScore = Math.min(...scores);
|
||||
const averageScore = scores.length > 0
|
||||
? scores.reduce((sum, score) => sum + score, 0) / scores.length
|
||||
: 0;
|
||||
|
||||
const containerClasses = clsx(
|
||||
"flex flex-row mb-4",
|
||||
gridView ? "gap-4 justify-between" : "gap-8"
|
||||
);
|
||||
|
||||
const columnClasses = clsx(
|
||||
"flex flex-col",
|
||||
gridView ? "gap-4" : "gap-8"
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
<div className={columnClasses}>
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<div className="flex w-14 h-14 bg-[#F5F5F5] items-center justify-center rounded-xl border border-[#DBDBDB]">
|
||||
<svg width="20" height="20" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.7083 3.16669C11.4166 3.16669 11.1701 3.06599 10.9687 2.8646C10.7673 2.66321 10.6666 2.41669 10.6666 2.12502C10.6666 1.83335 10.7673 1.58683 10.9687 1.38544C11.1701 1.18405 11.4166 1.08335 11.7083 1.08335C12 1.08335 12.2465 1.18405 12.4479 1.38544C12.6493 1.58683 12.75 1.83335 12.75 2.12502C12.75 2.41669 12.6493 2.66321 12.4479 2.8646C12.2465 3.06599 12 3.16669 11.7083 3.16669ZM11.7083 16.9167C11.4166 16.9167 11.1701 16.816 10.9687 16.6146C10.7673 16.4132 10.6666 16.1667 10.6666 15.875C10.6666 15.5834 10.7673 15.3368 10.9687 15.1354C11.1701 14.934 11.4166 14.8334 11.7083 14.8334C12 14.8334 12.2465 14.934 12.4479 15.1354C12.6493 15.3368 12.75 15.5834 12.75 15.875C12.75 16.1667 12.6493 16.4132 12.4479 16.6146C12.2465 16.816 12 16.9167 11.7083 16.9167ZM15.0416 6.08335C14.75 6.08335 14.5034 5.98266 14.302 5.78127C14.1007 5.57988 14 5.33335 14 5.04169C14 4.75002 14.1007 4.50349 14.302 4.3021C14.5034 4.10071 14.75 4.00002 15.0416 4.00002C15.3333 4.00002 15.5798 4.10071 15.7812 4.3021C15.9826 4.50349 16.0833 4.75002 16.0833 5.04169C16.0833 5.33335 15.9826 5.57988 15.7812 5.78127C15.5798 5.98266 15.3333 6.08335 15.0416 6.08335ZM15.0416 14C14.75 14 14.5034 13.8993 14.302 13.6979C14.1007 13.4965 14 13.25 14 12.9584C14 12.6667 14.1007 12.4202 14.302 12.2188C14.5034 12.0174 14.75 11.9167 15.0416 11.9167C15.3333 11.9167 15.5798 12.0174 15.7812 12.2188C15.9826 12.4202 16.0833 12.6667 16.0833 12.9584C16.0833 13.25 15.9826 13.4965 15.7812 13.6979C15.5798 13.8993 15.3333 14 15.0416 14ZM16.2916 10.0417C16 10.0417 15.7534 9.94099 15.552 9.7396C15.3507 9.53821 15.25 9.29169 15.25 9.00002C15.25 8.70835 15.3507 8.46183 15.552 8.26044C15.7534 8.05905 16 7.95835 16.2916 7.95835C16.5833 7.95835 16.8298 8.05905 17.0312 8.26044C17.2326 8.46183 17.3333 8.70835 17.3333 9.00002C17.3333 9.29169 17.2326 9.53821 17.0312 9.7396C16.8298 9.94099 16.5833 10.0417 16.2916 10.0417ZM8.99996 17.3334C7.84718 17.3334 6.76385 17.1146 5.74996 16.6771C4.73607 16.2396 3.85413 15.6459 3.10413 14.8959C2.35413 14.1459 1.76038 13.2639 1.32288 12.25C0.885376 11.2361 0.666626 10.1528 0.666626 9.00002C0.666626 7.84724 0.885376 6.76391 1.32288 5.75002C1.76038 4.73613 2.35413 3.85419 3.10413 3.10419C3.85413 2.35419 4.73607 1.76044 5.74996 1.32294C6.76385 0.885437 7.84718 0.666687 8.99996 0.666687V2.33335C7.13885 2.33335 5.56246 2.97919 4.27079 4.27085C2.97913 5.56252 2.33329 7.13891 2.33329 9.00002C2.33329 10.8611 2.97913 12.4375 4.27079 13.7292C5.56246 15.0209 7.13885 15.6667 8.99996 15.6667V17.3334ZM8.99996 10.6667C8.54163 10.6667 8.14927 10.5035 7.82288 10.1771C7.49649 9.85071 7.33329 9.45835 7.33329 9.00002C7.33329 8.93058 7.33676 8.85766 7.34371 8.78127C7.35065 8.70488 7.36801 8.63196 7.39579 8.56252L5.66663 6.83335L6.83329 5.66669L8.56246 7.39585C8.61801 7.38196 8.76385 7.36113 8.99996 7.33335C9.45829 7.33335 9.85065 7.49655 10.177 7.82294C10.5034 8.14933 10.6666 8.54169 10.6666 9.00002C10.6666 9.45835 10.5034 9.85071 10.177 10.1771C9.85065 10.5035 9.45829 10.6667 8.99996 10.6667Z" fill="#40A1EA" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="font-bold">{trainingContent.exams.length}</p>
|
||||
<p>Exams Selected</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<div className="flex w-14 h-14 bg-[#F5F5F5] items-center justify-center rounded-xl border border-[#DBDBDB]">
|
||||
<RiArrowRightUpLine color={"#22E1B3"} size={gridView ? 28 : 26} />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="font-bold">{highestScore}%</p>
|
||||
<p>Highest Score</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={columnClasses}>
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<div className="flex w-14 h-14 bg-[#F5F5F5] items-center justify-center rounded-xl border border-[#DBDBDB]">
|
||||
<FaChartLine color={"#40A1EA"} size={gridView ? 24 : 26} />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="font-bold">{averageScore}%</p>
|
||||
<p>Average Score</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<div className="flex w-14 h-14 bg-[#F5F5F5] items-center justify-center rounded-xl border border-[#DBDBDB]">
|
||||
<RiArrowLeftDownLine color={"#E13922"} size={gridView ? 28 : 26} />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="font-bold">{lowestScore}%</p>
|
||||
<p>Lowest Score</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{gridView && (
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
<div className="flex w-14 h-14 bg-[#F5F5F5] items-center justify-center rounded-xl border border-[#DBDBDB]">
|
||||
<GiLightBulb color={"#FFCC00"} size={28} />
|
||||
</div>
|
||||
<p><span className="font-bold">{trainingContent.tip_ids.length}</span> Tips</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrainingScore;
|
||||
44
src/pages/api/training/[id].ts
Normal file
44
src/pages/api/training/[id].ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { sessionOptions } from "@/lib/session";
|
||||
import {app} from "@/firebase";
|
||||
import { collection, doc, getDoc, getDocs, getFirestore, query } from "firebase/firestore";
|
||||
import { withIronSessionApiRoute } from "iron-session/next";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
const db = getFirestore(app);
|
||||
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!req.session.user) {
|
||||
res.status(401).json({ ok: false });
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === "GET") return get(req, res);
|
||||
}
|
||||
|
||||
|
||||
async function get(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { id } = req.query;
|
||||
|
||||
if (typeof id !== 'string') {
|
||||
return res.status(400).json({ message: 'Invalid ID' });
|
||||
}
|
||||
|
||||
const docRef = doc(db, "training", id);
|
||||
const docSnap = await getDoc(docRef);
|
||||
|
||||
if (docSnap.exists()) {
|
||||
res.status(200).json({
|
||||
id: docSnap.id,
|
||||
...docSnap.data(),
|
||||
});
|
||||
} else {
|
||||
res.status(404).json({ message: 'Document not found' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
res.status(500).json({ message: 'An unexpected error occurred' });
|
||||
}
|
||||
}
|
||||
51
src/pages/api/training/index.ts
Normal file
51
src/pages/api/training/index.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { sessionOptions } from "@/lib/session";
|
||||
import axios from "axios";
|
||||
import { app } from "@/firebase";
|
||||
import { collection, doc, getDoc, getDocs, getFirestore, query } from "firebase/firestore";
|
||||
import { withIronSessionApiRoute } from "iron-session/next";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
const db = getFirestore(app);
|
||||
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!req.session.user) {
|
||||
res.status(401).json({ ok: false });
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === "GET") return get(req, res);
|
||||
if (req.method === "POST") return post(req, res);
|
||||
}
|
||||
|
||||
async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const response = await axios.post(`${process.env.BACKEND_URL}/training_content`, req.body, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
|
||||
},
|
||||
});
|
||||
res.status(response.status).json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: 'An unexpected error occurred' });
|
||||
}
|
||||
}
|
||||
|
||||
async function get(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const q = query(collection(db, "training"));
|
||||
const snapshot = await getDocs(q);
|
||||
|
||||
res.status(200).json(
|
||||
snapshot.docs.map((doc) => ({
|
||||
id: doc.id,
|
||||
...doc.data(),
|
||||
}))
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
res.status(500).json({ message: 'An unexpected error occurred' });
|
||||
}
|
||||
}
|
||||
|
||||
44
src/pages/api/training/walkthrough/index.ts
Normal file
44
src/pages/api/training/walkthrough/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { sessionOptions } from "@/lib/session";
|
||||
import { app } from "@/firebase";
|
||||
import { collection, doc, documentId, getDoc, getDocs, getFirestore, query, where } from "firebase/firestore";
|
||||
import { withIronSessionApiRoute } from "iron-session/next";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
const db = getFirestore(app);
|
||||
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!req.session.user) {
|
||||
res.status(401).json({ ok: false });
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET") return get(req, res);
|
||||
}
|
||||
|
||||
|
||||
async function get(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { ids } = req.query;
|
||||
|
||||
if (!ids || !Array.isArray(ids)) {
|
||||
return res.status(400).json({ message: 'Invalid or missing ids!' });
|
||||
}
|
||||
|
||||
const walkthroughCollection = collection(db, 'walkthrough');
|
||||
|
||||
const q = query(walkthroughCollection, where(documentId(), 'in', ids));
|
||||
|
||||
const querySnapshot = await getDocs(q);
|
||||
|
||||
const documents = querySnapshot.docs.map(doc => ({
|
||||
id: doc.id,
|
||||
...doc.data()
|
||||
}));
|
||||
|
||||
res.status(200).json(documents);
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
res.status(500).json({ message: 'An unexpected error occurred' });
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,31 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import Head from "next/head";
|
||||
import {withIronSessionSsr} from "iron-session/next";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import {Stat, User} from "@/interfaces/user";
|
||||
import {useEffect, useState} from "react";
|
||||
import { withIronSessionSsr } from "iron-session/next";
|
||||
import { sessionOptions } from "@/lib/session";
|
||||
import { Stat, User } from "@/interfaces/user";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import useStats from "@/hooks/useStats";
|
||||
import {convertToUserSolutions, groupByDate} from "@/utils/stats";
|
||||
import { groupByDate } from "@/utils/stats";
|
||||
import moment from "moment";
|
||||
import useUsers from "@/hooks/useUsers";
|
||||
import useExamStore from "@/stores/examStore";
|
||||
import {Module} from "@/interfaces";
|
||||
import {ToastContainer} from "react-toastify";
|
||||
import {useRouter} from "next/router";
|
||||
import {uniqBy} from "lodash";
|
||||
import {getExamById} from "@/utils/exams";
|
||||
import {sortByModule} from "@/utils/moduleUtils";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import { useRouter } from "next/router";
|
||||
import Layout from "@/components/High/Layout";
|
||||
import clsx from "clsx";
|
||||
import {calculateBandScore} from "@/utils/score";
|
||||
import {BsBook, BsClipboard, BsClock, BsHeadphones, BsMegaphone, BsPen, BsPersonDash, BsPersonFillX, BsXCircle} from "react-icons/bs";
|
||||
import Select from "@/components/Low/Select";
|
||||
import useGroups from "@/hooks/useGroups";
|
||||
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||
import useAssignments from "@/hooks/useAssignments";
|
||||
import {uuidv4} from "@firebase/util";
|
||||
import {usePDFDownload} from "@/hooks/usePDFDownload";
|
||||
import { uuidv4 } from "@firebase/util";
|
||||
import { usePDFDownload } from "@/hooks/usePDFDownload";
|
||||
import useRecordStore from "@/stores/recordStore";
|
||||
import ai_usage from "@/utils/ai.detection";
|
||||
import useTrainingContentStore from "@/stores/trainingContentStore";
|
||||
import Button from "@/components/Low/Button";
|
||||
import StatsGridItem from "@/components/StatGridItem";
|
||||
|
||||
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||
|
||||
export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
|
||||
const user = req.session.user;
|
||||
|
||||
if (!user || !user.isVerified) {
|
||||
@@ -50,7 +47,7 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||
}
|
||||
|
||||
return {
|
||||
props: {user: req.session.user},
|
||||
props: { user: req.session.user },
|
||||
};
|
||||
}, sessionOptions);
|
||||
|
||||
@@ -59,16 +56,16 @@ const defaultSelectableCorporate = {
|
||||
label: "All",
|
||||
};
|
||||
|
||||
export default function History({user}: {user: User}) {
|
||||
const [statsUserId, setStatsUserId] = useRecordStore((state) => [state.selectedUser, state.setSelectedUser]);
|
||||
export default function History({ user }: { user: User }) {
|
||||
const [statsUserId, setStatsUserId, training, setTraining] = useRecordStore((state) => [state.selectedUser, state.setSelectedUser, state.training, state.setTraining]);
|
||||
// const [statsUserId, setStatsUserId] = useState<string | undefined>(user.id);
|
||||
const [groupedStats, setGroupedStats] = useState<{[key: string]: Stat[]}>();
|
||||
const [groupedStats, setGroupedStats] = useState<{ [key: string]: Stat[] }>();
|
||||
const [filter, setFilter] = useState<"months" | "weeks" | "days" | "assignments">();
|
||||
const {assignments} = useAssignments({});
|
||||
const { assignments } = useAssignments({});
|
||||
|
||||
const {users} = useUsers();
|
||||
const {stats, isLoading: isStatsLoading} = useStats(statsUserId);
|
||||
const {groups: allGroups} = useGroups();
|
||||
const { users } = useUsers();
|
||||
const { stats, isLoading: isStatsLoading } = useStats(statsUserId);
|
||||
const { groups: allGroups } = useGroups();
|
||||
|
||||
const groups = allGroups.filter((x) => x.admin === user.id);
|
||||
|
||||
@@ -78,8 +75,8 @@ export default function History({user}: {user: User}) {
|
||||
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
|
||||
const setInactivity = useExamStore((state) => state.setInactivity);
|
||||
const setTimeSpent = useExamStore((state) => state.setTimeSpent);
|
||||
const router = useRouter();
|
||||
const renderPdfIcon = usePDFDownload("stats");
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (stats && !isStatsLoading) {
|
||||
@@ -108,22 +105,21 @@ export default function History({user}: {user: User}) {
|
||||
setFilter((prev) => (prev === value ? undefined : value));
|
||||
};
|
||||
|
||||
const filterStatsByDate = (stats: {[key: string]: Stat[]}) => {
|
||||
const filterStatsByDate = (stats: { [key: string]: Stat[] }) => {
|
||||
if (filter && filter !== "assignments") {
|
||||
const filterDate = moment()
|
||||
.subtract({[filter as string]: 1})
|
||||
.subtract({ [filter as string]: 1 })
|
||||
.format("x");
|
||||
const filteredStats: {[key: string]: Stat[]} = {};
|
||||
const filteredStats: { [key: string]: Stat[] } = {};
|
||||
|
||||
Object.keys(stats).forEach((timestamp) => {
|
||||
if (timestamp >= filterDate) filteredStats[timestamp] = stats[timestamp];
|
||||
});
|
||||
|
||||
return filteredStats;
|
||||
}
|
||||
|
||||
if (filter && filter === "assignments") {
|
||||
const filteredStats: {[key: string]: Stat[]} = {};
|
||||
const filteredStats: { [key: string]: Stat[] } = {};
|
||||
|
||||
Object.keys(stats).forEach((timestamp) => {
|
||||
if (stats[timestamp].map((s) => s.assignment === undefined).includes(false))
|
||||
@@ -136,211 +132,59 @@ export default function History({user}: {user: User}) {
|
||||
return stats;
|
||||
};
|
||||
|
||||
const formatTimestamp = (timestamp: string) => {
|
||||
const date = moment(parseInt(timestamp));
|
||||
const formatter = "YYYY/MM/DD - HH:mm";
|
||||
const MAX_TRAINING_EXAMS = 10;
|
||||
const [selectedTrainingExams, setSelectedTrainingExams] = useState<string[]>([]);
|
||||
const setTrainingStats = useTrainingContentStore((state) => state.setStats);
|
||||
|
||||
return date.format(formatter);
|
||||
};
|
||||
useEffect(() => {
|
||||
const handleRouteChange = (url: string) => {
|
||||
setTraining(false)
|
||||
}
|
||||
router.events.on('routeChangeStart', handleRouteChange)
|
||||
return () => {
|
||||
router.events.off('routeChangeStart', handleRouteChange)
|
||||
}
|
||||
}, [router.events, setTraining])
|
||||
|
||||
const aggregateScoresByModule = (stats: Stat[]): {module: Module; total: number; missing: number; correct: number}[] => {
|
||||
const scores: {
|
||||
[key in Module]: {total: number; missing: number; correct: number};
|
||||
} = {
|
||||
reading: {
|
||||
total: 0,
|
||||
correct: 0,
|
||||
missing: 0,
|
||||
},
|
||||
listening: {
|
||||
total: 0,
|
||||
correct: 0,
|
||||
missing: 0,
|
||||
},
|
||||
writing: {
|
||||
total: 0,
|
||||
correct: 0,
|
||||
missing: 0,
|
||||
},
|
||||
speaking: {
|
||||
total: 0,
|
||||
correct: 0,
|
||||
missing: 0,
|
||||
},
|
||||
level: {
|
||||
total: 0,
|
||||
correct: 0,
|
||||
missing: 0,
|
||||
},
|
||||
};
|
||||
const handleTrainingContentSubmission = () => {
|
||||
if (groupedStats) {
|
||||
const allStats = Object.keys(filterStatsByDate(groupedStats));
|
||||
const selectedStats = selectedTrainingExams.reduce<Record<string, Stat[]>>((accumulator, timestamp) => {
|
||||
if (allStats.includes(timestamp)) {
|
||||
accumulator[timestamp] = filterStatsByDate(groupedStats)[timestamp];
|
||||
}
|
||||
return accumulator;
|
||||
}, {});
|
||||
setTrainingStats(Object.values(selectedStats).flat())
|
||||
router.push("/training");
|
||||
}
|
||||
}
|
||||
|
||||
stats.forEach((x) => {
|
||||
scores[x.module!] = {
|
||||
total: scores[x.module!].total + x.score.total,
|
||||
correct: scores[x.module!].correct + x.score.correct,
|
||||
missing: scores[x.module!].missing + x.score.missing,
|
||||
};
|
||||
});
|
||||
|
||||
return Object.keys(scores)
|
||||
.filter((x) => scores[x as Module].total > 0)
|
||||
.map((x) => ({module: x as Module, ...scores[x as Module]}));
|
||||
};
|
||||
|
||||
const customContent = (timestamp: string) => {
|
||||
if (!groupedStats) return <></>;
|
||||
|
||||
const dateStats = groupedStats[timestamp];
|
||||
const correct = dateStats.reduce((accumulator, current) => accumulator + current.score.correct, 0);
|
||||
const total = dateStats.reduce((accumulator, current) => accumulator + current.score.total, 0);
|
||||
const aggregatedScores = aggregateScoresByModule(dateStats).filter((x) => x.total > 0);
|
||||
const assignmentID = dateStats.reduce((_, current) => current.assignment as any, "");
|
||||
const assignment = assignments.find((a) => a.id === assignmentID);
|
||||
const isDisabled = dateStats.some((x) => x.isDisabled);
|
||||
|
||||
const aiUsage = Math.round(ai_usage(dateStats) * 100);
|
||||
|
||||
const aggregatedLevels = aggregatedScores.map((x) => ({
|
||||
module: x.module,
|
||||
level: calculateBandScore(x.correct, x.total, x.module, user.focus),
|
||||
}));
|
||||
|
||||
const {timeSpent, inactivity, session} = dateStats[0];
|
||||
|
||||
const selectExam = () => {
|
||||
const examPromises = uniqBy(dateStats, "exam").map((stat) => {
|
||||
console.log({stat});
|
||||
return getExamById(stat.module, stat.exam);
|
||||
});
|
||||
|
||||
if (isDisabled) return;
|
||||
|
||||
Promise.all(examPromises).then((exams) => {
|
||||
if (exams.every((x) => !!x)) {
|
||||
if (!!timeSpent) setTimeSpent(timeSpent);
|
||||
if (!!inactivity) setInactivity(inactivity);
|
||||
|
||||
setUserSolutions(convertToUserSolutions(dateStats));
|
||||
setShowSolutions(true);
|
||||
setExams(exams.map((x) => x!).sort(sortByModule));
|
||||
setSelectedModules(
|
||||
exams
|
||||
.map((x) => x!)
|
||||
.sort(sortByModule)
|
||||
.map((x) => x!.module),
|
||||
);
|
||||
router.push("/exercises");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const textColor = clsx(
|
||||
correct / total >= 0.7 && "text-mti-purple",
|
||||
correct / total >= 0.3 && correct / total < 0.7 && "text-mti-red",
|
||||
correct / total < 0.3 && "text-mti-rose",
|
||||
);
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<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 className="flex items-center gap-2">
|
||||
{!!timeSpent && (
|
||||
<span className="text-sm flex gap-2 items-center tooltip" data-tip="Time Spent">
|
||||
<BsClock /> {Math.floor(timeSpent / 60)} minutes
|
||||
</span>
|
||||
)}
|
||||
{!!inactivity && (
|
||||
<span className="text-sm flex gap-2 items-center tooltip" data-tip="Inactivity">
|
||||
<BsXCircle /> {Math.floor(inactivity / 60)} minutes
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-row gap-2">
|
||||
<span className={textColor}>
|
||||
Level{" "}
|
||||
{(
|
||||
aggregatedLevels.reduce((accumulator, current) => accumulator + current.level, 0) / aggregatedLevels.length
|
||||
).toFixed(1)}
|
||||
</span>
|
||||
{renderPdfIcon(session, textColor, textColor)}
|
||||
</div>
|
||||
{aiUsage >= 50 && user.type !== "student" && (
|
||||
<div
|
||||
className={clsx("ml-auto border px-1 rounded w-fit mr-1", {
|
||||
"bg-orange-100 border-orange-400 text-orange-700": aiUsage < 80,
|
||||
"bg-red-100 border-red-400 text-red-700": aiUsage >= 80,
|
||||
})}>
|
||||
<span className="text-xs">AI Usage</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full flex flex-col gap-1">
|
||||
<div className="grid grid-cols-4 gap-2 place-items-start w-full -md:mt-2">
|
||||
{aggregatedLevels.map(({module, level}) => (
|
||||
<div
|
||||
key={module}
|
||||
className={clsx(
|
||||
"flex gap-2 items-center w-fit text-white -md:px-4 xl:px-4 md:px-2 py-2 rounded-xl",
|
||||
module === "reading" && "bg-ielts-reading",
|
||||
module === "listening" && "bg-ielts-listening",
|
||||
module === "writing" && "bg-ielts-writing",
|
||||
module === "speaking" && "bg-ielts-speaking",
|
||||
module === "level" && "bg-ielts-level",
|
||||
)}>
|
||||
{module === "reading" && <BsBook className="w-4 h-4" />}
|
||||
{module === "listening" && <BsHeadphones className="w-4 h-4" />}
|
||||
{module === "writing" && <BsPen className="w-4 h-4" />}
|
||||
{module === "speaking" && <BsMegaphone className="w-4 h-4" />}
|
||||
{module === "level" && <BsClipboard className="w-4 h-4" />}
|
||||
<span className="text-sm">{level.toFixed(1)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{assignment && (
|
||||
<span className="font-light text-sm">
|
||||
Assignment: {assignment.name}, Teacher: {users.find((u) => u.id === assignment.assigner)?.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
key={uuidv4()}
|
||||
className={clsx(
|
||||
"flex flex-col justify-between gap-4 border border-mti-gray-platinum p-4 cursor-pointer rounded-xl transition ease-in-out duration-300 -md:hidden",
|
||||
isDisabled && "grayscale tooltip",
|
||||
correct / total >= 0.7 && "hover:border-mti-purple",
|
||||
correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red",
|
||||
correct / total < 0.3 && "hover:border-mti-rose",
|
||||
)}
|
||||
onClick={selectExam}
|
||||
data-tip="This exam is still being evaluated..."
|
||||
role="button">
|
||||
{content}
|
||||
</div>
|
||||
<div
|
||||
key={uuidv4()}
|
||||
className={clsx(
|
||||
"flex flex-col gap-4 border border-mti-gray-platinum p-4 cursor-pointer rounded-xl transition ease-in-out duration-300 -md:tooltip md:hidden",
|
||||
correct / total >= 0.7 && "hover:border-mti-purple",
|
||||
correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red",
|
||||
correct / total < 0.3 && "hover:border-mti-rose",
|
||||
)}
|
||||
data-tip="Your screen size is too small to view previous exams."
|
||||
role="button">
|
||||
{content}
|
||||
</div>
|
||||
</>
|
||||
<StatsGridItem
|
||||
key={uuidv4()}
|
||||
stats={dateStats}
|
||||
timestamp={timestamp}
|
||||
user={user}
|
||||
assignments={assignments}
|
||||
users={users}
|
||||
training={training}
|
||||
selectedTrainingExams={selectedTrainingExams}
|
||||
setSelectedTrainingExams={setSelectedTrainingExams}
|
||||
setExams={setExams}
|
||||
setShowSolutions={setShowSolutions}
|
||||
setUserSolutions={setUserSolutions}
|
||||
setSelectedModules={setSelectedModules}
|
||||
setInactivity={setInactivity}
|
||||
setTimeSpent={setTimeSpent}
|
||||
renderPdfIcon={renderPdfIcon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -394,13 +238,13 @@ export default function History({user}: {user: User}) {
|
||||
const selectedUser = getSelectedUser();
|
||||
const selectedUserSelectValue = selectedUser
|
||||
? {
|
||||
value: selectedUser.id,
|
||||
label: `${selectedUser.name} - ${selectedUser.email}`,
|
||||
}
|
||||
value: selectedUser.id,
|
||||
label: `${selectedUser.name} - ${selectedUser.email}`,
|
||||
}
|
||||
: {
|
||||
value: "",
|
||||
label: "",
|
||||
};
|
||||
value: "",
|
||||
label: "",
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -417,7 +261,7 @@ export default function History({user}: {user: User}) {
|
||||
<Layout user={user}>
|
||||
<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") && (
|
||||
{(user.type === "developer" || user.type === "admin") && !training && (
|
||||
<>
|
||||
<label className="font-normal text-base text-mti-gray-dim">Corporate</label>
|
||||
|
||||
@@ -426,7 +270,7 @@ export default function History({user}: {user: User}) {
|
||||
value={selectableCorporates.find((x) => x.value === selectedCorporate)}
|
||||
onChange={(value) => setSelectedCorporate(value?.value || "")}
|
||||
styles={{
|
||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||
option: (styles, state) => ({
|
||||
...styles,
|
||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||
@@ -443,7 +287,7 @@ export default function History({user}: {user: User}) {
|
||||
value={selectedUserSelectValue}
|
||||
onChange={(value) => setStatsUserId(value?.value!)}
|
||||
styles={{
|
||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||
option: (styles, state) => ({
|
||||
...styles,
|
||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||
@@ -453,7 +297,7 @@ export default function History({user}: {user: User}) {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{(user.type === "corporate" || user.type === "teacher") && groups.length > 0 && (
|
||||
{(user.type === "corporate" || user.type === "teacher") && groups.length > 0 && !training && (
|
||||
<>
|
||||
<label className="font-normal text-base text-mti-gray-dim">User</label>
|
||||
|
||||
@@ -467,7 +311,7 @@ export default function History({user}: {user: User}) {
|
||||
value={selectedUserSelectValue}
|
||||
onChange={(value) => setStatsUserId(value?.value!)}
|
||||
styles={{
|
||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||
option: (styles, state) => ({
|
||||
...styles,
|
||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||
@@ -477,6 +321,20 @@ export default function History({user}: {user: User}) {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{(training && (
|
||||
<div className="flex flex-row">
|
||||
<div className="font-semibold text-2xl mr-4">Select up to 10 exams {`(${selectedTrainingExams.length}/${MAX_TRAINING_EXAMS})`}</div>
|
||||
<button
|
||||
className={clsx(
|
||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light ml-4 disabled:cursor-not-allowed",
|
||||
"transition duration-300 ease-in-out",
|
||||
)}
|
||||
disabled={selectedTrainingExams.length == 0}
|
||||
onClick={handleTrainingContentSubmission}>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-4 w-full justify-center xl:justify-end">
|
||||
<button
|
||||
|
||||
295
src/pages/training/[id]/index.tsx
Normal file
295
src/pages/training/[id]/index.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import axios from 'axios';
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { AiOutlineFileSearch } from "react-icons/ai";
|
||||
import { MdOutlinePlaylistAddCheckCircle, MdOutlineSelfImprovement } from "react-icons/md";
|
||||
import { BsChatLeftDots } from "react-icons/bs";
|
||||
import Button from "@/components/Low/Button";
|
||||
import clsx from "clsx";
|
||||
import Exercise from "@/training/Exercise";
|
||||
import TrainingScore from "@/training/TrainingScore";
|
||||
import { ITrainingContent, ITrainingTip } from "@/training/TrainingInterfaces";
|
||||
import { Stat, User } from '@/interfaces/user';
|
||||
import Head from "next/head";
|
||||
import Layout from "@/components/High/Layout";
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import { withIronSessionSsr } from "iron-session/next";
|
||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||
import { sessionOptions } from "@/lib/session";
|
||||
import qs from 'qs';
|
||||
import StatsGridItem from '@/components/StatGridItem';
|
||||
import useExamStore from "@/stores/examStore";
|
||||
import { usePDFDownload } from "@/hooks/usePDFDownload";
|
||||
import useAssignments from '@/hooks/useAssignments';
|
||||
import useUsers from '@/hooks/useUsers';
|
||||
|
||||
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);
|
||||
|
||||
const TrainingContent: React.FC<{ user: User }> = ({ user }) => {
|
||||
// Record stuff
|
||||
const setExams = useExamStore((state) => state.setExams);
|
||||
const setShowSolutions = useExamStore((state) => state.setShowSolutions);
|
||||
const setUserSolutions = useExamStore((state) => state.setUserSolutions);
|
||||
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
|
||||
const setInactivity = useExamStore((state) => state.setInactivity);
|
||||
const setTimeSpent = useExamStore((state) => state.setTimeSpent);
|
||||
const renderPdfIcon = usePDFDownload("stats");
|
||||
|
||||
const [trainingContent, setTrainingContent] = useState<ITrainingContent | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [trainingTips, setTrainingTips] = useState<ITrainingTip[]>([]);
|
||||
const [currentTipIndex, setCurrentTipIndex] = useState(0);
|
||||
const { assignments } = useAssignments({});
|
||||
const { users } = useUsers();
|
||||
|
||||
|
||||
|
||||
const router = useRouter();
|
||||
const { id } = router.query;
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTrainingContent = async () => {
|
||||
if (!id || typeof id !== 'string') return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await axios.get<ITrainingContent>(`/api/training/${id}`);
|
||||
const trainingContent = response.data;
|
||||
|
||||
const withExamsStats = {
|
||||
...trainingContent,
|
||||
exams: await Promise.all(trainingContent.exams.map(async (exam) => {
|
||||
const stats = await Promise.all(exam.stat_ids.map(async (statId) => {
|
||||
const statResponse = await axios.get<Stat>(`/api/stats/${statId}`);
|
||||
return statResponse.data;
|
||||
}));
|
||||
return { ...exam, stats };
|
||||
}))
|
||||
};
|
||||
|
||||
const tips = await axios.get<ITrainingTip[]>('/api/training/walkthrough', {
|
||||
params: { ids: trainingContent.tip_ids },
|
||||
paramsSerializer: params => qs.stringify(params, { arrayFormat: 'repeat' })
|
||||
});
|
||||
setTrainingTips(tips.data);
|
||||
setTrainingContent(withExamsStats);
|
||||
} catch (error) {
|
||||
router.push('/training');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTrainingContent();
|
||||
}, [id]);
|
||||
|
||||
const handleNext = () => {
|
||||
setCurrentTipIndex((prevIndex) => (prevIndex + 1));
|
||||
};
|
||||
|
||||
const handlePrevious = () => {
|
||||
setCurrentTipIndex((prevIndex) => (prevIndex - 1));
|
||||
};
|
||||
|
||||
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 />
|
||||
|
||||
<Layout user={user}>
|
||||
{loading ? (
|
||||
<div className="absolute left-1/2 top-1/2 flex h-fit w-fit -translate-x-1/2 -translate-y-1/2 animate-pulse flex-col items-center gap-12">
|
||||
<span className="loading loading-infinity w-32 bg-mti-green-light" />
|
||||
</div>
|
||||
) : (trainingContent && (
|
||||
<>
|
||||
<div className='flex flex-row gap-4'>
|
||||
{trainingContent.exams.map((exam, examIndex) => (
|
||||
<StatsGridItem
|
||||
key={`exam-${examIndex}`}
|
||||
stats={exam.stats || []}
|
||||
timestamp={exam.date}
|
||||
user={user}
|
||||
assignments={assignments}
|
||||
users={users}
|
||||
setExams={setExams}
|
||||
setShowSolutions={setShowSolutions}
|
||||
setUserSolutions={setUserSolutions}
|
||||
setSelectedModules={setSelectedModules}
|
||||
setInactivity={setInactivity}
|
||||
setTimeSpent={setTimeSpent}
|
||||
renderPdfIcon={renderPdfIcon}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className='flex flex-col flex-grow'>
|
||||
<div className='flex flex-row gap-10 -md:flex-col'>
|
||||
<div className="rounded-3xl p-6 w-1/2 shadow-training-inset -md:w-full">
|
||||
<div className="flex flex-row items-center mb-6 gap-1">
|
||||
<MdOutlinePlaylistAddCheckCircle color={"#40A1EA"} size={26} />
|
||||
<h2 className={`text-xl font-semibold text-[#40A1EA]`}>General Evaluation</h2>
|
||||
</div>
|
||||
<TrainingScore
|
||||
trainingContent={trainingContent}
|
||||
gridView={false}
|
||||
/>
|
||||
<div className="w-full h-px bg-[#D9D9D929] my-6"></div>
|
||||
<div className="flex flex-row gap-2 items-center mb-6">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0_112_168" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
|
||||
<rect width="24" height="24" fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_112_168)">
|
||||
<path d="M4 21C3.45 21 2.97917 20.8042 2.5875 20.4125C2.19583 20.0208 2 19.55 2 19V7H4V19H19V21H4ZM8 17C7.45 17 6.97917 16.8042 6.5875 16.4125C6.19583 16.0208 6 15.55 6 15V3H23V15C23 15.55 22.8042 16.0208 22.4125 16.4125C22.0208 16.8042 21.55 17 21 17H8ZM8 15H21V5H8V15ZM10 12H14V7H10V12ZM15 12H19V10H15V12ZM15 9H19V7H15V9Z" fill="#53B2F9" />
|
||||
</g>
|
||||
</svg>
|
||||
<h3 className="text-xl font-semibold text-[#40A1EA]">Performance Breakdown by Exam:</h3>
|
||||
</div>
|
||||
<ul>
|
||||
{trainingContent.exams.flatMap((exam, index) => (
|
||||
<li key={index} className="flex flex-col mb-2 bg-[#22E1B30F] p-4 rounded-xl border">
|
||||
<div className="flex flex-row font-semibold border-b-2 border-[#D9D9D929] text-[#22E1B3] mb-2">
|
||||
<span className="border-r-2 border-[#D9D9D929] pr-2">Exam {index + 1}</span>
|
||||
<span className="pl-2">{exam.score}%</span>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<BsChatLeftDots size={16} />
|
||||
<p className="text-sm">{exam.performance_comment}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="rounded-3xl p-6 w-1/2 shadow-training-inset -md:w-full">
|
||||
<div className="flex flex-row items-center mb-4 gap-1">
|
||||
<MdOutlineSelfImprovement color={"#40A1EA"} size={24} />
|
||||
<h2 className={`text-xl font-semibold text-[#40A1EA]`}>Subjects that Need Improvement</h2>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#FBFBFB] border rounded-xl p-4">
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<div className="flex items-center justify-center w-[48px] h-[48px]">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0_112_445" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
|
||||
<rect width="24" height="24" fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_112_445)">
|
||||
<path d="M6 17H11V15H6V17ZM16 17H18V15H16V17ZM6 13H11V11H6V13ZM16 13H18V7H16V13ZM6 9H11V7H6V9ZM4 21C3.45 21 2.97917 20.8042 2.5875 20.4125C2.19583 20.0208 2 19.55 2 19V5C2 4.45 2.19583 3.97917 2.5875 3.5875C2.97917 3.19583 3.45 3 4 3H20C20.55 3 21.0208 3.19583 21.4125 3.5875C21.8042 3.97917 22 4.45 22 5V19C22 19.55 21.8042 20.0208 21.4125 20.4125C21.0208 20.8042 20.55 21 20 21H4ZM4 19H20V5H4V19Z" fill="#1C1B1F" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold">Detailed Breakdown</h3>
|
||||
</div>
|
||||
<ul>
|
||||
{trainingContent.exams.flatMap((exam, index) => (
|
||||
<li key={index} className="mb-2 border rounded-lg p-4 bg-white">
|
||||
<p> <span className="font-semibold mr-1">{`Exam ${index + 1}:`}</span><span>{exam.detailed_summary}</span></p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="w-full h-px bg-[#D9D9D929] my-6"></div>
|
||||
<div className="flex flex-row items-center mb-4 gap-1">
|
||||
<AiOutlineFileSearch color="#40A1EA" size={24} />
|
||||
<h3 className="text-xl font-semibold text-[#40A1EA]">Identified Weak Areas</h3>
|
||||
</div>
|
||||
<Tab.Group>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Tab.List>
|
||||
<div className="flex flex-row gap-6">
|
||||
{trainingContent.weak_areas.map((x, index) => (
|
||||
<Tab
|
||||
key={index}
|
||||
className={({ selected }) =>
|
||||
clsx(
|
||||
'text-[#53B2F9] pb-2 border-b-2',
|
||||
'focus:outline-none',
|
||||
selected ? 'border-[#1B78BE]' : 'border-[#1B78BE0F]'
|
||||
)
|
||||
}
|
||||
>
|
||||
{x.area}
|
||||
</Tab>
|
||||
))}
|
||||
</div>
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
{trainingContent.weak_areas.map((x, index) => (
|
||||
<Tab.Panel
|
||||
key={index}
|
||||
className="p-3 bg-[#FBFBFB] rounded-lg border border-[#0000000F]"
|
||||
>
|
||||
<p>{x.comment}</p>
|
||||
</Tab.Panel>
|
||||
))}
|
||||
</Tab.Panels>
|
||||
</div>
|
||||
</Tab.Group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-3xl p-6 shadow-training-inset w-full">
|
||||
<div className="flex flex-col p-10">
|
||||
<Exercise key={currentTipIndex} {...trainingTips[currentTipIndex]} />
|
||||
</div>
|
||||
<div className="self-end flex justify-between w-full gap-8 bottom-8 left-0 px-8">
|
||||
<Button
|
||||
color="purple"
|
||||
variant="outline"
|
||||
onClick={handlePrevious}
|
||||
disabled={currentTipIndex == 0}
|
||||
className="max-w-[200px] self-end w-full">
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
color="purple"
|
||||
disabled={currentTipIndex == (trainingTips.length - 1)}
|
||||
onClick={handleNext}
|
||||
className="max-w-[200px] self-end w-full">
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default TrainingContent;
|
||||
|
||||
406
src/pages/training/index.tsx
Normal file
406
src/pages/training/index.tsx
Normal file
@@ -0,0 +1,406 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import Head from "next/head";
|
||||
import { withIronSessionSsr } from "iron-session/next";
|
||||
import { sessionOptions } from "@/lib/session";
|
||||
import { Stat, User } from "@/interfaces/user";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import Layout from "@/components/High/Layout";
|
||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||
import { use, useEffect, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import { FaPlus } from "react-icons/fa";
|
||||
import useRecordStore from "@/stores/recordStore";
|
||||
import router from "next/router";
|
||||
import useTrainingContentStore from "@/stores/trainingContentStore";
|
||||
import axios from "axios";
|
||||
import Select from "@/components/Low/Select";
|
||||
import useUsers from "@/hooks/useUsers";
|
||||
import useGroups from "@/hooks/useGroups";
|
||||
import { ITrainingContent } from "@/training/TrainingInterfaces";
|
||||
import moment from "moment";
|
||||
import { uuidv4 } from "@firebase/util";
|
||||
import TrainingScore from "@/training/TrainingScore";
|
||||
import ModuleBadge from "@/components/ModuleBadge";
|
||||
|
||||
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);
|
||||
|
||||
const defaultSelectableCorporate = {
|
||||
value: "",
|
||||
label: "All",
|
||||
};
|
||||
|
||||
const Training: React.FC<{ user: User }> = ({ user }) => {
|
||||
// Record stuff
|
||||
const { users } = useUsers();
|
||||
const [selectedCorporate, setSelectedCorporate] = useState<string>(defaultSelectableCorporate.value);
|
||||
const [statsUserId, setStatsUserId, setRecordTraining] = useRecordStore((state) => [state.selectedUser, state.setSelectedUser, state.setTraining]);
|
||||
const { groups: allGroups } = useGroups();
|
||||
const groups = allGroups.filter((x) => x.admin === user.id);
|
||||
const [filter, setFilter] = useState<"months" | "weeks" | "days">();
|
||||
|
||||
const toggleFilter = (value: "months" | "weeks" | "days") => {
|
||||
setFilter((prev) => (prev === value ? undefined : value));
|
||||
};
|
||||
|
||||
const [stats, setTrainingStats] = useTrainingContentStore((state) => [state.stats, state.setStats]);
|
||||
const [trainingContent, setTrainingContent] = useState<ITrainingContent[]>([]);
|
||||
const [isNewContentLoading, setIsNewContentLoading] = useState(stats.length != 0);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [groupedByTrainingContent, setGroupedByTrainingContent] = useState<{ [key: string]: ITrainingContent }>();
|
||||
|
||||
useEffect(() => {
|
||||
const handleRouteChange = (url: string) => {
|
||||
setTrainingStats([])
|
||||
}
|
||||
router.events.on('routeChangeStart', handleRouteChange)
|
||||
return () => {
|
||||
router.events.off('routeChangeStart', handleRouteChange)
|
||||
}
|
||||
}, [router.events, setTrainingStats])
|
||||
|
||||
useEffect(() => {
|
||||
const postStats = async () => {
|
||||
try {
|
||||
const response = await axios.post<{id: string}>(`/api/training`, stats);
|
||||
return response.data.id;
|
||||
} catch (error) {
|
||||
setIsNewContentLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isNewContentLoading) {
|
||||
postStats().then( id => {
|
||||
setTrainingStats([]);
|
||||
if (id) {
|
||||
router.push(`/training/${id}`)
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [isNewContentLoading])
|
||||
|
||||
useEffect(() => {
|
||||
const loadTrainingContent = async () => {
|
||||
try {
|
||||
const response = await axios.get<ITrainingContent[]>('/api/training');
|
||||
setTrainingContent(response.data);
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
setTrainingContent([]);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
loadTrainingContent();
|
||||
}, []);
|
||||
|
||||
const handleNewTrainingContent = () => {
|
||||
setRecordTraining(true);
|
||||
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 } = {};
|
||||
|
||||
Object.keys(trainingContent).forEach((timestamp) => {
|
||||
if (timestamp >= filterDate) filteredTrainingContent[timestamp] = trainingContent[timestamp];
|
||||
});
|
||||
return filteredTrainingContent;
|
||||
}
|
||||
return trainingContent;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (trainingContent.length > 0) {
|
||||
const grouped = trainingContent.reduce((acc, content) => {
|
||||
acc[content.created_at] = content;
|
||||
return acc;
|
||||
}, {} as { [key: number]: ITrainingContent });
|
||||
|
||||
setGroupedByTrainingContent(grouped);
|
||||
}
|
||||
}, [trainingContent])
|
||||
|
||||
|
||||
// Record Stuff
|
||||
const selectableCorporates = [
|
||||
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);
|
||||
|
||||
// get the teacher ids for that group
|
||||
const selectedCorporateGroupsParticipants = selectedCorporateGroups.flatMap((x) => x.participants);
|
||||
|
||||
// // search for groups for these teachers
|
||||
// const teacherGroups = allGroups.filter((x) => {
|
||||
// return selectedCorporateGroupsParticipants.includes(x.admin);
|
||||
// });
|
||||
|
||||
// const usersList = [
|
||||
// ...selectedCorporateGroupsParticipants,
|
||||
// ...teacherGroups.flatMap((x) => x.participants),
|
||||
// ];
|
||||
const userListWithUsers = selectedCorporateGroupsParticipants.map((x) => users.find((y) => y.id === x)) as User[];
|
||||
return userListWithUsers.filter((x) => x);
|
||||
}
|
||||
|
||||
return users || [];
|
||||
};
|
||||
|
||||
const corporateFilteredUserList = getUsersList();
|
||||
const getSelectedUser = () => {
|
||||
if (selectedCorporate) {
|
||||
const userInCorporate = corporateFilteredUserList.find((x) => x.id === statsUserId);
|
||||
return userInCorporate || corporateFilteredUserList[0];
|
||||
}
|
||||
|
||||
return users.find((x) => x.id === statsUserId) || user;
|
||||
};
|
||||
|
||||
const selectedUser = getSelectedUser();
|
||||
const selectedUserSelectValue = selectedUser
|
||||
? {
|
||||
value: selectedUser.id,
|
||||
label: `${selectedUser.name} - ${selectedUser.email}`,
|
||||
}
|
||||
: {
|
||||
value: "",
|
||||
label: "",
|
||||
};
|
||||
|
||||
const formatTimestamp = (timestamp: string) => {
|
||||
const date = moment(parseInt(timestamp));
|
||||
const formatter = "YYYY/MM/DD - HH:mm";
|
||||
|
||||
return date.format(formatter);
|
||||
};
|
||||
|
||||
const selectTrainingContent = (trainingContent: ITrainingContent) => {
|
||||
router.push(`/training/${trainingContent.id}`)
|
||||
};
|
||||
|
||||
|
||||
const trainingContentContainer = (timestamp: string) => {
|
||||
if (!groupedByTrainingContent) return <></>;
|
||||
const trainingContent: ITrainingContent = groupedByTrainingContent[timestamp];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
key={uuidv4()}
|
||||
className={clsx(
|
||||
"flex flex-col justify-between gap-4 border border-mti-gray-platinum p-4 cursor-pointer rounded-xl transition ease-in-out duration-300 -md:hidden"
|
||||
)}
|
||||
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">
|
||||
{Object.values(groupedByTrainingContent || {}).flatMap((content) =>
|
||||
content.exams.map(({ module, id }) => (
|
||||
<ModuleBadge key={id} 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 />
|
||||
|
||||
<Layout user={user}>
|
||||
{(isNewContentLoading || isLoading ? (
|
||||
<div className="absolute left-1/2 top-1/2 flex h-fit w-fit -translate-x-1/2 -translate-y-1/2 animate-pulse flex-col items-center gap-12">
|
||||
<span className="loading loading-infinity w-32 bg-mti-green-light" />
|
||||
{ isNewContentLoading && (<span className="text-center text-2xl font-bold text-mti-green-light">
|
||||
Assessing your exams, please be patient...
|
||||
</span>)}
|
||||
</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>
|
||||
|
||||
<Select
|
||||
options={selectableCorporates}
|
||||
value={selectableCorporates.find((x) => x.value === selectedCorporate)}
|
||||
onChange={(value) => setSelectedCorporate(value?.value || "")}
|
||||
styles={{
|
||||
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||
option: (styles, state) => ({
|
||||
...styles,
|
||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||
color: state.isFocused ? "black" : styles.color,
|
||||
}),
|
||||
}}></Select>
|
||||
<label className="font-normal text-base text-mti-gray-dim">User</label>
|
||||
|
||||
<Select
|
||||
options={corporateFilteredUserList.map((x) => ({
|
||||
value: x.id,
|
||||
label: `${x.name} - ${x.email}`,
|
||||
}))}
|
||||
value={selectedUserSelectValue}
|
||||
onChange={(value) => setStatsUserId(value?.value)}
|
||||
styles={{
|
||||
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||
option: (styles, state) => ({
|
||||
...styles,
|
||||
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>
|
||||
|
||||
<Select
|
||||
options={users
|
||||
.filter((x) => groups.flatMap((y) => y.participants).includes(x.id))
|
||||
.map((x) => ({
|
||||
value: x.id,
|
||||
label: `${x.name} - ${x.email}`,
|
||||
}))}
|
||||
value={selectedUserSelectValue}
|
||||
onChange={(value) => setStatsUserId(value?.value)}
|
||||
styles={{
|
||||
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||
option: (styles, state) => ({
|
||||
...styles,
|
||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||
color: state.isFocused ? "black" : styles.color,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{(user.type === "student" && (
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
<div className="font-semibold text-2xl">Generate New Training Material</div>
|
||||
<button
|
||||
className={clsx(
|
||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light ml-4",
|
||||
"transition duration-300 ease-in-out",
|
||||
)}
|
||||
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-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;
|
||||
@@ -3,16 +3,20 @@ import {create} from "zustand";
|
||||
|
||||
export interface RecordState {
|
||||
selectedUser?: string;
|
||||
training: boolean;
|
||||
setSelectedUser: (selectedUser: string | undefined) => void;
|
||||
setTraining: (training: boolean) => void;
|
||||
}
|
||||
|
||||
export const initialState = {
|
||||
selectedUser: undefined,
|
||||
training: false
|
||||
};
|
||||
|
||||
const recordStore = create<RecordState>((set) => ({
|
||||
...initialState,
|
||||
setSelectedUser: (selectedUser: string | undefined) => set(() => ({selectedUser})),
|
||||
setTraining: (training: boolean) => set(() => ({training})),
|
||||
}));
|
||||
|
||||
export default recordStore;
|
||||
|
||||
18
src/stores/trainingContentStore.ts
Normal file
18
src/stores/trainingContentStore.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Stat } from "@/interfaces/user";
|
||||
import {create} from "zustand";
|
||||
|
||||
export interface TrainingContentState {
|
||||
stats: Stat[];
|
||||
setStats: (stats: Stat[]) => void;
|
||||
}
|
||||
|
||||
export const initialState = {
|
||||
stats: [],
|
||||
};
|
||||
|
||||
const trainingContentStore = create<TrainingContentState>((set) => ({
|
||||
...initialState,
|
||||
setStats: (stats: Stat[]) => set(() => ({stats})),
|
||||
}));
|
||||
|
||||
export default trainingContentStore;
|
||||
@@ -8,6 +8,9 @@ module.exports = {
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
boxShadow: {
|
||||
'training-inset': 'inset 0px 2px 18px 0px #00000029',
|
||||
},
|
||||
colors: {
|
||||
mti: {
|
||||
orange: {DEFAULT: "#FF6000", dark: "#cc4402", light: "#ff790a", ultralight: "#ffdaa5"},
|
||||
@@ -62,5 +65,5 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("daisyui"), require("tailwind-scrollbar-hide")],
|
||||
plugins: [require("daisyui"), require("tailwind-scrollbar-hide"),],
|
||||
};
|
||||
|
||||
@@ -21,9 +21,8 @@
|
||||
"baseUrl": ".",
|
||||
"downlevelIteration": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
"@/*": ["./src/*"],
|
||||
"@/training/*": ["./src/components/TrainingContent/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
|
||||
466
yarn.lock
466
yarn.lock
@@ -183,7 +183,7 @@
|
||||
"@emotion/utils" "0.11.3"
|
||||
"@emotion/weak-memoize" "0.2.5"
|
||||
|
||||
"@emotion/cache@^11.13.0", "@emotion/cache@^11.4.0":
|
||||
"@emotion/cache@^11.13.0":
|
||||
version "11.13.1"
|
||||
resolved "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.1.tgz"
|
||||
integrity sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw==
|
||||
@@ -194,16 +194,27 @@
|
||||
"@emotion/weak-memoize" "^0.4.0"
|
||||
stylis "4.2.0"
|
||||
|
||||
"@emotion/hash@0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz"
|
||||
integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
|
||||
"@emotion/cache@^11.4.0":
|
||||
version "11.13.1"
|
||||
resolved "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.1.tgz"
|
||||
integrity sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw==
|
||||
dependencies:
|
||||
"@emotion/memoize" "^0.9.0"
|
||||
"@emotion/sheet" "^1.4.0"
|
||||
"@emotion/utils" "^1.4.0"
|
||||
"@emotion/weak-memoize" "^0.4.0"
|
||||
stylis "4.2.0"
|
||||
|
||||
"@emotion/hash@^0.9.2":
|
||||
version "0.9.2"
|
||||
resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz"
|
||||
integrity sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==
|
||||
|
||||
"@emotion/hash@0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz"
|
||||
integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
|
||||
|
||||
"@emotion/is-prop-valid@^0.8.2":
|
||||
version "0.8.8"
|
||||
resolved "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz"
|
||||
@@ -211,16 +222,16 @@
|
||||
dependencies:
|
||||
"@emotion/memoize" "0.7.4"
|
||||
|
||||
"@emotion/memoize@0.7.4":
|
||||
version "0.7.4"
|
||||
resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz"
|
||||
integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==
|
||||
|
||||
"@emotion/memoize@^0.9.0":
|
||||
version "0.9.0"
|
||||
resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz"
|
||||
integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==
|
||||
|
||||
"@emotion/memoize@0.7.4":
|
||||
version "0.7.4"
|
||||
resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz"
|
||||
integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==
|
||||
|
||||
"@emotion/react@^11.8.1":
|
||||
version "11.13.0"
|
||||
resolved "https://registry.npmjs.org/@emotion/react/-/react-11.13.0.tgz"
|
||||
@@ -246,7 +257,7 @@
|
||||
"@emotion/utils" "0.11.3"
|
||||
csstype "^2.5.7"
|
||||
|
||||
"@emotion/serialize@^1.2.0", "@emotion/serialize@^1.3.0":
|
||||
"@emotion/serialize@^1.2.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.0.tgz"
|
||||
integrity sha512-jACuBa9SlYajnpIVXB+XOXnfJHyckDfe6fOpORIM6yhBDlqGuExvDdZYHDQGoDf3bZXGv7tNr+LpLjJqiEQ6EA==
|
||||
@@ -257,56 +268,67 @@
|
||||
"@emotion/utils" "^1.4.0"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@emotion/sheet@0.9.4":
|
||||
version "0.9.4"
|
||||
resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.4.tgz"
|
||||
integrity sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==
|
||||
"@emotion/serialize@^1.3.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.0.tgz"
|
||||
integrity sha512-jACuBa9SlYajnpIVXB+XOXnfJHyckDfe6fOpORIM6yhBDlqGuExvDdZYHDQGoDf3bZXGv7tNr+LpLjJqiEQ6EA==
|
||||
dependencies:
|
||||
"@emotion/hash" "^0.9.2"
|
||||
"@emotion/memoize" "^0.9.0"
|
||||
"@emotion/unitless" "^0.9.0"
|
||||
"@emotion/utils" "^1.4.0"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@emotion/sheet@^1.4.0":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz"
|
||||
integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==
|
||||
|
||||
"@emotion/sheet@0.9.4":
|
||||
version "0.9.4"
|
||||
resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.4.tgz"
|
||||
integrity sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==
|
||||
|
||||
"@emotion/stylis@0.8.5":
|
||||
version "0.8.5"
|
||||
resolved "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz"
|
||||
integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==
|
||||
|
||||
"@emotion/unitless@0.7.5":
|
||||
version "0.7.5"
|
||||
resolved "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz"
|
||||
integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==
|
||||
|
||||
"@emotion/unitless@^0.9.0":
|
||||
version "0.9.0"
|
||||
resolved "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.9.0.tgz"
|
||||
integrity sha512-TP6GgNZtmtFaFcsOgExdnfxLLpRDla4Q66tnenA9CktvVSdNKDvMVuUah4QvWPIpNjrWsGg3qeGo9a43QooGZQ==
|
||||
|
||||
"@emotion/unitless@0.7.5":
|
||||
version "0.7.5"
|
||||
resolved "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz"
|
||||
integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==
|
||||
|
||||
"@emotion/use-insertion-effect-with-fallbacks@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz"
|
||||
integrity sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==
|
||||
|
||||
"@emotion/utils@0.11.3":
|
||||
version "0.11.3"
|
||||
resolved "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.3.tgz"
|
||||
integrity sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==
|
||||
|
||||
"@emotion/utils@^1.4.0":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.0.tgz"
|
||||
integrity sha512-spEnrA1b6hDR/C68lC2M7m6ALPUHZC0lIY7jAS/B/9DuuO1ZP04eov8SMv/6fwRd8pzmsn2AuJEznRREWlQrlQ==
|
||||
|
||||
"@emotion/weak-memoize@0.2.5":
|
||||
version "0.2.5"
|
||||
resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz"
|
||||
integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
|
||||
"@emotion/utils@0.11.3":
|
||||
version "0.11.3"
|
||||
resolved "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.3.tgz"
|
||||
integrity sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==
|
||||
|
||||
"@emotion/weak-memoize@^0.4.0":
|
||||
version "0.4.0"
|
||||
resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz"
|
||||
integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==
|
||||
|
||||
"@emotion/weak-memoize@0.2.5":
|
||||
version "0.2.5"
|
||||
resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz"
|
||||
integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
|
||||
|
||||
"@eslint/eslintrc@^1.4.1":
|
||||
version "1.4.1"
|
||||
resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz"
|
||||
@@ -456,7 +478,7 @@
|
||||
"@firebase/util" "1.9.3"
|
||||
tslib "^2.1.0"
|
||||
|
||||
"@firebase/database-compat@0.3.4", "@firebase/database-compat@^0.3.4":
|
||||
"@firebase/database-compat@^0.3.4", "@firebase/database-compat@0.3.4":
|
||||
version "0.3.4"
|
||||
resolved "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.3.4.tgz"
|
||||
integrity sha512-kuAW+l+sLMUKBThnvxvUZ+Q1ZrF/vFJ58iUY9kAcbX48U03nVzIF6Tmkf0p3WVQwMqiXguSgtOPIB6ZCeF+5Gg==
|
||||
@@ -468,7 +490,7 @@
|
||||
"@firebase/util" "1.9.3"
|
||||
tslib "^2.1.0"
|
||||
|
||||
"@firebase/database-types@0.10.4", "@firebase/database-types@^0.10.4":
|
||||
"@firebase/database-types@^0.10.4", "@firebase/database-types@0.10.4":
|
||||
version "0.10.4"
|
||||
resolved "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.10.4.tgz"
|
||||
integrity sha512-dPySn0vJ/89ZeBac70T+2tWWPiJXWbmRygYv0smT5TfE3hDrQ09eKMF3Y+vMlTdrMWq7mUdYW5REWPSGH4kAZQ==
|
||||
@@ -944,66 +966,6 @@
|
||||
resolved "https://registry.npmjs.org/@next/font/-/font-13.1.6.tgz"
|
||||
integrity sha512-AITjmeb1RgX1HKMCiA39ztx2mxeAyxl4ljv2UoSBUGAbFFMg8MO7YAvjHCgFhD39hL7YTbFjol04e/BPBH5RzQ==
|
||||
|
||||
"@next/swc-android-arm-eabi@13.1.6":
|
||||
version "13.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.1.6.tgz#d766dfc10e27814d947b20f052067c239913dbcc"
|
||||
integrity sha512-F3/6Z8LH/pGlPzR1AcjPFxx35mPqjE5xZcf+IL+KgbW9tMkp7CYi1y7qKrEWU7W4AumxX/8OINnDQWLiwLasLQ==
|
||||
|
||||
"@next/swc-android-arm64@13.1.6":
|
||||
version "13.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-13.1.6.tgz#f37a98d5f18927d8c9970d750d516ac779465176"
|
||||
integrity sha512-cMwQjnB8vrYkWyK/H0Rf2c2pKIH4RGjpKUDvbjVAit6SbwPDpmaijLio0LWFV3/tOnY6kvzbL62lndVA0mkYpw==
|
||||
|
||||
"@next/swc-darwin-arm64@13.1.6":
|
||||
version "13.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.1.6.tgz#ec1b90fd9bf809d8b81004c5182e254dced4ad96"
|
||||
integrity sha512-KKRQH4DDE4kONXCvFMNBZGDb499Hs+xcFAwvj+rfSUssIDrZOlyfJNy55rH5t2Qxed1e4K80KEJgsxKQN1/fyw==
|
||||
|
||||
"@next/swc-darwin-x64@13.1.6":
|
||||
version "13.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.1.6.tgz#e869ac75d16995eee733a7d1550322d9051c1eb4"
|
||||
integrity sha512-/uOky5PaZDoaU99ohjtNcDTJ6ks/gZ5ykTQDvNZDjIoCxFe3+t06bxsTPY6tAO6uEAw5f6vVFX5H5KLwhrkZCA==
|
||||
|
||||
"@next/swc-freebsd-x64@13.1.6":
|
||||
version "13.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.1.6.tgz#84a7b2e423a2904afc2edca21c2f1ba6b53fa4c1"
|
||||
integrity sha512-qaEALZeV7to6weSXk3Br80wtFQ7cFTpos/q+m9XVRFggu+8Ib895XhMWdJBzew6aaOcMvYR6KQ6JmHA2/eMzWw==
|
||||
|
||||
"@next/swc-linux-arm-gnueabihf@13.1.6":
|
||||
version "13.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.1.6.tgz#980eed1f655ff8a72187d8a6ef9e73ac39d20d23"
|
||||
integrity sha512-OybkbC58A1wJ+JrJSOjGDvZzrVEQA4sprJejGqMwiZyLqhr9Eo8FXF0y6HL+m1CPCpPhXEHz/2xKoYsl16kNqw==
|
||||
|
||||
"@next/swc-linux-arm64-gnu@13.1.6":
|
||||
version "13.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.1.6.tgz#87a71db21cded3f7c63d1d19079845c59813c53d"
|
||||
integrity sha512-yCH+yDr7/4FDuWv6+GiYrPI9kcTAO3y48UmaIbrKy8ZJpi7RehJe3vIBRUmLrLaNDH3rY1rwoHi471NvR5J5NQ==
|
||||
|
||||
"@next/swc-linux-arm64-musl@13.1.6":
|
||||
version "13.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.1.6.tgz#c5aac8619331b9fd030603bbe2b36052011e11de"
|
||||
integrity sha512-ECagB8LGX25P9Mrmlc7Q/TQBb9rGScxHbv/kLqqIWs2fIXy6Y/EiBBiM72NTwuXUFCNrWR4sjUPSooVBJJ3ESQ==
|
||||
|
||||
"@next/swc-linux-x64-gnu@13.1.6":
|
||||
version "13.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.1.6.tgz#9513d36d540bbfea575576746736054c31aacdea"
|
||||
integrity sha512-GT5w2mruk90V/I5g6ScuueE7fqj/d8Bui2qxdw6lFxmuTgMeol5rnzAv4uAoVQgClOUO/MULilzlODg9Ib3Y4Q==
|
||||
|
||||
"@next/swc-linux-x64-musl@13.1.6":
|
||||
version "13.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.1.6.tgz#d61fc6884899f5957251f4ce3f522e34a2c479b7"
|
||||
integrity sha512-keFD6KvwOPzmat4TCnlnuxJCQepPN+8j3Nw876FtULxo8005Y9Ghcl7ACcR8GoiKoddAq8gxNBrpjoxjQRHeAQ==
|
||||
|
||||
"@next/swc-win32-arm64-msvc@13.1.6":
|
||||
version "13.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.1.6.tgz#fac2077a8ae9768e31444c9ae90807e64117cda7"
|
||||
integrity sha512-OwertslIiGQluFvHyRDzBCIB07qJjqabAmINlXUYt7/sY7Q7QPE8xVi5beBxX/rxTGPIbtyIe3faBE6Z2KywhQ==
|
||||
|
||||
"@next/swc-win32-ia32-msvc@13.1.6":
|
||||
version "13.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.1.6.tgz#498bc11c91b4c482a625bf4b978f98ae91111e46"
|
||||
integrity sha512-g8zowiuP8FxUR9zslPmlju7qYbs2XBtTLVSxVikPtUDQedhcls39uKYLvOOd1JZg0ehyhopobRoH1q+MHlIN/w==
|
||||
|
||||
"@next/swc-win32-x64-msvc@13.1.6":
|
||||
version "13.1.6"
|
||||
resolved "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.1.6.tgz"
|
||||
@@ -1017,7 +979,7 @@
|
||||
"@nodelib/fs.stat" "2.0.5"
|
||||
run-parallel "^1.1.9"
|
||||
|
||||
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
|
||||
"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5":
|
||||
version "2.0.5"
|
||||
resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
|
||||
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
|
||||
@@ -1295,12 +1257,57 @@
|
||||
resolved "https://registry.npmjs.org/@react-pdf/types/-/types-2.5.0.tgz"
|
||||
integrity sha512-XsVRkt0hQ60I4e3leAVt+aZR3KJCaJd179BfJHAv4F4x6Vq3yqkry8lcbUWKGKDw1j3/8sW4FsgGR41SFvsG9A==
|
||||
|
||||
"@react-spring/animated@~9.7.4":
|
||||
version "9.7.4"
|
||||
resolved "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.4.tgz"
|
||||
integrity sha512-7As+8Pty2QlemJ9O5ecsuPKjmO0NKvmVkRR1n6mEotFgWar8FKuQt2xgxz3RTgxcccghpx1YdS1FCdElQNexmQ==
|
||||
dependencies:
|
||||
"@react-spring/shared" "~9.7.4"
|
||||
"@react-spring/types" "~9.7.4"
|
||||
|
||||
"@react-spring/core@~9.7.4":
|
||||
version "9.7.4"
|
||||
resolved "https://registry.npmjs.org/@react-spring/core/-/core-9.7.4.tgz"
|
||||
integrity sha512-GzjA44niEJBFUe9jN3zubRDDDP2E4tBlhNlSIkTChiNf9p4ZQlgXBg50qbXfSXHQPHak/ExYxwhipKVsQ/sUTw==
|
||||
dependencies:
|
||||
"@react-spring/animated" "~9.7.4"
|
||||
"@react-spring/shared" "~9.7.4"
|
||||
"@react-spring/types" "~9.7.4"
|
||||
|
||||
"@react-spring/rafz@~9.7.4":
|
||||
version "9.7.4"
|
||||
resolved "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.4.tgz"
|
||||
integrity sha512-mqDI6rW0Ca8IdryOMiXRhMtVGiEGLIO89vIOyFQXRIwwIMX30HLya24g9z4olDvFyeDW3+kibiKwtZnA4xhldA==
|
||||
|
||||
"@react-spring/shared@~9.7.4":
|
||||
version "9.7.4"
|
||||
resolved "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.4.tgz"
|
||||
integrity sha512-bEPI7cQp94dOtCFSEYpxvLxj0+xQfB5r9Ru1h8OMycsIq7zFZon1G0sHrBLaLQIWeMCllc4tVDYRTLIRv70C8w==
|
||||
dependencies:
|
||||
"@react-spring/rafz" "~9.7.4"
|
||||
"@react-spring/types" "~9.7.4"
|
||||
|
||||
"@react-spring/types@~9.7.4":
|
||||
version "9.7.4"
|
||||
resolved "https://registry.npmjs.org/@react-spring/types/-/types-9.7.4.tgz"
|
||||
integrity sha512-iQVztO09ZVfsletMiY+DpT/JRiBntdsdJ4uqk3UJFhrhS8mIC9ZOZbmfGSRs/kdbNPQkVyzucceDicQ/3Mlj9g==
|
||||
|
||||
"@react-spring/web@^9.7.4":
|
||||
version "9.7.4"
|
||||
resolved "https://registry.npmjs.org/@react-spring/web/-/web-9.7.4.tgz"
|
||||
integrity sha512-UMvCZp7I5HCVIleSa4BwbNxynqvj+mJjG2m20VO2yPoi2pnCYANy58flvz9v/YcXTAvsmL655FV3pm5fbr6akA==
|
||||
dependencies:
|
||||
"@react-spring/animated" "~9.7.4"
|
||||
"@react-spring/core" "~9.7.4"
|
||||
"@react-spring/shared" "~9.7.4"
|
||||
"@react-spring/types" "~9.7.4"
|
||||
|
||||
"@rushstack/eslint-patch@^1.1.3":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz"
|
||||
integrity sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==
|
||||
|
||||
"@swc/helpers@0.4.14", "@swc/helpers@^0.4.2":
|
||||
"@swc/helpers@^0.4.2", "@swc/helpers@0.4.14":
|
||||
version "0.4.14"
|
||||
resolved "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.14.tgz"
|
||||
integrity sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==
|
||||
@@ -1508,7 +1515,7 @@
|
||||
resolved "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz"
|
||||
integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==
|
||||
|
||||
"@types/node@*", "@types/node@18.13.0", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@>=8.1.0":
|
||||
"@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@>=8.1.0", "@types/node@18.13.0":
|
||||
version "18.13.0"
|
||||
resolved "https://registry.npmjs.org/@types/node/-/node-18.13.0.tgz"
|
||||
integrity sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg==
|
||||
@@ -2156,7 +2163,7 @@ classnames@^2.2.6, classnames@^2.3.0, classnames@^2.5.1:
|
||||
resolved "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz"
|
||||
integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
|
||||
|
||||
client-only@0.0.1, client-only@^0.0.1:
|
||||
client-only@^0.0.1, client-only@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz"
|
||||
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
||||
@@ -2179,6 +2186,15 @@ cliui@^7.0.2:
|
||||
strip-ansi "^6.0.0"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
cliui@^8.0.1:
|
||||
version "8.0.1"
|
||||
resolved "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz"
|
||||
integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==
|
||||
dependencies:
|
||||
string-width "^4.2.0"
|
||||
strip-ansi "^6.0.1"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
clone@^2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz"
|
||||
@@ -2203,16 +2219,16 @@ color-convert@^2.0.1:
|
||||
dependencies:
|
||||
color-name "~1.1.4"
|
||||
|
||||
color-name@1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz"
|
||||
integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
|
||||
|
||||
color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
|
||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||
|
||||
color-name@1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz"
|
||||
integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
|
||||
|
||||
color-string@^1.9.1:
|
||||
version "1.9.1"
|
||||
resolved "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz"
|
||||
@@ -2405,13 +2421,6 @@ date-fns@^2.0.1, date-fns@^2.30.0:
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.21.0"
|
||||
|
||||
debug@4, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz"
|
||||
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
debug@^3.2.7:
|
||||
version "3.2.7"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz"
|
||||
@@ -2419,6 +2428,13 @@ debug@^3.2.7:
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@4:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz"
|
||||
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
decamelize@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz"
|
||||
@@ -2588,7 +2604,7 @@ eastasianwidth@^0.2.0:
|
||||
resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz"
|
||||
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
|
||||
|
||||
ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11:
|
||||
ecdsa-sig-formatter@^1.0.11, ecdsa-sig-formatter@1.0.11:
|
||||
version "1.0.11"
|
||||
resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz"
|
||||
integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
|
||||
@@ -3301,11 +3317,6 @@ fs.realpath@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
|
||||
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
|
||||
|
||||
fsevents@~2.3.2:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
|
||||
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
|
||||
|
||||
function-bind@^1.1.1, function-bind@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
|
||||
@@ -3393,7 +3404,7 @@ get-tsconfig@^4.2.0:
|
||||
resolved "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.4.0.tgz"
|
||||
integrity sha512-0Gdjo/9+FzsYhXCEFueo2aY1z1tpXrxWZzP7k8ul9qt1U5o8rYJwTJYmaeHdrVosYIVYkOy2iwCJ9FdpocJhPQ==
|
||||
|
||||
glob-parent@^5.1.2, glob-parent@~5.1.2:
|
||||
glob-parent@^5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
|
||||
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
|
||||
@@ -3407,29 +3418,12 @@ glob-parent@^6.0.2:
|
||||
dependencies:
|
||||
is-glob "^4.0.3"
|
||||
|
||||
glob@7.1.6:
|
||||
version "7.1.6"
|
||||
resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz"
|
||||
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
|
||||
glob-parent@~5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
|
||||
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^3.0.4"
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@7.1.7, glob@^7.1.3:
|
||||
version "7.1.7"
|
||||
resolved "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz"
|
||||
integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^3.0.4"
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
is-glob "^4.0.1"
|
||||
|
||||
glob@^10.4.2:
|
||||
version "10.4.5"
|
||||
@@ -3443,6 +3437,18 @@ glob@^10.4.2:
|
||||
package-json-from-dist "^1.0.0"
|
||||
path-scurry "^1.11.1"
|
||||
|
||||
glob@^7.1.3, glob@7.1.7:
|
||||
version "7.1.7"
|
||||
resolved "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz"
|
||||
integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^3.0.4"
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@^8.0.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz"
|
||||
@@ -3454,6 +3460,18 @@ glob@^8.0.0:
|
||||
minimatch "^5.0.1"
|
||||
once "^1.3.0"
|
||||
|
||||
glob@7.1.6:
|
||||
version "7.1.6"
|
||||
resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz"
|
||||
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^3.0.4"
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
globals@^11.1.0:
|
||||
version "11.12.0"
|
||||
resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz"
|
||||
@@ -3747,7 +3765,7 @@ inflight@^1.0.4:
|
||||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2, inherits@^2.0.3, inherits@~2.0.3:
|
||||
inherits@^2.0.3, inherits@~2.0.3, inherits@2:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
@@ -4344,18 +4362,18 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
|
||||
dependencies:
|
||||
js-tokens "^3.0.0 || ^4.0.0"
|
||||
|
||||
lru-cache@6.0.0, lru-cache@^6.0.0:
|
||||
lru-cache@^10.2.0:
|
||||
version "10.4.3"
|
||||
resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz"
|
||||
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
|
||||
|
||||
lru-cache@^6.0.0, lru-cache@6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz"
|
||||
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
|
||||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
|
||||
lru-cache@^10.2.0:
|
||||
version "10.4.3"
|
||||
resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz"
|
||||
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
|
||||
|
||||
lru-memoizer@^2.2.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz"
|
||||
@@ -4426,7 +4444,7 @@ micromatch@^4.0.4, micromatch@^4.0.5:
|
||||
braces "^3.0.2"
|
||||
picomatch "^2.3.1"
|
||||
|
||||
mime-db@1.52.0, "mime-db@>= 1.43.0 < 2":
|
||||
"mime-db@>= 1.43.0 < 2", mime-db@1.52.0:
|
||||
version "1.52.0"
|
||||
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz"
|
||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||
@@ -4511,7 +4529,7 @@ moment@^2.29.4:
|
||||
resolved "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz"
|
||||
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
|
||||
|
||||
ms@2.1.2, ms@^2.1.1:
|
||||
ms@^2.1.1, ms@2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"
|
||||
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
||||
@@ -4575,14 +4593,21 @@ node-addon-api@^5.0.0:
|
||||
resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz"
|
||||
integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==
|
||||
|
||||
node-fetch@2.6.7, node-fetch@^2.6.1, node-fetch@^2.6.7:
|
||||
node-fetch@^2.6.1, node-fetch@^2.6.7, node-fetch@2.6.7:
|
||||
version "2.6.7"
|
||||
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz"
|
||||
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
|
||||
dependencies:
|
||||
whatwg-url "^5.0.0"
|
||||
|
||||
node-fetch@^2.6.12, node-fetch@^2.6.9:
|
||||
node-fetch@^2.6.12:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz"
|
||||
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
|
||||
dependencies:
|
||||
whatwg-url "^5.0.0"
|
||||
|
||||
node-fetch@^2.6.9:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz"
|
||||
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
|
||||
@@ -4938,15 +4963,6 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^
|
||||
resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz"
|
||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||
|
||||
postcss@8.4.14:
|
||||
version "8.4.14"
|
||||
resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz"
|
||||
integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==
|
||||
dependencies:
|
||||
nanoid "^3.3.4"
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
postcss@^8, postcss@^8.0.9, postcss@^8.4.21:
|
||||
version "8.4.22"
|
||||
resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.22.tgz"
|
||||
@@ -4956,6 +4972,15 @@ postcss@^8, postcss@^8.0.9, postcss@^8.4.21:
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
postcss@8.4.14:
|
||||
version "8.4.14"
|
||||
resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz"
|
||||
integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==
|
||||
dependencies:
|
||||
nanoid "^3.3.4"
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
prelude-ls@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz"
|
||||
@@ -4989,15 +5014,6 @@ promise-polyfill@^8.3.0:
|
||||
resolved "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz"
|
||||
integrity sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==
|
||||
|
||||
prop-types@15.7.2:
|
||||
version "15.7.2"
|
||||
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz"
|
||||
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
|
||||
dependencies:
|
||||
loose-envify "^1.4.0"
|
||||
object-assign "^4.1.1"
|
||||
react-is "^16.8.1"
|
||||
|
||||
prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
|
||||
version "15.8.1"
|
||||
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
|
||||
@@ -5007,6 +5023,15 @@ prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
|
||||
object-assign "^4.1.1"
|
||||
react-is "^16.13.1"
|
||||
|
||||
prop-types@15.7.2:
|
||||
version "15.7.2"
|
||||
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz"
|
||||
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
|
||||
dependencies:
|
||||
loose-envify "^1.4.0"
|
||||
object-assign "^4.1.1"
|
||||
react-is "^16.8.1"
|
||||
|
||||
proto3-json-serializer@^1.0.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.1.tgz"
|
||||
@@ -5030,24 +5055,6 @@ protobufjs-cli@1.1.1:
|
||||
tmp "^0.2.1"
|
||||
uglify-js "^3.7.7"
|
||||
|
||||
protobufjs@7.2.4:
|
||||
version "7.2.4"
|
||||
resolved "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz"
|
||||
integrity sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==
|
||||
dependencies:
|
||||
"@protobufjs/aspromise" "^1.1.2"
|
||||
"@protobufjs/base64" "^1.1.2"
|
||||
"@protobufjs/codegen" "^2.0.4"
|
||||
"@protobufjs/eventemitter" "^1.1.0"
|
||||
"@protobufjs/fetch" "^1.1.0"
|
||||
"@protobufjs/float" "^1.0.2"
|
||||
"@protobufjs/inquire" "^1.1.0"
|
||||
"@protobufjs/path" "^1.1.2"
|
||||
"@protobufjs/pool" "^1.1.0"
|
||||
"@protobufjs/utf8" "^1.1.0"
|
||||
"@types/node" ">=13.7.0"
|
||||
long "^5.0.0"
|
||||
|
||||
protobufjs@^6.11.3:
|
||||
version "6.11.3"
|
||||
resolved "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz"
|
||||
@@ -5103,6 +5110,24 @@ protobufjs@^7.2.5:
|
||||
"@types/node" ">=13.7.0"
|
||||
long "^5.0.0"
|
||||
|
||||
protobufjs@7.2.4:
|
||||
version "7.2.4"
|
||||
resolved "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz"
|
||||
integrity sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==
|
||||
dependencies:
|
||||
"@protobufjs/aspromise" "^1.1.2"
|
||||
"@protobufjs/base64" "^1.1.2"
|
||||
"@protobufjs/codegen" "^2.0.4"
|
||||
"@protobufjs/eventemitter" "^1.1.0"
|
||||
"@protobufjs/fetch" "^1.1.0"
|
||||
"@protobufjs/float" "^1.0.2"
|
||||
"@protobufjs/inquire" "^1.1.0"
|
||||
"@protobufjs/path" "^1.1.2"
|
||||
"@protobufjs/pool" "^1.1.0"
|
||||
"@protobufjs/utf8" "^1.1.0"
|
||||
"@types/node" ">=13.7.0"
|
||||
long "^5.0.0"
|
||||
|
||||
proxy-from-env@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz"
|
||||
@@ -5506,7 +5531,7 @@ run-parallel@^1.1.9:
|
||||
dependencies:
|
||||
queue-microtask "^1.2.2"
|
||||
|
||||
safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@~5.2.0:
|
||||
safe-buffer@^5.0.1, safe-buffer@>=5.1.0, safe-buffer@~5.2.0:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz"
|
||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
@@ -5689,7 +5714,30 @@ stream-shift@^1.0.2:
|
||||
resolved "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz"
|
||||
integrity sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==
|
||||
|
||||
"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:
|
||||
string_decoder@^1.1.1:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz"
|
||||
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
|
||||
dependencies:
|
||||
safe-buffer "~5.2.0"
|
||||
|
||||
string_decoder@~1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz"
|
||||
integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
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:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@@ -5739,21 +5787,14 @@ string.prototype.trimstart@^1.0.6:
|
||||
define-properties "^1.1.4"
|
||||
es-abstract "^1.20.4"
|
||||
|
||||
string_decoder@^1.1.1:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz"
|
||||
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
safe-buffer "~5.2.0"
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
string_decoder@~1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz"
|
||||
integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@@ -6130,7 +6171,7 @@ use-isomorphic-layout-effect@^1.1.2:
|
||||
resolved "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz"
|
||||
integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==
|
||||
|
||||
use-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"
|
||||
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz"
|
||||
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||
@@ -6297,6 +6338,15 @@ wrap-ansi@^6.2.0:
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"
|
||||
@@ -6349,6 +6399,11 @@ yargs-parser@^20.2.2:
|
||||
resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz"
|
||||
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
|
||||
|
||||
yargs-parser@^21.1.1:
|
||||
version "21.1.1"
|
||||
resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz"
|
||||
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
|
||||
|
||||
yargs@^15.3.1:
|
||||
version "15.4.1"
|
||||
resolved "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz"
|
||||
@@ -6379,6 +6434,19 @@ yargs@^16.2.0:
|
||||
y18n "^5.0.5"
|
||||
yargs-parser "^20.2.2"
|
||||
|
||||
yargs@^17.7.2:
|
||||
version "17.7.2"
|
||||
resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz"
|
||||
integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
|
||||
dependencies:
|
||||
cliui "^8.0.1"
|
||||
escalade "^3.1.1"
|
||||
get-caller-file "^2.0.5"
|
||||
require-directory "^2.1.1"
|
||||
string-width "^4.2.3"
|
||||
y18n "^5.0.5"
|
||||
yargs-parser "^21.1.1"
|
||||
|
||||
yocto-queue@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"
|
||||
|
||||
Reference in New Issue
Block a user