Dropdown on training view
This commit is contained in:
84
src/components/Dropdown.tsx
Normal file
84
src/components/Dropdown.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import React, { useState, ReactNode, useRef, useEffect } from 'react';
|
||||
import { animated, useSpring } from '@react-spring/web';
|
||||
|
||||
interface DropdownProps {
|
||||
title: string;
|
||||
open?: boolean;
|
||||
className?: string;
|
||||
contentWrapperClassName?: string;
|
||||
bottomPadding?: number;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const Dropdown: React.FC<DropdownProps> = ({
|
||||
title,
|
||||
open = false,
|
||||
className = "w-full text-left font-semibold flex justify-between items-center p-4",
|
||||
contentWrapperClassName = "px-6",
|
||||
bottomPadding = 12,
|
||||
children
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(open);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const [contentHeight, setContentHeight] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
if (contentRef.current) {
|
||||
resizeObserver = new ResizeObserver(entries => {
|
||||
for (let entry of entries) {
|
||||
if (entry.borderBoxSize && entry.borderBoxSize.length > 0) {
|
||||
const height = entry.borderBoxSize[0].blockSize;
|
||||
setContentHeight(height + bottomPadding);
|
||||
} else {
|
||||
// Fallback for browsers that don't support borderBoxSize
|
||||
const height = entry.contentRect.height;
|
||||
setContentHeight(height + bottomPadding);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
resizeObserver.observe(contentRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
}
|
||||
};
|
||||
}, [bottomPadding]);
|
||||
|
||||
const springProps = useSpring({
|
||||
height: isOpen ? contentHeight : 0,
|
||||
opacity: isOpen ? 1 : 0,
|
||||
config: { tension: 300, friction: 30 }
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={className}
|
||||
>
|
||||
{title}
|
||||
<svg
|
||||
className={`w-4 h-4 transform transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<animated.div style={springProps} className="overflow-hidden">
|
||||
<div ref={contentRef} className={contentWrapperClassName} style={{paddingBottom: bottomPadding}}>
|
||||
{children}
|
||||
</div>
|
||||
</animated.div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dropdown;
|
||||
@@ -23,6 +23,7 @@ import useExamStore from "@/stores/examStore";
|
||||
import { usePDFDownload } from "@/hooks/usePDFDownload";
|
||||
import useAssignments from '@/hooks/useAssignments';
|
||||
import useUsers from '@/hooks/useUsers';
|
||||
import Dropdown from "@/components/Dropdown";
|
||||
|
||||
export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
|
||||
const user = req.session.user;
|
||||
@@ -136,155 +137,165 @@ const TrainingContent: React.FC<{ user: User }> = ({ user }) => {
|
||||
<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="flex flex-col gap-10">
|
||||
<div className="flex h-screen flex-col gap-4">
|
||||
<div className='flex flex-row h-[15%] gap-4'>
|
||||
{/*<Carousel itemsPerFrame={4} itemsPerScroll={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 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>
|
||||
))}
|
||||
{/* </Carousel> */}
|
||||
</div>
|
||||
<div className='flex flex-col h-[75%]' style={{ maxHeight: '85%' }}>
|
||||
<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 max-h-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="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 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="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 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>
|
||||
</Tab.Group>
|
||||
|
||||
<div className="bg-[#FBFBFB] border rounded-xl p-4 max-h-[500px] overflow-y-auto scrollbar-hide">
|
||||
<div className='flex flex-col'>
|
||||
<div className="flex flex-row items-center gap-1 mb-4">
|
||||
<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 className="space-y-4 pb-2">
|
||||
{trainingContent.exams.map((exam, index) => (
|
||||
<li key={index} className="border rounded-lg bg-white">
|
||||
<Dropdown title={`Exam ${index + 1}`}>
|
||||
<span>{exam.detailed_summary}</span>
|
||||
</Dropdown>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</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>
|
||||
<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 className="flex">
|
||||
<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>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
))}
|
||||
</Layout>
|
||||
</>
|
||||
|
||||
@@ -302,7 +302,7 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
|
||||
label: `${x.name} - ${x.email}`,
|
||||
}))}
|
||||
value={selectedUserSelectValue}
|
||||
onChange={(value) => setStatsUserId(value?.value)}
|
||||
onChange={(value) => setStatsUserId(value?.value!)}
|
||||
styles={{
|
||||
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||
option: (styles, state) => ({
|
||||
@@ -326,7 +326,7 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
|
||||
label: `${x.name} - ${x.email}`,
|
||||
}))}
|
||||
value={selectedUserSelectValue}
|
||||
onChange={(value) => setStatsUserId(value?.value)}
|
||||
onChange={(value) => setStatsUserId(value?.value!)}
|
||||
styles={{
|
||||
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||
option: (styles, state) => ({
|
||||
|
||||
@@ -2,6 +2,16 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer utilities {
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari and Opera */
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--max-width: 1100px;
|
||||
--border-radius: 12px;
|
||||
|
||||
Reference in New Issue
Block a user