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 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) => {
|
Promise.all(examPromises).then((exams) => {
|
||||||
if (exams.every((x) => !!x)) {
|
if (exams.every((x) => !!x)) {
|
||||||
|
|||||||
@@ -42,7 +42,9 @@ export default function CountrySelect({value, disabled = false, onChange}: Props
|
|||||||
displayValue={(code: string) => {
|
displayValue={(code: string) => {
|
||||||
const country = countries[code as unknown as keyof TCountries];
|
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">
|
<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
|
<Link
|
||||||
href={!disabled ? keyPath : ""}
|
href={!disabled ? keyPath : ""}
|
||||||
className={clsx(
|
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",
|
"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",
|
path === keyPath && "bg-mti-purple-light text-white",
|
||||||
isMinimized ? "w-fit" : "w-full min-w-[200px] 2xl:min-w-[220px] px-8",
|
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}
|
options={CURRENCIES_OPTIONS}
|
||||||
value={CURRENCIES_OPTIONS.find((c) => c.value === paymentCurrency)}
|
value={CURRENCIES_OPTIONS.find((c) => c.value === paymentCurrency)}
|
||||||
onChange={(value) => setPaymentCurrency(value?.value)}
|
onChange={(value) => setPaymentCurrency(value?.value)}
|
||||||
|
menuPortalTarget={document?.body}
|
||||||
styles={{
|
styles={{
|
||||||
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
...styles,
|
...styles,
|
||||||
paddingLeft: "4px",
|
paddingLeft: "4px",
|
||||||
@@ -282,8 +284,10 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
|
|||||||
value: referralAgent,
|
value: referralAgent,
|
||||||
label: referralAgentLabel,
|
label: referralAgentLabel,
|
||||||
}}
|
}}
|
||||||
|
menuPortalTarget={document?.body}
|
||||||
onChange={(value) => setReferralAgent(value?.value)}
|
onChange={(value) => setReferralAgent(value?.value)}
|
||||||
styles={{
|
styles={{
|
||||||
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
...styles,
|
...styles,
|
||||||
paddingLeft: "4px",
|
paddingLeft: "4px",
|
||||||
@@ -314,7 +318,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
|
|||||||
type="number"
|
type="number"
|
||||||
defaultValue={commissionValue || 0}
|
defaultValue={commissionValue || 0}
|
||||||
className="col-span-3"
|
className="col-span-3"
|
||||||
disabled={disabled}
|
disabled={disabled || loggedInUser.type === "agent"}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -520,6 +524,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
|
|||||||
<Select
|
<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"
|
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}
|
options={USER_STATUS_OPTIONS}
|
||||||
|
menuPortalTarget={document?.body}
|
||||||
value={USER_STATUS_OPTIONS.find((o) => o.value === status)}
|
value={USER_STATUS_OPTIONS.find((o) => o.value === status)}
|
||||||
onChange={(value) => setStatus(value?.value as typeof user.status)}
|
onChange={(value) => setStatus(value?.value as typeof user.status)}
|
||||||
styles={{
|
styles={{
|
||||||
@@ -532,6 +537,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
|
|||||||
outline: "none",
|
outline: "none",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
option: (styles, state) => ({
|
option: (styles, state) => ({
|
||||||
...styles,
|
...styles,
|
||||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||||
@@ -546,6 +552,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
|
|||||||
<Select
|
<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"
|
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}
|
options={USER_TYPE_OPTIONS}
|
||||||
|
menuPortalTarget={document?.body}
|
||||||
value={USER_TYPE_OPTIONS.find((o) => o.value === type)}
|
value={USER_TYPE_OPTIONS.find((o) => o.value === type)}
|
||||||
onChange={(value) => setType(value?.value as typeof user.type)}
|
onChange={(value) => setType(value?.value as typeof user.type)}
|
||||||
styles={{
|
styles={{
|
||||||
@@ -558,6 +565,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
|
|||||||
outline: "none",
|
outline: "none",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
option: (styles, state) => ({
|
option: (styles, state) => ({
|
||||||
...styles,
|
...styles,
|
||||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
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="flex gap-4 justify-between mt-4 w-full">
|
||||||
<div className="self-start flex gap-4 justify-start items-center 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}>
|
<Button className="w-full max-w-[200px]" variant="outline" color="rose" onClick={onViewCorporate}>
|
||||||
View Corporate
|
View Corporate
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{onViewStudents && (
|
{onViewStudents && ["corporate", "teacher"].includes(user.type) && (
|
||||||
<Button className="w-full max-w-[200px]" variant="outline" color="rose" onClick={onViewStudents}>
|
<Button className="w-full max-w-[200px]" variant="outline" color="rose" onClick={onViewStudents}>
|
||||||
View Students
|
View Students
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{onViewTeachers && (
|
{onViewTeachers && ["student", "corporate"].includes(user.type) && (
|
||||||
<Button className="w-full max-w-[200px]" variant="outline" color="rose" onClick={onViewTeachers}>
|
<Button className="w-full max-w-[200px]" variant="outline" color="rose" onClick={onViewTeachers}>
|
||||||
View Teachers
|
View Teachers
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ export const PERMISSIONS = {
|
|||||||
developer: ["developer"],
|
developer: ["developer"],
|
||||||
},
|
},
|
||||||
updateUser: {
|
updateUser: {
|
||||||
student: ["teacher", "corporate", "developer", "admin"],
|
student: ["developer", "admin"],
|
||||||
teacher: ["corporate", "developer", "admin"],
|
teacher: ["developer", "admin"],
|
||||||
corporate: ["admin", "developer"],
|
corporate: ["admin", "developer"],
|
||||||
admin: ["developer", "admin"],
|
admin: ["developer", "admin"],
|
||||||
agent: ["developer", "admin"],
|
agent: ["developer", "admin"],
|
||||||
|
|||||||
@@ -151,8 +151,9 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AssignmentsPage = () => {
|
const AssignmentsPage = () => {
|
||||||
const activeFilter = (a: Assignment) => moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment());
|
const activeFilter = (a: Assignment) =>
|
||||||
const pastFilter = (a: Assignment) => moment(a.endDate).isBefore(moment());
|
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());
|
const futureFilter = (a: Assignment) => moment(a.startDate).isAfter(moment());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -12,17 +12,19 @@ import {calculateAverageLevel} from "@/utils/score";
|
|||||||
import {sortByModuleName} from "@/utils/moduleUtils";
|
import {sortByModuleName} from "@/utils/moduleUtils";
|
||||||
import {capitalize} from "lodash";
|
import {capitalize} from "lodash";
|
||||||
import ProfileSummary from "@/components/ProfileSummary";
|
import ProfileSummary from "@/components/ProfileSummary";
|
||||||
|
import {Variant} from "@/interfaces/exam";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
page: "exercises" | "exams";
|
page: "exercises" | "exams";
|
||||||
onStart: (modules: Module[], avoidRepeated: boolean) => void;
|
onStart: (modules: Module[], avoidRepeated: boolean, variant: Variant) => void;
|
||||||
disableSelection?: boolean;
|
disableSelection?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Selection({user, page, onStart, disableSelection = false}: Props) {
|
export default function Selection({user, page, onStart, disableSelection = false}: Props) {
|
||||||
const [selectedModules, setSelectedModules] = useState<Module[]>([]);
|
const [selectedModules, setSelectedModules] = useState<Module[]>([]);
|
||||||
const [avoidRepeatedExams, setAvoidRepeatedExams] = useState(true);
|
const [avoidRepeatedExams, setAvoidRepeatedExams] = useState(true);
|
||||||
|
const [variant, setVariant] = useState<Variant>("full");
|
||||||
const {stats} = useStats(user?.id);
|
const {stats} = useStats(user?.id);
|
||||||
|
|
||||||
const toggleModule = (module: Module) => {
|
const toggleModule = (module: Module) => {
|
||||||
@@ -202,9 +204,9 @@ export default function Selection({user, page, onStart, disableSelection = false
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
<div className="flex w-full -md:flex-col -md:gap-4 -md:justify-center md:justify-between items-center">
|
<div className="flex w-full -md:flex-col -md:gap-4 -md:justify-center md:justify-between items-center">
|
||||||
|
<div className="flex flex-col gap-3 items-center w-full">
|
||||||
<div
|
<div
|
||||||
className="flex gap-3 items-center text-mti-gray-dim text-sm cursor-pointer tooltip w-full -md:justify-center"
|
className="flex gap-3 items-center text-mti-gray-dim text-sm cursor-pointer w-full -md:justify-center"
|
||||||
data-tip="If possible, the platform will choose exams not yet done"
|
|
||||||
onClick={() => setAvoidRepeatedExams((prev) => !prev)}>
|
onClick={() => setAvoidRepeatedExams((prev) => !prev)}>
|
||||||
<input type="checkbox" className="hidden" />
|
<input type="checkbox" className="hidden" />
|
||||||
<div
|
<div
|
||||||
@@ -215,7 +217,24 @@ export default function Selection({user, page, onStart, disableSelection = false
|
|||||||
)}>
|
)}>
|
||||||
<BsCheck color="white" className="w-full h-full" />
|
<BsCheck color="white" className="w-full h-full" />
|
||||||
</div>
|
</div>
|
||||||
<span>Avoid Repeated Questions</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<div className="tooltip w-full" data-tip={`Your screen size is too small to do ${page}`}>
|
<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>
|
<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(
|
onStart(
|
||||||
!disableSelection ? selectedModules.sort(sortByModuleName) : ["reading", "listening", "writing", "speaking"],
|
!disableSelection ? selectedModules.sort(sortByModuleName) : ["reading", "listening", "writing", "speaking"],
|
||||||
avoidRepeatedExams,
|
avoidRepeatedExams,
|
||||||
|
variant,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
color="purple"
|
color="purple"
|
||||||
|
|||||||
@@ -5,19 +5,12 @@ import { View, Text, Image } from "@react-pdf/renderer";
|
|||||||
import {styles} from "../styles";
|
import {styles} from "../styles";
|
||||||
import {ModuleScore} from "@/interfaces/module.scores";
|
import {ModuleScore} from "@/interfaces/module.scores";
|
||||||
|
|
||||||
export const RadialResult = ({
|
export const RadialResult = ({module, score, total, png}: ModuleScore) => (
|
||||||
module,
|
|
||||||
score,
|
|
||||||
total,
|
|
||||||
png,
|
|
||||||
}: ModuleScore) => (
|
|
||||||
<View style={[styles.textFont, styles.radialContainer]}>
|
<View style={[styles.textFont, styles.radialContainer]}>
|
||||||
<Text style={[styles.textColor, styles.textBold, { fontSize: 10 }]}>
|
<Text style={[styles.textColor, styles.textBold, {fontSize: 10}]}>{module}</Text>
|
||||||
{module}
|
|
||||||
</Text>
|
|
||||||
<Image src={png} style={styles.image64}></Image>
|
<Image src={png} style={styles.image64}></Image>
|
||||||
<View style={[styles.textColor, styles.radialResultContainer]}>
|
<View style={[styles.textColor, styles.radialResultContainer]}>
|
||||||
<Text style={styles.textBold}>{score}</Text>
|
<Text style={styles.textBold}>{score.toFixed(2)}</Text>
|
||||||
<Text style={{fontSize: 8}}>out of {total}</Text>
|
<Text style={{fontSize: 8}}>out of {total}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {Module} from ".";
|
import {Module} from ".";
|
||||||
|
|
||||||
export type Exam = ReadingExam | ListeningExam | WritingExam | SpeakingExam | LevelExam;
|
export type Exam = ReadingExam | ListeningExam | WritingExam | SpeakingExam | LevelExam;
|
||||||
export type Variant = "diagnostic" | "partial";
|
export type Variant = "full" | "diagnostic" | "partial";
|
||||||
|
|
||||||
export interface ReadingExam {
|
export interface ReadingExam {
|
||||||
parts: ReadingPart[];
|
parts: ReadingPart[];
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ export interface ModuleScore {
|
|||||||
score: number;
|
score: number;
|
||||||
total: number;
|
total: number;
|
||||||
code: Module;
|
code: Module;
|
||||||
module: Module | 'Overall';
|
module: Module | "Overall";
|
||||||
png?: string,
|
png?: string;
|
||||||
evaluation?: string,
|
evaluation?: string;
|
||||||
suggestions?: string,
|
suggestions?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StudentData {
|
export interface StudentData {
|
||||||
|
|||||||
@@ -125,7 +125,9 @@ const CreatePanel = ({user, users, group, onClose}: CreateDialogProps) => {
|
|||||||
onChange={(value) => setParticipants(value.map((x) => x.value))}
|
onChange={(value) => setParticipants(value.map((x) => x.value))}
|
||||||
isMulti
|
isMulti
|
||||||
isSearchable
|
isSearchable
|
||||||
|
menuPortalTarget={document?.body}
|
||||||
styles={{
|
styles={{
|
||||||
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
...styles,
|
...styles,
|
||||||
backgroundColor: "white",
|
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"}}
|
defaultValue={{value: currency || "EUR", label: CURRENCIES.find((c) => c.currency === currency)?.label || "Euro"}}
|
||||||
onChange={(value) => setCurrency(value?.value || "EUR")}
|
onChange={(value) => setCurrency(value?.value || "EUR")}
|
||||||
value={{value: currency || "EUR", label: CURRENCIES.find((c) => c.currency === currency)?.label || "Euro"}}
|
value={{value: currency || "EUR", label: CURRENCIES.find((c) => c.currency === currency)?.label || "Euro"}}
|
||||||
|
menuPortalTarget={document?.body}
|
||||||
styles={{
|
styles={{
|
||||||
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
...styles,
|
...styles,
|
||||||
paddingLeft: "4px",
|
paddingLeft: "4px",
|
||||||
@@ -105,7 +107,9 @@ function PackageCreator({pack, onClose}: {pack?: Package; onClose: () => void})
|
|||||||
defaultValue={{value: "months", label: "Months"}}
|
defaultValue={{value: "months", label: "Months"}}
|
||||||
onChange={(value) => setUnit((value?.value as DurationUnit) || "months")}
|
onChange={(value) => setUnit((value?.value as DurationUnit) || "months")}
|
||||||
value={{value: unit, label: capitalize(unit)}}
|
value={{value: unit, label: capitalize(unit)}}
|
||||||
|
menuPortalTarget={document?.body}
|
||||||
styles={{
|
styles={{
|
||||||
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
...styles,
|
...styles,
|
||||||
paddingLeft: "4px",
|
paddingLeft: "4px",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {Module} from "@/interfaces";
|
|||||||
|
|
||||||
import Selection from "@/exams/Selection";
|
import Selection from "@/exams/Selection";
|
||||||
import Reading from "@/exams/Reading";
|
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 Listening from "@/exams/Listening";
|
||||||
import Writing from "@/exams/Writing";
|
import Writing from "@/exams/Writing";
|
||||||
import {ToastContainer, toast} from "react-toastify";
|
import {ToastContainer, toast} from "react-toastify";
|
||||||
@@ -38,6 +38,7 @@ export default function ExamPage({page}: Props) {
|
|||||||
const [avoidRepeated, setAvoidRepeated] = useState(false);
|
const [avoidRepeated, setAvoidRepeated] = useState(false);
|
||||||
const [timeSpent, setTimeSpent] = useState(0);
|
const [timeSpent, setTimeSpent] = useState(0);
|
||||||
const [statsAwaitingEvaluation, setStatsAwaitingEvaluation] = useState<string[]>([]);
|
const [statsAwaitingEvaluation, setStatsAwaitingEvaluation] = useState<string[]>([]);
|
||||||
|
const [variant, setVariant] = useState<Variant>("full");
|
||||||
|
|
||||||
const [exams, setExams] = useExamStore((state) => [state.exams, state.setExams]);
|
const [exams, setExams] = useExamStore((state) => [state.exams, state.setExams]);
|
||||||
const [userSolutions, setUserSolutions] = useExamStore((state) => [state.userSolutions, state.setUserSolutions]);
|
const [userSolutions, setUserSolutions] = useExamStore((state) => [state.userSolutions, state.setUserSolutions]);
|
||||||
@@ -84,7 +85,7 @@ export default function ExamPage({page}: Props) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (selectedModules.length > 0 && exams.length === 0) {
|
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) => {
|
Promise.all(examPromises).then((values) => {
|
||||||
if (values.every((x) => !!x)) {
|
if (values.every((x) => !!x)) {
|
||||||
setExams(values.map((x) => x!));
|
setExams(values.map((x) => x!));
|
||||||
@@ -253,10 +254,11 @@ export default function ExamPage({page}: Props) {
|
|||||||
page={page}
|
page={page}
|
||||||
user={user!}
|
user={user!}
|
||||||
disableSelection={page === "exams"}
|
disableSelection={page === "exams"}
|
||||||
onStart={(modules, avoid) => {
|
onStart={(modules: Module[], avoid: boolean, variant: Variant) => {
|
||||||
setModuleIndex(0);
|
setModuleIndex(0);
|
||||||
setAvoidRepeated(avoid);
|
setAvoidRepeated(avoid);
|
||||||
setSelectedModules(modules);
|
setSelectedModules(modules);
|
||||||
|
setVariant(variant);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ const TaskTab = ({exam, setExam}: {exam?: LevelExam; setExam: (exam: LevelExam)
|
|||||||
axios
|
axios
|
||||||
.get(`/api/exam/level/generate/level`)
|
.get(`/api/exam/level/generate/level`)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
playSound("check");
|
playSound(typeof result.data === "string" ? "error" : "check");
|
||||||
console.log(result.data);
|
if (typeof result.data === "string") return toast.error("Something went wrong, please try to generate again.");
|
||||||
setExam(result.data);
|
setExam(result.data);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import {Tab} from "@headlessui/react";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {useRouter} from "next/router";
|
import {useRouter} from "next/router";
|
||||||
import {useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {BsArrowRepeat} from "react-icons/bs";
|
import {BsArrowRepeat, BsCheck} from "react-icons/bs";
|
||||||
import {toast} from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
|
|
||||||
const PartTab = ({part, types, index, setPart}: {part?: ListeningPart; types: string[]; index: number; setPart: (part?: ListeningPart) => void}) => {
|
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
|
axios
|
||||||
.get(`/api/exam/listening/generate/listening_section_${index}${topic || types ? `?${url.toString()}` : ""}`)
|
.get(`/api/exam/listening/generate/listening_section_${index}${topic || types ? `?${url.toString()}` : ""}`)
|
||||||
.then((result) => {
|
.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);
|
setPart(result.data);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.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} />
|
<Input type="text" placeholder="Grand Canyon..." name="topic" label="Topic" onChange={setTopic} roundness="xl" defaultValue={topic} />
|
||||||
<button
|
<button
|
||||||
onClick={generate}
|
onClick={generate}
|
||||||
disabled={isLoading}
|
disabled={isLoading || types.length === 0}
|
||||||
data-tip="The passage is currently being generated"
|
data-tip="The passage is currently being generated"
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"bg-ielts-listening/70 border border-ielts-listening text-white w-full max-w-[200px] rounded-xl h-[70px]",
|
"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 [part2, setPart2] = useState<ListeningPart>();
|
||||||
const [part3, setPart3] = useState<ListeningPart>();
|
const [part3, setPart3] = useState<ListeningPart>();
|
||||||
const [part4, setPart4] = useState<ListeningPart>();
|
const [part4, setPart4] = useState<ListeningPart>();
|
||||||
|
const [minTimer, setMinTimer] = useState(30);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [resultingExam, setResultingExam] = useState<ListeningExam>();
|
const [resultingExam, setResultingExam] = useState<ListeningExam>();
|
||||||
const [types, setTypes] = useState<string[]>([]);
|
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 = [
|
const availableTypes = [
|
||||||
{type: "multipleChoice", label: "Multiple Choice"},
|
{type: "multipleChoice", label: "Multiple Choice"},
|
||||||
{type: "writeBlanksQuestions", label: "Write the Blanks: Questions"},
|
{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 toggleType = (type: string) => setTypes((prev) => (prev.includes(type) ? [...prev.filter((x) => x !== type)] : [...prev, type]));
|
||||||
|
|
||||||
const submitExam = () => {
|
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);
|
setIsLoading(true);
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.post(`/api/exam/listening/generate/listening`, {parts: [part1, part2, part3, part4]})
|
.post(`/api/exam/listening/generate/listening`, {parts, minTimer})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
playSound("sent");
|
playSound("sent");
|
||||||
console.log(`Generated Exam ID: ${result.data.id}`);
|
console.log(`Generated Exam ID: ${result.data.id}`);
|
||||||
@@ -172,6 +186,17 @@ const ListeningGeneration = () => {
|
|||||||
|
|
||||||
return (
|
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">
|
<div className="flex flex-col gap-3">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Exercises</label>
|
<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">
|
<div className="flex flex-row -2xl:flex-wrap w-full gap-4 -md:justify-center justify-between">
|
||||||
@@ -197,46 +222,46 @@ const ListeningGeneration = () => {
|
|||||||
<Tab
|
<Tab
|
||||||
className={({selected}) =>
|
className={({selected}) =>
|
||||||
clsx(
|
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",
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-listening focus:outline-none focus:ring-2",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-listening",
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-listening",
|
||||||
)
|
)
|
||||||
}>
|
}>
|
||||||
Section 1
|
Section 1 {part1 && <BsCheck />}
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
className={({selected}) =>
|
className={({selected}) =>
|
||||||
clsx(
|
clsx(
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-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",
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-listening focus:outline-none focus:ring-2",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-listening",
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-listening",
|
||||||
)
|
)
|
||||||
}>
|
}>
|
||||||
Section 2
|
Section 2 {part2 && <BsCheck />}
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
className={({selected}) =>
|
className={({selected}) =>
|
||||||
clsx(
|
clsx(
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-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",
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-listening focus:outline-none focus:ring-2",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-listening",
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-listening",
|
||||||
)
|
)
|
||||||
}>
|
}>
|
||||||
Section 3
|
Section 3 {part3 && <BsCheck />}
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
className={({selected}) =>
|
className={({selected}) =>
|
||||||
clsx(
|
clsx(
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-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",
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-listening focus:outline-none focus:ring-2",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-listening",
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-listening",
|
||||||
)
|
)
|
||||||
}>
|
}>
|
||||||
Section 4
|
Section 4 {part4 && <BsCheck />}
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tab.List>
|
</Tab.List>
|
||||||
<Tab.Panels>
|
<Tab.Panels>
|
||||||
@@ -264,14 +289,14 @@ const ListeningGeneration = () => {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
disabled={!part1 || !part2 || !part3 || !part4 || isLoading}
|
disabled={(!part1 && !part2 && !part3 && !part4) || isLoading}
|
||||||
data-tip="Please generate all three passages"
|
data-tip="Please generate all three passages"
|
||||||
onClick={submitExam}
|
onClick={submitExam}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"bg-ielts-listening/70 border border-ielts-listening text-white w-full max-w-[200px] rounded-xl h-[70px] self-end",
|
"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",
|
"hover:bg-ielts-listening disabled:bg-ielts-listening/40 disabled:cursor-not-allowed",
|
||||||
"transition ease-in-out duration-300",
|
"transition ease-in-out duration-300",
|
||||||
(!part1 || !part2 || !part3 || !part4) && "tooltip",
|
!part1 && !part2 && !part3 && !part4 && "tooltip",
|
||||||
)}>
|
)}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import {Tab} from "@headlessui/react";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {useRouter} from "next/router";
|
import {useRouter} from "next/router";
|
||||||
import {useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {BsArrowRepeat} from "react-icons/bs";
|
import {BsArrowRepeat, BsCheck} from "react-icons/bs";
|
||||||
import {toast} from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
import {v4} from "uuid";
|
import {v4} from "uuid";
|
||||||
|
|
||||||
@@ -27,7 +27,8 @@ const PartTab = ({part, types, index, setPart}: {part?: ReadingPart; types: stri
|
|||||||
axios
|
axios
|
||||||
.get(`/api/exam/reading/generate/reading_passage_${index}${topic || types ? `?${url.toString()}` : ""}`)
|
.get(`/api/exam/reading/generate/reading_passage_${index}${topic || types ? `?${url.toString()}` : ""}`)
|
||||||
.then((result) => {
|
.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);
|
setPart(result.data);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.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} />
|
<Input type="text" placeholder="Grand Canyon..." name="topic" label="Topic" onChange={setTopic} roundness="xl" defaultValue={topic} />
|
||||||
<button
|
<button
|
||||||
onClick={generate}
|
onClick={generate}
|
||||||
disabled={isLoading}
|
disabled={isLoading || types.length === 0}
|
||||||
data-tip="The passage is currently being generated"
|
data-tip="The passage is currently being generated"
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"bg-ielts-reading/70 border border-ielts-reading text-white w-full max-w-[200px] rounded-xl h-[70px]",
|
"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 [part1, setPart1] = useState<ReadingPart>();
|
||||||
const [part2, setPart2] = useState<ReadingPart>();
|
const [part2, setPart2] = useState<ReadingPart>();
|
||||||
const [part3, setPart3] = useState<ReadingPart>();
|
const [part3, setPart3] = useState<ReadingPart>();
|
||||||
|
const [minTimer, setMinTimer] = useState(60);
|
||||||
const [types, setTypes] = useState<string[]>([]);
|
const [types, setTypes] = useState<string[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [resultingExam, setResultingExam] = useState<ReadingExam>();
|
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 router = useRouter();
|
||||||
|
|
||||||
const setExams = useExamStore((state) => state.setExams);
|
const setExams = useExamStore((state) => state.setExams);
|
||||||
@@ -122,19 +129,21 @@ const ReadingGeneration = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const submitExam = () => {
|
const submitExam = () => {
|
||||||
if (!part1 || !part2 || !part3) {
|
const parts = [part1, part2, part3].filter((x) => !!x) as ReadingPart[];
|
||||||
toast.error("Please generate all three passages before submitting");
|
if (parts.length === 0) {
|
||||||
|
toast.error("Please generate at least one passage before submitting");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const exam: ReadingExam = {
|
const exam: ReadingExam = {
|
||||||
parts: [part1, part2, part3],
|
parts,
|
||||||
isDiagnostic: false,
|
isDiagnostic: false,
|
||||||
minTimer: 60,
|
minTimer,
|
||||||
module: "reading",
|
module: "reading",
|
||||||
id: v4(),
|
id: v4(),
|
||||||
type: "academic",
|
type: "academic",
|
||||||
|
variant: parts.length === 3 ? "full" : "partial",
|
||||||
};
|
};
|
||||||
|
|
||||||
axios
|
axios
|
||||||
@@ -148,6 +157,7 @@ const ReadingGeneration = () => {
|
|||||||
setPart1(undefined);
|
setPart1(undefined);
|
||||||
setPart2(undefined);
|
setPart2(undefined);
|
||||||
setPart3(undefined);
|
setPart3(undefined);
|
||||||
|
setMinTimer(60);
|
||||||
setTypes([]);
|
setTypes([]);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -159,6 +169,17 @@ const ReadingGeneration = () => {
|
|||||||
|
|
||||||
return (
|
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">
|
<div className="flex flex-col gap-3">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Exercises</label>
|
<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">
|
<div className="flex flex-row -2xl:flex-wrap w-full gap-4 -md:justify-center justify-between">
|
||||||
@@ -182,35 +203,35 @@ const ReadingGeneration = () => {
|
|||||||
<Tab
|
<Tab
|
||||||
className={({selected}) =>
|
className={({selected}) =>
|
||||||
clsx(
|
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",
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-reading focus:outline-none focus:ring-2",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-reading",
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-reading",
|
||||||
)
|
)
|
||||||
}>
|
}>
|
||||||
Passage 1
|
Passage 1 {part1 && <BsCheck />}
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
className={({selected}) =>
|
className={({selected}) =>
|
||||||
clsx(
|
clsx(
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-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",
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-reading focus:outline-none focus:ring-2",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-reading",
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-reading",
|
||||||
)
|
)
|
||||||
}>
|
}>
|
||||||
Passage 2
|
Passage 2 {part2 && <BsCheck />}
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
className={({selected}) =>
|
className={({selected}) =>
|
||||||
clsx(
|
clsx(
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-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",
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-reading focus:outline-none focus:ring-2",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-reading",
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-reading",
|
||||||
)
|
)
|
||||||
}>
|
}>
|
||||||
Passage 3
|
Passage 3 {part3 && <BsCheck />}
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tab.List>
|
</Tab.List>
|
||||||
<Tab.Panels>
|
<Tab.Panels>
|
||||||
@@ -237,14 +258,14 @@ const ReadingGeneration = () => {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
disabled={!part1 || !part2 || !part3 || isLoading}
|
disabled={(!part1 && !part2 && !part3) || isLoading}
|
||||||
data-tip="Please generate all three passages"
|
data-tip="Please generate all three passages"
|
||||||
onClick={submitExam}
|
onClick={submitExam}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"bg-ielts-reading/70 border border-ielts-reading text-white w-full max-w-[200px] rounded-xl h-[70px] self-end",
|
"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",
|
"hover:bg-ielts-reading disabled:bg-ielts-reading/40 disabled:cursor-not-allowed",
|
||||||
"transition ease-in-out duration-300",
|
"transition ease-in-out duration-300",
|
||||||
(!part1 || !part2 || !part3) && "tooltip",
|
!part1 && !part2 && !part3 && "tooltip",
|
||||||
)}>
|
)}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Input from "@/components/Low/Input";
|
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 useExamStore from "@/stores/examStore";
|
||||||
import {getExamById} from "@/utils/exams";
|
import {getExamById} from "@/utils/exams";
|
||||||
import {playSound} from "@/utils/sound";
|
import {playSound} from "@/utils/sound";
|
||||||
@@ -7,10 +7,12 @@ import {convertCamelCaseToReadable} from "@/utils/string";
|
|||||||
import {Tab} from "@headlessui/react";
|
import {Tab} from "@headlessui/react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import moment from "moment";
|
||||||
import {useRouter} from "next/router";
|
import {useRouter} from "next/router";
|
||||||
import {useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {BsArrowRepeat} from "react-icons/bs";
|
import {BsArrowRepeat, BsCheck} from "react-icons/bs";
|
||||||
import {toast} from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
|
import {v4} from "uuid";
|
||||||
|
|
||||||
const PartTab = ({part, index, setPart}: {part?: SpeakingPart; index: number; setPart: (part?: SpeakingPart) => void}) => {
|
const PartTab = ({part, index, setPart}: {part?: SpeakingPart; index: number; setPart: (part?: SpeakingPart) => void}) => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@@ -18,10 +20,12 @@ const PartTab = ({part, index, setPart}: {part?: SpeakingPart; index: number; se
|
|||||||
const generate = () => {
|
const generate = () => {
|
||||||
setPart(undefined);
|
setPart(undefined);
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.get(`/api/exam/speaking/generate/speaking_task_${index}`)
|
.get(`/api/exam/speaking/generate/speaking_task_${index}`)
|
||||||
.then((result) => {
|
.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);
|
setPart(result.data);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -31,6 +35,29 @@ const PartTab = ({part, index, setPart}: {part?: SpeakingPart; index: number; se
|
|||||||
.finally(() => setIsLoading(false));
|
.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 (
|
return (
|
||||||
<Tab.Panel className="w-full bg-ielts-speaking/20 min-h-[600px] h-full rounded-xl p-6 flex flex-col gap-4">
|
<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">
|
<div className="flex gap-4 items-end">
|
||||||
@@ -52,6 +79,24 @@ const PartTab = ({part, index, setPart}: {part?: SpeakingPart; index: number; se
|
|||||||
"Generate"
|
"Generate"
|
||||||
)}
|
)}
|
||||||
</button>
|
</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>
|
</div>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="w-fit h-fit mt-12 self-center animate-pulse flex flex-col gap-8 items-center">
|
<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>
|
<span className={clsx("font-bold text-2xl text-ielts-speaking")}>Generating...</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{part && (
|
{part && !isLoading && (
|
||||||
<div className="flex flex-col gap-2 w-full overflow-y-scroll scrollbar-hide h-96">
|
<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>
|
<h3 className="text-xl font-semibold">{part.topic}</h3>
|
||||||
{part.question && <span className="w-full">{part.question}</span>}
|
{part.question && <span className="w-full">{part.question}</span>}
|
||||||
@@ -82,6 +127,7 @@ const PartTab = ({part, index, setPart}: {part?: SpeakingPart; index: number; se
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{part.result && <span className="font-bold mt-4">Video Generated: ✅</span>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
@@ -93,27 +139,43 @@ interface SpeakingPart {
|
|||||||
question?: string;
|
question?: string;
|
||||||
questions?: string[];
|
questions?: string[];
|
||||||
topic: string;
|
topic: string;
|
||||||
|
result?: SpeakingExercise | InteractiveSpeakingExercise;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SpeakingGeneration = () => {
|
const SpeakingGeneration = () => {
|
||||||
const [part1, setPart1] = useState<SpeakingPart>();
|
const [part1, setPart1] = useState<SpeakingPart>();
|
||||||
const [part2, setPart2] = useState<SpeakingPart>();
|
const [part2, setPart2] = useState<SpeakingPart>();
|
||||||
const [part3, setPart3] = useState<SpeakingPart>();
|
const [part3, setPart3] = useState<SpeakingPart>();
|
||||||
|
const [minTimer, setMinTimer] = useState(14);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [resultingExam, setResultingExam] = useState<SpeakingExam>();
|
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 router = useRouter();
|
||||||
|
|
||||||
const setExams = useExamStore((state) => state.setExams);
|
const setExams = useExamStore((state) => state.setExams);
|
||||||
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
|
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
|
||||||
|
|
||||||
const submitExam = () => {
|
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);
|
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
|
axios
|
||||||
.post(`/api/exam/speaking/generate/speaking`, {exercises: [part1, part2, part3]})
|
.post(`/api/exam/speaking`, exam)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
playSound("sent");
|
playSound("sent");
|
||||||
console.log(`Generated Exam ID: ${result.data.id}`);
|
console.log(`Generated Exam ID: ${result.data.id}`);
|
||||||
@@ -123,10 +185,11 @@ const SpeakingGeneration = () => {
|
|||||||
setPart1(undefined);
|
setPart1(undefined);
|
||||||
setPart2(undefined);
|
setPart2(undefined);
|
||||||
setPart3(undefined);
|
setPart3(undefined);
|
||||||
|
setMinTimer(14);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
toast.error("Something went wrong!");
|
toast.error("Something went wrong while generating, please try again later.");
|
||||||
})
|
})
|
||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
};
|
};
|
||||||
@@ -149,40 +212,51 @@ const SpeakingGeneration = () => {
|
|||||||
|
|
||||||
return (
|
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.Group>
|
||||||
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-speaking/20 p-1">
|
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-speaking/20 p-1">
|
||||||
<Tab
|
<Tab
|
||||||
className={({selected}) =>
|
className={({selected}) =>
|
||||||
clsx(
|
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",
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking",
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking",
|
||||||
)
|
)
|
||||||
}>
|
}>
|
||||||
Task 1
|
Exercise 1 {part1 && part1.result && <BsCheck />}
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
className={({selected}) =>
|
className={({selected}) =>
|
||||||
clsx(
|
clsx(
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/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",
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking",
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking",
|
||||||
)
|
)
|
||||||
}>
|
}>
|
||||||
Task 2
|
Exercise 2 {part2 && part2.result && <BsCheck />}
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
className={({selected}) =>
|
className={({selected}) =>
|
||||||
clsx(
|
clsx(
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/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",
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking focus:outline-none focus:ring-2",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking",
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking",
|
||||||
)
|
)
|
||||||
}>
|
}>
|
||||||
Task 3
|
Interactive {part3 && part3.result && <BsCheck />}
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tab.List>
|
</Tab.List>
|
||||||
<Tab.Panels>
|
<Tab.Panels>
|
||||||
@@ -209,14 +283,14 @@ const SpeakingGeneration = () => {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
disabled={!part1 || !part2 || !part3 || isLoading}
|
disabled={(!part1?.result && !part2?.result && !part3?.result) || isLoading}
|
||||||
data-tip="Please generate all three passages"
|
data-tip="Please generate all three passages"
|
||||||
onClick={submitExam}
|
onClick={submitExam}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"bg-ielts-speaking/70 border border-ielts-speaking text-white w-full max-w-[200px] rounded-xl h-[70px] self-end",
|
"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",
|
"hover:bg-ielts-speaking disabled:bg-ielts-speaking/40 disabled:cursor-not-allowed",
|
||||||
"transition ease-in-out duration-300",
|
"transition ease-in-out duration-300",
|
||||||
(!part1 || !part2 || !part3) && "tooltip",
|
!part1 && !part2 && !part3 && "tooltip",
|
||||||
)}>
|
)}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Input from "@/components/Low/Input";
|
import Input from "@/components/Low/Input";
|
||||||
import {WritingExam} from "@/interfaces/exam";
|
import {WritingExam, WritingExercise} from "@/interfaces/exam";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import {getExamById} from "@/utils/exams";
|
import {getExamById} from "@/utils/exams";
|
||||||
import {playSound} from "@/utils/sound";
|
import {playSound} from "@/utils/sound";
|
||||||
@@ -7,8 +7,8 @@ import {Tab} from "@headlessui/react";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {useRouter} from "next/router";
|
import {useRouter} from "next/router";
|
||||||
import {useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {BsArrowRepeat} from "react-icons/bs";
|
import {BsArrowRepeat, BsCheck} from "react-icons/bs";
|
||||||
import {toast} from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
import {v4} from "uuid";
|
import {v4} from "uuid";
|
||||||
|
|
||||||
@@ -20,7 +20,8 @@ const TaskTab = ({task, index, setTask}: {task?: string; index: number; setTask:
|
|||||||
axios
|
axios
|
||||||
.get(`/api/exam/writing/generate/writing_task${index}_general`)
|
.get(`/api/exam/writing/generate/writing_task${index}_general`)
|
||||||
.then((result) => {
|
.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);
|
setTask(result.data.question);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -68,9 +69,16 @@ const TaskTab = ({task, index, setTask}: {task?: string; index: number; setTask:
|
|||||||
const WritingGeneration = () => {
|
const WritingGeneration = () => {
|
||||||
const [task1, setTask1] = useState<string>();
|
const [task1, setTask1] = useState<string>();
|
||||||
const [task2, setTask2] = useState<string>();
|
const [task2, setTask2] = useState<string>();
|
||||||
|
const [minTimer, setMinTimer] = useState(60);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [resultingExam, setResultingExam] = useState<WritingExam>();
|
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 router = useRouter();
|
||||||
|
|
||||||
const setExams = useExamStore((state) => state.setExams);
|
const setExams = useExamStore((state) => state.setExams);
|
||||||
@@ -93,18 +101,13 @@ const WritingGeneration = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const submitExam = () => {
|
const submitExam = () => {
|
||||||
if (!task1 || !task2) {
|
if (!task1 && !task2) {
|
||||||
toast.error("Please generate all tasks before submitting");
|
toast.error("Please generate a task before submitting");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
const exercise1 = task1
|
||||||
const exam: WritingExam = {
|
? ({
|
||||||
isDiagnostic: false,
|
|
||||||
minTimer: 60,
|
|
||||||
module: "writing",
|
|
||||||
exercises: [
|
|
||||||
{
|
|
||||||
id: v4(),
|
id: v4(),
|
||||||
type: "writing",
|
type: "writing",
|
||||||
prefix: `You should spend about 20 minutes on this task.`,
|
prefix: `You should spend about 20 minutes on this task.`,
|
||||||
@@ -115,8 +118,11 @@ const WritingGeneration = () => {
|
|||||||
limit: 150,
|
limit: 150,
|
||||||
type: "min",
|
type: "min",
|
||||||
},
|
},
|
||||||
},
|
} as WritingExercise)
|
||||||
{
|
: undefined;
|
||||||
|
|
||||||
|
const exercise2 = task2
|
||||||
|
? ({
|
||||||
id: v4(),
|
id: v4(),
|
||||||
type: "writing",
|
type: "writing",
|
||||||
prefix: `You should spend about 40 minutes on this task.`,
|
prefix: `You should spend about 40 minutes on this task.`,
|
||||||
@@ -127,9 +133,17 @@ const WritingGeneration = () => {
|
|||||||
limit: 250,
|
limit: 250,
|
||||||
type: "min",
|
type: "min",
|
||||||
},
|
},
|
||||||
},
|
} as WritingExercise)
|
||||||
],
|
: undefined;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
const exam: WritingExam = {
|
||||||
|
isDiagnostic: false,
|
||||||
|
minTimer,
|
||||||
|
module: "writing",
|
||||||
|
exercises: [...(exercise1 ? [exercise1] : []), ...(exercise2 ? [exercise2] : [])],
|
||||||
id: v4(),
|
id: v4(),
|
||||||
|
variant: exercise1 && exercise2 ? "full" : "partial",
|
||||||
};
|
};
|
||||||
|
|
||||||
axios
|
axios
|
||||||
@@ -152,29 +166,40 @@ const WritingGeneration = () => {
|
|||||||
|
|
||||||
return (
|
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.Group>
|
||||||
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-writing/20 p-1">
|
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-writing/20 p-1">
|
||||||
<Tab
|
<Tab
|
||||||
className={({selected}) =>
|
className={({selected}) =>
|
||||||
clsx(
|
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",
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-writing focus:outline-none focus:ring-2",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-writing",
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-writing",
|
||||||
)
|
)
|
||||||
}>
|
}>
|
||||||
Task 1
|
Task 1 {task1 && <BsCheck />}
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
className={({selected}) =>
|
className={({selected}) =>
|
||||||
clsx(
|
clsx(
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-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",
|
"ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-writing focus:outline-none focus:ring-2",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-writing",
|
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-writing",
|
||||||
)
|
)
|
||||||
}>
|
}>
|
||||||
Task 2
|
Task 2 {task2 && <BsCheck />}
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tab.List>
|
</Tab.List>
|
||||||
<Tab.Panels>
|
<Tab.Panels>
|
||||||
@@ -200,14 +225,14 @@ const WritingGeneration = () => {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
disabled={!task1 || !task2 || isLoading}
|
disabled={(!task1 && !task2) || isLoading}
|
||||||
data-tip="Please generate all three passages"
|
data-tip="Please generate all three passages"
|
||||||
onClick={submitExam}
|
onClick={submitExam}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"bg-ielts-writing/70 border border-ielts-writing text-white w-full max-w-[200px] rounded-xl h-[70px] self-end",
|
"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",
|
"hover:bg-ielts-writing disabled:bg-ielts-writing/40 disabled:cursor-not-allowed",
|
||||||
"transition ease-in-out duration-300",
|
"transition ease-in-out duration-300",
|
||||||
(!task1 || !task2) && "tooltip",
|
!task1 && !task2 && "tooltip",
|
||||||
)}>
|
)}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center">
|
<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) return res.status(401).json({ok: false});
|
||||||
if (req.session.user.type !== "developer") return res.status(403).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 {endpoint, topic, exercises} = req.query as {module: Module; endpoint: string[]; topic?: string; exercises?: string[]};
|
||||||
const url = `${process.env.BACKEND_URL}/${endpoint}`;
|
const url = `${process.env.BACKEND_URL}/${endpoint.join("/")}`;
|
||||||
|
|
||||||
const result = await axios.post(
|
const result = await axios.post(
|
||||||
`${url}${topic && exercises ? `?topic=${topic.toLowerCase()}&exercises=${exercises.join("&exercises=")}` : ""}`,
|
`${url}${topic && exercises ? `?topic=${topic.toLowerCase()}&exercises=${exercises.join("&exercises=")}` : ""}`,
|
||||||
@@ -4,7 +4,7 @@ import {app} from "@/firebase";
|
|||||||
import {getFirestore, setDoc, doc} from "firebase/firestore";
|
import {getFirestore, setDoc, doc} from "firebase/firestore";
|
||||||
import {withIronSessionApiRoute} from "iron-session/next";
|
import {withIronSessionApiRoute} from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import {Exam} from "@/interfaces/exam";
|
import {Exam, Variant} from "@/interfaces/exam";
|
||||||
import {getExams} from "@/utils/exams.be";
|
import {getExams} from "@/utils/exams.be";
|
||||||
const db = getFirestore(app);
|
const db = getFirestore(app);
|
||||||
|
|
||||||
@@ -23,12 +23,9 @@ async function GET(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {module, avoidRepeated, variant} = req.query as {module: string; avoidRepeated: string; variant?: Variant};
|
||||||
module,
|
|
||||||
avoidRepeated,
|
|
||||||
} = req.query as {module: string; avoidRepeated: string};
|
|
||||||
|
|
||||||
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);
|
res.status(200).json(exams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,10 @@ async function patch(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
if (user.type === "admin" || user.type === "developer") {
|
if (user.type === "admin" || user.type === "developer") {
|
||||||
await setDoc(snapshot.ref, req.body, {merge: true});
|
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});
|
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;
|
const updatedDoc = (await getDoc(doc(db, "payments", paymentId))).data() as Payment;
|
||||||
if (updatedDoc.commissionTransfer && updatedDoc.corporateTransfer) {
|
if (updatedDoc.commissionTransfer && updatedDoc.corporateTransfer) {
|
||||||
await setDoc(doc(db, "payments", paymentId), {isPaid: true}, {merge: true});
|
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});
|
res.status(200).json({ref});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,15 +1,6 @@
|
|||||||
import type {NextApiRequest, NextApiResponse} from "next";
|
import type {NextApiRequest, NextApiResponse} from "next";
|
||||||
import {app, storage} from "@/firebase";
|
import {app, storage} from "@/firebase";
|
||||||
import {
|
import {getFirestore, doc, getDoc, updateDoc, getDocs, query, collection, where} from "firebase/firestore";
|
||||||
getFirestore,
|
|
||||||
doc,
|
|
||||||
getDoc,
|
|
||||||
updateDoc,
|
|
||||||
getDocs,
|
|
||||||
query,
|
|
||||||
collection,
|
|
||||||
where,
|
|
||||||
} from "firebase/firestore";
|
|
||||||
import {withIronSessionApiRoute} from "iron-session/next";
|
import {withIronSessionApiRoute} from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import ReactPDF from "@react-pdf/renderer";
|
import ReactPDF from "@react-pdf/renderer";
|
||||||
@@ -23,11 +14,7 @@ import { LevelExamDetails } from "@/exams/pdf/details/level.exam";
|
|||||||
import {calculateBandScore} from "@/utils/score";
|
import {calculateBandScore} from "@/utils/score";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {moduleLabels} from "@/utils/moduleUtils";
|
import {moduleLabels} from "@/utils/moduleUtils";
|
||||||
import {
|
import {generateQRCode, getRadialProgressPNG, streamToBuffer} from "@/utils/pdf";
|
||||||
generateQRCode,
|
|
||||||
getRadialProgressPNG,
|
|
||||||
streamToBuffer,
|
|
||||||
} from "@/utils/pdf";
|
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
|
|
||||||
const db = getFirestore(app);
|
const db = getFirestore(app);
|
||||||
@@ -102,16 +89,14 @@ const getSkillsFeedback = async (sections: SkillsFeedbackRequest[]) => {
|
|||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
|
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return backendRequest.data?.sections;
|
return backendRequest.data?.sections;
|
||||||
};
|
};
|
||||||
|
|
||||||
// perform the request with several retries if needed
|
// perform the request with several retries if needed
|
||||||
const handleSkillsFeedbackRequest = async (
|
const handleSkillsFeedbackRequest = async (sections: SkillsFeedbackRequest[]): Promise<SkillsFeedbackResponse[] | null> => {
|
||||||
sections: SkillsFeedbackRequest[]
|
|
||||||
): Promise<SkillsFeedbackResponse[] | null> => {
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
try {
|
try {
|
||||||
const data = await getSkillsFeedback(sections);
|
const data = await getSkillsFeedback(sections);
|
||||||
@@ -131,13 +116,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
if (req.session.user) {
|
if (req.session.user) {
|
||||||
const {id} = req.query as {id: string};
|
const {id} = req.query as {id: string};
|
||||||
// fetch stats entries for this particular user with the requested exam session
|
// fetch stats entries for this particular user with the requested exam session
|
||||||
const docsSnap = await getDocs(
|
const docsSnap = await getDocs(query(collection(db, "stats"), where("session", "==", id)));
|
||||||
query(
|
|
||||||
collection(db, "stats"),
|
|
||||||
where("session", "==", id),
|
|
||||||
where("user", "==", req.session.user.id)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (docsSnap.empty) {
|
if (docsSnap.empty) {
|
||||||
res.status(400).end();
|
res.status(400).end();
|
||||||
@@ -166,9 +145,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const user = docUser.data() as User;
|
const user = docUser.data() as User;
|
||||||
|
|
||||||
// generate the QR code for the report
|
// generate the QR code for the report
|
||||||
const qrcode = await generateQRCode(
|
const qrcode = await generateQRCode((req.headers.origin || "") + req.url);
|
||||||
(req.headers.origin || "") + req.url
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!qrcode) {
|
if (!qrcode) {
|
||||||
res.status(500).json({ok: false});
|
res.status(500).json({ok: false});
|
||||||
@@ -178,8 +155,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
// stats may contain multiple exams of the same type so we need to aggregate them
|
// stats may contain multiple exams of the same type so we need to aggregate them
|
||||||
const results = (
|
const results = (
|
||||||
stats.reduce((accm: ModuleScore[], {module, score}) => {
|
stats.reduce((accm: ModuleScore[], {module, score}) => {
|
||||||
const fixedModuleStr =
|
const fixedModuleStr = module[0].toUpperCase() + module.substring(1);
|
||||||
module[0].toUpperCase() + module.substring(1);
|
|
||||||
if (accm.find((e: ModuleScore) => e.module === fixedModuleStr)) {
|
if (accm.find((e: ModuleScore) => e.module === fixedModuleStr)) {
|
||||||
return accm.map((e: ModuleScore) => {
|
return accm.map((e: ModuleScore) => {
|
||||||
if (e.module === fixedModuleStr) {
|
if (e.module === fixedModuleStr) {
|
||||||
@@ -207,12 +183,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
).map((moduleScore) => {
|
).map((moduleScore) => {
|
||||||
const {score, total} = moduleScore;
|
const {score, total} = moduleScore;
|
||||||
// with all the scores aggreated we can calculate the band score for each module
|
// with all the scores aggreated we can calculate the band score for each module
|
||||||
const bandScore = calculateBandScore(
|
const bandScore = calculateBandScore(score, total, moduleScore.code as Module, user.focus);
|
||||||
score,
|
|
||||||
total,
|
|
||||||
moduleScore.code as Module,
|
|
||||||
user.focus
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...moduleScore,
|
...moduleScore,
|
||||||
@@ -228,7 +199,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
code,
|
code,
|
||||||
name: moduleLabels[code],
|
name: moduleLabels[code],
|
||||||
grade: bandScore,
|
grade: bandScore,
|
||||||
}))
|
})),
|
||||||
)) as SkillsFeedbackResponse[];
|
)) as SkillsFeedbackResponse[];
|
||||||
|
|
||||||
if (!skillsFeedback) {
|
if (!skillsFeedback) {
|
||||||
@@ -238,9 +209,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
|
|
||||||
// assign the feedback to the results
|
// assign the feedback to the results
|
||||||
const finalResults = results.map((result) => {
|
const finalResults = results.map((result) => {
|
||||||
const feedback = skillsFeedback.find(
|
const feedback = skillsFeedback.find((f: SkillsFeedbackResponse) => f.code === result.code);
|
||||||
(f: SkillsFeedbackResponse) => f.code === result.code
|
|
||||||
);
|
|
||||||
|
|
||||||
if (feedback) {
|
if (feedback) {
|
||||||
return {
|
return {
|
||||||
@@ -254,14 +223,8 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// calculate the overall score out of all the aggregated results
|
// calculate the overall score out of all the aggregated results
|
||||||
const overallScore = results.reduce(
|
const overallScore = results.reduce((accm, {score}) => accm + score, 0);
|
||||||
(accm, { score }) => accm + score,
|
const overallTotal = results.reduce((accm, {total}) => accm + total, 0);
|
||||||
0
|
|
||||||
);
|
|
||||||
const overallTotal = results.reduce(
|
|
||||||
(accm, { total }) => accm + total,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
const overallResult = overallScore / overallTotal;
|
const overallResult = overallScore / overallTotal;
|
||||||
|
|
||||||
const overallPNG = getRadialProgressPNG("laranja", overallScore, overallTotal);
|
const overallPNG = getRadialProgressPNG("laranja", overallScore, overallTotal);
|
||||||
@@ -278,22 +241,14 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const [stat] = stats;
|
const [stat] = stats;
|
||||||
|
|
||||||
// generate the performance summary based on the overall result
|
// generate the performance summary based on the overall result
|
||||||
const performanceSummary = getPerformanceSummary(
|
const performanceSummary = getPerformanceSummary(stat.module, overallResult);
|
||||||
stat.module,
|
|
||||||
overallResult
|
|
||||||
);
|
|
||||||
|
|
||||||
// level exams have a different report structure than the skill exams
|
// level exams have a different report structure than the skill exams
|
||||||
const getCustomData = () => {
|
const getCustomData = () => {
|
||||||
if (stat.module === "level") {
|
if (stat.module === "level") {
|
||||||
return {
|
return {
|
||||||
title: "ENGLISH LEVEL TEST RESULT REPORT ",
|
title: "ENGLISH LEVEL TEST RESULT REPORT ",
|
||||||
details: (
|
details: <LevelExamDetails detail={overallDetail} title="Level as per CEFR Levels" />,
|
||||||
<LevelExamDetails
|
|
||||||
detail={overallDetail}
|
|
||||||
title="Level as per CEFR Levels"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,7 +264,9 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const pdfStream = await ReactPDF.renderToStream(
|
const pdfStream = await ReactPDF.renderToStream(
|
||||||
<TestReport
|
<TestReport
|
||||||
title={title}
|
title={title}
|
||||||
date={moment(stat.date).tz(user.demographicInformation?.timezone || 'UTC').format('ll HH:mm:ss')}
|
date={moment(stat.date)
|
||||||
|
.tz(user.demographicInformation?.timezone || "UTC")
|
||||||
|
.format("ll HH:mm:ss")}
|
||||||
name={user.name}
|
name={user.name}
|
||||||
email={user.email}
|
email={user.email}
|
||||||
id={user.id}
|
id={user.id}
|
||||||
@@ -322,7 +279,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
summaryPNG={overallPNG}
|
summaryPNG={overallPNG}
|
||||||
summaryScore={`${(overallResult * 100).toFixed(0)}%`}
|
summaryScore={`${(overallResult * 100).toFixed(0)}%`}
|
||||||
passportId={demographicInformation?.passport_id || ""}
|
passportId={demographicInformation?.passport_id || ""}
|
||||||
/>
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// generate the file ref for storage
|
// generate the file ref for storage
|
||||||
@@ -361,9 +318,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
|
|
||||||
async function get(req: NextApiRequest, res: NextApiResponse) {
|
async function get(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const {id} = req.query as {id: string};
|
const {id} = req.query as {id: string};
|
||||||
const docsSnap = await getDocs(
|
const docsSnap = await getDocs(query(collection(db, "stats"), where("session", "==", id)));
|
||||||
query(collection(db, "stats"), where("session", "==", id))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (docsSnap.empty) {
|
if (docsSnap.empty) {
|
||||||
res.status(404).end();
|
res.status(404).end();
|
||||||
|
|||||||
@@ -111,11 +111,13 @@ const PaymentCreator = ({onClose, reload, showComission = false}: {onClose: () =
|
|||||||
options={(users.filter((u) => u.type === "corporate") as CorporateUser[]).map((user) => ({
|
options={(users.filter((u) => u.type === "corporate") as CorporateUser[]).map((user) => ({
|
||||||
value: user.id,
|
value: user.id,
|
||||||
meta: user,
|
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"}}
|
defaultValue={{value: "undefined", label: "Select an account"}}
|
||||||
onChange={(value) => setCorporate((value as any)?.meta ?? undefined)}
|
onChange={(value) => setCorporate((value as any)?.meta ?? undefined)}
|
||||||
|
menuPortalTarget={document?.body}
|
||||||
styles={{
|
styles={{
|
||||||
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
...styles,
|
...styles,
|
||||||
paddingLeft: "4px",
|
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"}}
|
defaultValue={{value: currency || "EUR", label: CURRENCIES.find((c) => c.currency === currency)?.label || "Euro"}}
|
||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
value={{value: currency || "EUR", label: CURRENCIES.find((c) => c.currency === currency)?.label || "Euro"}}
|
value={{value: currency || "EUR", label: CURRENCIES.find((c) => c.currency === currency)?.label || "Euro"}}
|
||||||
|
menuPortalTarget={document?.body}
|
||||||
styles={{
|
styles={{
|
||||||
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
...styles,
|
...styles,
|
||||||
paddingLeft: "4px",
|
paddingLeft: "4px",
|
||||||
@@ -824,7 +828,9 @@ export default function PaymentRecord() {
|
|||||||
}
|
}
|
||||||
isDisabled={user.type === "corporate"}
|
isDisabled={user.type === "corporate"}
|
||||||
onChange={(value) => setCorporate((value as any)?.meta ?? undefined)}
|
onChange={(value) => setCorporate((value as any)?.meta ?? undefined)}
|
||||||
|
menuPortalTarget={document?.body}
|
||||||
styles={{
|
styles={{
|
||||||
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
...styles,
|
...styles,
|
||||||
paddingLeft: "4px",
|
paddingLeft: "4px",
|
||||||
@@ -859,7 +865,9 @@ export default function PaymentRecord() {
|
|||||||
}))}
|
}))}
|
||||||
value={agent ? {value: agent?.id, label: `${agent.name} - ${agent.email}`} : undefined}
|
value={agent ? {value: agent?.id, label: `${agent.name} - ${agent.email}`} : undefined}
|
||||||
onChange={(value) => setAgent(value !== null ? (value as any).meta : undefined)}
|
onChange={(value) => setAgent(value !== null ? (value as any).meta : undefined)}
|
||||||
|
menuPortalTarget={document?.body}
|
||||||
styles={{
|
styles={{
|
||||||
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
...styles,
|
...styles,
|
||||||
paddingLeft: "4px",
|
paddingLeft: "4px",
|
||||||
@@ -892,7 +900,9 @@ export default function PaymentRecord() {
|
|||||||
if (value) return setPaid(value.value);
|
if (value) return setPaid(value.value);
|
||||||
setPaid(null);
|
setPaid(null);
|
||||||
}}
|
}}
|
||||||
|
menuPortalTarget={document?.body}
|
||||||
styles={{
|
styles={{
|
||||||
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
...styles,
|
...styles,
|
||||||
paddingLeft: "4px",
|
paddingLeft: "4px",
|
||||||
@@ -947,7 +957,9 @@ export default function PaymentRecord() {
|
|||||||
if (value) return setCommissionTransfer(value.value);
|
if (value) return setCommissionTransfer(value.value);
|
||||||
setCommissionTransfer(null);
|
setCommissionTransfer(null);
|
||||||
}}
|
}}
|
||||||
|
menuPortalTarget={document?.body}
|
||||||
styles={{
|
styles={{
|
||||||
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
...styles,
|
...styles,
|
||||||
paddingLeft: "4px",
|
paddingLeft: "4px",
|
||||||
@@ -979,7 +991,9 @@ export default function PaymentRecord() {
|
|||||||
if (value) return setCorporateTransfer(value.value);
|
if (value) return setCorporateTransfer(value.value);
|
||||||
setCorporateTransfer(null);
|
setCorporateTransfer(null);
|
||||||
}}
|
}}
|
||||||
|
menuPortalTarget={document?.body}
|
||||||
styles={{
|
styles={{
|
||||||
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
control: (styles) => ({
|
control: (styles) => ({
|
||||||
...styles,
|
...styles,
|
||||||
paddingLeft: "4px",
|
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}`}))}
|
options={users.map((x) => ({value: x.id, label: `${x.name} - ${x.email}`}))}
|
||||||
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}}
|
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}}
|
||||||
onChange={(value) => setStatsUserId(value?.value)}
|
onChange={(value) => setStatsUserId(value?.value)}
|
||||||
|
menuPortalTarget={document?.body}
|
||||||
styles={{
|
styles={{
|
||||||
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
option: (styles, state) => ({
|
option: (styles, state) => ({
|
||||||
...styles,
|
...styles,
|
||||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
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}`}))}
|
.map((x) => ({value: x.id, label: `${x.name} - ${x.email}`}))}
|
||||||
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}}
|
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}}
|
||||||
onChange={(value) => setStatsUserId(value?.value)}
|
onChange={(value) => setStatsUserId(value?.value)}
|
||||||
|
menuPortalTarget={document?.body}
|
||||||
styles={{
|
styles={{
|
||||||
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
option: (styles, state) => ({
|
option: (styles, state) => ({
|
||||||
...styles,
|
...styles,
|
||||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
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" />,
|
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",
|
label: "Average Score",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
@@ -193,7 +193,9 @@ export default function Stats() {
|
|||||||
options={users.map((x) => ({value: x.id, label: `${x.name} - ${x.email}`}))}
|
options={users.map((x) => ({value: x.id, label: `${x.name} - ${x.email}`}))}
|
||||||
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}}
|
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}}
|
||||||
onChange={(value) => setStatsUserId(value?.value)}
|
onChange={(value) => setStatsUserId(value?.value)}
|
||||||
|
menuPortalTarget={document?.body}
|
||||||
styles={{
|
styles={{
|
||||||
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
option: (styles, state) => ({
|
option: (styles, state) => ({
|
||||||
...styles,
|
...styles,
|
||||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
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}`}))}
|
.map((x) => ({value: x.id, label: `${x.name} - ${x.email}`}))}
|
||||||
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}}
|
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}}
|
||||||
onChange={(value) => setStatsUserId(value?.value)}
|
onChange={(value) => setStatsUserId(value?.value)}
|
||||||
|
menuPortalTarget={document?.body}
|
||||||
styles={{
|
styles={{
|
||||||
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
option: (styles, state) => ({
|
option: (styles, state) => ({
|
||||||
...styles,
|
...styles,
|
||||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
import {
|
import {collection, getDocs, query, where, setDoc, doc, Firestore} from "firebase/firestore";
|
||||||
collection,
|
|
||||||
getDocs,
|
|
||||||
query,
|
|
||||||
where,
|
|
||||||
setDoc,
|
|
||||||
doc,
|
|
||||||
Firestore,
|
|
||||||
} from "firebase/firestore";
|
|
||||||
import {shuffle} from "lodash";
|
import {shuffle} from "lodash";
|
||||||
import { Exam } from "@/interfaces/exam";
|
import {Exam, Variant} from "@/interfaces/exam";
|
||||||
import {Stat} from "@/interfaces/user";
|
import {Stat} from "@/interfaces/user";
|
||||||
|
|
||||||
export const getExams = async (
|
export const getExams = async (
|
||||||
@@ -18,20 +10,24 @@ export const getExams = async (
|
|||||||
// added userId as due to assignments being set from the teacher to the student
|
// 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
|
// we need to make sure we are serving exams not executed by the user and not
|
||||||
// by the teacher that performed the request
|
// by the teacher that performed the request
|
||||||
userId: string | undefined
|
userId: string | undefined,
|
||||||
|
variant?: Variant,
|
||||||
): Promise<Exam[]> => {
|
): Promise<Exam[]> => {
|
||||||
const moduleRef = collection(db, module);
|
const moduleRef = collection(db, module);
|
||||||
|
|
||||||
const q = query(moduleRef, where("isDiagnostic", "==", false));
|
const q = query(moduleRef, where("isDiagnostic", "==", false));
|
||||||
const snapshot = await getDocs(q);
|
const snapshot = await getDocs(q);
|
||||||
|
|
||||||
const exams: Exam[] = shuffle(
|
const exams: Exam[] = filterByVariant(
|
||||||
|
shuffle(
|
||||||
snapshot.docs.map((doc) => ({
|
snapshot.docs.map((doc) => ({
|
||||||
id: doc.id,
|
id: doc.id,
|
||||||
...doc.data(),
|
...doc.data(),
|
||||||
module,
|
module,
|
||||||
}))
|
})),
|
||||||
) as Exam[];
|
) as Exam[],
|
||||||
|
variant,
|
||||||
|
);
|
||||||
|
|
||||||
if (avoidRepeated === "true") {
|
if (avoidRepeated === "true") {
|
||||||
const statsQ = query(collection(db, "stats"), where("user", "==", userId));
|
const statsQ = query(collection(db, "stats"), where("user", "==", userId));
|
||||||
@@ -41,12 +37,15 @@ export const getExams = async (
|
|||||||
id: doc.id,
|
id: doc.id,
|
||||||
...doc.data(),
|
...doc.data(),
|
||||||
})) as unknown as Stat[];
|
})) as unknown as Stat[];
|
||||||
const filteredExams = exams.filter(
|
const filteredExams = exams.filter((x) => !stats.map((s) => s.exam).includes(x.id));
|
||||||
(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 {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";
|
import axios from "axios";
|
||||||
|
|
||||||
export const getExam = async (module: Module, avoidRepeated: boolean): Promise<Exam | undefined> => {
|
export const getExam = async (module: Module, avoidRepeated: boolean, variant?: Variant): Promise<Exam | undefined> => {
|
||||||
const examRequest = await axios<Exam[]>(`/api/exam/${module}?avoidRepeated=${avoidRepeated}`);
|
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) {
|
if (examRequest.status !== 200) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import moment from "moment";
|
|||||||
|
|
||||||
export function dateSorter(a: any, b: any, direction: "asc" | "desc", key: string) {
|
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 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(a[key]).isAfter(b[key])) return direction === "asc" ? 1 : -1;
|
||||||
if (moment(b[key]).isAfter(a[key])) return direction === "asc" ? -1 : 1;
|
if (moment(b[key]).isAfter(a[key])) return direction === "asc" ? -1 : 1;
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {Howl, Howler} from "howler";
|
import {Howl, Howler} from "howler";
|
||||||
|
|
||||||
export type Sound = "check" | "sent";
|
export type Sound = "check" | "sent" | "error";
|
||||||
export const playSound = (path: Sound) => {
|
export const playSound = (path: Sound) => {
|
||||||
const sound = new Howl({
|
const sound = new Howl({
|
||||||
src: [`audio/${path}.mp3`],
|
src: [`audio/${path}.mp3`],
|
||||||
|
|||||||
Reference in New Issue
Block a user