Hooked training content to backend, did refactors to training components and record.tsx
This commit is contained in:
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;
|
||||
|
||||
Reference in New Issue
Block a user