Merge branch 'develop'
This commit is contained in:
BIN
public/audio/error.mp3
Normal file
BIN
public/audio/error.mp3
Normal file
Binary file not shown.
@@ -36,7 +36,7 @@ export default function Diagnostic({onFinish}: Props) {
|
||||
};
|
||||
|
||||
const selectExam = () => {
|
||||
const examPromises = MODULE_ARRAY.map((module) => getExam(module, true));
|
||||
const examPromises = MODULE_ARRAY.map((module) => getExam(module, true, "partial"));
|
||||
|
||||
Promise.all(examPromises).then((exams) => {
|
||||
if (exams.every((x) => !!x)) {
|
||||
|
||||
@@ -42,7 +42,9 @@ export default function CountrySelect({value, disabled = false, onChange}: Props
|
||||
displayValue={(code: string) => {
|
||||
const country = countries[code as unknown as keyof TCountries];
|
||||
|
||||
return `${countryCodes.findOne("countryCode" as any, code).flag} ${country.name} (+${country.phone})`;
|
||||
return `${countryCodes.findOne("countryCode" as any, code)?.flag || ""} ${country?.name || "N/A"} (+${
|
||||
country?.phone || "N/A"
|
||||
})`;
|
||||
}}
|
||||
/>
|
||||
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center pr-8">
|
||||
|
||||
@@ -45,8 +45,9 @@ const Nav = ({Icon, label, path, keyPath, disabled = false, isMinimized = false}
|
||||
<Link
|
||||
href={!disabled ? keyPath : ""}
|
||||
className={clsx(
|
||||
"p-4 rounded-full flex gap-4 items-center cursor-pointer text-gray-500 hover:bg-mti-purple-light hover:text-white",
|
||||
"p-4 rounded-full flex gap-4 items-center text-gray-500 hover:text-white",
|
||||
"transition-all duration-300 ease-in-out",
|
||||
disabled ? "hover:bg-mti-gray-dim cursor-not-allowed" : "hover:bg-mti-purple-light cursor-pointer",
|
||||
path === keyPath && "bg-mti-purple-light text-white",
|
||||
isMinimized ? "w-fit" : "w-full min-w-[200px] 2xl:min-w-[220px] px-8",
|
||||
)}>
|
||||
|
||||
@@ -243,7 +243,9 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
|
||||
options={CURRENCIES_OPTIONS}
|
||||
value={CURRENCIES_OPTIONS.find((c) => c.value === paymentCurrency)}
|
||||
onChange={(value) => setPaymentCurrency(value?.value)}
|
||||
menuPortalTarget={document?.body}
|
||||
styles={{
|
||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||
control: (styles) => ({
|
||||
...styles,
|
||||
paddingLeft: "4px",
|
||||
@@ -282,8 +284,10 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
|
||||
value: referralAgent,
|
||||
label: referralAgentLabel,
|
||||
}}
|
||||
menuPortalTarget={document?.body}
|
||||
onChange={(value) => setReferralAgent(value?.value)}
|
||||
styles={{
|
||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||
control: (styles) => ({
|
||||
...styles,
|
||||
paddingLeft: "4px",
|
||||
@@ -314,7 +318,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
|
||||
type="number"
|
||||
defaultValue={commissionValue || 0}
|
||||
className="col-span-3"
|
||||
disabled={disabled}
|
||||
disabled={disabled || loggedInUser.type === "agent"}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
@@ -520,6 +524,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
|
||||
<Select
|
||||
className="px-4 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed bg-white rounded-full border border-mti-gray-platinum focus:outline-none"
|
||||
options={USER_STATUS_OPTIONS}
|
||||
menuPortalTarget={document?.body}
|
||||
value={USER_STATUS_OPTIONS.find((o) => o.value === status)}
|
||||
onChange={(value) => setStatus(value?.value as typeof user.status)}
|
||||
styles={{
|
||||
@@ -532,6 +537,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
|
||||
outline: "none",
|
||||
},
|
||||
}),
|
||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||
option: (styles, state) => ({
|
||||
...styles,
|
||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||
@@ -546,6 +552,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
|
||||
<Select
|
||||
className="px-4 py-4 w-full text-sm font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed bg-white rounded-full border border-mti-gray-platinum focus:outline-none"
|
||||
options={USER_TYPE_OPTIONS}
|
||||
menuPortalTarget={document?.body}
|
||||
value={USER_TYPE_OPTIONS.find((o) => o.value === type)}
|
||||
onChange={(value) => setType(value?.value as typeof user.type)}
|
||||
styles={{
|
||||
@@ -558,6 +565,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
|
||||
outline: "none",
|
||||
},
|
||||
}),
|
||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||
option: (styles, state) => ({
|
||||
...styles,
|
||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||
@@ -574,17 +582,17 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
|
||||
|
||||
<div className="flex gap-4 justify-between mt-4 w-full">
|
||||
<div className="self-start flex gap-4 justify-start items-center w-full">
|
||||
{onViewCorporate && (
|
||||
{onViewCorporate && ["student", "teacher"].includes(user.type) && (
|
||||
<Button className="w-full max-w-[200px]" variant="outline" color="rose" onClick={onViewCorporate}>
|
||||
View Corporate
|
||||
</Button>
|
||||
)}
|
||||
{onViewStudents && (
|
||||
{onViewStudents && ["corporate", "teacher"].includes(user.type) && (
|
||||
<Button className="w-full max-w-[200px]" variant="outline" color="rose" onClick={onViewStudents}>
|
||||
View Students
|
||||
</Button>
|
||||
)}
|
||||
{onViewTeachers && (
|
||||
{onViewTeachers && ["student", "corporate"].includes(user.type) && (
|
||||
<Button className="w-full max-w-[200px]" variant="outline" color="rose" onClick={onViewTeachers}>
|
||||
View Teachers
|
||||
</Button>
|
||||
|
||||
@@ -18,8 +18,8 @@ export const PERMISSIONS = {
|
||||
developer: ["developer"],
|
||||
},
|
||||
updateUser: {
|
||||
student: ["teacher", "corporate", "developer", "admin"],
|
||||
teacher: ["corporate", "developer", "admin"],
|
||||
student: ["developer", "admin"],
|
||||
teacher: ["developer", "admin"],
|
||||
corporate: ["admin", "developer"],
|
||||
admin: ["developer", "admin"],
|
||||
agent: ["developer", "admin"],
|
||||
|
||||
@@ -151,8 +151,9 @@ export default function TeacherDashboard({user}: Props) {
|
||||
};
|
||||
|
||||
const AssignmentsPage = () => {
|
||||
const activeFilter = (a: Assignment) => moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment());
|
||||
const pastFilter = (a: Assignment) => moment(a.endDate).isBefore(moment());
|
||||
const activeFilter = (a: Assignment) =>
|
||||
moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment()) && a.assignees.length > a.results.length;
|
||||
const pastFilter = (a: Assignment) => moment(a.endDate).isBefore(moment()) || a.assignees.length === a.results.length;
|
||||
const futureFilter = (a: Assignment) => moment(a.startDate).isAfter(moment());
|
||||
|
||||
return (
|
||||
|
||||
@@ -12,17 +12,19 @@ import {calculateAverageLevel} from "@/utils/score";
|
||||
import {sortByModuleName} from "@/utils/moduleUtils";
|
||||
import {capitalize} from "lodash";
|
||||
import ProfileSummary from "@/components/ProfileSummary";
|
||||
import {Variant} from "@/interfaces/exam";
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
page: "exercises" | "exams";
|
||||
onStart: (modules: Module[], avoidRepeated: boolean) => void;
|
||||
onStart: (modules: Module[], avoidRepeated: boolean, variant: Variant) => void;
|
||||
disableSelection?: boolean;
|
||||
}
|
||||
|
||||
export default function Selection({user, page, onStart, disableSelection = false}: Props) {
|
||||
const [selectedModules, setSelectedModules] = useState<Module[]>([]);
|
||||
const [avoidRepeatedExams, setAvoidRepeatedExams] = useState(true);
|
||||
const [variant, setVariant] = useState<Variant>("full");
|
||||
const {stats} = useStats(user?.id);
|
||||
|
||||
const toggleModule = (module: Module) => {
|
||||
@@ -202,20 +204,37 @@ export default function Selection({user, page, onStart, disableSelection = false
|
||||
)}
|
||||
</section>
|
||||
<div className="flex w-full -md:flex-col -md:gap-4 -md:justify-center md:justify-between items-center">
|
||||
<div
|
||||
className="flex gap-3 items-center text-mti-gray-dim text-sm cursor-pointer tooltip w-full -md:justify-center"
|
||||
data-tip="If possible, the platform will choose exams not yet done"
|
||||
onClick={() => setAvoidRepeatedExams((prev) => !prev)}>
|
||||
<input type="checkbox" className="hidden" />
|
||||
<div className="flex flex-col gap-3 items-center w-full">
|
||||
<div
|
||||
className={clsx(
|
||||
"w-6 h-6 rounded-md flex items-center justify-center border border-mti-purple-light bg-white",
|
||||
"transition duration-300 ease-in-out",
|
||||
avoidRepeatedExams && "!bg-mti-purple-light ",
|
||||
)}>
|
||||
<BsCheck color="white" className="w-full h-full" />
|
||||
className="flex gap-3 items-center text-mti-gray-dim text-sm cursor-pointer w-full -md:justify-center"
|
||||
onClick={() => setAvoidRepeatedExams((prev) => !prev)}>
|
||||
<input type="checkbox" className="hidden" />
|
||||
<div
|
||||
className={clsx(
|
||||
"w-6 h-6 rounded-md flex items-center justify-center border border-mti-purple-light bg-white",
|
||||
"transition duration-300 ease-in-out",
|
||||
avoidRepeatedExams && "!bg-mti-purple-light ",
|
||||
)}>
|
||||
<BsCheck color="white" className="w-full h-full" />
|
||||
</div>
|
||||
<span className="tooltip" data-tip="If possible, the platform will choose exams not yet done.">
|
||||
Avoid Repeated Questions
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="flex gap-3 items-center text-mti-gray-dim text-sm cursor-pointer w-full -md:justify-center"
|
||||
onClick={() => setVariant((prev) => (prev === "full" ? "partial" : "full"))}>
|
||||
<input type="checkbox" className="hidden" />
|
||||
<div
|
||||
className={clsx(
|
||||
"w-6 h-6 rounded-md flex items-center justify-center border border-mti-purple-light bg-white",
|
||||
"transition duration-300 ease-in-out",
|
||||
variant === "full" && "!bg-mti-purple-light ",
|
||||
)}>
|
||||
<BsCheck color="white" className="w-full h-full" />
|
||||
</div>
|
||||
<span>Full length exams</span>
|
||||
</div>
|
||||
<span>Avoid Repeated Questions</span>
|
||||
</div>
|
||||
<div className="tooltip w-full" data-tip={`Your screen size is too small to do ${page}`}>
|
||||
<Button color="purple" className="px-12 w-full max-w-xs md:hidden" disabled>
|
||||
@@ -227,6 +246,7 @@ export default function Selection({user, page, onStart, disableSelection = false
|
||||
onStart(
|
||||
!disableSelection ? selectedModules.sort(sortByModuleName) : ["reading", "listening", "writing", "speaking"],
|
||||
avoidRepeatedExams,
|
||||
variant,
|
||||
)
|
||||
}
|
||||
color="purple"
|
||||
|
||||
@@ -1,24 +1,17 @@
|
||||
/* eslint-disable jsx-a11y/alt-text */
|
||||
import React from "react";
|
||||
|
||||
import { View, Text, Image } from "@react-pdf/renderer";
|
||||
import { styles } from "../styles";
|
||||
import { ModuleScore } from "@/interfaces/module.scores";
|
||||
import {View, Text, Image} from "@react-pdf/renderer";
|
||||
import {styles} from "../styles";
|
||||
import {ModuleScore} from "@/interfaces/module.scores";
|
||||
|
||||
export const RadialResult = ({
|
||||
module,
|
||||
score,
|
||||
total,
|
||||
png,
|
||||
}: ModuleScore) => (
|
||||
<View style={[styles.textFont, styles.radialContainer]}>
|
||||
<Text style={[styles.textColor, styles.textBold, { fontSize: 10 }]}>
|
||||
{module}
|
||||
</Text>
|
||||
<Image src={png} style={styles.image64}></Image>
|
||||
<View style={[styles.textColor, styles.radialResultContainer]}>
|
||||
<Text style={styles.textBold}>{score}</Text>
|
||||
<Text style={{ fontSize: 8 }}>out of {total}</Text>
|
||||
</View>
|
||||
</View>
|
||||
export const RadialResult = ({module, score, total, png}: ModuleScore) => (
|
||||
<View style={[styles.textFont, styles.radialContainer]}>
|
||||
<Text style={[styles.textColor, styles.textBold, {fontSize: 10}]}>{module}</Text>
|
||||
<Image src={png} style={styles.image64}></Image>
|
||||
<View style={[styles.textColor, styles.radialResultContainer]}>
|
||||
<Text style={styles.textBold}>{score.toFixed(2)}</Text>
|
||||
<Text style={{fontSize: 8}}>out of {total}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import React from "react";
|
||||
import { View, StyleSheet } from "@react-pdf/renderer";
|
||||
import { ModuleScore } from "@/interfaces/module.scores";
|
||||
import { RadialResult } from "./radial.result";
|
||||
import {View, StyleSheet} from "@react-pdf/renderer";
|
||||
import {ModuleScore} from "@/interfaces/module.scores";
|
||||
import {RadialResult} from "./radial.result";
|
||||
interface Props {
|
||||
testDetails: ModuleScore[];
|
||||
testDetails: ModuleScore[];
|
||||
}
|
||||
|
||||
const customStyles = StyleSheet.create({
|
||||
container: { display: "flex", flexDirection: "row", gap: 30 },
|
||||
container: {display: "flex", flexDirection: "row", gap: 30},
|
||||
});
|
||||
|
||||
export const SkillExamDetails = ({ testDetails }: Props) => (
|
||||
<View style={customStyles.container}>
|
||||
{testDetails.map((detail) => {
|
||||
const { module } = detail;
|
||||
return <RadialResult key={module} {...detail} />;
|
||||
})}
|
||||
</View>
|
||||
export const SkillExamDetails = ({testDetails}: Props) => (
|
||||
<View style={customStyles.container}>
|
||||
{testDetails.map((detail) => {
|
||||
const {module} = detail;
|
||||
return <RadialResult key={module} {...detail} />;
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Module} from ".";
|
||||
|
||||
export type Exam = ReadingExam | ListeningExam | WritingExam | SpeakingExam | LevelExam;
|
||||
export type Variant = "diagnostic" | "partial";
|
||||
export type Variant = "full" | "diagnostic" | "partial";
|
||||
|
||||
export interface ReadingExam {
|
||||
parts: ReadingPart[];
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import {Module} from "@/interfaces";
|
||||
|
||||
export interface ModuleScore {
|
||||
score: number;
|
||||
total: number;
|
||||
code: Module;
|
||||
module: Module | 'Overall';
|
||||
png?: string,
|
||||
evaluation?: string,
|
||||
suggestions?: string,
|
||||
}
|
||||
score: number;
|
||||
total: number;
|
||||
code: Module;
|
||||
module: Module | "Overall";
|
||||
png?: string;
|
||||
evaluation?: string;
|
||||
suggestions?: string;
|
||||
}
|
||||
|
||||
export interface StudentData {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
gender: string;
|
||||
date: string;
|
||||
result: string;
|
||||
level?: string;
|
||||
bandScore: number;
|
||||
}
|
||||
export interface StudentData {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
gender: string;
|
||||
date: string;
|
||||
result: string;
|
||||
level?: string;
|
||||
bandScore: number;
|
||||
}
|
||||
|
||||
@@ -125,7 +125,9 @@ const CreatePanel = ({user, users, group, onClose}: CreateDialogProps) => {
|
||||
onChange={(value) => setParticipants(value.map((x) => x.value))}
|
||||
isMulti
|
||||
isSearchable
|
||||
menuPortalTarget={document?.body}
|
||||
styles={{
|
||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||
control: (styles) => ({
|
||||
...styles,
|
||||
backgroundColor: "white",
|
||||
|
||||
@@ -71,7 +71,9 @@ function PackageCreator({pack, onClose}: {pack?: Package; onClose: () => void})
|
||||
defaultValue={{value: currency || "EUR", label: CURRENCIES.find((c) => c.currency === currency)?.label || "Euro"}}
|
||||
onChange={(value) => setCurrency(value?.value || "EUR")}
|
||||
value={{value: currency || "EUR", label: CURRENCIES.find((c) => c.currency === currency)?.label || "Euro"}}
|
||||
menuPortalTarget={document?.body}
|
||||
styles={{
|
||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||
control: (styles) => ({
|
||||
...styles,
|
||||
paddingLeft: "4px",
|
||||
@@ -105,7 +107,9 @@ function PackageCreator({pack, onClose}: {pack?: Package; onClose: () => void})
|
||||
defaultValue={{value: "months", label: "Months"}}
|
||||
onChange={(value) => setUnit((value?.value as DurationUnit) || "months")}
|
||||
value={{value: unit, label: capitalize(unit)}}
|
||||
menuPortalTarget={document?.body}
|
||||
styles={{
|
||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||
control: (styles) => ({
|
||||
...styles,
|
||||
paddingLeft: "4px",
|
||||
|
||||
@@ -5,7 +5,7 @@ import {Module} from "@/interfaces";
|
||||
|
||||
import Selection from "@/exams/Selection";
|
||||
import Reading from "@/exams/Reading";
|
||||
import {Exam, InteractiveSpeakingExercise, SpeakingExercise, UserSolution, WritingExercise} from "@/interfaces/exam";
|
||||
import {Exam, InteractiveSpeakingExercise, SpeakingExercise, UserSolution, Variant, WritingExercise} from "@/interfaces/exam";
|
||||
import Listening from "@/exams/Listening";
|
||||
import Writing from "@/exams/Writing";
|
||||
import {ToastContainer, toast} from "react-toastify";
|
||||
@@ -38,6 +38,7 @@ export default function ExamPage({page}: Props) {
|
||||
const [avoidRepeated, setAvoidRepeated] = useState(false);
|
||||
const [timeSpent, setTimeSpent] = useState(0);
|
||||
const [statsAwaitingEvaluation, setStatsAwaitingEvaluation] = useState<string[]>([]);
|
||||
const [variant, setVariant] = useState<Variant>("full");
|
||||
|
||||
const [exams, setExams] = useExamStore((state) => [state.exams, state.setExams]);
|
||||
const [userSolutions, setUserSolutions] = useExamStore((state) => [state.userSolutions, state.setUserSolutions]);
|
||||
@@ -84,7 +85,7 @@ export default function ExamPage({page}: Props) {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (selectedModules.length > 0 && exams.length === 0) {
|
||||
const examPromises = selectedModules.map((module) => getExam(module, avoidRepeated));
|
||||
const examPromises = selectedModules.map((module) => getExam(module, avoidRepeated, variant));
|
||||
Promise.all(examPromises).then((values) => {
|
||||
if (values.every((x) => !!x)) {
|
||||
setExams(values.map((x) => x!));
|
||||
@@ -253,10 +254,11 @@ export default function ExamPage({page}: Props) {
|
||||
page={page}
|
||||
user={user!}
|
||||
disableSelection={page === "exams"}
|
||||
onStart={(modules, avoid) => {
|
||||
onStart={(modules: Module[], avoid: boolean, variant: Variant) => {
|
||||
setModuleIndex(0);
|
||||
setAvoidRepeated(avoid);
|
||||
setSelectedModules(modules);
|
||||
setVariant(variant);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -19,8 +19,8 @@ const TaskTab = ({exam, setExam}: {exam?: LevelExam; setExam: (exam: LevelExam)
|
||||
axios
|
||||
.get(`/api/exam/level/generate/level`)
|
||||
.then((result) => {
|
||||
playSound("check");
|
||||
console.log(result.data);
|
||||
playSound(typeof result.data === "string" ? "error" : "check");
|
||||
if (typeof result.data === "string") return toast.error("Something went wrong, please try to generate again.");
|
||||
setExam(result.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
|
||||
@@ -8,8 +8,8 @@ import {Tab} from "@headlessui/react";
|
||||
import axios from "axios";
|
||||
import clsx from "clsx";
|
||||
import {useRouter} from "next/router";
|
||||
import {useState} from "react";
|
||||
import {BsArrowRepeat} from "react-icons/bs";
|
||||
import {useEffect, useState} from "react";
|
||||
import {BsArrowRepeat, BsCheck} from "react-icons/bs";
|
||||
import {toast} from "react-toastify";
|
||||
|
||||
const PartTab = ({part, types, index, setPart}: {part?: ListeningPart; types: string[]; index: number; setPart: (part?: ListeningPart) => void}) => {
|
||||
@@ -26,7 +26,8 @@ const PartTab = ({part, types, index, setPart}: {part?: ListeningPart; types: st
|
||||
axios
|
||||
.get(`/api/exam/listening/generate/listening_section_${index}${topic || types ? `?${url.toString()}` : ""}`)
|
||||
.then((result) => {
|
||||
playSound("check");
|
||||
playSound(typeof result.data === "string" ? "error" : "check");
|
||||
if (typeof result.data === "string") return toast.error("Something went wrong, please try to generate again.");
|
||||
setPart(result.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -42,7 +43,7 @@ const PartTab = ({part, types, index, setPart}: {part?: ListeningPart; types: st
|
||||
<Input type="text" placeholder="Grand Canyon..." name="topic" label="Topic" onChange={setTopic} roundness="xl" defaultValue={topic} />
|
||||
<button
|
||||
onClick={generate}
|
||||
disabled={isLoading}
|
||||
disabled={isLoading || types.length === 0}
|
||||
data-tip="The passage is currently being generated"
|
||||
className={clsx(
|
||||
"bg-ielts-listening/70 border border-ielts-listening text-white w-full max-w-[200px] rounded-xl h-[70px]",
|
||||
@@ -110,10 +111,21 @@ const ListeningGeneration = () => {
|
||||
const [part2, setPart2] = useState<ListeningPart>();
|
||||
const [part3, setPart3] = useState<ListeningPart>();
|
||||
const [part4, setPart4] = useState<ListeningPart>();
|
||||
const [minTimer, setMinTimer] = useState(30);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [resultingExam, setResultingExam] = useState<ListeningExam>();
|
||||
const [types, setTypes] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const part1Timer = part1 ? 5 : 0;
|
||||
const part2Timer = part2 ? 8 : 0;
|
||||
const part3Timer = part3 ? 8 : 0;
|
||||
const part4Timer = part4 ? 9 : 0;
|
||||
|
||||
const sum = part1Timer + part2Timer + part3Timer + part4Timer;
|
||||
setMinTimer(sum > 0 ? sum : 5);
|
||||
}, [part1, part2, part3, part4]);
|
||||
|
||||
const availableTypes = [
|
||||
{type: "multipleChoice", label: "Multiple Choice"},
|
||||
{type: "writeBlanksQuestions", label: "Write the Blanks: Questions"},
|
||||
@@ -129,12 +141,14 @@ const ListeningGeneration = () => {
|
||||
const toggleType = (type: string) => setTypes((prev) => (prev.includes(type) ? [...prev.filter((x) => x !== type)] : [...prev, type]));
|
||||
|
||||
const submitExam = () => {
|
||||
if (!part1 || !part2 || !part3 || !part4) return toast.error("Please generate all for sections!");
|
||||
const parts = [part1, part2, part3, part4].filter((x) => !!x);
|
||||
console.log({parts});
|
||||
if (parts.length === 0) return toast.error("Please generate at least one section!");
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
axios
|
||||
.post(`/api/exam/listening/generate/listening`, {parts: [part1, part2, part3, part4]})
|
||||
.post(`/api/exam/listening/generate/listening`, {parts, minTimer})
|
||||
.then((result) => {
|
||||
playSound("sent");
|
||||
console.log(`Generated Exam ID: ${result.data.id}`);
|
||||
@@ -172,6 +186,17 @@ const ListeningGeneration = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-3">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Timer</label>
|
||||
<Input
|
||||
type="number"
|
||||
name="minTimer"
|
||||
onChange={(e) => setMinTimer(parseInt(e) < 15 ? 15 : parseInt(e))}
|
||||
value={minTimer}
|
||||
className="max-w-[300px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Exercises</label>
|
||||
<div className="flex flex-row -2xl:flex-wrap w-full gap-4 -md:justify-center justify-between">
|
||||
@@ -197,46 +222,46 @@ const ListeningGeneration = () => {
|
||||
<Tab
|
||||
className={({selected}) =>
|
||||
clsx(
|
||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-listening/70",
|
||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-listening/70 flex gap-2 items-center justify-center",
|
||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-listening 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-listening",
|
||||
)
|
||||
}>
|
||||
Section 1
|
||||
Section 1 {part1 && <BsCheck />}
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({selected}) =>
|
||||
clsx(
|
||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-listening/70",
|
||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-listening/70 flex gap-2 items-center justify-center",
|
||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-listening 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-listening",
|
||||
)
|
||||
}>
|
||||
Section 2
|
||||
Section 2 {part2 && <BsCheck />}
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({selected}) =>
|
||||
clsx(
|
||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-listening/70",
|
||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-listening/70 flex gap-2 items-center justify-center",
|
||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-listening 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-listening",
|
||||
)
|
||||
}>
|
||||
Section 3
|
||||
Section 3 {part3 && <BsCheck />}
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({selected}) =>
|
||||
clsx(
|
||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-listening/70",
|
||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-listening/70 flex gap-2 items-center justify-center",
|
||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-listening 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-listening",
|
||||
)
|
||||
}>
|
||||
Section 4
|
||||
Section 4 {part4 && <BsCheck />}
|
||||
</Tab>
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
@@ -264,14 +289,14 @@ const ListeningGeneration = () => {
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
disabled={!part1 || !part2 || !part3 || !part4 || isLoading}
|
||||
disabled={(!part1 && !part2 && !part3 && !part4) || isLoading}
|
||||
data-tip="Please generate all three passages"
|
||||
onClick={submitExam}
|
||||
className={clsx(
|
||||
"bg-ielts-listening/70 border border-ielts-listening text-white w-full max-w-[200px] rounded-xl h-[70px] self-end",
|
||||
"hover:bg-ielts-listening disabled:bg-ielts-listening/40 disabled:cursor-not-allowed",
|
||||
"transition ease-in-out duration-300",
|
||||
(!part1 || !part2 || !part3 || !part4) && "tooltip",
|
||||
!part1 && !part2 && !part3 && !part4 && "tooltip",
|
||||
)}>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center">
|
||||
|
||||
@@ -8,8 +8,8 @@ import {Tab} from "@headlessui/react";
|
||||
import axios from "axios";
|
||||
import clsx from "clsx";
|
||||
import {useRouter} from "next/router";
|
||||
import {useState} from "react";
|
||||
import {BsArrowRepeat} from "react-icons/bs";
|
||||
import {useEffect, useState} from "react";
|
||||
import {BsArrowRepeat, BsCheck} from "react-icons/bs";
|
||||
import {toast} from "react-toastify";
|
||||
import {v4} from "uuid";
|
||||
|
||||
@@ -27,7 +27,8 @@ const PartTab = ({part, types, index, setPart}: {part?: ReadingPart; types: stri
|
||||
axios
|
||||
.get(`/api/exam/reading/generate/reading_passage_${index}${topic || types ? `?${url.toString()}` : ""}`)
|
||||
.then((result) => {
|
||||
playSound("check");
|
||||
playSound(typeof result.data === "string" ? "error" : "check");
|
||||
if (typeof result.data === "string") return toast.error("Something went wrong, please try to generate again.");
|
||||
setPart(result.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -43,7 +44,7 @@ const PartTab = ({part, types, index, setPart}: {part?: ReadingPart; types: stri
|
||||
<Input type="text" placeholder="Grand Canyon..." name="topic" label="Topic" onChange={setTopic} roundness="xl" defaultValue={topic} />
|
||||
<button
|
||||
onClick={generate}
|
||||
disabled={isLoading}
|
||||
disabled={isLoading || types.length === 0}
|
||||
data-tip="The passage is currently being generated"
|
||||
className={clsx(
|
||||
"bg-ielts-reading/70 border border-ielts-reading text-white w-full max-w-[200px] rounded-xl h-[70px]",
|
||||
@@ -87,10 +88,16 @@ const ReadingGeneration = () => {
|
||||
const [part1, setPart1] = useState<ReadingPart>();
|
||||
const [part2, setPart2] = useState<ReadingPart>();
|
||||
const [part3, setPart3] = useState<ReadingPart>();
|
||||
const [minTimer, setMinTimer] = useState(60);
|
||||
const [types, setTypes] = useState<string[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [resultingExam, setResultingExam] = useState<ReadingExam>();
|
||||
|
||||
useEffect(() => {
|
||||
const parts = [part1, part2, part3].filter((x) => !!x);
|
||||
setMinTimer(parts.length === 0 ? 60 : parts.length * 20);
|
||||
}, [part1, part2, part3]);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const setExams = useExamStore((state) => state.setExams);
|
||||
@@ -122,19 +129,21 @@ const ReadingGeneration = () => {
|
||||
};
|
||||
|
||||
const submitExam = () => {
|
||||
if (!part1 || !part2 || !part3) {
|
||||
toast.error("Please generate all three passages before submitting");
|
||||
const parts = [part1, part2, part3].filter((x) => !!x) as ReadingPart[];
|
||||
if (parts.length === 0) {
|
||||
toast.error("Please generate at least one passage before submitting");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const exam: ReadingExam = {
|
||||
parts: [part1, part2, part3],
|
||||
parts,
|
||||
isDiagnostic: false,
|
||||
minTimer: 60,
|
||||
minTimer,
|
||||
module: "reading",
|
||||
id: v4(),
|
||||
type: "academic",
|
||||
variant: parts.length === 3 ? "full" : "partial",
|
||||
};
|
||||
|
||||
axios
|
||||
@@ -148,6 +157,7 @@ const ReadingGeneration = () => {
|
||||
setPart1(undefined);
|
||||
setPart2(undefined);
|
||||
setPart3(undefined);
|
||||
setMinTimer(60);
|
||||
setTypes([]);
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -159,6 +169,17 @@ const ReadingGeneration = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-3">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Timer</label>
|
||||
<Input
|
||||
type="number"
|
||||
name="minTimer"
|
||||
onChange={(e) => setMinTimer(parseInt(e) < 15 ? 15 : parseInt(e))}
|
||||
value={minTimer}
|
||||
className="max-w-[300px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Exercises</label>
|
||||
<div className="flex flex-row -2xl:flex-wrap w-full gap-4 -md:justify-center justify-between">
|
||||
@@ -182,35 +203,35 @@ const ReadingGeneration = () => {
|
||||
<Tab
|
||||
className={({selected}) =>
|
||||
clsx(
|
||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-reading/70",
|
||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-reading/70 flex gap-2 items-center justify-center",
|
||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-reading 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-reading",
|
||||
)
|
||||
}>
|
||||
Passage 1
|
||||
Passage 1 {part1 && <BsCheck />}
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({selected}) =>
|
||||
clsx(
|
||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-reading/70",
|
||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-reading/70 flex gap-2 items-center justify-center",
|
||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-reading 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-reading",
|
||||
)
|
||||
}>
|
||||
Passage 2
|
||||
Passage 2 {part2 && <BsCheck />}
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({selected}) =>
|
||||
clsx(
|
||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-reading/70",
|
||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-reading/70 flex gap-2 items-center justify-center",
|
||||
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-reading 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-reading",
|
||||
)
|
||||
}>
|
||||
Passage 3
|
||||
Passage 3 {part3 && <BsCheck />}
|
||||
</Tab>
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
@@ -237,14 +258,14 @@ const ReadingGeneration = () => {
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
disabled={!part1 || !part2 || !part3 || isLoading}
|
||||
disabled={(!part1 && !part2 && !part3) || isLoading}
|
||||
data-tip="Please generate all three passages"
|
||||
onClick={submitExam}
|
||||
className={clsx(
|
||||
"bg-ielts-reading/70 border border-ielts-reading text-white w-full max-w-[200px] rounded-xl h-[70px] self-end",
|
||||
"hover:bg-ielts-reading disabled:bg-ielts-reading/40 disabled:cursor-not-allowed",
|
||||
"transition ease-in-out duration-300",
|
||||
(!part1 || !part2 || !part3) && "tooltip",
|
||||
!part1 && !part2 && !part3 && "tooltip",
|
||||
)}>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Input from "@/components/Low/Input";
|
||||
import {Exercise, SpeakingExam} from "@/interfaces/exam";
|
||||
import {Exercise, InteractiveSpeakingExercise, SpeakingExam, SpeakingExercise} from "@/interfaces/exam";
|
||||
import useExamStore from "@/stores/examStore";
|
||||
import {getExamById} from "@/utils/exams";
|
||||
import {playSound} from "@/utils/sound";
|
||||
@@ -7,10 +7,12 @@ import {convertCamelCaseToReadable} from "@/utils/string";
|
||||
import {Tab} from "@headlessui/react";
|
||||
import axios from "axios";
|
||||
import clsx from "clsx";
|
||||
import moment from "moment";
|
||||
import {useRouter} from "next/router";
|
||||
import {useState} from "react";
|
||||
import {BsArrowRepeat} from "react-icons/bs";
|
||||
import {useEffect, useState} from "react";
|
||||
import {BsArrowRepeat, BsCheck} from "react-icons/bs";
|
||||
import {toast} from "react-toastify";
|
||||
import {v4} from "uuid";
|
||||
|
||||
const PartTab = ({part, index, setPart}: {part?: SpeakingPart; index: number; setPart: (part?: SpeakingPart) => void}) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -18,10 +20,12 @@ const PartTab = ({part, index, setPart}: {part?: SpeakingPart; index: number; se
|
||||
const generate = () => {
|
||||
setPart(undefined);
|
||||
setIsLoading(true);
|
||||
|
||||
axios
|
||||
.get(`/api/exam/speaking/generate/speaking_task_${index}`)
|
||||
.then((result) => {
|
||||
playSound("check");
|
||||
playSound(typeof result.data === "string" ? "error" : "check");
|
||||
if (typeof result.data === "string") return toast.error("Something went wrong, please try to generate again.");
|
||||
setPart(result.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -31,6 +35,29 @@ const PartTab = ({part, index, setPart}: {part?: SpeakingPart; index: number; se
|
||||
.finally(() => setIsLoading(false));
|
||||
};
|
||||
|
||||
const generateVideo = () => {
|
||||
if (!part) return toast.error("Please generate the first part before generating the video!");
|
||||
toast.info("This will take quite a while, please do not leave this page or close the tab/window.");
|
||||
|
||||
setIsLoading(true);
|
||||
const initialTime = moment();
|
||||
|
||||
axios
|
||||
.post(`/api/exam/speaking/generate/speaking/generate_${index === 3 ? "interactive" : "speaking"}_video`, part)
|
||||
.then((result) => {
|
||||
const isError = typeof result.data === "string" || moment().diff(initialTime, "seconds") < 60;
|
||||
|
||||
playSound(isError ? "error" : "check");
|
||||
if (isError) return toast.error("Something went wrong, please try to generate the video again.");
|
||||
setPart({...part, result: result.data});
|
||||
})
|
||||
.catch((e) => {
|
||||
toast.error("Something went wrong!");
|
||||
console.log(e);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<Tab.Panel className="w-full bg-ielts-speaking/20 min-h-[600px] h-full rounded-xl p-6 flex flex-col gap-4">
|
||||
<div className="flex gap-4 items-end">
|
||||
@@ -52,6 +79,24 @@ const PartTab = ({part, index, setPart}: {part?: SpeakingPart; index: number; se
|
||||
"Generate"
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={generateVideo}
|
||||
disabled={isLoading || !part}
|
||||
data-tip="The passage is currently being generated"
|
||||
className={clsx(
|
||||
"bg-ielts-speaking/70 border border-ielts-speaking text-white w-full rounded-xl h-[70px]",
|
||||
"hover:bg-ielts-speaking disabled:bg-ielts-speaking/40 disabled:cursor-not-allowed",
|
||||
"transition ease-in-out duration-300",
|
||||
isLoading && "tooltip",
|
||||
)}>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<BsArrowRepeat className="text-white animate-spin" size={25} />
|
||||
</div>
|
||||
) : (
|
||||
"Generate Video"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{isLoading && (
|
||||
<div className="w-fit h-fit mt-12 self-center animate-pulse flex flex-col gap-8 items-center">
|
||||
@@ -59,7 +104,7 @@ const PartTab = ({part, index, setPart}: {part?: SpeakingPart; index: number; se
|
||||
<span className={clsx("font-bold text-2xl text-ielts-speaking")}>Generating...</span>
|
||||
</div>
|
||||
)}
|
||||
{part && (
|
||||
{part && !isLoading && (
|
||||
<div className="flex flex-col gap-2 w-full overflow-y-scroll scrollbar-hide h-96">
|
||||
<h3 className="text-xl font-semibold">{part.topic}</h3>
|
||||
{part.question && <span className="w-full">{part.question}</span>}
|
||||
@@ -82,6 +127,7 @@ const PartTab = ({part, index, setPart}: {part?: SpeakingPart; index: number; se
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{part.result && <span className="font-bold mt-4">Video Generated: ✅</span>}
|
||||
</div>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
@@ -93,27 +139,43 @@ interface SpeakingPart {
|
||||
question?: string;
|
||||
questions?: string[];
|
||||
topic: string;
|
||||
result?: SpeakingExercise | InteractiveSpeakingExercise;
|
||||
}
|
||||
|
||||
const SpeakingGeneration = () => {
|
||||
const [part1, setPart1] = useState<SpeakingPart>();
|
||||
const [part2, setPart2] = useState<SpeakingPart>();
|
||||
const [part3, setPart3] = useState<SpeakingPart>();
|
||||
const [minTimer, setMinTimer] = useState(14);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [resultingExam, setResultingExam] = useState<SpeakingExam>();
|
||||
|
||||
useEffect(() => {
|
||||
const parts = [part1, part2, part3].filter((x) => !!x);
|
||||
setMinTimer(parts.length === 0 ? 5 : parts.length * 5);
|
||||
}, [part1, part2, part3]);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const setExams = useExamStore((state) => state.setExams);
|
||||
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
|
||||
|
||||
const submitExam = () => {
|
||||
if (!part1 || !part2 || !part3) return toast.error("Please generate all for tasks!");
|
||||
if (!part1?.result && !part2?.result && !part3?.result) return toast.error("Please generate at least one task!");
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const exam: SpeakingExam = {
|
||||
id: v4(),
|
||||
isDiagnostic: false,
|
||||
exercises: [part1?.result, part2?.result, part3?.result].filter((x) => !!x) as (SpeakingExercise | InteractiveSpeakingExercise)[],
|
||||
minTimer,
|
||||
variant: minTimer >= 14 ? "full" : "partial",
|
||||
module: "speaking",
|
||||
};
|
||||
|
||||
axios
|
||||
.post(`/api/exam/speaking/generate/speaking`, {exercises: [part1, part2, part3]})
|
||||
.post(`/api/exam/speaking`, exam)
|
||||
.then((result) => {
|
||||
playSound("sent");
|
||||
console.log(`Generated Exam ID: ${result.data.id}`);
|
||||
@@ -123,10 +185,11 @@ const SpeakingGeneration = () => {
|
||||
setPart1(undefined);
|
||||
setPart2(undefined);
|
||||
setPart3(undefined);
|
||||
setMinTimer(14);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
toast.error("Something went wrong!");
|
||||
toast.error("Something went wrong while generating, please try again later.");
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
};
|
||||
@@ -149,40 +212,51 @@ const SpeakingGeneration = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-3">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Timer</label>
|
||||
<Input
|
||||
type="number"
|
||||
name="minTimer"
|
||||
onChange={(e) => setMinTimer(parseInt(e) < 5 ? 5 : parseInt(e))}
|
||||
value={minTimer}
|
||||
className="max-w-[300px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Tab.Group>
|
||||
<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/70",
|
||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/70 flex gap-2 items-center justify-center",
|
||||
"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",
|
||||
)
|
||||
}>
|
||||
Task 1
|
||||
Exercise 1 {part1 && part1.result && <BsCheck />}
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({selected}) =>
|
||||
clsx(
|
||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/70",
|
||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/70 flex gap-2 items-center justify-center",
|
||||
"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",
|
||||
)
|
||||
}>
|
||||
Task 2
|
||||
Exercise 2 {part2 && part2.result && <BsCheck />}
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({selected}) =>
|
||||
clsx(
|
||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/70",
|
||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/70 flex gap-2 items-center justify-center",
|
||||
"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",
|
||||
)
|
||||
}>
|
||||
Task 3
|
||||
Interactive {part3 && part3.result && <BsCheck />}
|
||||
</Tab>
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
@@ -209,14 +283,14 @@ const SpeakingGeneration = () => {
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
disabled={!part1 || !part2 || !part3 || isLoading}
|
||||
disabled={(!part1?.result && !part2?.result && !part3?.result) || isLoading}
|
||||
data-tip="Please generate all three passages"
|
||||
onClick={submitExam}
|
||||
className={clsx(
|
||||
"bg-ielts-speaking/70 border border-ielts-speaking text-white w-full max-w-[200px] rounded-xl h-[70px] self-end",
|
||||
"hover:bg-ielts-speaking disabled:bg-ielts-speaking/40 disabled:cursor-not-allowed",
|
||||
"transition ease-in-out duration-300",
|
||||
(!part1 || !part2 || !part3) && "tooltip",
|
||||
!part1 && !part2 && !part3 && "tooltip",
|
||||
)}>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Input from "@/components/Low/Input";
|
||||
import {WritingExam} from "@/interfaces/exam";
|
||||
import {WritingExam, WritingExercise} from "@/interfaces/exam";
|
||||
import useExamStore from "@/stores/examStore";
|
||||
import {getExamById} from "@/utils/exams";
|
||||
import {playSound} from "@/utils/sound";
|
||||
@@ -7,8 +7,8 @@ import {Tab} from "@headlessui/react";
|
||||
import axios from "axios";
|
||||
import clsx from "clsx";
|
||||
import {useRouter} from "next/router";
|
||||
import {useState} from "react";
|
||||
import {BsArrowRepeat} from "react-icons/bs";
|
||||
import {useEffect, useState} from "react";
|
||||
import {BsArrowRepeat, BsCheck} from "react-icons/bs";
|
||||
import {toast} from "react-toastify";
|
||||
import {v4} from "uuid";
|
||||
|
||||
@@ -20,7 +20,8 @@ const TaskTab = ({task, index, setTask}: {task?: string; index: number; setTask:
|
||||
axios
|
||||
.get(`/api/exam/writing/generate/writing_task${index}_general`)
|
||||
.then((result) => {
|
||||
playSound("check");
|
||||
playSound(typeof result.data === "string" ? "error" : "check");
|
||||
if (typeof result.data === "string") return toast.error("Something went wrong, please try to generate again.");
|
||||
setTask(result.data.question);
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -68,9 +69,16 @@ const TaskTab = ({task, index, setTask}: {task?: string; index: number; setTask:
|
||||
const WritingGeneration = () => {
|
||||
const [task1, setTask1] = useState<string>();
|
||||
const [task2, setTask2] = useState<string>();
|
||||
const [minTimer, setMinTimer] = useState(60);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [resultingExam, setResultingExam] = useState<WritingExam>();
|
||||
|
||||
useEffect(() => {
|
||||
const task1Timer = task1 ? 20 : 0;
|
||||
const task2Timer = task2 ? 40 : 0;
|
||||
setMinTimer(task1Timer > 0 || task2Timer > 0 ? task1Timer + task2Timer : 20);
|
||||
}, [task1, task2]);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const setExams = useExamStore((state) => state.setExams);
|
||||
@@ -93,18 +101,13 @@ const WritingGeneration = () => {
|
||||
};
|
||||
|
||||
const submitExam = () => {
|
||||
if (!task1 || !task2) {
|
||||
toast.error("Please generate all tasks before submitting");
|
||||
if (!task1 && !task2) {
|
||||
toast.error("Please generate a task before submitting");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const exam: WritingExam = {
|
||||
isDiagnostic: false,
|
||||
minTimer: 60,
|
||||
module: "writing",
|
||||
exercises: [
|
||||
{
|
||||
const exercise1 = task1
|
||||
? ({
|
||||
id: v4(),
|
||||
type: "writing",
|
||||
prefix: `You should spend about 20 minutes on this task.`,
|
||||
@@ -115,8 +118,11 @@ const WritingGeneration = () => {
|
||||
limit: 150,
|
||||
type: "min",
|
||||
},
|
||||
},
|
||||
{
|
||||
} as WritingExercise)
|
||||
: undefined;
|
||||
|
||||
const exercise2 = task2
|
||||
? ({
|
||||
id: v4(),
|
||||
type: "writing",
|
||||
prefix: `You should spend about 40 minutes on this task.`,
|
||||
@@ -127,9 +133,17 @@ const WritingGeneration = () => {
|
||||
limit: 250,
|
||||
type: "min",
|
||||
},
|
||||
},
|
||||
],
|
||||
} as WritingExercise)
|
||||
: undefined;
|
||||
|
||||
setIsLoading(true);
|
||||
const exam: WritingExam = {
|
||||
isDiagnostic: false,
|
||||
minTimer,
|
||||
module: "writing",
|
||||
exercises: [...(exercise1 ? [exercise1] : []), ...(exercise2 ? [exercise2] : [])],
|
||||
id: v4(),
|
||||
variant: exercise1 && exercise2 ? "full" : "partial",
|
||||
};
|
||||
|
||||
axios
|
||||
@@ -152,29 +166,40 @@ const WritingGeneration = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-3">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Timer</label>
|
||||
<Input
|
||||
type="number"
|
||||
name="minTimer"
|
||||
onChange={(e) => setMinTimer(parseInt(e) < 15 ? 15 : parseInt(e))}
|
||||
value={minTimer}
|
||||
className="max-w-[300px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Tab.Group>
|
||||
<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/70",
|
||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-writing/70 flex gap-2 items-center justify-center",
|
||||
"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",
|
||||
)
|
||||
}>
|
||||
Task 1
|
||||
Task 1 {task1 && <BsCheck />}
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({selected}) =>
|
||||
clsx(
|
||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-writing/70",
|
||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-writing/70 flex gap-2 items-center justify-center",
|
||||
"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",
|
||||
)
|
||||
}>
|
||||
Task 2
|
||||
Task 2 {task2 && <BsCheck />}
|
||||
</Tab>
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
@@ -200,14 +225,14 @@ const WritingGeneration = () => {
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
disabled={!task1 || !task2 || isLoading}
|
||||
disabled={(!task1 && !task2) || isLoading}
|
||||
data-tip="Please generate all three passages"
|
||||
onClick={submitExam}
|
||||
className={clsx(
|
||||
"bg-ielts-writing/70 border border-ielts-writing text-white w-full max-w-[200px] rounded-xl h-[70px] self-end",
|
||||
"hover:bg-ielts-writing disabled:bg-ielts-writing/40 disabled:cursor-not-allowed",
|
||||
"transition ease-in-out duration-300",
|
||||
(!task1 || !task2) && "tooltip",
|
||||
!task1 && !task2 && "tooltip",
|
||||
)}>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center">
|
||||
|
||||
@@ -39,8 +39,8 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!req.session.user) return res.status(401).json({ok: false});
|
||||
if (req.session.user.type !== "developer") return res.status(403).json({ok: false});
|
||||
|
||||
const {endpoint, topic, exercises} = req.query as {module: Module; endpoint: string; topic?: string; exercises?: string[]};
|
||||
const url = `${process.env.BACKEND_URL}/${endpoint}`;
|
||||
const {endpoint, topic, exercises} = req.query as {module: Module; endpoint: string[]; topic?: string; exercises?: string[]};
|
||||
const url = `${process.env.BACKEND_URL}/${endpoint.join("/")}`;
|
||||
|
||||
const result = await axios.post(
|
||||
`${url}${topic && exercises ? `?topic=${topic.toLowerCase()}&exercises=${exercises.join("&exercises=")}` : ""}`,
|
||||
@@ -4,8 +4,8 @@ import {app} from "@/firebase";
|
||||
import {getFirestore, setDoc, doc} from "firebase/firestore";
|
||||
import {withIronSessionApiRoute} from "iron-session/next";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import {Exam} from "@/interfaces/exam";
|
||||
import { getExams } from "@/utils/exams.be";
|
||||
import {Exam, Variant} from "@/interfaces/exam";
|
||||
import {getExams} from "@/utils/exams.be";
|
||||
const db = getFirestore(app);
|
||||
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
@@ -23,12 +23,9 @@ async function GET(req: NextApiRequest, res: NextApiResponse) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
module,
|
||||
avoidRepeated,
|
||||
} = req.query as {module: string; avoidRepeated: string};
|
||||
const {module, avoidRepeated, variant} = req.query as {module: string; avoidRepeated: string; variant?: Variant};
|
||||
|
||||
const exams: Exam[] = await getExams(db, module, avoidRepeated, req.session.user.id);
|
||||
const exams: Exam[] = await getExams(db, module, avoidRepeated, req.session.user.id, variant);
|
||||
res.status(200).json(exams);
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,10 @@ async function patch(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (user.type === "admin" || user.type === "developer") {
|
||||
await setDoc(snapshot.ref, req.body, {merge: true});
|
||||
|
||||
if (req.body.isPaid) {
|
||||
const corporateID = req.body.corporate;
|
||||
await setDoc(doc(db, "users", corporateID), {status: "active"}, {merge: true});
|
||||
}
|
||||
return res.status(200).json({ok: true});
|
||||
}
|
||||
|
||||
|
||||
@@ -119,6 +119,8 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||
const updatedDoc = (await getDoc(doc(db, "payments", paymentId))).data() as Payment;
|
||||
if (updatedDoc.commissionTransfer && updatedDoc.corporateTransfer) {
|
||||
await setDoc(doc(db, "payments", paymentId), {isPaid: true}, {merge: true});
|
||||
|
||||
await setDoc(doc(db, "users", updatedDoc.corporate), {status: "active"}, {merge: true});
|
||||
}
|
||||
res.status(200).json({ref});
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,33 +1,20 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { app, storage } from "@/firebase";
|
||||
import {
|
||||
getFirestore,
|
||||
doc,
|
||||
getDoc,
|
||||
updateDoc,
|
||||
getDocs,
|
||||
query,
|
||||
collection,
|
||||
where,
|
||||
} from "firebase/firestore";
|
||||
import { withIronSessionApiRoute } from "iron-session/next";
|
||||
import { sessionOptions } from "@/lib/session";
|
||||
import type {NextApiRequest, NextApiResponse} from "next";
|
||||
import {app, storage} from "@/firebase";
|
||||
import {getFirestore, doc, getDoc, updateDoc, getDocs, query, collection, where} from "firebase/firestore";
|
||||
import {withIronSessionApiRoute} from "iron-session/next";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import ReactPDF from "@react-pdf/renderer";
|
||||
import TestReport from "@/exams/pdf/test.report";
|
||||
import { ref, uploadBytes, getDownloadURL } from "firebase/storage";
|
||||
import { DemographicInformation, User } from "@/interfaces/user";
|
||||
import { Module } from "@/interfaces";
|
||||
import { ModuleScore } from "@/interfaces/module.scores";
|
||||
import { SkillExamDetails } from "@/exams/pdf/details/skill.exam";
|
||||
import { LevelExamDetails } from "@/exams/pdf/details/level.exam";
|
||||
import { calculateBandScore } from "@/utils/score";
|
||||
import {ref, uploadBytes, getDownloadURL} from "firebase/storage";
|
||||
import {DemographicInformation, User} from "@/interfaces/user";
|
||||
import {Module} from "@/interfaces";
|
||||
import {ModuleScore} from "@/interfaces/module.scores";
|
||||
import {SkillExamDetails} from "@/exams/pdf/details/skill.exam";
|
||||
import {LevelExamDetails} from "@/exams/pdf/details/level.exam";
|
||||
import {calculateBandScore} from "@/utils/score";
|
||||
import axios from "axios";
|
||||
import { moduleLabels } from "@/utils/moduleUtils";
|
||||
import {
|
||||
generateQRCode,
|
||||
getRadialProgressPNG,
|
||||
streamToBuffer,
|
||||
} from "@/utils/pdf";
|
||||
import {moduleLabels} from "@/utils/moduleUtils";
|
||||
import {generateQRCode, getRadialProgressPNG, streamToBuffer} from "@/utils/pdf";
|
||||
import moment from "moment-timezone";
|
||||
|
||||
const db = getFirestore(app);
|
||||
@@ -35,350 +22,318 @@ const db = getFirestore(app);
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method === "GET") return get(req, res);
|
||||
if (req.method === "POST") return post(req, res);
|
||||
if (req.method === "GET") return get(req, res);
|
||||
if (req.method === "POST") return post(req, res);
|
||||
}
|
||||
|
||||
const getExamSummary = (score: number) => {
|
||||
if (score > 0.8) {
|
||||
return "Scoring between 81% and 100% on the English exam demonstrates an outstanding level of proficiency in writing, speaking, listening, and reading. Mastery of key concepts is evident across all language domains, showcasing not only a high level of skill but also a dedication to excellence. Continuing to challenge oneself with advanced material in writing, speaking, listening, and reading will further refine the already impressive command of the English language.";
|
||||
}
|
||||
if (score > 0.8) {
|
||||
return "Scoring between 81% and 100% on the English exam demonstrates an outstanding level of proficiency in writing, speaking, listening, and reading. Mastery of key concepts is evident across all language domains, showcasing not only a high level of skill but also a dedication to excellence. Continuing to challenge oneself with advanced material in writing, speaking, listening, and reading will further refine the already impressive command of the English language.";
|
||||
}
|
||||
|
||||
if (score > 0.6) {
|
||||
return "Scoring between 61% and 80% on the English exam, encompassing writing, speaking, listening, and reading, reflects a commendable level of proficiency in each domain. There's evidence of a solid grasp of key concepts, and effective application of skills. Room for refinement and deeper exploration in writing, speaking, listening, and reading remains, presenting an opportunity for further mastery.";
|
||||
}
|
||||
if (score > 0.6) {
|
||||
return "Scoring between 61% and 80% on the English exam, encompassing writing, speaking, listening, and reading, reflects a commendable level of proficiency in each domain. There's evidence of a solid grasp of key concepts, and effective application of skills. Room for refinement and deeper exploration in writing, speaking, listening, and reading remains, presenting an opportunity for further mastery.";
|
||||
}
|
||||
|
||||
if (score > 0.4) {
|
||||
return "Scoring between 41% and 60% on the English exam across writing, speaking, listening, and reading demonstrates a moderate level of understanding in each domain. While there's a commendable grasp of key concepts, refining fundamental skills in writing, speaking, listening, and reading can lead to notable improvement. Consistent effort and targeted focus on weaker areas are recommended.";
|
||||
}
|
||||
if (score > 0.4) {
|
||||
return "Scoring between 41% and 60% on the English exam across writing, speaking, listening, and reading demonstrates a moderate level of understanding in each domain. While there's a commendable grasp of key concepts, refining fundamental skills in writing, speaking, listening, and reading can lead to notable improvement. Consistent effort and targeted focus on weaker areas are recommended.";
|
||||
}
|
||||
|
||||
if (score > 0.2) {
|
||||
return "Scoring between 21% and 40% on the English exam, spanning writing, speaking, listening, and reading, indicates some understanding of key concepts in each domain. However, there's room for improvement in fundamental skills. Strengthening writing, speaking, listening, and reading abilities through consistent effort and focused study will contribute to overall proficiency.";
|
||||
}
|
||||
if (score > 0.2) {
|
||||
return "Scoring between 21% and 40% on the English exam, spanning writing, speaking, listening, and reading, indicates some understanding of key concepts in each domain. However, there's room for improvement in fundamental skills. Strengthening writing, speaking, listening, and reading abilities through consistent effort and focused study will contribute to overall proficiency.";
|
||||
}
|
||||
|
||||
return "This student's performance on the English exam, encompassing writing, speaking, listening, and reading, reflects a significant need for improvement, scoring between 0% and 20%. There's a notable gap in understanding key concepts across all language domains. Strengthening fundamental skills in writing, speaking, listening, and reading is crucial. Developing a consistent study routine and seeking additional support in each area can contribute to substantial progress.";
|
||||
return "This student's performance on the English exam, encompassing writing, speaking, listening, and reading, reflects a significant need for improvement, scoring between 0% and 20%. There's a notable gap in understanding key concepts across all language domains. Strengthening fundamental skills in writing, speaking, listening, and reading is crucial. Developing a consistent study routine and seeking additional support in each area can contribute to substantial progress.";
|
||||
};
|
||||
|
||||
const getLevelSummary = (score: number) => {
|
||||
if (score > 0.8) {
|
||||
return "Scoring between 81% and 100% on the English exam showcases an outstanding level of understanding and proficiency. Your performance reflects a mastery of key concepts, including grammar, vocabulary, and comprehension. You exhibit a high level of skill in applying these elements effectively. Your dedication to excellence is evident, and your consistent, stellar performance is commendable. Continue to challenge yourself with advanced material to further refine your already impressive command of the English language. Your commitment to excellence positions you as a standout student in English studies, and your achievements are a testament to your hard work and capability.";
|
||||
}
|
||||
if (score > 0.8) {
|
||||
return "Scoring between 81% and 100% on the English exam showcases an outstanding level of understanding and proficiency. Your performance reflects a mastery of key concepts, including grammar, vocabulary, and comprehension. You exhibit a high level of skill in applying these elements effectively. Your dedication to excellence is evident, and your consistent, stellar performance is commendable. Continue to challenge yourself with advanced material to further refine your already impressive command of the English language. Your commitment to excellence positions you as a standout student in English studies, and your achievements are a testament to your hard work and capability.";
|
||||
}
|
||||
|
||||
if (score > 0.6) {
|
||||
return "Scoring between 61% and 80% on the English exam reflects a commendable level of understanding and proficiency. You have demonstrated a solid grasp of key concepts, including grammar, vocabulary, and comprehension. There's evidence of effective application of skills, but room for refinement and deeper exploration remains. Consistent effort in honing nuanced aspects of language will contribute to even greater mastery. Continue engaging with challenging material and seeking opportunities for advanced comprehension. With sustained dedication, you have the potential to elevate your performance to an exceptional level and further excel in your English studies.";
|
||||
}
|
||||
if (score > 0.6) {
|
||||
return "Scoring between 61% and 80% on the English exam reflects a commendable level of understanding and proficiency. You have demonstrated a solid grasp of key concepts, including grammar, vocabulary, and comprehension. There's evidence of effective application of skills, but room for refinement and deeper exploration remains. Consistent effort in honing nuanced aspects of language will contribute to even greater mastery. Continue engaging with challenging material and seeking opportunities for advanced comprehension. With sustained dedication, you have the potential to elevate your performance to an exceptional level and further excel in your English studies.";
|
||||
}
|
||||
|
||||
if (score > 0.4) {
|
||||
return "Scoring between 41% and 60% on the English exam reflects a moderate level of understanding. You demonstrate a grasp of some key concepts, but there's room for refinement in areas like grammar, vocabulary, and comprehension. Consistent effort and a strategic focus on weaker areas can lead to notable improvement. Engaging with supplementary resources and seeking feedback will further enhance your skills. With continued dedication, there's a solid foundation to build upon, and achieving a higher level of proficiency is within reach. Keep up the good work and aim for sustained progress in your English studies.";
|
||||
}
|
||||
if (score > 0.4) {
|
||||
return "Scoring between 41% and 60% on the English exam reflects a moderate level of understanding. You demonstrate a grasp of some key concepts, but there's room for refinement in areas like grammar, vocabulary, and comprehension. Consistent effort and a strategic focus on weaker areas can lead to notable improvement. Engaging with supplementary resources and seeking feedback will further enhance your skills. With continued dedication, there's a solid foundation to build upon, and achieving a higher level of proficiency is within reach. Keep up the good work and aim for sustained progress in your English studies.";
|
||||
}
|
||||
|
||||
if (score > 0.2) {
|
||||
return "Scoring between 21% and 40% on the English exam shows some understanding of key concepts, but there's still ample room for improvement. Strengthening foundational skills, such as grammar, vocabulary, and comprehension, is essential. Consistent effort and focused study can help bridge gaps in knowledge and elevate your performance. Consider seeking additional guidance or resources to refine your understanding of the material. With commitment and targeted improvements, you have the potential to make significant strides in your English proficiency.";
|
||||
}
|
||||
if (score > 0.2) {
|
||||
return "Scoring between 21% and 40% on the English exam shows some understanding of key concepts, but there's still ample room for improvement. Strengthening foundational skills, such as grammar, vocabulary, and comprehension, is essential. Consistent effort and focused study can help bridge gaps in knowledge and elevate your performance. Consider seeking additional guidance or resources to refine your understanding of the material. With commitment and targeted improvements, you have the potential to make significant strides in your English proficiency.";
|
||||
}
|
||||
|
||||
return "Your performance on the English exam falls within the 0% to 20% range, indicating a need for improvement. There's room to enhance your grasp of fundamental concepts like grammar, vocabulary, and comprehension. Establishing a consistent study routine and seeking extra support can be beneficial. With dedication and targeted efforts, you have the potential to significantly boost your performance in upcoming assessments.";
|
||||
return "Your performance on the English exam falls within the 0% to 20% range, indicating a need for improvement. There's room to enhance your grasp of fundamental concepts like grammar, vocabulary, and comprehension. Establishing a consistent study routine and seeking extra support can be beneficial. With dedication and targeted efforts, you have the potential to significantly boost your performance in upcoming assessments.";
|
||||
};
|
||||
|
||||
const getPerformanceSummary = (module: Module, score: number) => {
|
||||
if (module === "level") return getLevelSummary(score);
|
||||
return getExamSummary(score);
|
||||
if (module === "level") return getLevelSummary(score);
|
||||
return getExamSummary(score);
|
||||
};
|
||||
interface SkillsFeedbackRequest {
|
||||
code: Module;
|
||||
name: string;
|
||||
grade: number;
|
||||
code: Module;
|
||||
name: string;
|
||||
grade: number;
|
||||
}
|
||||
|
||||
interface SkillsFeedbackResponse extends SkillsFeedbackRequest {
|
||||
evaluation: string;
|
||||
suggestions: string;
|
||||
evaluation: string;
|
||||
suggestions: string;
|
||||
}
|
||||
|
||||
const getSkillsFeedback = async (sections: SkillsFeedbackRequest[]) => {
|
||||
const backendRequest = await axios.post(
|
||||
`${process.env.BACKEND_URL}/grading_summary`,
|
||||
{ sections },
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
const backendRequest = await axios.post(
|
||||
`${process.env.BACKEND_URL}/grading_summary`,
|
||||
{sections},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return backendRequest.data?.sections;
|
||||
return backendRequest.data?.sections;
|
||||
};
|
||||
|
||||
// perform the request with several retries if needed
|
||||
const handleSkillsFeedbackRequest = async (
|
||||
sections: SkillsFeedbackRequest[]
|
||||
): Promise<SkillsFeedbackResponse[] | null> => {
|
||||
let i = 0;
|
||||
try {
|
||||
const data = await getSkillsFeedback(sections);
|
||||
return data;
|
||||
} catch (err) {
|
||||
if (i < 3) {
|
||||
i++;
|
||||
return handleSkillsFeedbackRequest(sections);
|
||||
}
|
||||
const handleSkillsFeedbackRequest = async (sections: SkillsFeedbackRequest[]): Promise<SkillsFeedbackResponse[] | null> => {
|
||||
let i = 0;
|
||||
try {
|
||||
const data = await getSkillsFeedback(sections);
|
||||
return data;
|
||||
} catch (err) {
|
||||
if (i < 3) {
|
||||
i++;
|
||||
return handleSkillsFeedbackRequest(sections);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||
// verify if it's a logged user that is trying to export
|
||||
if (req.session.user) {
|
||||
const { id } = req.query as { id: string };
|
||||
// fetch stats entries for this particular user with the requested exam session
|
||||
const docsSnap = await getDocs(
|
||||
query(
|
||||
collection(db, "stats"),
|
||||
where("session", "==", id),
|
||||
where("user", "==", req.session.user.id)
|
||||
)
|
||||
);
|
||||
// verify if it's a logged user that is trying to export
|
||||
if (req.session.user) {
|
||||
const {id} = req.query as {id: string};
|
||||
// fetch stats entries for this particular user with the requested exam session
|
||||
const docsSnap = await getDocs(query(collection(db, "stats"), where("session", "==", id)));
|
||||
|
||||
if (docsSnap.empty) {
|
||||
res.status(400).end();
|
||||
return;
|
||||
}
|
||||
if (docsSnap.empty) {
|
||||
res.status(400).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const stats = docsSnap.docs.map((d) => d.data());
|
||||
// verify if the stats already have a pdf generated
|
||||
const hasPDF = stats.find((s) => s.pdf);
|
||||
const stats = docsSnap.docs.map((d) => d.data());
|
||||
// verify if the stats already have a pdf generated
|
||||
const hasPDF = stats.find((s) => s.pdf);
|
||||
|
||||
if (hasPDF) {
|
||||
// if it does, return the pdf url
|
||||
const fileRef = ref(storage, hasPDF.pdf);
|
||||
const url = await getDownloadURL(fileRef);
|
||||
if (hasPDF) {
|
||||
// if it does, return the pdf url
|
||||
const fileRef = ref(storage, hasPDF.pdf);
|
||||
const url = await getDownloadURL(fileRef);
|
||||
|
||||
res.status(200).end(url);
|
||||
return;
|
||||
}
|
||||
res.status(200).end(url);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// generate the pdf report
|
||||
const docUser = await getDoc(doc(db, "users", req.session.user.id));
|
||||
try {
|
||||
// generate the pdf report
|
||||
const docUser = await getDoc(doc(db, "users", req.session.user.id));
|
||||
|
||||
if (docUser.exists()) {
|
||||
// we'll need the user in order to get the user data (name, email, focus, etc);
|
||||
const user = docUser.data() as User;
|
||||
if (docUser.exists()) {
|
||||
// we'll need the user in order to get the user data (name, email, focus, etc);
|
||||
const user = docUser.data() as User;
|
||||
|
||||
// generate the QR code for the report
|
||||
const qrcode = await generateQRCode(
|
||||
(req.headers.origin || "") + req.url
|
||||
);
|
||||
// generate the QR code for the report
|
||||
const qrcode = await generateQRCode((req.headers.origin || "") + req.url);
|
||||
|
||||
if (!qrcode) {
|
||||
res.status(500).json({ ok: false });
|
||||
return;
|
||||
}
|
||||
if (!qrcode) {
|
||||
res.status(500).json({ok: false});
|
||||
return;
|
||||
}
|
||||
|
||||
// stats may contain multiple exams of the same type so we need to aggregate them
|
||||
const results = (
|
||||
stats.reduce((accm: ModuleScore[], { module, score }) => {
|
||||
const fixedModuleStr =
|
||||
module[0].toUpperCase() + module.substring(1);
|
||||
if (accm.find((e: ModuleScore) => e.module === fixedModuleStr)) {
|
||||
return accm.map((e: ModuleScore) => {
|
||||
if (e.module === fixedModuleStr) {
|
||||
return {
|
||||
...e,
|
||||
score: e.score + score.correct,
|
||||
total: e.total + score.total,
|
||||
};
|
||||
}
|
||||
// stats may contain multiple exams of the same type so we need to aggregate them
|
||||
const results = (
|
||||
stats.reduce((accm: ModuleScore[], {module, score}) => {
|
||||
const fixedModuleStr = module[0].toUpperCase() + module.substring(1);
|
||||
if (accm.find((e: ModuleScore) => e.module === fixedModuleStr)) {
|
||||
return accm.map((e: ModuleScore) => {
|
||||
if (e.module === fixedModuleStr) {
|
||||
return {
|
||||
...e,
|
||||
score: e.score + score.correct,
|
||||
total: e.total + score.total,
|
||||
};
|
||||
}
|
||||
|
||||
return e;
|
||||
});
|
||||
}
|
||||
return e;
|
||||
});
|
||||
}
|
||||
|
||||
return [
|
||||
...accm,
|
||||
{
|
||||
module: fixedModuleStr,
|
||||
score: score.correct,
|
||||
total: score.total,
|
||||
code: module,
|
||||
},
|
||||
];
|
||||
}, []) as ModuleScore[]
|
||||
).map((moduleScore) => {
|
||||
const { score, total } = moduleScore;
|
||||
// with all the scores aggreated we can calculate the band score for each module
|
||||
const bandScore = calculateBandScore(
|
||||
score,
|
||||
total,
|
||||
moduleScore.code as Module,
|
||||
user.focus
|
||||
);
|
||||
return [
|
||||
...accm,
|
||||
{
|
||||
module: fixedModuleStr,
|
||||
score: score.correct,
|
||||
total: score.total,
|
||||
code: module,
|
||||
},
|
||||
];
|
||||
}, []) as ModuleScore[]
|
||||
).map((moduleScore) => {
|
||||
const {score, total} = moduleScore;
|
||||
// with all the scores aggreated we can calculate the band score for each module
|
||||
const bandScore = calculateBandScore(score, total, moduleScore.code as Module, user.focus);
|
||||
|
||||
return {
|
||||
...moduleScore,
|
||||
// generate the closest radial progress png for the score
|
||||
png: getRadialProgressPNG("azul", score, total),
|
||||
bandScore,
|
||||
};
|
||||
});
|
||||
return {
|
||||
...moduleScore,
|
||||
// generate the closest radial progress png for the score
|
||||
png: getRadialProgressPNG("azul", score, total),
|
||||
bandScore,
|
||||
};
|
||||
});
|
||||
|
||||
// get the skills feedback from the backend based on the module grade
|
||||
const skillsFeedback = (await handleSkillsFeedbackRequest(
|
||||
results.map(({ code, bandScore }) => ({
|
||||
code,
|
||||
name: moduleLabels[code],
|
||||
grade: bandScore,
|
||||
}))
|
||||
)) as SkillsFeedbackResponse[];
|
||||
// get the skills feedback from the backend based on the module grade
|
||||
const skillsFeedback = (await handleSkillsFeedbackRequest(
|
||||
results.map(({code, bandScore}) => ({
|
||||
code,
|
||||
name: moduleLabels[code],
|
||||
grade: bandScore,
|
||||
})),
|
||||
)) as SkillsFeedbackResponse[];
|
||||
|
||||
if (!skillsFeedback) {
|
||||
res.status(500).json({ ok: false });
|
||||
return;
|
||||
}
|
||||
if (!skillsFeedback) {
|
||||
res.status(500).json({ok: false});
|
||||
return;
|
||||
}
|
||||
|
||||
// assign the feedback to the results
|
||||
const finalResults = results.map((result) => {
|
||||
const feedback = skillsFeedback.find(
|
||||
(f: SkillsFeedbackResponse) => f.code === result.code
|
||||
);
|
||||
// assign the feedback to the results
|
||||
const finalResults = results.map((result) => {
|
||||
const feedback = skillsFeedback.find((f: SkillsFeedbackResponse) => f.code === result.code);
|
||||
|
||||
if (feedback) {
|
||||
return {
|
||||
...result,
|
||||
evaluation: feedback?.evaluation,
|
||||
suggestions: feedback?.suggestions,
|
||||
};
|
||||
}
|
||||
if (feedback) {
|
||||
return {
|
||||
...result,
|
||||
evaluation: feedback?.evaluation,
|
||||
suggestions: feedback?.suggestions,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
return result;
|
||||
});
|
||||
|
||||
// calculate the overall score out of all the aggregated results
|
||||
const overallScore = results.reduce(
|
||||
(accm, { score }) => accm + score,
|
||||
0
|
||||
);
|
||||
const overallTotal = results.reduce(
|
||||
(accm, { total }) => accm + total,
|
||||
0
|
||||
);
|
||||
const overallResult = overallScore / overallTotal;
|
||||
// calculate the overall score out of all the aggregated results
|
||||
const overallScore = results.reduce((accm, {score}) => accm + score, 0);
|
||||
const overallTotal = results.reduce((accm, {total}) => accm + total, 0);
|
||||
const overallResult = overallScore / overallTotal;
|
||||
|
||||
const overallPNG = getRadialProgressPNG("laranja", overallScore, overallTotal);
|
||||
const overallPNG = getRadialProgressPNG("laranja", overallScore, overallTotal);
|
||||
|
||||
// generate the overall detail report
|
||||
const overallDetail = {
|
||||
module: "Overall",
|
||||
score: overallScore,
|
||||
total: overallTotal,
|
||||
png: overallPNG,
|
||||
} as ModuleScore;
|
||||
const testDetails = [overallDetail, ...finalResults];
|
||||
// generate the overall detail report
|
||||
const overallDetail = {
|
||||
module: "Overall",
|
||||
score: overallScore,
|
||||
total: overallTotal,
|
||||
png: overallPNG,
|
||||
} as ModuleScore;
|
||||
const testDetails = [overallDetail, ...finalResults];
|
||||
|
||||
const [stat] = stats;
|
||||
const [stat] = stats;
|
||||
|
||||
// generate the performance summary based on the overall result
|
||||
const performanceSummary = getPerformanceSummary(
|
||||
stat.module,
|
||||
overallResult
|
||||
);
|
||||
// generate the performance summary based on the overall result
|
||||
const performanceSummary = getPerformanceSummary(stat.module, overallResult);
|
||||
|
||||
// level exams have a different report structure than the skill exams
|
||||
const getCustomData = () => {
|
||||
if (stat.module === "level") {
|
||||
return {
|
||||
title: "ENGLISH LEVEL TEST RESULT REPORT ",
|
||||
details: (
|
||||
<LevelExamDetails
|
||||
detail={overallDetail}
|
||||
title="Level as per CEFR Levels"
|
||||
/>
|
||||
),
|
||||
};
|
||||
}
|
||||
// level exams have a different report structure than the skill exams
|
||||
const getCustomData = () => {
|
||||
if (stat.module === "level") {
|
||||
return {
|
||||
title: "ENGLISH LEVEL TEST RESULT REPORT ",
|
||||
details: <LevelExamDetails detail={overallDetail} title="Level as per CEFR Levels" />,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: "ENGLISH SKILLS TEST RESULT REPORT",
|
||||
details: <SkillExamDetails testDetails={testDetails} />,
|
||||
};
|
||||
};
|
||||
return {
|
||||
title: "ENGLISH SKILLS TEST RESULT REPORT",
|
||||
details: <SkillExamDetails testDetails={testDetails} />,
|
||||
};
|
||||
};
|
||||
|
||||
const { title, details } = getCustomData();
|
||||
|
||||
const demographicInformation = user.demographicInformation as DemographicInformation;
|
||||
const pdfStream = await ReactPDF.renderToStream(
|
||||
<TestReport
|
||||
title={title}
|
||||
date={moment(stat.date).tz(user.demographicInformation?.timezone || 'UTC').format('ll HH:mm:ss')}
|
||||
name={user.name}
|
||||
email={user.email}
|
||||
id={user.id}
|
||||
gender={demographicInformation?.gender}
|
||||
summary={performanceSummary}
|
||||
testDetails={testDetails}
|
||||
renderDetails={details}
|
||||
logo={"public/logo_title.png"}
|
||||
qrcode={qrcode}
|
||||
summaryPNG={overallPNG}
|
||||
summaryScore={`${(overallResult * 100).toFixed(0)}%`}
|
||||
passportId={demographicInformation?.passport_id || ""}
|
||||
/>
|
||||
);
|
||||
const {title, details} = getCustomData();
|
||||
|
||||
// generate the file ref for storage
|
||||
const fileName = `${Date.now().toString()}.pdf`;
|
||||
const refName = `exam_report/${fileName}`;
|
||||
const fileRef = ref(storage, refName);
|
||||
const demographicInformation = user.demographicInformation as DemographicInformation;
|
||||
const pdfStream = await ReactPDF.renderToStream(
|
||||
<TestReport
|
||||
title={title}
|
||||
date={moment(stat.date)
|
||||
.tz(user.demographicInformation?.timezone || "UTC")
|
||||
.format("ll HH:mm:ss")}
|
||||
name={user.name}
|
||||
email={user.email}
|
||||
id={user.id}
|
||||
gender={demographicInformation?.gender}
|
||||
summary={performanceSummary}
|
||||
testDetails={testDetails}
|
||||
renderDetails={details}
|
||||
logo={"public/logo_title.png"}
|
||||
qrcode={qrcode}
|
||||
summaryPNG={overallPNG}
|
||||
summaryScore={`${(overallResult * 100).toFixed(0)}%`}
|
||||
passportId={demographicInformation?.passport_id || ""}
|
||||
/>,
|
||||
);
|
||||
|
||||
// upload the pdf to storage
|
||||
const pdfBuffer = await streamToBuffer(pdfStream);
|
||||
const snapshot = await uploadBytes(fileRef, pdfBuffer, {
|
||||
contentType: "application/pdf",
|
||||
});
|
||||
// generate the file ref for storage
|
||||
const fileName = `${Date.now().toString()}.pdf`;
|
||||
const refName = `exam_report/${fileName}`;
|
||||
const fileRef = ref(storage, refName);
|
||||
|
||||
// update the stats entries with the pdf url to prevent duplication
|
||||
docsSnap.docs.forEach(async (doc) => {
|
||||
await updateDoc(doc.ref, {
|
||||
pdf: refName,
|
||||
});
|
||||
});
|
||||
const url = await getDownloadURL(fileRef);
|
||||
res.status(200).end(url);
|
||||
return;
|
||||
}
|
||||
// upload the pdf to storage
|
||||
const pdfBuffer = await streamToBuffer(pdfStream);
|
||||
const snapshot = await uploadBytes(fileRef, pdfBuffer, {
|
||||
contentType: "application/pdf",
|
||||
});
|
||||
|
||||
res.status(401).json({ ok: false });
|
||||
return;
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false });
|
||||
return;
|
||||
}
|
||||
}
|
||||
// update the stats entries with the pdf url to prevent duplication
|
||||
docsSnap.docs.forEach(async (doc) => {
|
||||
await updateDoc(doc.ref, {
|
||||
pdf: refName,
|
||||
});
|
||||
});
|
||||
const url = await getDownloadURL(fileRef);
|
||||
res.status(200).end(url);
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(401).json({ ok: false });
|
||||
return;
|
||||
res.status(401).json({ok: false});
|
||||
return;
|
||||
} catch (err) {
|
||||
res.status(500).json({ok: false});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
res.status(401).json({ok: false});
|
||||
return;
|
||||
}
|
||||
|
||||
async function get(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { id } = req.query as { id: string };
|
||||
const docsSnap = await getDocs(
|
||||
query(collection(db, "stats"), where("session", "==", id))
|
||||
);
|
||||
const {id} = req.query as {id: string};
|
||||
const docsSnap = await getDocs(query(collection(db, "stats"), where("session", "==", id)));
|
||||
|
||||
if (docsSnap.empty) {
|
||||
res.status(404).end();
|
||||
return;
|
||||
}
|
||||
if (docsSnap.empty) {
|
||||
res.status(404).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const stats = docsSnap.docs.map((d) => d.data());
|
||||
const stats = docsSnap.docs.map((d) => d.data());
|
||||
|
||||
const hasPDF = stats.find((s) => s.pdf);
|
||||
const hasPDF = stats.find((s) => s.pdf);
|
||||
|
||||
if (hasPDF) {
|
||||
const fileRef = ref(storage, hasPDF.pdf);
|
||||
const url = await getDownloadURL(fileRef);
|
||||
return res.redirect(url);
|
||||
}
|
||||
if (hasPDF) {
|
||||
const fileRef = ref(storage, hasPDF.pdf);
|
||||
const url = await getDownloadURL(fileRef);
|
||||
return res.redirect(url);
|
||||
}
|
||||
|
||||
res.status(500).end();
|
||||
res.status(500).end();
|
||||
}
|
||||
|
||||
@@ -111,11 +111,13 @@ const PaymentCreator = ({onClose, reload, showComission = false}: {onClose: () =
|
||||
options={(users.filter((u) => u.type === "corporate") as CorporateUser[]).map((user) => ({
|
||||
value: user.id,
|
||||
meta: user,
|
||||
label: `${user.corporateInformation.companyInformation.name || user.name} - ${user.email}`,
|
||||
label: `${user.corporateInformation?.companyInformation?.name || user.name} - ${user.email}`,
|
||||
}))}
|
||||
defaultValue={{value: "undefined", label: "Select an account"}}
|
||||
onChange={(value) => setCorporate((value as any)?.meta ?? undefined)}
|
||||
menuPortalTarget={document?.body}
|
||||
styles={{
|
||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||
control: (styles) => ({
|
||||
...styles,
|
||||
paddingLeft: "4px",
|
||||
@@ -144,7 +146,9 @@ const PaymentCreator = ({onClose, reload, showComission = false}: {onClose: () =
|
||||
defaultValue={{value: currency || "EUR", label: CURRENCIES.find((c) => c.currency === currency)?.label || "Euro"}}
|
||||
onChange={() => {}}
|
||||
value={{value: currency || "EUR", label: CURRENCIES.find((c) => c.currency === currency)?.label || "Euro"}}
|
||||
menuPortalTarget={document?.body}
|
||||
styles={{
|
||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||
control: (styles) => ({
|
||||
...styles,
|
||||
paddingLeft: "4px",
|
||||
@@ -824,7 +828,9 @@ export default function PaymentRecord() {
|
||||
}
|
||||
isDisabled={user.type === "corporate"}
|
||||
onChange={(value) => setCorporate((value as any)?.meta ?? undefined)}
|
||||
menuPortalTarget={document?.body}
|
||||
styles={{
|
||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||
control: (styles) => ({
|
||||
...styles,
|
||||
paddingLeft: "4px",
|
||||
@@ -859,7 +865,9 @@ export default function PaymentRecord() {
|
||||
}))}
|
||||
value={agent ? {value: agent?.id, label: `${agent.name} - ${agent.email}`} : undefined}
|
||||
onChange={(value) => setAgent(value !== null ? (value as any).meta : undefined)}
|
||||
menuPortalTarget={document?.body}
|
||||
styles={{
|
||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||
control: (styles) => ({
|
||||
...styles,
|
||||
paddingLeft: "4px",
|
||||
@@ -892,7 +900,9 @@ export default function PaymentRecord() {
|
||||
if (value) return setPaid(value.value);
|
||||
setPaid(null);
|
||||
}}
|
||||
menuPortalTarget={document?.body}
|
||||
styles={{
|
||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||
control: (styles) => ({
|
||||
...styles,
|
||||
paddingLeft: "4px",
|
||||
@@ -947,7 +957,9 @@ export default function PaymentRecord() {
|
||||
if (value) return setCommissionTransfer(value.value);
|
||||
setCommissionTransfer(null);
|
||||
}}
|
||||
menuPortalTarget={document?.body}
|
||||
styles={{
|
||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||
control: (styles) => ({
|
||||
...styles,
|
||||
paddingLeft: "4px",
|
||||
@@ -979,7 +991,9 @@ export default function PaymentRecord() {
|
||||
if (value) return setCorporateTransfer(value.value);
|
||||
setCorporateTransfer(null);
|
||||
}}
|
||||
menuPortalTarget={document?.body}
|
||||
styles={{
|
||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||
control: (styles) => ({
|
||||
...styles,
|
||||
paddingLeft: "4px",
|
||||
|
||||
@@ -306,7 +306,9 @@ export default function History({user}: {user: User}) {
|
||||
options={users.map((x) => ({value: x.id, label: `${x.name} - ${x.email}`}))}
|
||||
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}}
|
||||
onChange={(value) => setStatsUserId(value?.value)}
|
||||
menuPortalTarget={document?.body}
|
||||
styles={{
|
||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||
option: (styles, state) => ({
|
||||
...styles,
|
||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||
@@ -322,7 +324,9 @@ export default function History({user}: {user: User}) {
|
||||
.map((x) => ({value: x.id, label: `${x.name} - ${x.email}`}))}
|
||||
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}}
|
||||
onChange={(value) => setStatsUserId(value?.value)}
|
||||
menuPortalTarget={document?.body}
|
||||
styles={{
|
||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||
option: (styles, state) => ({
|
||||
...styles,
|
||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||
|
||||
@@ -178,7 +178,7 @@ export default function Stats() {
|
||||
},
|
||||
{
|
||||
icon: <BsStar className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />,
|
||||
value: `${stats.length > 0 ? averageScore(userStats) : 0}%`,
|
||||
value: `${userStats.length > 0 ? averageScore(userStats) : 0}%`,
|
||||
label: "Average Score",
|
||||
},
|
||||
]}
|
||||
@@ -193,7 +193,9 @@ export default function Stats() {
|
||||
options={users.map((x) => ({value: x.id, label: `${x.name} - ${x.email}`}))}
|
||||
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}}
|
||||
onChange={(value) => setStatsUserId(value?.value)}
|
||||
menuPortalTarget={document?.body}
|
||||
styles={{
|
||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||
option: (styles, state) => ({
|
||||
...styles,
|
||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||
@@ -210,7 +212,9 @@ export default function Stats() {
|
||||
.map((x) => ({value: x.id, label: `${x.name} - ${x.email}`}))}
|
||||
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}}
|
||||
onChange={(value) => setStatsUserId(value?.value)}
|
||||
menuPortalTarget={document?.body}
|
||||
styles={{
|
||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||
option: (styles, state) => ({
|
||||
...styles,
|
||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||
|
||||
@@ -1,52 +1,51 @@
|
||||
import {
|
||||
collection,
|
||||
getDocs,
|
||||
query,
|
||||
where,
|
||||
setDoc,
|
||||
doc,
|
||||
Firestore,
|
||||
} from "firebase/firestore";
|
||||
import { shuffle } from "lodash";
|
||||
import { Exam } from "@/interfaces/exam";
|
||||
import { Stat } from "@/interfaces/user";
|
||||
import {collection, getDocs, query, where, setDoc, doc, Firestore} from "firebase/firestore";
|
||||
import {shuffle} from "lodash";
|
||||
import {Exam, Variant} from "@/interfaces/exam";
|
||||
import {Stat} from "@/interfaces/user";
|
||||
|
||||
export const getExams = async (
|
||||
db: Firestore,
|
||||
module: string,
|
||||
avoidRepeated: string,
|
||||
// added userId as due to assignments being set from the teacher to the student
|
||||
// we need to make sure we are serving exams not executed by the user and not
|
||||
// by the teacher that performed the request
|
||||
userId: string | undefined
|
||||
db: Firestore,
|
||||
module: string,
|
||||
avoidRepeated: string,
|
||||
// added userId as due to assignments being set from the teacher to the student
|
||||
// we need to make sure we are serving exams not executed by the user and not
|
||||
// by the teacher that performed the request
|
||||
userId: string | undefined,
|
||||
variant?: Variant,
|
||||
): Promise<Exam[]> => {
|
||||
const moduleRef = collection(db, module);
|
||||
const moduleRef = collection(db, module);
|
||||
|
||||
const q = query(moduleRef, where("isDiagnostic", "==", false));
|
||||
const snapshot = await getDocs(q);
|
||||
const q = query(moduleRef, where("isDiagnostic", "==", false));
|
||||
const snapshot = await getDocs(q);
|
||||
|
||||
const exams: Exam[] = shuffle(
|
||||
snapshot.docs.map((doc) => ({
|
||||
id: doc.id,
|
||||
...doc.data(),
|
||||
module,
|
||||
}))
|
||||
) as Exam[];
|
||||
const exams: Exam[] = filterByVariant(
|
||||
shuffle(
|
||||
snapshot.docs.map((doc) => ({
|
||||
id: doc.id,
|
||||
...doc.data(),
|
||||
module,
|
||||
})),
|
||||
) as Exam[],
|
||||
variant,
|
||||
);
|
||||
|
||||
if (avoidRepeated === "true") {
|
||||
const statsQ = query(collection(db, "stats"), where("user", "==", userId));
|
||||
const statsSnapshot = await getDocs(statsQ);
|
||||
if (avoidRepeated === "true") {
|
||||
const statsQ = query(collection(db, "stats"), where("user", "==", userId));
|
||||
const statsSnapshot = await getDocs(statsQ);
|
||||
|
||||
const stats: Stat[] = statsSnapshot.docs.map((doc) => ({
|
||||
id: doc.id,
|
||||
...doc.data(),
|
||||
})) as unknown as Stat[];
|
||||
const filteredExams = exams.filter(
|
||||
(x) => !stats.map((s) => s.exam).includes(x.id)
|
||||
);
|
||||
const stats: Stat[] = statsSnapshot.docs.map((doc) => ({
|
||||
id: doc.id,
|
||||
...doc.data(),
|
||||
})) as unknown as Stat[];
|
||||
const filteredExams = exams.filter((x) => !stats.map((s) => s.exam).includes(x.id));
|
||||
|
||||
return filteredExams.length > 0 ? filteredExams : exams;
|
||||
}
|
||||
return filteredExams.length > 0 ? filteredExams : exams;
|
||||
}
|
||||
|
||||
return exams;
|
||||
return exams;
|
||||
};
|
||||
|
||||
const filterByVariant = (exams: Exam[], variant?: Variant) => {
|
||||
const filtered = variant && variant === "partial" ? exams.filter((x) => x.variant === "partial") : exams.filter((x) => x.variant !== "partial");
|
||||
return filtered.length > 0 ? filtered : exams;
|
||||
};
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import {Module} from "@/interfaces";
|
||||
import {Exam, ReadingExam, ListeningExam, WritingExam, SpeakingExam, Exercise, UserSolution, LevelExam} from "@/interfaces/exam";
|
||||
import {Exam, ReadingExam, ListeningExam, WritingExam, SpeakingExam, Exercise, UserSolution, LevelExam, Variant} from "@/interfaces/exam";
|
||||
import axios from "axios";
|
||||
|
||||
export const getExam = async (module: Module, avoidRepeated: boolean): Promise<Exam | undefined> => {
|
||||
const examRequest = await axios<Exam[]>(`/api/exam/${module}?avoidRepeated=${avoidRepeated}`);
|
||||
export const getExam = async (module: Module, avoidRepeated: boolean, variant?: Variant): Promise<Exam | undefined> => {
|
||||
const url = new URLSearchParams();
|
||||
url.append("avoidRepeated", avoidRepeated.toString());
|
||||
if (variant) url.append("variant", variant);
|
||||
|
||||
const examRequest = await axios<Exam[]>(`/api/exam/${module}?${url.toString()}`);
|
||||
if (examRequest.status !== 200) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import moment from "moment";
|
||||
|
||||
export function dateSorter(a: any, b: any, direction: "asc" | "desc", key: string) {
|
||||
if (!a[key] && !b[key]) return 0;
|
||||
if (a[key] && !b[key]) return direction === "asc" ? -1 : 1;
|
||||
if (!a[key] && b[key]) return direction === "asc" ? 1 : -1;
|
||||
if (a[key] && !b[key]) return direction === "asc" ? 1 : -1;
|
||||
if (!a[key] && b[key]) return direction === "asc" ? -1 : 1;
|
||||
if (moment(a[key]).isAfter(b[key])) return direction === "asc" ? 1 : -1;
|
||||
if (moment(b[key]).isAfter(a[key])) return direction === "asc" ? -1 : 1;
|
||||
return 0;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Howl, Howler} from "howler";
|
||||
|
||||
export type Sound = "check" | "sent";
|
||||
export type Sound = "check" | "sent" | "error";
|
||||
export const playSound = (path: Sound) => {
|
||||
const sound = new Howl({
|
||||
src: [`audio/${path}.mp3`],
|
||||
|
||||
Reference in New Issue
Block a user