Merged in feature/training-content (pull request #62)
Feature/training content Approved-by: Tiago Ribeiro
This commit is contained in:
643
package-lock.json
generated
643
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,7 @@
|
|||||||
"@types/node": "18.13.0",
|
"@types/node": "18.13.0",
|
||||||
"@types/react": "18.0.27",
|
"@types/react": "18.0.27",
|
||||||
"@types/react-dom": "18.0.10",
|
"@types/react-dom": "18.0.10",
|
||||||
|
"@use-gesture/react": "^10.3.1",
|
||||||
"axios": "^1.3.5",
|
"axios": "^1.3.5",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"chart.js": "^4.2.1",
|
"chart.js": "^4.2.1",
|
||||||
@@ -67,14 +68,12 @@
|
|||||||
"react-phone-number-input": "^3.3.6",
|
"react-phone-number-input": "^3.3.6",
|
||||||
"react-player": "^2.12.0",
|
"react-player": "^2.12.0",
|
||||||
"react-select": "^5.7.5",
|
"react-select": "^5.7.5",
|
||||||
"react-slick": "^0.30.2",
|
|
||||||
"react-string-replace": "^1.1.0",
|
"react-string-replace": "^1.1.0",
|
||||||
"react-toastify": "^9.1.2",
|
"react-toastify": "^9.1.2",
|
||||||
"react-tooltip": "^5.27.1",
|
"react-tooltip": "^5.27.1",
|
||||||
"react-xarrows": "^2.0.2",
|
"react-xarrows": "^2.0.2",
|
||||||
"read-excel-file": "^5.7.1",
|
"read-excel-file": "^5.7.1",
|
||||||
"short-unique-id": "5.0.2",
|
"short-unique-id": "5.0.2",
|
||||||
"slick-carousel": "^1.8.1",
|
|
||||||
"stripe": "^13.10.0",
|
"stripe": "^13.10.0",
|
||||||
"swr": "^2.1.3",
|
"swr": "^2.1.3",
|
||||||
"tailwind-scrollbar-hide": "^1.1.7",
|
"tailwind-scrollbar-hide": "^1.1.7",
|
||||||
@@ -94,7 +93,6 @@
|
|||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/react-csv": "^1.1.10",
|
"@types/react-csv": "^1.1.10",
|
||||||
"@types/react-datepicker": "^4.15.1",
|
"@types/react-datepicker": "^4.15.1",
|
||||||
"@types/react-slick": "^0.23.13",
|
|
||||||
"@types/uuid": "^9.0.1",
|
"@types/uuid": "^9.0.1",
|
||||||
"@types/wavesurfer.js": "^6.0.6",
|
"@types/wavesurfer.js": "^6.0.6",
|
||||||
"@wixc3/react-board": "^2.2.0",
|
"@wixc3/react-board": "^2.2.0",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useState, ReactNode, useRef, useEffect } from 'react';
|
|||||||
import { animated, useSpring } from '@react-spring/web';
|
import { animated, useSpring } from '@react-spring/web';
|
||||||
|
|
||||||
interface DropdownProps {
|
interface DropdownProps {
|
||||||
title: string;
|
title: ReactNode;
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
contentWrapperClassName?: string;
|
contentWrapperClassName?: string;
|
||||||
|
|||||||
@@ -1,33 +1,34 @@
|
|||||||
import {SpeakingExercise} from "@/interfaces/exam";
|
import { SpeakingExercise } from "@/interfaces/exam";
|
||||||
import {CommonProps} from ".";
|
import { CommonProps } from ".";
|
||||||
import {Fragment, useEffect, useState} from "react";
|
import { Fragment, useEffect, useState } from "react";
|
||||||
import {BsCheckCircleFill, BsMicFill, BsPauseCircle, BsPlayCircle, BsTrashFill} from "react-icons/bs";
|
import { BsCheckCircleFill, BsMicFill, BsPauseCircle, BsPlayCircle, BsTrashFill } from "react-icons/bs";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import {downloadBlob} from "@/utils/evaluation";
|
import { downloadBlob } from "@/utils/evaluation";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import Modal from "../Modal";
|
import Modal from "../Modal";
|
||||||
|
|
||||||
const Waveform = dynamic(() => import("../Waveform"), {ssr: false});
|
const Waveform = dynamic(() => import("../Waveform"), { ssr: false });
|
||||||
const ReactMediaRecorder = dynamic(() => import("react-media-recorder").then((mod) => mod.ReactMediaRecorder), {
|
const ReactMediaRecorder = dynamic(() => import("react-media-recorder").then((mod) => mod.ReactMediaRecorder), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function Speaking({id, title, text, video_url, type, prompts, suffix, userSolutions, onNext, onBack}: SpeakingExercise & CommonProps) {
|
export default function Speaking({ id, title, text, video_url, type, prompts, suffix, userSolutions, onNext, onBack }: SpeakingExercise & CommonProps) {
|
||||||
const [recordingDuration, setRecordingDuration] = useState(0);
|
const [recordingDuration, setRecordingDuration] = useState(0);
|
||||||
const [isRecording, setIsRecording] = useState(false);
|
const [isRecording, setIsRecording] = useState(false);
|
||||||
const [mediaBlob, setMediaBlob] = useState<string>();
|
const [mediaBlob, setMediaBlob] = useState<string>();
|
||||||
const [audioURL, setAudioURL] = useState<string>();
|
const [audioURL, setAudioURL] = useState<string>();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isPromptsModalOpen, setIsPromptsModalOpen] = useState(false);
|
const [isPromptsModalOpen, setIsPromptsModalOpen] = useState(false);
|
||||||
|
const [inputText, setInputText] = useState("");
|
||||||
|
|
||||||
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
||||||
|
|
||||||
const saveToStorage = async () => {
|
const saveToStorage = async () => {
|
||||||
if (mediaBlob && mediaBlob.startsWith("blob")) {
|
if (mediaBlob && mediaBlob.startsWith("blob")) {
|
||||||
const blobBuffer = await downloadBlob(mediaBlob);
|
const blobBuffer = await downloadBlob(mediaBlob);
|
||||||
const audioFile = new File([blobBuffer], "audio.wav", {type: "audio/wav"});
|
const audioFile = new File([blobBuffer], "audio.wav", { type: "audio/wav" });
|
||||||
|
|
||||||
const seed = Math.random().toString().replace("0.", "");
|
const seed = Math.random().toString().replace("0.", "");
|
||||||
|
|
||||||
@@ -41,8 +42,8 @@ export default function Speaking({id, title, text, video_url, type, prompts, suf
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await axios.post<{path: string}>("/api/storage/insert", formData, config);
|
const response = await axios.post<{ path: string }>("/api/storage/insert", formData, config);
|
||||||
if (audioURL) await axios.post("/api/storage/delete", {path: audioURL});
|
if (audioURL) await axios.post("/api/storage/delete", { path: audioURL });
|
||||||
return response.data.path;
|
return response.data.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ export default function Speaking({id, title, text, video_url, type, prompts, suf
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userSolutions.length > 0) {
|
if (userSolutions.length > 0) {
|
||||||
const {solution} = userSolutions[0] as {solution?: string};
|
const { solution } = userSolutions[0] as { solution?: string };
|
||||||
if (solution && !mediaBlob) setMediaBlob(solution);
|
if (solution && !mediaBlob) setMediaBlob(solution);
|
||||||
if (solution && !solution.startsWith("blob")) setAudioURL(solution);
|
if (solution && !solution.startsWith("blob")) setAudioURL(solution);
|
||||||
}
|
}
|
||||||
@@ -78,8 +79,8 @@ export default function Speaking({id, title, text, video_url, type, prompts, suf
|
|||||||
const next = async () => {
|
const next = async () => {
|
||||||
onNext({
|
onNext({
|
||||||
exercise: id,
|
exercise: id,
|
||||||
solutions: mediaBlob ? [{id, solution: mediaBlob}] : [],
|
solutions: mediaBlob ? [{ id, solution: mediaBlob }] : [],
|
||||||
score: {correct: 0, total: 100, missing: 0},
|
score: { correct: 0, total: 100, missing: 0 },
|
||||||
type,
|
type,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -87,12 +88,33 @@ export default function Speaking({id, title, text, video_url, type, prompts, suf
|
|||||||
const back = async () => {
|
const back = async () => {
|
||||||
onBack({
|
onBack({
|
||||||
exercise: id,
|
exercise: id,
|
||||||
solutions: mediaBlob ? [{id, solution: mediaBlob}] : [],
|
solutions: mediaBlob ? [{ id, solution: mediaBlob }] : [],
|
||||||
score: {correct: 0, total: 100, missing: 0},
|
score: { correct: 0, total: 100, missing: 0 },
|
||||||
type,
|
type,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleNoteWriting = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
const newText = e.target.value;
|
||||||
|
const words = newText.match(/\S+/g);
|
||||||
|
const wordCount = words ? words.length : 0;
|
||||||
|
|
||||||
|
if (wordCount <= 100) {
|
||||||
|
setInputText(newText);
|
||||||
|
} else {
|
||||||
|
let count = 0;
|
||||||
|
let lastIndex = 0;
|
||||||
|
const matches = newText.matchAll(/\S+/g);
|
||||||
|
for (const match of matches) {
|
||||||
|
count++;
|
||||||
|
if (count > 100) break;
|
||||||
|
lastIndex = match.index! + match[0].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
setInputText(newText.slice(0, lastIndex));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full w-full gap-9">
|
<div className="flex flex-col h-full w-full gap-9">
|
||||||
<Modal title="Prompts" className="!w-96 aspect-square" isOpen={isPromptsModalOpen} onClose={() => setIsPromptsModalOpen(false)}>
|
<Modal title="Prompts" className="!w-96 aspect-square" isOpen={isPromptsModalOpen} onClose={() => setIsPromptsModalOpen(false)}>
|
||||||
@@ -112,7 +134,7 @@ export default function Speaking({id, title, text, video_url, type, prompts, suf
|
|||||||
<div className="flex flex-col gap-0">
|
<div className="flex flex-col gap-0">
|
||||||
<span className="font-semibold">{title}</span>
|
<span className="font-semibold">{title}</span>
|
||||||
{prompts.length > 0 && (
|
{prompts.length > 0 && (
|
||||||
<span className="font-semibold">You should talk for at least 30 seconds for your answer to be valid.</span>
|
<span className="font-semibold">You should talk for at least 1 minute and 30 seconds for your answer to be valid.</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!video_url && (
|
{!video_url && (
|
||||||
@@ -138,10 +160,24 @@ export default function Speaking({id, title, text, video_url, type, prompts, suf
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{prompts && prompts.length > 0 && (
|
||||||
|
<div className="w-full h-full flex flex-col gap-4">
|
||||||
|
<textarea
|
||||||
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
|
className="w-full h-full min-h-[200px] cursor-text px-7 py-8 input border-2 border-mti-gray-platinum bg-white rounded-3xl"
|
||||||
|
onChange={handleNoteWriting}
|
||||||
|
value={inputText}
|
||||||
|
placeholder="Write your notes here..."
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
<span className="text-base self-end text-mti-gray-cool">Word Count: {(inputText.match(/\S+/g) || []).length}/100</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<ReactMediaRecorder
|
<ReactMediaRecorder
|
||||||
audio
|
audio
|
||||||
onStop={(blob) => setMediaBlob(blob)}
|
onStop={(blob) => setMediaBlob(blob)}
|
||||||
render={({status, startRecording, stopRecording, pauseRecording, resumeRecording, clearBlobUrl, mediaBlobUrl}) => (
|
render={({ status, startRecording, stopRecording, pauseRecording, resumeRecording, clearBlobUrl, mediaBlobUrl }) => (
|
||||||
<div className="w-full p-4 px-8 bg-transparent border-2 border-mti-gray-platinum rounded-2xl flex-col gap-8 items-center">
|
<div className="w-full p-4 px-8 bg-transparent border-2 border-mti-gray-platinum rounded-2xl flex-col gap-8 items-center">
|
||||||
<p className="text-base font-normal">Record your answer:</p>
|
<p className="text-base font-normal">Record your answer:</p>
|
||||||
<div className="flex gap-8 items-center justify-center py-8">
|
<div className="flex gap-8 items-center justify-center py-8">
|
||||||
|
|||||||
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") && (
|
{checkAccess(user, getTypesOfUser(["agent"]), "viewRecords") && (
|
||||||
<Nav disabled={disableNavigation} Icon={BsClockHistory} label="Record" path={path} keyPath="/record" isMinimized={true} />
|
<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"])) && (
|
{checkAccess(user, getTypesOfUser(["student"])) && (
|
||||||
<Nav disabled={disableNavigation} Icon={BsShieldFill} label="Settings" path={path} keyPath="/settings" isMinimized={true} />
|
<Nav disabled={disableNavigation} Icon={BsShieldFill} label="Settings" path={path} keyPath="/settings" isMinimized={true} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default function InteractiveSpeaking({
|
|||||||
onBack,
|
onBack,
|
||||||
}: InteractiveSpeakingExercise & CommonProps) {
|
}: InteractiveSpeakingExercise & CommonProps) {
|
||||||
const [solutionsURL, setSolutionsURL] = useState<string[]>([]);
|
const [solutionsURL, setSolutionsURL] = useState<string[]>([]);
|
||||||
const [diffNumber, setDiffNumber] = useState<0 | 1 | 2 | 3>(0);
|
const [diffNumber, setDiffNumber] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userSolutions && userSolutions.length > 0 && userSolutions[0].solution) {
|
if (userSolutions && userSolutions.length > 0 && userSolutions[0].solution) {
|
||||||
@@ -115,13 +115,13 @@ export default function InteractiveSpeaking({
|
|||||||
{userSolutions &&
|
{userSolutions &&
|
||||||
userSolutions.length > 0 &&
|
userSolutions.length > 0 &&
|
||||||
userSolutions[0].evaluation &&
|
userSolutions[0].evaluation &&
|
||||||
userSolutions[0].evaluation[`transcript_${(index + 1) as 1 | 2 | 3}`] &&
|
userSolutions[0].evaluation[`transcript_${(index + 1)}`] &&
|
||||||
userSolutions[0].evaluation[`fixed_text_${(index + 1) as 1 | 2 | 3}`] && (
|
userSolutions[0].evaluation[`fixed_text_${(index + 1)}`] && (
|
||||||
<Button
|
<Button
|
||||||
className="w-full max-w-[180px] !py-2 self-center"
|
className="w-full max-w-[180px] !py-2 self-center"
|
||||||
color="pink"
|
color="pink"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setDiffNumber((index + 1) as 1 | 2 | 3)}>
|
onClick={() => setDiffNumber((index + 1))}>
|
||||||
View Correction
|
View Correction
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -144,9 +144,20 @@ export default function InteractiveSpeaking({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{userSolutions[0].evaluation &&
|
{userSolutions[0].evaluation &&
|
||||||
Object.keys(userSolutions[0].evaluation).filter((x) => x.startsWith("perfect_answer")).length === 3 ? (
|
Object.keys(userSolutions[0].evaluation).filter((x) => x.startsWith("perfect_answer")).length > 0 ? (
|
||||||
<Tab.Group>
|
<Tab.Group>
|
||||||
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-speaking/20 p-1">
|
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-speaking/20 p-1">
|
||||||
|
<Tab
|
||||||
|
className={({selected}) =>
|
||||||
|
clsx(
|
||||||
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/80",
|
||||||
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
||||||
|
"transition duration-300 ease-in-out",
|
||||||
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking",
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
General Feedback
|
||||||
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
className={({selected}) =>
|
className={({selected}) =>
|
||||||
clsx(
|
clsx(
|
||||||
@@ -158,8 +169,10 @@ export default function InteractiveSpeaking({
|
|||||||
}>
|
}>
|
||||||
Evaluation
|
Evaluation
|
||||||
</Tab>
|
</Tab>
|
||||||
|
{Object.keys(userSolutions[0].evaluation).filter((x) => x.startsWith("perfect_answer")).map((key, index) => (
|
||||||
<Tab
|
<Tab
|
||||||
className={({selected}) =>
|
key={key}
|
||||||
|
className={({ selected }) =>
|
||||||
clsx(
|
clsx(
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/80",
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/80",
|
||||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
||||||
@@ -167,61 +180,11 @@ export default function InteractiveSpeaking({
|
|||||||
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking",
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking",
|
||||||
)
|
)
|
||||||
}>
|
}>
|
||||||
Recommended Answer (Prompt 1)
|
Recommended Answer<br />(Prompt {index + 1})
|
||||||
</Tab>
|
|
||||||
<Tab
|
|
||||||
className={({selected}) =>
|
|
||||||
clsx(
|
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/80",
|
|
||||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
|
||||||
"transition duration-300 ease-in-out",
|
|
||||||
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking",
|
|
||||||
)
|
|
||||||
}>
|
|
||||||
Recommended Answer (Prompt 2)
|
|
||||||
</Tab>
|
|
||||||
<Tab
|
|
||||||
className={({selected}) =>
|
|
||||||
clsx(
|
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/80",
|
|
||||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
|
||||||
"transition duration-300 ease-in-out",
|
|
||||||
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking",
|
|
||||||
)
|
|
||||||
}>
|
|
||||||
Recommended Answer (Prompt 3)
|
|
||||||
</Tab>
|
|
||||||
<Tab
|
|
||||||
className={({selected}) =>
|
|
||||||
clsx(
|
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/80",
|
|
||||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
|
||||||
"transition duration-300 ease-in-out",
|
|
||||||
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking",
|
|
||||||
)
|
|
||||||
}>
|
|
||||||
Global Overview
|
|
||||||
</Tab>
|
</Tab>
|
||||||
|
))}
|
||||||
</Tab.List>
|
</Tab.List>
|
||||||
<Tab.Panels>
|
<Tab.Panels>
|
||||||
<Tab.Panel className="w-full bg-ielts-speaking/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
|
||||||
<span className="w-full h-full min-h-fit cursor-text">{userSolutions[0].evaluation!.comment}</span>
|
|
||||||
</Tab.Panel>
|
|
||||||
<Tab.Panel className="w-full bg-ielts-speaking/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
|
||||||
<span className="w-full h-full min-h-fit cursor-text whitespace-pre-wrap">
|
|
||||||
{userSolutions[0].evaluation!.perfect_answer_1!.answer.replaceAll(/\s{2,}/g, "\n\n")}
|
|
||||||
</span>
|
|
||||||
</Tab.Panel>
|
|
||||||
<Tab.Panel className="w-full bg-ielts-speaking/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
|
||||||
<span className="w-full h-full min-h-fit cursor-text whitespace-pre-wrap">
|
|
||||||
{userSolutions[0].evaluation!.perfect_answer_2!.answer.replaceAll(/\s{2,}/g, "\n\n")}
|
|
||||||
</span>
|
|
||||||
</Tab.Panel>
|
|
||||||
<Tab.Panel className="w-full bg-ielts-speaking/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
|
||||||
<span className="w-full h-full min-h-fit cursor-text whitespace-pre-wrap">
|
|
||||||
{userSolutions[0].evaluation!.perfect_answer_3!.answer.replaceAll(/\s{2,}/g, "\n\n")}
|
|
||||||
</span>
|
|
||||||
</Tab.Panel>
|
|
||||||
<Tab.Panel className="w-full bg-ielts-speaking/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
<Tab.Panel className="w-full bg-ielts-speaking/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{Object.keys(userSolutions[0].evaluation!.task_response).map((key) => {
|
{Object.keys(userSolutions[0].evaluation!.task_response).map((key) => {
|
||||||
@@ -230,15 +193,25 @@ export default function InteractiveSpeaking({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={key} className="flex flex-col gap-2">
|
<div key={key} className="flex flex-col gap-2">
|
||||||
<span className={"font-semibold"}>
|
<div className={clsx("bg-ielts-speaking text-ielts-speaking-light rounded-xl px-4 py-2 w-fit")} key={key}>
|
||||||
{key}: Level {grade}
|
{key}: Level {grade}
|
||||||
</span>
|
</div>
|
||||||
{typeof taskResponse !== "number" && <span>{taskResponse.comment}</span>}
|
{typeof taskResponse !== "number" && <span className="px-2 py-2">{taskResponse.comment}</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
|
<Tab.Panel className="w-full bg-ielts-speaking/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
||||||
|
<span className="w-full h-full min-h-fit cursor-text">{userSolutions[0].evaluation!.comment}</span>
|
||||||
|
</Tab.Panel>
|
||||||
|
{Object.keys(userSolutions[0].evaluation).filter((x) => x.startsWith("perfect_answer")).map((key, index) => (
|
||||||
|
<Tab.Panel key={key} className="w-full bg-ielts-speaking/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
||||||
|
<span className="w-full h-full min-h-fit cursor-text whitespace-pre-wrap">
|
||||||
|
{userSolutions[0].evaluation![`perfect_answer_${(index + 1)}`].answer.replaceAll(/\s{2,}/g, "\n\n")}
|
||||||
|
</span>
|
||||||
|
</Tab.Panel>
|
||||||
|
))}
|
||||||
</Tab.Panels>
|
</Tab.Panels>
|
||||||
</Tab.Group>
|
</Tab.Group>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -150,10 +150,21 @@ export default function Speaking({id, type, title, video_url, text, prompts, use
|
|||||||
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking",
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking",
|
||||||
)
|
)
|
||||||
}>
|
}>
|
||||||
|
General Feedback
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
className={({ selected }) =>
|
||||||
|
clsx(
|
||||||
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/80",
|
||||||
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
||||||
|
"transition duration-300 ease-in-out",
|
||||||
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking",
|
||||||
|
)
|
||||||
|
}>
|
||||||
Evaluation
|
Evaluation
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
className={({selected}) =>
|
className={({ selected }) =>
|
||||||
clsx(
|
clsx(
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/80",
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/80",
|
||||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
||||||
@@ -163,30 +174,9 @@ export default function Speaking({id, type, title, video_url, text, prompts, use
|
|||||||
}>
|
}>
|
||||||
Recommended Answer
|
Recommended Answer
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
|
||||||
className={({selected}) =>
|
|
||||||
clsx(
|
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/80",
|
|
||||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
|
||||||
"transition duration-300 ease-in-out",
|
|
||||||
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking",
|
|
||||||
)
|
|
||||||
}>
|
|
||||||
Global Overview
|
|
||||||
</Tab>
|
|
||||||
</Tab.List>
|
</Tab.List>
|
||||||
<Tab.Panels>
|
<Tab.Panels>
|
||||||
<Tab.Panel className="w-full bg-ielts-speaking/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
{/* General Feedback */}
|
||||||
<span className="w-full h-full min-h-fit cursor-text">{userSolutions[0].evaluation!.comment}</span>
|
|
||||||
</Tab.Panel>
|
|
||||||
<Tab.Panel className="w-full bg-ielts-speaking/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
|
||||||
<span className="w-full h-full min-h-fit cursor-text whitespace-pre-wrap">
|
|
||||||
{userSolutions[0].evaluation!.perfect_answer &&
|
|
||||||
userSolutions[0].evaluation!.perfect_answer.replaceAll(/\s{2,}/g, "\n\n")}
|
|
||||||
{userSolutions[0].evaluation!.perfect_answer_1 &&
|
|
||||||
userSolutions[0].evaluation!.perfect_answer_1.replaceAll(/\s{2,}/g, "\n\n")}
|
|
||||||
</span>
|
|
||||||
</Tab.Panel>
|
|
||||||
<Tab.Panel className="w-full bg-ielts-speaking/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
<Tab.Panel className="w-full bg-ielts-speaking/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{Object.keys(userSolutions[0].evaluation!.task_response).map((key) => {
|
{Object.keys(userSolutions[0].evaluation!.task_response).map((key) => {
|
||||||
@@ -195,15 +185,28 @@ export default function Speaking({id, type, title, video_url, text, prompts, use
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={key} className="flex flex-col gap-2">
|
<div key={key} className="flex flex-col gap-2">
|
||||||
<span className={"font-semibold"}>
|
<div className={clsx("bg-ielts-speaking text-ielts-speaking-light rounded-xl px-4 py-2 w-fit")} key={key}>
|
||||||
{key}: Level {grade}
|
{key}: Level {grade}
|
||||||
</span>
|
</div>
|
||||||
{typeof taskResponse !== "number" && <span>{taskResponse.comment}</span>}
|
{typeof taskResponse !== "number" && <span className="px-2 py-2">{taskResponse.comment}</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
|
{/* Evaluation */}
|
||||||
|
<Tab.Panel className="w-full bg-ielts-speaking/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
||||||
|
<span className="w-full h-full min-h-fit cursor-text">{userSolutions[0].evaluation!.comment}</span>
|
||||||
|
</Tab.Panel>
|
||||||
|
{/* Recommended Answer */}
|
||||||
|
<Tab.Panel className="w-full bg-ielts-speaking/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
||||||
|
<span className="w-full h-full min-h-fit cursor-text whitespace-pre-wrap">
|
||||||
|
{userSolutions[0].evaluation!.perfect_answer &&
|
||||||
|
userSolutions[0].evaluation!.perfect_answer.replaceAll(/\s{2,}/g, "\n\n")}
|
||||||
|
{userSolutions[0].evaluation!.perfect_answer_1 &&
|
||||||
|
userSolutions[0].evaluation!.perfect_answer_1.replaceAll(/\s{2,}/g, "\n\n")}
|
||||||
|
</span>
|
||||||
|
</Tab.Panel>
|
||||||
</Tab.Panels>
|
</Tab.Panels>
|
||||||
</Tab.Group>
|
</Tab.Group>
|
||||||
) : (
|
) : (
|
||||||
@@ -224,7 +227,7 @@ export default function Speaking({id, type, title, video_url, text, prompts, use
|
|||||||
onBack({
|
onBack({
|
||||||
exercise: id,
|
exercise: id,
|
||||||
solutions: userSolutions,
|
solutions: userSolutions,
|
||||||
score: {total: 100, missing: 0, correct: speakingReverseMarking[userSolutions[0]!.evaluation!.overall] || 0},
|
score: { total: 100, missing: 0, correct: speakingReverseMarking[userSolutions[0]!.evaluation!.overall] || 0 },
|
||||||
type,
|
type,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,17 @@ export default function Writing({id, type, prompt, attachment, userSolutions, on
|
|||||||
{userSolutions[0].evaluation && userSolutions[0].evaluation.perfect_answer ? (
|
{userSolutions[0].evaluation && userSolutions[0].evaluation.perfect_answer ? (
|
||||||
<Tab.Group>
|
<Tab.Group>
|
||||||
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-writing/20 p-1">
|
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-writing/20 p-1">
|
||||||
|
<Tab
|
||||||
|
className={({selected}) =>
|
||||||
|
clsx(
|
||||||
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-writing/80",
|
||||||
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-writing focus:outline-none focus:ring-2",
|
||||||
|
"transition duration-300 ease-in-out",
|
||||||
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-writing",
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
General Feedback
|
||||||
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
className={({selected}) =>
|
className={({selected}) =>
|
||||||
clsx(
|
clsx(
|
||||||
@@ -159,17 +170,6 @@ export default function Writing({id, type, prompt, attachment, userSolutions, on
|
|||||||
}>
|
}>
|
||||||
Recommended Answer
|
Recommended Answer
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
|
||||||
className={({selected}) =>
|
|
||||||
clsx(
|
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-writing/80",
|
|
||||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-writing focus:outline-none focus:ring-2",
|
|
||||||
"transition duration-300 ease-in-out",
|
|
||||||
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-writing",
|
|
||||||
)
|
|
||||||
}>
|
|
||||||
Global Overview
|
|
||||||
</Tab>
|
|
||||||
{aiEval && user?.type !== "student" && (
|
{aiEval && user?.type !== "student" && (
|
||||||
<Tab
|
<Tab
|
||||||
className={({selected}) =>
|
className={({selected}) =>
|
||||||
@@ -185,14 +185,7 @@ export default function Writing({id, type, prompt, attachment, userSolutions, on
|
|||||||
)}
|
)}
|
||||||
</Tab.List>
|
</Tab.List>
|
||||||
<Tab.Panels>
|
<Tab.Panels>
|
||||||
<Tab.Panel className="w-full bg-ielts-writing/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
{/* Global */}
|
||||||
<span className="w-full h-full min-h-fit cursor-text">{userSolutions[0].evaluation!.comment}</span>
|
|
||||||
</Tab.Panel>
|
|
||||||
<Tab.Panel className="w-full bg-ielts-writing/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
|
||||||
<span className="w-full h-full min-h-fit cursor-text whitespace-pre-wrap">
|
|
||||||
{userSolutions[0].evaluation!.perfect_answer.replaceAll(/\s{2,}/g, "\n\n").replaceAll("\\n", "\n")}
|
|
||||||
</span>
|
|
||||||
</Tab.Panel>
|
|
||||||
<Tab.Panel className="w-full bg-ielts-writing/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
<Tab.Panel className="w-full bg-ielts-writing/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{Object.keys(userSolutions[0].evaluation!.task_response).map((key) => {
|
{Object.keys(userSolutions[0].evaluation!.task_response).map((key) => {
|
||||||
@@ -201,15 +194,25 @@ export default function Writing({id, type, prompt, attachment, userSolutions, on
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={key} className="flex flex-col gap-2">
|
<div key={key} className="flex flex-col gap-2">
|
||||||
<span className={"font-semibold"}>
|
<div className={clsx("bg-ielts-writing text-ielts-writing-light rounded-xl px-4 py-2 w-fit")} key={key}>
|
||||||
{key}: Level {grade}
|
{key}: Level {grade}
|
||||||
</span>
|
</div>
|
||||||
{typeof taskResponse !== "number" && <span>{taskResponse.comment}</span>}
|
{typeof taskResponse !== "number" && <span className="px-2 py-2">{taskResponse.comment}</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
|
{/* Evaluation */}
|
||||||
|
<Tab.Panel className="w-full bg-ielts-writing/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
||||||
|
<span className="w-full h-full min-h-fit cursor-text">{userSolutions[0].evaluation!.comment}</span>
|
||||||
|
</Tab.Panel>
|
||||||
|
{/* Recommended Answer */}
|
||||||
|
<Tab.Panel className="w-full bg-ielts-writing/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
||||||
|
<span className="w-full h-full min-h-fit cursor-text whitespace-pre-wrap">
|
||||||
|
{userSolutions[0].evaluation!.perfect_answer.replaceAll(/\s{2,}/g, "\n\n").replaceAll("\\n", "\n")}
|
||||||
|
</span>
|
||||||
|
</Tab.Panel>
|
||||||
{aiEval && user?.type !== "student" && (
|
{aiEval && user?.type !== "student" && (
|
||||||
<Tab.Panel className="w-full bg-ielts-writing/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
<Tab.Panel className="w-full bg-ielts-writing/10 h-fit rounded-xl p-6 flex flex-col gap-4">
|
||||||
<AIDetection {...aiEval} />
|
<AIDetection {...aiEval} />
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ const aggregateScoresByModule = (stats: Stat[]): { module: Module; total: number
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface StatsGridItemProps {
|
interface StatsGridItemProps {
|
||||||
|
width?: string | undefined;
|
||||||
|
height?: string | undefined;
|
||||||
|
examNumber?: number | undefined;
|
||||||
stats: Stat[];
|
stats: Stat[];
|
||||||
timestamp: string | number;
|
timestamp: string | number;
|
||||||
user: User,
|
user: User,
|
||||||
@@ -75,6 +78,7 @@ interface StatsGridItemProps {
|
|||||||
users: User[];
|
users: User[];
|
||||||
training?: boolean,
|
training?: boolean,
|
||||||
selectedTrainingExams?: string[];
|
selectedTrainingExams?: string[];
|
||||||
|
maxTrainingExams?: number;
|
||||||
setSelectedTrainingExams?: React.Dispatch<React.SetStateAction<string[]>>;
|
setSelectedTrainingExams?: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
setExams: (exams: Exam[]) => void;
|
setExams: (exams: Exam[]) => void;
|
||||||
setShowSolutions: (show: boolean) => void;
|
setShowSolutions: (show: boolean) => void;
|
||||||
@@ -100,7 +104,11 @@ const StatsGridItem: React.FC<StatsGridItemProps> = ({
|
|||||||
setSelectedModules,
|
setSelectedModules,
|
||||||
setInactivity,
|
setInactivity,
|
||||||
setTimeSpent,
|
setTimeSpent,
|
||||||
renderPdfIcon
|
renderPdfIcon,
|
||||||
|
width = undefined,
|
||||||
|
height = undefined,
|
||||||
|
examNumber = undefined,
|
||||||
|
maxTrainingExams = undefined
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const correct = stats.reduce((accumulator, current) => accumulator + current.score.correct, 0);
|
const correct = stats.reduce((accumulator, current) => accumulator + current.score.correct, 0);
|
||||||
@@ -126,16 +134,22 @@ const StatsGridItem: React.FC<StatsGridItemProps> = ({
|
|||||||
const { timeSpent, inactivity, session } = stats[0];
|
const { timeSpent, inactivity, session } = stats[0];
|
||||||
|
|
||||||
const selectExam = () => {
|
const selectExam = () => {
|
||||||
if (training && !isDisabled && typeof setSelectedTrainingExams !== "undefined" && typeof timestamp == "string") {
|
if (training && !isDisabled && typeof maxTrainingExams !== "undefined" && typeof setSelectedTrainingExams !== "undefined" && typeof timestamp == "string") {
|
||||||
setSelectedTrainingExams(prevExams => {
|
setSelectedTrainingExams(prevExams => {
|
||||||
const index = prevExams.indexOf(timestamp);
|
const uniqueExams = [...new Set(stats.map(stat => `${stat.module}-${stat.date}`))];
|
||||||
|
const indexes = uniqueExams.map(exam => prevExams.indexOf(exam)).filter(index => index !== -1);
|
||||||
if (index !== -1) {
|
if (indexes.length > 0) {
|
||||||
const newExams = [...prevExams];
|
const newExams = [...prevExams];
|
||||||
|
indexes.sort((a, b) => b - a).forEach(index => {
|
||||||
newExams.splice(index, 1);
|
newExams.splice(index, 1);
|
||||||
|
});
|
||||||
return newExams;
|
return newExams;
|
||||||
} else {
|
} else {
|
||||||
return [...prevExams, timestamp];
|
if (prevExams.length + uniqueExams.length <= maxTrainingExams) {
|
||||||
|
return [...prevExams, ...uniqueExams];
|
||||||
|
} else {
|
||||||
|
return prevExams;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -190,6 +204,8 @@ const StatsGridItem: React.FC<StatsGridItemProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
{renderPdfIcon(session, textColor, textColor)}
|
{renderPdfIcon(session, textColor, textColor)}
|
||||||
</div>
|
</div>
|
||||||
|
{examNumber === undefined ? (
|
||||||
|
<>
|
||||||
{aiUsage >= 50 && user.type !== "student" && (
|
{aiUsage >= 50 && user.type !== "student" && (
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
"ml-auto border px-1 rounded w-fit mr-1",
|
"ml-auto border px-1 rounded w-fit mr-1",
|
||||||
@@ -201,11 +217,20 @@ const StatsGridItem: React.FC<StatsGridItemProps> = ({
|
|||||||
<span className="text-xs">AI Usage</span>
|
<span className="text-xs">AI Usage</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full flex flex-col gap-1">
|
<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">
|
<div className={clsx(
|
||||||
|
"grid grid-cols-4 gap-2 place-items-start w-full -md:mt-2",
|
||||||
|
examNumber !== undefined && "pr-10"
|
||||||
|
)}>
|
||||||
{aggregatedLevels.map(({ module, level }) => (
|
{aggregatedLevels.map(({ module, level }) => (
|
||||||
<ModuleBadge key={module} module={module} level={level} />
|
<ModuleBadge key={module} module={module} level={level} />
|
||||||
))}
|
))}
|
||||||
@@ -230,9 +255,13 @@ const StatsGridItem: React.FC<StatsGridItemProps> = ({
|
|||||||
correct / total >= 0.7 && "hover:border-mti-purple",
|
correct / total >= 0.7 && "hover:border-mti-purple",
|
||||||
correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red",
|
correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red",
|
||||||
correct / total < 0.3 && "hover:border-mti-rose",
|
correct / total < 0.3 && "hover:border-mti-rose",
|
||||||
typeof selectedTrainingExams !== "undefined" && typeof timestamp === "string" && selectedTrainingExams.includes(timestamp) && "border-2 border-slate-600",
|
typeof selectedTrainingExams !== "undefined" && typeof timestamp === "string" && selectedTrainingExams.some(exam => exam.includes(timestamp)) && "border-2 border-slate-600",
|
||||||
)}
|
)}
|
||||||
onClick={selectExam}
|
onClick={examNumber === undefined ? selectExam : undefined}
|
||||||
|
style={{
|
||||||
|
...(width !== undefined && { width }),
|
||||||
|
...(height !== undefined && { height }),
|
||||||
|
}}
|
||||||
data-tip="This exam is still being evaluated..."
|
data-tip="This exam is still being evaluated..."
|
||||||
role="button">
|
role="button">
|
||||||
{content}
|
{content}
|
||||||
@@ -246,6 +275,10 @@ const StatsGridItem: React.FC<StatsGridItemProps> = ({
|
|||||||
correct / total < 0.3 && "hover:border-mti-rose",
|
correct / total < 0.3 && "hover:border-mti-rose",
|
||||||
)}
|
)}
|
||||||
data-tip="Your screen size is too small to view previous exams."
|
data-tip="Your screen size is too small to view previous exams."
|
||||||
|
style={{
|
||||||
|
...(width !== undefined && { width }),
|
||||||
|
...(height !== undefined && { height }),
|
||||||
|
}}
|
||||||
role="button">
|
role="button">
|
||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,9 +17,10 @@ const TrainingScore: React.FC<TrainingScoreProps> = ({
|
|||||||
const scores = trainingContent.exams.map(exam => exam.score);
|
const scores = trainingContent.exams.map(exam => exam.score);
|
||||||
const highestScore = Math.max(...scores);
|
const highestScore = Math.max(...scores);
|
||||||
const lowestScore = Math.min(...scores);
|
const lowestScore = Math.min(...scores);
|
||||||
const averageScore = scores.length > 0
|
let averageScore = scores.length > 0
|
||||||
? scores.reduce((sum, score) => sum + score, 0) / scores.length
|
? scores.reduce((sum, score) => sum + score, 0) / scores.length
|
||||||
: 0;
|
: 0;
|
||||||
|
averageScore = Math.round(averageScore);
|
||||||
|
|
||||||
const containerClasses = clsx(
|
const containerClasses = clsx(
|
||||||
"flex flex-row mb-4",
|
"flex flex-row mb-4",
|
||||||
@@ -76,7 +77,7 @@ const TrainingScore: React.FC<TrainingScoreProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{gridView && (
|
{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]">
|
<div className="flex w-14 h-14 bg-[#F5F5F5] items-center justify-center rounded-xl border border-[#DBDBDB]">
|
||||||
<GiLightBulb color={"#FFCC00"} size={28} />
|
<GiLightBulb color={"#FFCC00"} size={28} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {Module} from ".";
|
import { Module } from ".";
|
||||||
|
|
||||||
export type Exam = ReadingExam | ListeningExam | WritingExam | SpeakingExam | LevelExam;
|
export type Exam = ReadingExam | ListeningExam | WritingExam | SpeakingExam | LevelExam;
|
||||||
export type Variant = "full" | "partial";
|
export type Variant = "full" | "partial";
|
||||||
@@ -100,17 +100,21 @@ export interface Evaluation {
|
|||||||
misspelled_pairs?: {correction: string | null; misspelled: string}[];
|
misspelled_pairs?: {correction: string | null; misspelled: string}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InteractiveSpeakingEvaluation extends Evaluation {
|
|
||||||
perfect_answer_1?: {answer: string};
|
type InteractivePerfectAnswerKey = `perfect_answer_${number}`;
|
||||||
transcript_1?: string;
|
type InteractiveTranscriptKey = `transcript_${number}`;
|
||||||
fixed_text_1?: string;
|
type InteractiveFixedTextKey = `fixed_text_${number}`;
|
||||||
perfect_answer_2?: {answer: string};
|
|
||||||
transcript_2?: string;
|
type InteractivePerfectAnswerType = { [key in InteractivePerfectAnswerKey]: { answer: string } };
|
||||||
fixed_text_2?: string;
|
type InteractiveTranscriptType = { [key in InteractiveTranscriptKey]?: string };
|
||||||
perfect_answer_3?: {answer: string};
|
type InteractiveFixedTextType = { [key in InteractiveFixedTextKey]?: string };
|
||||||
transcript_3?: string;
|
|
||||||
fixed_text_3?: string;
|
interface InteractiveSpeakingEvaluation extends Evaluation,
|
||||||
}
|
InteractivePerfectAnswerType,
|
||||||
|
InteractiveTranscriptType,
|
||||||
|
InteractiveFixedTextType
|
||||||
|
{}
|
||||||
|
|
||||||
|
|
||||||
interface SpeakingEvaluation extends CommonEvaluation {
|
interface SpeakingEvaluation extends CommonEvaluation {
|
||||||
perfect_answer_1?: string;
|
perfect_answer_1?: string;
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import { uuidv4 } from "@firebase/util";
|
|||||||
import { usePDFDownload } from "@/hooks/usePDFDownload";
|
import { usePDFDownload } from "@/hooks/usePDFDownload";
|
||||||
import useRecordStore from "@/stores/recordStore";
|
import useRecordStore from "@/stores/recordStore";
|
||||||
import useTrainingContentStore from "@/stores/trainingContentStore";
|
import useTrainingContentStore from "@/stores/trainingContentStore";
|
||||||
import Button from "@/components/Low/Button";
|
|
||||||
import StatsGridItem from "@/components/StatGridItem";
|
import StatsGridItem from "@/components/StatGridItem";
|
||||||
|
|
||||||
|
|
||||||
@@ -148,10 +147,12 @@ export default function History({ user }: { user: User }) {
|
|||||||
|
|
||||||
const handleTrainingContentSubmission = () => {
|
const handleTrainingContentSubmission = () => {
|
||||||
if (groupedStats) {
|
if (groupedStats) {
|
||||||
const allStats = Object.keys(filterStatsByDate(groupedStats));
|
const groupedStatsByDate = filterStatsByDate(groupedStats);
|
||||||
const selectedStats = selectedTrainingExams.reduce<Record<string, Stat[]>>((accumulator, timestamp) => {
|
const allStats = Object.keys(groupedStatsByDate);
|
||||||
if (allStats.includes(timestamp)) {
|
const selectedStats = selectedTrainingExams.reduce<Record<string, Stat[]>>((accumulator, moduleAndTimestamp) => {
|
||||||
accumulator[timestamp] = filterStatsByDate(groupedStats)[timestamp];
|
const timestamp = moduleAndTimestamp.split("-")[1];
|
||||||
|
if (allStats.includes(timestamp) && !accumulator.hasOwnProperty(timestamp)) {
|
||||||
|
accumulator[timestamp] = groupedStatsByDate[timestamp];
|
||||||
}
|
}
|
||||||
return accumulator;
|
return accumulator;
|
||||||
}, {});
|
}, {});
|
||||||
@@ -177,6 +178,7 @@ export default function History({ user }: { user: User }) {
|
|||||||
training={training}
|
training={training}
|
||||||
selectedTrainingExams={selectedTrainingExams}
|
selectedTrainingExams={selectedTrainingExams}
|
||||||
setSelectedTrainingExams={setSelectedTrainingExams}
|
setSelectedTrainingExams={setSelectedTrainingExams}
|
||||||
|
maxTrainingExams={MAX_TRAINING_EXAMS}
|
||||||
setExams={setExams}
|
setExams={setExams}
|
||||||
setShowSolutions={setShowSolutions}
|
setShowSolutions={setShowSolutions}
|
||||||
setUserSolutions={setUserSolutions}
|
setUserSolutions={setUserSolutions}
|
||||||
@@ -323,7 +325,8 @@ export default function History({ user }: { user: User }) {
|
|||||||
)}
|
)}
|
||||||
{(training && (
|
{(training && (
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<div className="font-semibold text-2xl mr-4">Select up to 10 exams {`(${selectedTrainingExams.length}/${MAX_TRAINING_EXAMS})`}</div>
|
<div className="font-semibold text-2xl mr-4">Select up to 10 exercises
|
||||||
|
{`(${selectedTrainingExams.length}/${MAX_TRAINING_EXAMS})`}</div>
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
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",
|
"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",
|
||||||
@@ -385,6 +388,11 @@ export default function History({ user }: { user: User }) {
|
|||||||
{groupedStats && Object.keys(groupedStats).length === 0 && !isStatsLoading && (
|
{groupedStats && Object.keys(groupedStats).length === 0 && !isStatsLoading && (
|
||||||
<span className="font-semibold ml-1">No record to display...</span>
|
<span className="font-semibold ml-1">No record to display...</span>
|
||||||
)}
|
)}
|
||||||
|
{isStatsLoading && (
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
</Layout>
|
</Layout>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ import { usePDFDownload } from "@/hooks/usePDFDownload";
|
|||||||
import useAssignments from '@/hooks/useAssignments';
|
import useAssignments from '@/hooks/useAssignments';
|
||||||
import useUsers from '@/hooks/useUsers';
|
import useUsers from '@/hooks/useUsers';
|
||||||
import Dropdown from "@/components/Dropdown";
|
import Dropdown from "@/components/Dropdown";
|
||||||
|
import InfiniteCarousel from '@/components/InfiniteCarousel';
|
||||||
|
import { LuExternalLink } from "react-icons/lu";
|
||||||
|
import { uniqBy } from 'lodash';
|
||||||
|
import { getExamById } from '@/utils/exams';
|
||||||
|
import { convertToUserSolutions } from '@/utils/stats';
|
||||||
|
import { sortByModule } from '@/utils/moduleUtils';
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
|
export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
@@ -68,12 +74,9 @@ const TrainingContent: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
const { assignments } = useAssignments({});
|
const { assignments } = useAssignments({});
|
||||||
const { users } = useUsers();
|
const { users } = useUsers();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { id } = router.query;
|
const { id } = router.query;
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchTrainingContent = async () => {
|
const fetchTrainingContent = async () => {
|
||||||
if (!id || typeof id !== 'string') return;
|
if (!id || typeof id !== 'string') return;
|
||||||
@@ -118,6 +121,32 @@ const TrainingContent: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
setCurrentTipIndex((prevIndex) => (prevIndex - 1));
|
setCurrentTipIndex((prevIndex) => (prevIndex - 1));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const goToExam = (examNumber: number) => {
|
||||||
|
const stats = trainingContent?.exams[examNumber].stats!;
|
||||||
|
const examPromises = uniqBy(stats, "exam").map((stat) => {
|
||||||
|
return getExamById(stat.module, stat.exam);
|
||||||
|
});
|
||||||
|
|
||||||
|
const { timeSpent, inactivity } = stats[0];
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@@ -137,13 +166,25 @@ const TrainingContent: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
<span className="loading loading-infinity w-32 bg-mti-green-light" />
|
<span className="loading loading-infinity w-32 bg-mti-green-light" />
|
||||||
</div>
|
</div>
|
||||||
) : (trainingContent && (
|
) : (trainingContent && (
|
||||||
<div className="flex flex-col gap-10">
|
<div className="flex flex-col gap-8">
|
||||||
<div className="flex h-screen flex-col gap-4">
|
<div className="flex flex-row items-center">
|
||||||
<div className='flex flex-row h-[15%] gap-4'>
|
<span className="bg-gray-200 text-gray-800 px-3 py-0.5 rounded-full font-semibold text-lg mr-2">{trainingContent.exams.length}</span>
|
||||||
{/*<Carousel itemsPerFrame={4} itemsPerScroll={4}>*/}
|
<span>Exams Selected</span>
|
||||||
|
</div>
|
||||||
|
<div className='h-[15vh] mb-4'>
|
||||||
|
<InfiniteCarousel height="150px"
|
||||||
|
overlay={
|
||||||
|
<LuExternalLink size={20} />
|
||||||
|
}
|
||||||
|
overlayFunc={goToExam}
|
||||||
|
overlayClassName='bottom-6 right-5 cursor-pointer'
|
||||||
|
>
|
||||||
{trainingContent.exams.map((exam, examIndex) => (
|
{trainingContent.exams.map((exam, examIndex) => (
|
||||||
<StatsGridItem
|
<StatsGridItem
|
||||||
key={`exam-${examIndex}`}
|
key={`exam-${examIndex}`}
|
||||||
|
width='380px'
|
||||||
|
height='150px'
|
||||||
|
examNumber={examIndex + 1}
|
||||||
stats={exam.stats || []}
|
stats={exam.stats || []}
|
||||||
timestamp={exam.date}
|
timestamp={exam.date}
|
||||||
user={user}
|
user={user}
|
||||||
@@ -158,11 +199,11 @@ const TrainingContent: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
renderPdfIcon={renderPdfIcon}
|
renderPdfIcon={renderPdfIcon}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{/* </Carousel> */}
|
</InfiniteCarousel>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-col h-[75%]' style={{ maxHeight: '85%' }}>
|
<div className='flex flex-col'>
|
||||||
<div className='flex flex-row gap-10 -md:flex-col'>
|
<div className='flex flex-row gap-10 -md:flex-col h-full'>
|
||||||
<div className="rounded-3xl p-6 w-1/2 shadow-training-inset -md:w-full max-h-full">
|
<div className="flex flex-col 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">
|
<div className="flex flex-row items-center mb-6 gap-1">
|
||||||
<MdOutlinePlaylistAddCheckCircle color={"#40A1EA"} size={26} />
|
<MdOutlinePlaylistAddCheckCircle color={"#40A1EA"} size={26} />
|
||||||
<h2 className={`text-xl font-semibold text-[#40A1EA]`}>General Evaluation</h2>
|
<h2 className={`text-xl font-semibold text-[#40A1EA]`}>General Evaluation</h2>
|
||||||
@@ -183,11 +224,14 @@ const TrainingContent: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
</svg>
|
</svg>
|
||||||
<h3 className="text-xl font-semibold text-[#40A1EA]">Performance Breakdown by Exam:</h3>
|
<h3 className="text-xl font-semibold text-[#40A1EA]">Performance Breakdown by Exam:</h3>
|
||||||
</div>
|
</div>
|
||||||
<ul>
|
<ul className='overflow-auto scrollbar-hide flex-grow'>
|
||||||
{trainingContent.exams.flatMap((exam, index) => (
|
{trainingContent.exams.flatMap((exam, index) => (
|
||||||
<li key={index} className="flex flex-col mb-2 bg-[#22E1B30F] p-4 rounded-xl border">
|
<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">
|
<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>
|
<div className='flex items-center border-r-2 border-[#D9D9D929] pr-2'>
|
||||||
|
<span className='mr-1'>Exam</span>
|
||||||
|
<span className="font-semibold bg-gray-200 text-gray-800 px-2 rounded-full text-sm">{index + 1}</span>
|
||||||
|
</div>
|
||||||
<span className="pl-2">{exam.score}%</span>
|
<span className="pl-2">{exam.score}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
@@ -198,39 +242,8 @@ const TrainingContent: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-3xl p-6 w-1/2 shadow-training-inset -md:w-full">
|
<div className="flex flex-col 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 max-h-[500px] overflow-y-auto scrollbar-hide">
|
|
||||||
<div className='flex flex-col'>
|
<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">
|
<div className="flex flex-row items-center mb-4 gap-1">
|
||||||
<AiOutlineFileSearch color="#40A1EA" size={24} />
|
<AiOutlineFileSearch color="#40A1EA" size={24} />
|
||||||
<h3 className="text-xl font-semibold text-[#40A1EA]">Identified Weak Areas</h3>
|
<h3 className="text-xl font-semibold text-[#40A1EA]">Identified Weak Areas</h3>
|
||||||
@@ -238,7 +251,7 @@ const TrainingContent: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
<Tab.Group>
|
<Tab.Group>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<Tab.List>
|
<Tab.List>
|
||||||
<div className="flex flex-row gap-6">
|
<div className="flex flex-row gap-6 overflow-x-auto pb-1 training-scrollbar">
|
||||||
{trainingContent.weak_areas.map((x, index) => (
|
{trainingContent.weak_areas.map((x, index) => (
|
||||||
<Tab
|
<Tab
|
||||||
key={index}
|
key={index}
|
||||||
@@ -268,10 +281,47 @@ const TrainingContent: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
</div>
|
</div>
|
||||||
</Tab.Group>
|
</Tab.Group>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="w-full h-px bg-[#D9D9D929] my-6"></div>
|
||||||
|
<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="flex flex-grow bg-[#FBFBFB] border rounded-xl p-4">
|
||||||
|
<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="flex flex-col flex-grow space-y-4 pb-2 overflow-y-auto scrollbar-hide">
|
||||||
|
{trainingContent.exams.map((exam, index) => (
|
||||||
|
<li key={index} className="border rounded-lg bg-white">
|
||||||
|
<Dropdown title={
|
||||||
|
<div className='flex flex-row items-center'>
|
||||||
|
<span className="mr-1">Exam</span>
|
||||||
|
<span className="font-semibold bg-gray-200 text-gray-800 px-2 rounded-full text-sm mt-0.5">{index + 1}</span>
|
||||||
|
</div>
|
||||||
|
} open={index == 0}>
|
||||||
|
<span>{exam.detailed_summary}</span>
|
||||||
|
</Dropdown>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex">
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex -md:hidden">
|
||||||
<div className="rounded-3xl p-6 shadow-training-inset w-full">
|
<div className="rounded-3xl p-6 shadow-training-inset w-full">
|
||||||
<div className="flex flex-col p-10">
|
<div className="flex flex-col p-10">
|
||||||
<Exercise key={currentTipIndex} {...trainingTips[currentTipIndex]} />
|
<Exercise key={currentTipIndex} {...trainingTips[currentTipIndex]} />
|
||||||
@@ -294,6 +344,7 @@ const TrainingContent: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -389,7 +389,7 @@ const Training: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{groupedByTrainingContent && Object.keys(groupedByTrainingContent).length > 0 && (
|
{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">
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-2 2xl:grid-cols-3 w-full gap-4 xl:gap-6">
|
||||||
{Object.keys(filterTrainingContentByDate(groupedByTrainingContent))
|
{Object.keys(filterTrainingContentByDate(groupedByTrainingContent))
|
||||||
.sort((a, b) => parseInt(b) - parseInt(a))
|
.sort((a, b) => parseInt(b) - parseInt(a))
|
||||||
.map(trainingContentContainer)}
|
.map(trainingContentContainer)}
|
||||||
|
|||||||
@@ -4,14 +4,35 @@
|
|||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
.scrollbar-hide {
|
.scrollbar-hide {
|
||||||
-ms-overflow-style: none; /* IE and Edge */
|
-ms-overflow-style: none;
|
||||||
scrollbar-width: none; /* Firefox */
|
/* IE and Edge */
|
||||||
|
scrollbar-width: none;
|
||||||
|
/* Firefox */
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar-hide::-webkit-scrollbar {
|
.scrollbar-hide::-webkit-scrollbar {
|
||||||
display: none; /* Chrome, Safari and Opera */
|
display: none;
|
||||||
|
/* Chrome, Safari and Opera */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.training-scrollbar::-webkit-scrollbar {
|
||||||
|
@apply w-1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.training-scrollbar::-webkit-scrollbar-track {
|
||||||
|
@apply bg-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.training-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
@apply bg-gray-400 hover:bg-gray-500 rounded-full transition-colors opacity-50 hover:opacity-75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.training-scrollbar {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--max-width: 1100px;
|
--max-width: 1100px;
|
||||||
--border-radius: 12px;
|
--border-radius: 12px;
|
||||||
|
|||||||
81
yarn.lock
81
yarn.lock
@@ -711,6 +711,13 @@
|
|||||||
node-fetch "2.6.7"
|
node-fetch "2.6.7"
|
||||||
tslib "^2.1.0"
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/util@^1.9.7":
|
||||||
|
version "1.9.7"
|
||||||
|
resolved "https://registry.npmjs.org/@firebase/util/-/util-1.9.7.tgz"
|
||||||
|
integrity sha512-fBVNH/8bRbYjqlbIhZ+lBtdAAS4WqZumx03K06/u7fJSpz1TGjEMm1ImvKD47w+xaFKIP2ori6z8BrbakRfjJA==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
"@firebase/util@1.9.3":
|
"@firebase/util@1.9.3":
|
||||||
version "1.9.3"
|
version "1.9.3"
|
||||||
resolved "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz"
|
resolved "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz"
|
||||||
@@ -718,13 +725,6 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib "^2.1.0"
|
tslib "^2.1.0"
|
||||||
|
|
||||||
"@firebase/util@^1.9.7":
|
|
||||||
version "1.9.7"
|
|
||||||
resolved "https://registry.yarnpkg.com/@firebase/util/-/util-1.9.7.tgz#c03b0ae065b3bba22800da0bd5314ef030848038"
|
|
||||||
integrity sha512-fBVNH/8bRbYjqlbIhZ+lBtdAAS4WqZumx03K06/u7fJSpz1TGjEMm1ImvKD47w+xaFKIP2ori6z8BrbakRfjJA==
|
|
||||||
dependencies:
|
|
||||||
tslib "^2.1.0"
|
|
||||||
|
|
||||||
"@firebase/webchannel-wrapper@0.9.0":
|
"@firebase/webchannel-wrapper@0.9.0":
|
||||||
version "0.9.0"
|
version "0.9.0"
|
||||||
resolved "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.9.0.tgz"
|
resolved "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.9.0.tgz"
|
||||||
@@ -1591,13 +1591,6 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react-slick@^0.23.13":
|
|
||||||
version "0.23.13"
|
|
||||||
resolved "https://registry.npmjs.org/@types/react-slick/-/react-slick-0.23.13.tgz"
|
|
||||||
integrity sha512-bNZfDhe/L8t5OQzIyhrRhBr/61pfBcWaYJoq6UDqFtv5LMwfg4NsVDD2J8N01JqdAdxLjOt66OZEp6PX+dGs/A==
|
|
||||||
dependencies:
|
|
||||||
"@types/react" "*"
|
|
||||||
|
|
||||||
"@types/react-transition-group@^4.4.0", "@types/react-transition-group@^4.4.1":
|
"@types/react-transition-group@^4.4.0", "@types/react-transition-group@^4.4.1":
|
||||||
version "4.4.5"
|
version "4.4.5"
|
||||||
resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz"
|
resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz"
|
||||||
@@ -1691,6 +1684,18 @@
|
|||||||
"@typescript-eslint/types" "5.51.0"
|
"@typescript-eslint/types" "5.51.0"
|
||||||
eslint-visitor-keys "^3.3.0"
|
eslint-visitor-keys "^3.3.0"
|
||||||
|
|
||||||
|
"@use-gesture/core@10.3.1":
|
||||||
|
version "10.3.1"
|
||||||
|
resolved "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz"
|
||||||
|
integrity sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==
|
||||||
|
|
||||||
|
"@use-gesture/react@^10.3.1":
|
||||||
|
version "10.3.1"
|
||||||
|
resolved "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz"
|
||||||
|
integrity sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==
|
||||||
|
dependencies:
|
||||||
|
"@use-gesture/core" "10.3.1"
|
||||||
|
|
||||||
"@wixc3/board-core@^2.2.0":
|
"@wixc3/board-core@^2.2.0":
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.npmjs.org/@wixc3/board-core/-/board-core-2.2.0.tgz"
|
resolved "https://registry.npmjs.org/@wixc3/board-core/-/board-core-2.2.0.tgz"
|
||||||
@@ -2165,7 +2170,7 @@ chownr@^2.0.0:
|
|||||||
resolved "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz"
|
resolved "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz"
|
||||||
integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
|
integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
|
||||||
|
|
||||||
classnames@^2.2.5, classnames@^2.2.6, classnames@^2.3.0, classnames@^2.5.1:
|
classnames@^2.2.6, classnames@^2.3.0, classnames@^2.5.1:
|
||||||
version "2.5.1"
|
version "2.5.1"
|
||||||
resolved "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz"
|
resolved "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz"
|
||||||
integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
|
integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
|
||||||
@@ -2666,11 +2671,6 @@ enhanced-resolve@^5.10.0:
|
|||||||
graceful-fs "^4.2.4"
|
graceful-fs "^4.2.4"
|
||||||
tapable "^2.2.0"
|
tapable "^2.2.0"
|
||||||
|
|
||||||
enquire.js@^2.1.6:
|
|
||||||
version "2.1.6"
|
|
||||||
resolved "https://registry.npmjs.org/enquire.js/-/enquire.js-2.1.6.tgz"
|
|
||||||
integrity sha512-/KujNpO+PT63F7Hlpu4h3pE3TokKRHN26JYmQpPyjkRD/N57R7bPDNojMXdi7uveAKjYB7yQnartCxZnFWr0Xw==
|
|
||||||
|
|
||||||
ent@^2.2.0:
|
ent@^2.2.0:
|
||||||
version "2.2.1"
|
version "2.2.1"
|
||||||
resolved "https://registry.npmjs.org/ent/-/ent-2.2.1.tgz"
|
resolved "https://registry.npmjs.org/ent/-/ent-2.2.1.tgz"
|
||||||
@@ -4135,13 +4135,6 @@ json-stable-stringify-without-jsonify@^1.0.1:
|
|||||||
resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz"
|
resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz"
|
||||||
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
|
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
|
||||||
|
|
||||||
json2mq@^0.2.0:
|
|
||||||
version "0.2.0"
|
|
||||||
resolved "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz"
|
|
||||||
integrity sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==
|
|
||||||
dependencies:
|
|
||||||
string-convert "^0.2.0"
|
|
||||||
|
|
||||||
json5@^1.0.1:
|
json5@^1.0.1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz"
|
resolved "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz"
|
||||||
@@ -4319,11 +4312,6 @@ lodash.clonedeep@^4.5.0:
|
|||||||
resolved "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz"
|
resolved "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz"
|
||||||
integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==
|
integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==
|
||||||
|
|
||||||
lodash.debounce@^4.0.8:
|
|
||||||
version "4.0.8"
|
|
||||||
resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz"
|
|
||||||
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
|
|
||||||
|
|
||||||
lodash.includes@^4.3.0:
|
lodash.includes@^4.3.0:
|
||||||
version "4.3.0"
|
version "4.3.0"
|
||||||
resolved "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz"
|
resolved "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz"
|
||||||
@@ -5355,17 +5343,6 @@ react-select@^5.7.5:
|
|||||||
react-transition-group "^4.3.0"
|
react-transition-group "^4.3.0"
|
||||||
use-isomorphic-layout-effect "^1.1.2"
|
use-isomorphic-layout-effect "^1.1.2"
|
||||||
|
|
||||||
react-slick@^0.30.2:
|
|
||||||
version "0.30.2"
|
|
||||||
resolved "https://registry.npmjs.org/react-slick/-/react-slick-0.30.2.tgz"
|
|
||||||
integrity sha512-XvQJi7mRHuiU3b9irsqS9SGIgftIfdV5/tNcURTb5LdIokRA5kIIx3l4rlq2XYHfxcSntXapoRg/GxaVOM1yfg==
|
|
||||||
dependencies:
|
|
||||||
classnames "^2.2.5"
|
|
||||||
enquire.js "^2.1.6"
|
|
||||||
json2mq "^0.2.0"
|
|
||||||
lodash.debounce "^4.0.8"
|
|
||||||
resize-observer-polyfill "^1.5.0"
|
|
||||||
|
|
||||||
react-string-replace@^1.1.0:
|
react-string-replace@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.npmjs.org/react-string-replace/-/react-string-replace-1.1.0.tgz"
|
resolved "https://registry.npmjs.org/react-string-replace/-/react-string-replace-1.1.0.tgz"
|
||||||
@@ -5506,11 +5483,6 @@ requizzle@^0.2.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
lodash "^4.17.21"
|
lodash "^4.17.21"
|
||||||
|
|
||||||
resize-observer-polyfill@^1.5.0:
|
|
||||||
version "1.5.1"
|
|
||||||
resolved "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz"
|
|
||||||
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
|
|
||||||
|
|
||||||
resolve-from@^4.0.0:
|
resolve-from@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz"
|
resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz"
|
||||||
@@ -5720,11 +5692,6 @@ slash@^4.0.0:
|
|||||||
resolved "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz"
|
resolved "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz"
|
||||||
integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==
|
integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==
|
||||||
|
|
||||||
slick-carousel@^1.8.1:
|
|
||||||
version "1.8.1"
|
|
||||||
resolved "https://registry.npmjs.org/slick-carousel/-/slick-carousel-1.8.1.tgz"
|
|
||||||
integrity sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA==
|
|
||||||
|
|
||||||
source-map-js@^1.0.2:
|
source-map-js@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz"
|
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz"
|
||||||
@@ -5773,11 +5740,6 @@ string_decoder@~1.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer "~5.1.0"
|
safe-buffer "~5.1.0"
|
||||||
|
|
||||||
string-convert@^0.2.0:
|
|
||||||
version "0.2.1"
|
|
||||||
resolved "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz"
|
|
||||||
integrity sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==
|
|
||||||
|
|
||||||
"string-width-cjs@npm:string-width@^4.2.0":
|
"string-width-cjs@npm:string-width@^4.2.0":
|
||||||
version "4.2.3"
|
version "4.2.3"
|
||||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
||||||
@@ -6369,8 +6331,7 @@ wordwrap@^1.0.0:
|
|||||||
resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz"
|
resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz"
|
||||||
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
|
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
|
||||||
|
|
||||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||||
name wrap-ansi-cjs
|
|
||||||
version "7.0.0"
|
version "7.0.0"
|
||||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
|
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
|
||||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||||
|
|||||||
Reference in New Issue
Block a user