diff --git a/src/components/ModuleBadge.tsx b/src/components/ModuleBadge.tsx new file mode 100644 index 00000000..fc763487 --- /dev/null +++ b/src/components/ModuleBadge.tsx @@ -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 }) => ( +
+ {module === "reading" && } + {module === "listening" && } + {module === "writing" && } + {module === "speaking" && } + {module === "level" && } + {/* do not switch to level && it will convert the 0.0 to 0*/} + {level !== undefined && ({level.toFixed(1)})} +
+); + +export default ModuleBadge; \ No newline at end of file diff --git a/src/components/StatGridItem.tsx b/src/components/StatGridItem.tsx new file mode 100644 index 00000000..839c8400 --- /dev/null +++ b/src/components/StatGridItem.tsx @@ -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>; + 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 = ({ + 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 = ( + <> +
+
+ {formatTimestamp(timestamp)} +
+ {!!timeSpent && ( + + {Math.floor(timeSpent / 60)} minutes + + )} + {!!inactivity && ( + + {Math.floor(inactivity / 60)} minutes + + )} +
+
+
+
+ + Level{" "} + {(aggregatedLevels.reduce((accumulator, current) => accumulator + current.level, 0) / aggregatedLevels.length).toFixed(1)} + + {renderPdfIcon(session, textColor, textColor)} +
+ {aiUsage >= 50 && user.type !== "student" && ( +
= 80, + } + )}> + AI Usage +
+ )} +
+
+ +
+
+ {aggregatedLevels.map(({ module, level }) => ( + + ))} +
+ + {assignment && ( + + Assignment: {assignment.name}, Teacher: {users.find((u) => u.id === assignment.assigner)?.name} + + )} +
+ + ); + + return ( + <> +
= 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} +
+
= 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} +
+ + ); +}; + +export default StatsGridItem; \ No newline at end of file diff --git a/src/components/TrainingContent/Exercise.tsx b/src/components/TrainingContent/Exercise.tsx index d742348b..984a30be 100644 --- a/src/components/TrainingContent/Exercise.tsx +++ b/src/components/TrainingContent/Exercise.tsx @@ -1,10 +1,10 @@ import React, { useState, useCallback } from "react"; import ExerciseWalkthrough from "@/training/ExerciseWalkthrough"; -import { WalkthroughConfigs } from "./TrainingInterfaces"; +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 = () => { +const TrainingExercise: React.FC = (trainingTip: ITrainingTip) => { const leftText = "
CategoryOption AOption B
SelfYou need to take care of yourself and connect with the people around you.Focus on your interests and talents and meet people who are like you.
HomeIt's a good idea to paint your living room yellow.You should arrange your home so that it makes you feel happy.
Financial LifeYou can be happy if you have enough money, but don't want money too much.If you waste money on things you don't need, you won't have enough money for things that you do need.
Social LifeA good group of friends can increase your happiness.Researchers say that a happy friend can increase our mood by nine percent.
WorkplaceYou spend a lot of time at work, so you should like your workplace.Your boss needs to be someone you enjoy working for.
CommunityThe place where you live is more important for happiness than anything else.Live around people who have the same amount of money as you do.
"; const tip = { category: "Strategy", @@ -68,14 +68,21 @@ const TrainingExercise: React.FC = () => { } ] + const mockTip: ITrainingTip = { + id: "some random id", + tipCategory: tip.category, + tipHtml: tip.body, + standalone: false, + exercise: { + question: question, + highlightable: leftText, + segments: rightTextData + } + } return ( -
- +
); diff --git a/src/components/TrainingContent/ExerciseWalkthrough.tsx b/src/components/TrainingContent/ExerciseWalkthrough.tsx index 0c70fc62..07ea16ff 100644 --- a/src/components/TrainingContent/ExerciseWalkthrough.tsx +++ b/src/components/TrainingContent/ExerciseWalkthrough.tsx @@ -2,24 +2,10 @@ 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 { SegmentRef, TimelineEvent, WalkthroughConfigs } from './TrainingInterfaces'; +import { ITrainingTip, SegmentRef, TimelineEvent } from './TrainingInterfaces'; -interface ExerciseWalkthroughProps { - highlightable: string; - walkthrough: WalkthroughConfigs[]; - question: string; - tip: { - category: string; - body: string; - }; -} -const ExerciseWalkthrough: React.FC = ({ - highlightable, - walkthrough, - question, - tip -}) => { +const ExerciseWalkthrough: React.FC = (tip: ITrainingTip) => { const [isAutoPlaying, setIsAutoPlaying] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [walkthroughHtml, setWalkthroughHtml] = useState(''); @@ -47,8 +33,9 @@ const ExerciseWalkthrough: React.FC = ({ }, []); const getMaxTime = (): number => { - return walkthrough.reduce((sum, segment) => - sum + segment.wordDelay * segment.html.split(/\s+/).length + segment.holdDelay, 0); + return tip.exercise?.segments.reduce((sum, segment) => + sum + segment.wordDelay * segment.html.split(/\s+/).length + segment.holdDelay, 0 + ) ?? 0; }; useEffect(() => { @@ -56,7 +43,7 @@ const ExerciseWalkthrough: React.FC = ({ let currentTimePosition = 0; segmentsRef.current = []; - walkthrough.forEach((segment, index) => { + tip.exercise?.segments.forEach((segment, index) => { const parser = new DOMParser(); const doc = parser.parseFromString(segment.html, 'text/html'); const words: string[] = []; @@ -99,7 +86,7 @@ const ExerciseWalkthrough: React.FC = ({ }); timelineRef.current = timeline; - }, [walkthrough]); + }, [tip.exercise?.segments]); const updateText = useCallback(() => { const currentEvent = timelineRef.current.find( @@ -231,11 +218,24 @@ const ExerciseWalkthrough: React.FC = ({ } }; + if (tip.standalone || !tip.exercise) { + return ( +
+

The exercise for this tip is not available yet!

+
+

{tip.tipCategory}

+
+
+
+ ); + } + + return (
-

{tip.category}

-
+

{tip.tipCategory}

+
@@ -266,8 +266,8 @@ const ExerciseWalkthrough: React.FC = ({
{/*

Question

*/} -
- +
+
diff --git a/src/components/TrainingContent/TrainingInterfaces.ts b/src/components/TrainingContent/TrainingInterfaces.ts index 8f9a426b..549531d2 100644 --- a/src/components/TrainingContent/TrainingInterfaces.ts +++ b/src/components/TrainingContent/TrainingInterfaces.ts @@ -1,14 +1,20 @@ +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: string; - stat_ids: string[] + score: number; + module: string; + stat_ids: string[]; + stats?: Stat[]; }[]; tip_ids: string[]; + tips?: ITrainingTip[]; weak_areas: { area: string; comment: string; @@ -20,7 +26,7 @@ export interface ITrainingTip { tipCategory: string; tipHtml: string; standalone: boolean; - exercise: { + exercise?: { question: string; highlightable: string; segments: WalkthroughConfigs[] diff --git a/src/components/TrainingContent/TrainingScore.tsx b/src/components/TrainingContent/TrainingScore.tsx new file mode 100644 index 00000000..08699013 --- /dev/null +++ b/src/components/TrainingContent/TrainingScore.tsx @@ -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 = ({ + 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 ( +
+
+
+
+ + + +
+
+

{trainingContent.exams.length}

+

Exams Selected

+
+
+
+
+ +
+
+

{highestScore}%

+

Highest Score

+
+
+
+
+
+
+ +
+
+

{averageScore}%

+

Average Score

+
+
+
+
+ +
+
+

{lowestScore}%

+

Lowest Score

+
+
+
+ {gridView && ( +
+
+ +
+

{trainingContent.tip_ids.length} Tips

+
+ )} +
+ ); +}; + +export default TrainingScore; diff --git a/src/pages/api/training/[id].ts b/src/pages/api/training/[id].ts new file mode 100644 index 00000000..4532c366 --- /dev/null +++ b/src/pages/api/training/[id].ts @@ -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' }); + } +} diff --git a/src/pages/api/training/index.ts b/src/pages/api/training/index.ts index 200b0b2a..fe694e61 100644 --- a/src/pages/api/training/index.ts +++ b/src/pages/api/training/index.ts @@ -1,7 +1,7 @@ import { sessionOptions } from "@/lib/session"; import axios from "axios"; -import {app} from "@/firebase"; -import { collection, getDocs, getFirestore, query } from "firebase/firestore"; +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"; @@ -35,16 +35,17 @@ async function post(req: NextApiRequest, res: NextApiResponse) { 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' }); } } + diff --git a/src/pages/api/training/walkthrough/index.ts b/src/pages/api/training/walkthrough/index.ts new file mode 100644 index 00000000..727df308 --- /dev/null +++ b/src/pages/api/training/walkthrough/index.ts @@ -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' }); + } +} diff --git a/src/pages/record.tsx b/src/pages/record.tsx index f4d38b5d..64093e27 100644 --- a/src/pages/record.tsx +++ b/src/pages/record.tsx @@ -5,20 +5,14 @@ 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 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"; @@ -27,8 +21,8 @@ import { uuidv4 } from "@firebase/util"; import { usePDFDownload } from "@/hooks/usePDFDownload"; import useRecordStore from "@/stores/recordStore"; import useTrainingContentStore from "@/stores/trainingContentStore"; -import ai_usage from "@/utils/ai.detection"; import Button from "@/components/Low/Button"; +import StatsGridItem from "@/components/StatGridItem"; export const getServerSideProps = withIronSessionSsr(({ req, res }) => { @@ -81,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) { @@ -138,57 +132,6 @@ 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"; - - 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] })); - }; - const MAX_TRAINING_EXAMS = 10; const [selectedTrainingExams, setSelectedTrainingExams] = useState([]); const setTrainingStats = useTrainingContentStore((state) => state.setStats); @@ -222,169 +165,26 @@ export default function History({ user }: { user: User }) { 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 = () => { - if (training && !isDisabled) { - 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(dateStats, "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(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 = ( - <> -
-
- {formatTimestamp(timestamp)} -
- {!!timeSpent && ( - - {Math.floor(timeSpent / 60)} minutes - - )} - {!!inactivity && ( - - {Math.floor(inactivity / 60)} minutes - - )} -
-
-
-
- - Level{" "} - {(aggregatedLevels.reduce((accumulator, current) => accumulator + current.level, 0) / aggregatedLevels.length).toFixed(1)} - - {renderPdfIcon(session, textColor, textColor)} -
- {aiUsage >= 50 && user.type !== "student" && ( -
= 80, - } - )}> - AI Usage -
- )} -
-
- -
-
- {aggregatedLevels.map(({ module, level }) => ( -
- {module === "reading" && } - {module === "listening" && } - {module === "writing" && } - {module === "speaking" && } - {module === "level" && } - {level.toFixed(1)} -
- ))} -
- - {assignment && ( - - Assignment: {assignment.name}, Teacher: {users.find((u) => u.id === assignment.assigner)?.name} - - )} -
- - ); return ( - <> -
= 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", - selectedTrainingExams.includes(timestamp) && "border-2 border-slate-600", - )} - onClick={selectExam} - data-tip="This exam is still being evaluated..." - role="button"> - {content} -
-
= 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} -
- + ); }; @@ -522,15 +322,17 @@ export default function History({ user }: { user: User }) { )} {(training && ( -
-
Select up to 10 exams {`(${selectedTrainingExams.length}/${MAX_TRAINING_EXAMS})`}
- +
))}
diff --git a/src/pages/training.tsx b/src/pages/training.tsx deleted file mode 100644 index a808ecd8..00000000 --- a/src/pages/training.tsx +++ /dev/null @@ -1,553 +0,0 @@ -/* 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 Button from "@/components/Low/Button"; -import { MdOutlinePlaylistAddCheckCircle } from "react-icons/md"; -import { MdOutlineSelfImprovement } from "react-icons/md"; -import { FaChartLine } from "react-icons/fa6"; -import { RiArrowRightUpLine } from "react-icons/ri"; -import { RiArrowLeftDownLine } from "react-icons/ri"; -import { BsChatLeftDots } from "react-icons/bs"; -import { AiOutlineFileSearch } from "react-icons/ai"; -import { Tab } from "@headlessui/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, WalkthroughConfigs } from "@/training/TrainingInterfaces"; -import moment from "moment"; -import Exercise from "@/training/Exercise"; - -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 TrainingContent: React.FC<{ user: User }> = ({ user }) => { - // Record stuff - const { users } = useUsers(); - const [selectedCorporate, setSelectedCorporate] = useState(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 [landingPage, setIsLandingPage] = useState(stats.length != 0); - - const [currentTipIndex, setCurrentTipIndex] = useState(0); - const [trainingContent, setTrainingContent] = useState([]); - const [isLoading, setIsLoading] = useState(stats.length != 0); - const [groupedByTrainingContent, setGroupedByTrainingContent] = useState<{ [key: string]: ITrainingContent[] }>(); - const [trainingExercises, setTrainingExercise] = useState(); - - useEffect(() => { - const handleRouteChange = (url: string) => { - setTrainingStats([]) - } - router.events.on('routeChangeStart', handleRouteChange) - return () => { - router.events.off('routeChangeStart', handleRouteChange) - } - }, [router.events, setTrainingStats]) - - // TODO: Figure out how dafuq I'm gonna reference the exams - const examAssessments = [ - { exam: "Exam 1", score: 80, description: 'Demonstrates comprehension but lacks depth in analysis.' }, - { exam: "Exam 2", score: 70, description: 'Issues with sentence structure and punctuation.' }, - { exam: "Exam 3", score: 95, description: 'Excellent critical thinking and textual evidence use.' }, - { exam: "Exam 4", score: 65, description: 'Needs deeper analysis of literary devices.' }, - { exam: "Exam 5", score: 75, description: 'Overall comprehension and theme articulation needs improvement.' }, - ]; - - const weakAreas = [ - { topic: "Grammar and Syntax", description: "Issues with sentence structure and punctuation.\nCommon grammatical errors (e.g., subject-verb agreement, incorrect tense usage)." }, - { topic: "Depth of Analysis", description: "Lack of critical engagement with texts.\nSuperificial analysis without deep insights or original thought." }, - { topic: "Comprehension of Themes", description: "Difficulty in identifying and articulating central themes and motifs.\nMisinterpretaion of author's intentions and literary devices." }, - ]; - - const detailedBreakdown = [ - { exam: "Exam 2", description: "Needs deeper analysis of literary devices." }, - { exam: "Exam 4", description: "Minor grammar and punctuation errors." }, - { exam: "Exam 5", description: "Overall comprehension and theme articulation needs improvement." } - ] - - useEffect(() => { - const postStats = async () => { - try { - await axios.post(`/api/training`, stats); - setIsLoading(false); - } catch (error) { - setIsLoading(false); - console.error(error); - } - }; - - if (isLoading) { - postStats(); - } - }, [isLoading]) - - /*useEffect(() => { - const loadExercises = async () => { - }; - - loadExercises(); - }, []);*/ - - - const handleNext = () => { - /*setCurrentTipIndex((prevIndex) => (prevIndex + 1) % tips.length);*/ - }; - - const handlePrevious = () => { - /*setCurrentTipIndex((prevIndex) => (prevIndex - 1 + tips.length) % tips.length);*/ - }; - - /*const currentTip = tips[currentTipIndex];*/ - - 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 (stats && !isLoading) { - setGroupedByTrainingContent(groupBy(trainingContent, "created_at")) - } - }, [trainingContent, isLoading]);*/ - - - // 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 trainingContentContainer = (timestamp: string) => { - if (!groupedByTrainingContent) return <>; - } - - return ( - <> - - Training | EnCoach - - - - - - - - {(landingPage ? ( - <> -
-
- {(user.type === "developer" || user.type === "admin") && ( - <> - - - - - - 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" && landingPage && ( - <> -
-
Generate New Training Material
- -
- - ))} -
-
- - - -
-
-
- {trainingContent.length == 0 && (No training content to display...)} -
- - ) : ( - <> {isLoading ? ( -
- - - Assessing your exams, please be patient... - -
- ) : ( - <> -
-
-
-
- -

General Evaluation

-
-
-
-
-
- - - - -
-
-

5

-

Exams Selected

-
-
-
-
- -
-
-

90%

-

Highest Score

-
-
-
-
-
-
- -
-
-

78%

-

Average Score

-
-
-
-
- -
-
-

65%

-

Lowest Score

-
-
-
-
-
-
- - - - - - - - -

Performance Breakdown by Exam:

-
-
    - {examAssessments.map((exam, index) => ( -
  • -
    - Exam {index + 1} - {exam.score}% -
    -
    - -

    {exam.description}

    -
    -
  • - ))} -
-
-
-
- -

Subjects that Need Improvement

-
- -
-
-
- - - - - - - - -
-

Detailed Breakdown

-
-
    - {detailedBreakdown.map((item, index) => ( -
  • -

    {`${item.exam}:`}{item.description}

    -
  • - ))} -
-
-
-
- -

Identified Weak Areas

-
- -
- -
- {weakAreas.map((x, index) => ( - - clsx( - 'text-[#53B2F9] pb-2 border-b-2', - selected ? 'border-[#1B78BE]' : 'border-[#1B78BE0F]' - ) - } - > - {x.topic} - - ))} -
-
- - {weakAreas.map((x, index) => ( - -

{x.description}

-
- ))} -
-
-
-
-
-
-
-
- -
-
- - -
-
- - )} - ))} -
- - ); -} - -export default TrainingContent; diff --git a/src/pages/training/[id]/index.tsx b/src/pages/training/[id]/index.tsx new file mode 100644 index 00000000..3a9a5a88 --- /dev/null +++ b/src/pages/training/[id]/index.tsx @@ -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(null); + const [loading, setLoading] = useState(true); + const [trainingTips, setTrainingTips] = useState([]); + 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(`/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(`/api/stats/${statId}`); + return statResponse.data; + })); + return { ...exam, stats }; + })) + }; + + const tips = await axios.get('/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 ( + <> + + Training | EnCoach + + + + + + + + {loading ? ( +
+ +
+ ) : (trainingContent && ( + <> +
+ {trainingContent.exams.map((exam, examIndex) => ( + + ))} +
+
+
+
+
+ +

General Evaluation

+
+ +
+
+ + + + + + + + +

Performance Breakdown by Exam:

+
+
    + {trainingContent.exams.flatMap((exam, index) => ( +
  • +
    + Exam {index + 1} + {exam.score}% +
    +
    + +

    {exam.performance_comment}

    +
    +
  • + ))} +
+
+
+
+ +

Subjects that Need Improvement

+
+ +
+
+
+ + + + + + + + +
+

Detailed Breakdown

+
+
    + {trainingContent.exams.flatMap((exam, index) => ( +
  • +

    {`Exam ${index + 1}:`}{exam.detailed_summary}

    +
  • + ))} +
+
+
+
+ +

Identified Weak Areas

+
+ +
+ +
+ {trainingContent.weak_areas.map((x, index) => ( + + clsx( + 'text-[#53B2F9] pb-2 border-b-2', + 'focus:outline-none', + selected ? 'border-[#1B78BE]' : 'border-[#1B78BE0F]' + ) + } + > + {x.area} + + ))} +
+
+ + {trainingContent.weak_areas.map((x, index) => ( + +

{x.comment}

+
+ ))} +
+
+
+
+
+
+
+
+ +
+
+ + +
+
+ + ))} +
+ + ); +} + +export default TrainingContent; + diff --git a/src/pages/training/index.tsx b/src/pages/training/index.tsx new file mode 100644 index 00000000..fb64dbc1 --- /dev/null +++ b/src/pages/training/index.tsx @@ -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(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([]); + const [isNewContentLoading, setIsNewContentLoading] = useState(stats.length != 0); + const [isLoading, setIsLoading] = useState(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('/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 ( + <> +
selectTrainingContent(trainingContent)} + role="button"> +
+
+ {formatTimestamp(timestamp)} +
+
+
+ {Object.values(groupedByTrainingContent || {}).flatMap((content) => + content.exams.map(({ module, id }) => ( + + )) + )} +
+
+
+ +
+ + ); + }; + + return ( + <> + + Training | EnCoach + + + + + + + + {(isNewContentLoading || isLoading ? ( +
+ + { isNewContentLoading && ( + Assessing your exams, please be patient... + )} +
+ ) : ( + <> +
+
+ {(user.type === "developer" || user.type === "admin") && ( + <> + + + + + + 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" && ( + <> +
+
Generate New Training Material
+ +
+ + ))} +
+
+ + + +
+
+ {trainingContent.length == 0 && ( +
+ No training content to display... +
+ )} + {groupedByTrainingContent && Object.keys(groupedByTrainingContent).length > 0 && ( +
+ {Object.keys(filterTrainingContentByDate(groupedByTrainingContent)) + .sort((a, b) => parseInt(b) - parseInt(a)) + .map(trainingContentContainer)} +
+ )} + + ))} +
+ + ); +} + +export default Training;