Fixed the training content view
This commit is contained in:
@@ -2,7 +2,7 @@ import React, { useState, ReactNode, useRef, useEffect } from 'react';
|
||||
import { animated, useSpring } from '@react-spring/web';
|
||||
|
||||
interface DropdownProps {
|
||||
title: string;
|
||||
title: ReactNode;
|
||||
open?: boolean;
|
||||
className?: string;
|
||||
contentWrapperClassName?: string;
|
||||
@@ -81,4 +81,4 @@ const Dropdown: React.FC<DropdownProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default Dropdown;
|
||||
export default Dropdown;
|
||||
|
||||
168
src/components/InfiniteCarousel.tsx
Normal file
168
src/components/InfiniteCarousel.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import React, { useRef, useEffect, useState, useCallback, ReactNode } from 'react';
|
||||
import { useSpring, animated } from '@react-spring/web';
|
||||
import { useDrag } from '@use-gesture/react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
interface InfiniteCarouselProps {
|
||||
children: React.ReactNode;
|
||||
height: string;
|
||||
speed?: number;
|
||||
gap?: number;
|
||||
overlay?: ReactNode;
|
||||
overlayFunc?: (index: number) => void;
|
||||
overlayClassName?: string;
|
||||
}
|
||||
|
||||
const InfiniteCarousel: React.FC<InfiniteCarouselProps> = ({
|
||||
children,
|
||||
height,
|
||||
speed = 20000,
|
||||
gap = 16,
|
||||
overlay = undefined,
|
||||
overlayFunc = undefined,
|
||||
overlayClassName = ""
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [containerWidth, setContainerWidth] = useState<number>(0);
|
||||
const itemCount = React.Children.count(children);
|
||||
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||
const [itemWidth, setItemWidth] = useState<number>(0);
|
||||
const [isInfinite, setIsInfinite] = useState<boolean>(true);
|
||||
const dragStartX = useRef<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (containerRef.current) {
|
||||
const containerWidth = containerRef.current.clientWidth;
|
||||
setContainerWidth(containerWidth);
|
||||
|
||||
const firstChild = containerRef.current.firstElementChild?.firstElementChild as HTMLElement;
|
||||
if (firstChild) {
|
||||
const childWidth = firstChild.offsetWidth;
|
||||
setItemWidth(childWidth);
|
||||
|
||||
const totalContentWidth = (childWidth + gap) * itemCount - gap;
|
||||
setIsInfinite(totalContentWidth > containerWidth);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, [gap, itemCount]);
|
||||
|
||||
const totalWidth = (itemWidth + gap) * itemCount;
|
||||
|
||||
const [{ x }, api] = useSpring(() => ({
|
||||
from: { x: 0 },
|
||||
to: { x: -totalWidth },
|
||||
config: { duration: speed },
|
||||
loop: true,
|
||||
}));
|
||||
|
||||
const startAnimation = useCallback(() => {
|
||||
if (isInfinite) {
|
||||
api.start({
|
||||
from: { x: x.get() },
|
||||
to: { x: x.get() - totalWidth },
|
||||
config: { duration: speed },
|
||||
loop: true,
|
||||
});
|
||||
} else {
|
||||
api.stop();
|
||||
api.start({ x: 0, immediate: true });
|
||||
}
|
||||
}, [api, x, totalWidth, speed, isInfinite]);
|
||||
|
||||
useEffect(() => {
|
||||
if (containerWidth > 0 && !isDragging) {
|
||||
startAnimation();
|
||||
}
|
||||
}, [containerWidth, isDragging, startAnimation]);
|
||||
|
||||
const bind = useDrag(({ down, movement: [mx], first }) => {
|
||||
if (!isInfinite) return;
|
||||
if (first) {
|
||||
setIsDragging(true);
|
||||
api.stop();
|
||||
dragStartX.current = x.get();
|
||||
}
|
||||
if (down) {
|
||||
let newX = dragStartX.current + mx;
|
||||
newX = ((newX % totalWidth) + totalWidth) % totalWidth;
|
||||
if (newX > 0) newX -= totalWidth;
|
||||
api.start({ x: newX, immediate: true });
|
||||
} else {
|
||||
setIsDragging(false);
|
||||
startAnimation();
|
||||
}
|
||||
}, {
|
||||
filterTaps: true,
|
||||
from: () => [x.get(), 0],
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="overflow-hidden relative select-none"
|
||||
style={{ height, touchAction: 'pan-y' }}
|
||||
ref={containerRef}
|
||||
{...(isInfinite ? bind() : {})}
|
||||
>
|
||||
<animated.div
|
||||
className="flex"
|
||||
style={{
|
||||
display: 'flex',
|
||||
willChange: 'transform',
|
||||
transform: isInfinite
|
||||
? x.to((x) => `translate3d(${x}px, 0, 0)`)
|
||||
: 'none',
|
||||
gap: `${gap}px`,
|
||||
width: 'fit-content',
|
||||
}}
|
||||
>
|
||||
{React.Children.map(children, (child, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex-shrink-0 relative"
|
||||
>
|
||||
{overlay !== undefined && overlayFunc !== undefined && (
|
||||
<div className={clsx('absolute', overlayClassName)} onClick={() => overlayFunc(i)}>
|
||||
{overlay}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="select-none"
|
||||
style={{ pointerEvents: 'none' }}
|
||||
>
|
||||
{child}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{isInfinite && React.Children.map(children, (child, i) => (
|
||||
<div
|
||||
key={`clone-${i}`}
|
||||
className="flex-shrink-0 relative"
|
||||
>
|
||||
{overlay !== undefined && overlayFunc !== undefined && (
|
||||
<div className={clsx('absolute', overlayClassName)} onClick={() => overlayFunc(i)}>
|
||||
{overlay}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="select-none"
|
||||
style={{ pointerEvents: 'none' }}
|
||||
>
|
||||
{child}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</animated.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InfiniteCarousel;
|
||||
@@ -175,6 +175,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={true} />
|
||||
)}
|
||||
{checkAccess(user, getTypesOfUser(["agent"]), "viewRecords") && (
|
||||
<Nav disabled={disableNavigation} Icon={CiDumbbell} label="Training" path={path} keyPath="/training" isMinimized={true} />
|
||||
)}
|
||||
{checkAccess(user, getTypesOfUser(["student"])) && (
|
||||
<Nav disabled={disableNavigation} Icon={BsShieldFill} label="Settings" path={path} keyPath="/settings" isMinimized={true} />
|
||||
)}
|
||||
|
||||
@@ -17,7 +17,7 @@ import { Exam, UserSolution } from '@/interfaces/exam';
|
||||
import ModuleBadge from './ModuleBadge';
|
||||
|
||||
const formatTimestamp = (timestamp: string | number) => {
|
||||
const time = typeof timestamp === "string" ? parseInt(timestamp) : timestamp;
|
||||
const time = typeof timestamp === "string" ? parseInt(timestamp) : timestamp;
|
||||
const date = moment(time);
|
||||
const formatter = "YYYY/MM/DD - HH:mm";
|
||||
return date.format(formatter);
|
||||
@@ -68,6 +68,9 @@ const aggregateScoresByModule = (stats: Stat[]): { module: Module; total: number
|
||||
};
|
||||
|
||||
interface StatsGridItemProps {
|
||||
width?: string | undefined;
|
||||
height?: string | undefined;
|
||||
examNumber?: number | undefined;
|
||||
stats: Stat[];
|
||||
timestamp: string | number;
|
||||
user: User,
|
||||
@@ -100,7 +103,10 @@ const StatsGridItem: React.FC<StatsGridItemProps> = ({
|
||||
setSelectedModules,
|
||||
setInactivity,
|
||||
setTimeSpent,
|
||||
renderPdfIcon
|
||||
renderPdfIcon,
|
||||
width = undefined,
|
||||
height = undefined,
|
||||
examNumber = undefined
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const correct = stats.reduce((accumulator, current) => accumulator + current.score.correct, 0);
|
||||
@@ -190,15 +196,23 @@ const StatsGridItem: React.FC<StatsGridItemProps> = ({
|
||||
</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>
|
||||
{examNumber === undefined ? (
|
||||
<>
|
||||
{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 className='flex justify-end'>
|
||||
<span className="font-semibold bg-gray-200 text-gray-800 px-2.5 py-0.5 rounded-full mt-0.5">{examNumber}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -232,7 +246,10 @@ const StatsGridItem: React.FC<StatsGridItemProps> = ({
|
||||
correct / total < 0.3 && "hover:border-mti-rose",
|
||||
typeof selectedTrainingExams !== "undefined" && typeof timestamp === "string" && selectedTrainingExams.includes(timestamp) && "border-2 border-slate-600",
|
||||
)}
|
||||
onClick={selectExam}
|
||||
style={{
|
||||
...(width !== undefined && { width }),
|
||||
...(height !== undefined && { height }),
|
||||
}}
|
||||
data-tip="This exam is still being evaluated..."
|
||||
role="button">
|
||||
{content}
|
||||
@@ -246,6 +263,10 @@ const StatsGridItem: React.FC<StatsGridItemProps> = ({
|
||||
correct / total < 0.3 && "hover:border-mti-rose",
|
||||
)}
|
||||
data-tip="Your screen size is too small to view previous exams."
|
||||
style={{
|
||||
...(width !== undefined && { width }),
|
||||
...(height !== undefined && { height }),
|
||||
}}
|
||||
role="button">
|
||||
{content}
|
||||
</div>
|
||||
|
||||
@@ -76,7 +76,7 @@ const TrainingScore: React.FC<TrainingScoreProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
{gridView && (
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
<div className="flex flex-col items-center justify-center gap-2 -lg:hidden">
|
||||
<div className="flex w-14 h-14 bg-[#F5F5F5] items-center justify-center rounded-xl border border-[#DBDBDB]">
|
||||
<GiLightBulb color={"#FFCC00"} size={28} />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user