Compare commits
4 Commits
feature/ex
...
feature/sp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03f78ceb46 | ||
|
|
872cc62fe4 | ||
|
|
ce7032c8a7 | ||
|
|
71f07af2eb |
@@ -137,7 +137,7 @@ export default function TicketDisplay({ user, ticket, onClose }: Props) {
|
|||||||
options={[
|
options={[
|
||||||
{ value: "me", label: "Assign to me" },
|
{ value: "me", label: "Assign to me" },
|
||||||
...users
|
...users
|
||||||
.filter((x) => ["admin", "developer"].includes(x.type))
|
.filter((x) => ["admin", "developer", "agent"].includes(x.type))
|
||||||
.map((u) => ({
|
.map((u) => ({
|
||||||
value: u.id,
|
value: u.id,
|
||||||
label: `${u.name} - ${u.email}`,
|
label: `${u.name} - ${u.email}`,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { ComponentProps } from "react";
|
import {ComponentProps, useEffect, useState} from "react";
|
||||||
import ReactSelect from "react-select";
|
import ReactSelect from "react-select";
|
||||||
|
|
||||||
interface Option {
|
interface Option {
|
||||||
@@ -18,27 +18,24 @@ interface Props {
|
|||||||
isClearable?: boolean;
|
isClearable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Select({
|
export default function Select({value, defaultValue, options, placeholder, disabled, onChange, isClearable}: Props) {
|
||||||
value,
|
const [target, setTarget] = useState<HTMLElement>();
|
||||||
defaultValue,
|
|
||||||
options,
|
useEffect(() => {
|
||||||
placeholder,
|
if (document) setTarget(document.body);
|
||||||
disabled,
|
}, []);
|
||||||
onChange,
|
|
||||||
isClearable,
|
|
||||||
}: Props) {
|
|
||||||
return (
|
return (
|
||||||
<ReactSelect
|
<ReactSelect
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"placeholder:text-mti-gray-cool border-mti-gray-platinum w-full rounded-full border bg-white px-4 py-4 text-sm font-normal focus:outline-none",
|
"placeholder:text-mti-gray-cool border-mti-gray-platinum w-full rounded-full border bg-white px-4 py-4 text-sm font-normal focus:outline-none",
|
||||||
disabled &&
|
disabled && "!bg-mti-gray-platinum/40 !text-mti-gray-dim cursor-not-allowed",
|
||||||
"!bg-mti-gray-platinum/40 !text-mti-gray-dim cursor-not-allowed",
|
|
||||||
)}
|
)}
|
||||||
options={options}
|
options={options}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
menuPortalTarget={document?.body}
|
menuPortalTarget={target}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
styles={{
|
styles={{
|
||||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
menuPortal: (base) => ({...base, zIndex: 9999}),
|
||||||
@@ -53,11 +50,7 @@ export default function Select({
|
|||||||
}),
|
}),
|
||||||
option: (styles, state) => ({
|
option: (styles, state) => ({
|
||||||
...styles,
|
...styles,
|
||||||
backgroundColor: state.isFocused
|
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||||
? "#D5D9F0"
|
|
||||||
: state.isSelected
|
|
||||||
? "#7872BF"
|
|
||||||
: "white",
|
|
||||||
color: state.isFocused ? "black" : styles.color,
|
color: state.isFocused ? "black" : styles.color,
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ import {toast} from "react-toastify";
|
|||||||
import {uuidv4} from "@firebase/util";
|
import {uuidv4} from "@firebase/util";
|
||||||
import {Assignment} from "@/interfaces/results";
|
import {Assignment} from "@/interfaces/results";
|
||||||
import Checkbox from "@/components/Low/Checkbox";
|
import Checkbox from "@/components/Low/Checkbox";
|
||||||
import {Variant} from "@/interfaces/exam";
|
import {InstructorGender, Variant} from "@/interfaces/exam";
|
||||||
|
import Select from "@/components/Low/Select";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isCreating: boolean;
|
isCreating: boolean;
|
||||||
@@ -40,6 +41,7 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
|
|||||||
assignment ? moment(assignment.endDate).toDate() : moment().hours(23).minutes(59).add(8, "day").toDate(),
|
assignment ? moment(assignment.endDate).toDate() : moment().hours(23).minutes(59).add(8, "day").toDate(),
|
||||||
);
|
);
|
||||||
const [variant, setVariant] = useState<Variant>("full");
|
const [variant, setVariant] = useState<Variant>("full");
|
||||||
|
const [instructorGender, setInstructorGender] = useState<InstructorGender>(assignment?.instructorGender || "varied");
|
||||||
// creates a new exam for each assignee or just one exam for all assignees
|
// creates a new exam for each assignee or just one exam for all assignees
|
||||||
const [generateMultiple, setGenerateMultiple] = useState<boolean>(false);
|
const [generateMultiple, setGenerateMultiple] = useState<boolean>(false);
|
||||||
|
|
||||||
@@ -63,6 +65,7 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
|
|||||||
selectedModules,
|
selectedModules,
|
||||||
generateMultiple,
|
generateMultiple,
|
||||||
variant,
|
variant,
|
||||||
|
instructorGender,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(`The assignment "${name}" has been ${assignment ? "updated" : "created"} successfully!`);
|
toast.success(`The assignment "${name}" has been ${assignment ? "updated" : "created"} successfully!`);
|
||||||
@@ -226,6 +229,20 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3 w-full">
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">Speaking Instructor's Gender</label>
|
||||||
|
<Select
|
||||||
|
value={{value: instructorGender, label: capitalize(instructorGender)}}
|
||||||
|
onChange={(value) => (value ? setInstructorGender(value.value as InstructorGender) : null)}
|
||||||
|
disabled={!selectedModules.includes("speaking") || !!assignment}
|
||||||
|
options={[
|
||||||
|
{value: "male", label: "Male"},
|
||||||
|
{value: "female", label: "Female"},
|
||||||
|
{value: "varied", label: "Varied"},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<section className="w-full flex flex-col gap-3">
|
<section className="w-full flex flex-col gap-3">
|
||||||
<span className="font-semibold">Assignees ({assignees.length} selected)</span>
|
<span className="font-semibold">Assignees ({assignees.length} selected)</span>
|
||||||
<div className="flex gap-4 overflow-x-scroll scrollbar-hide">
|
<div className="flex gap-4 overflow-x-scroll scrollbar-hide">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {Module} from ".";
|
|||||||
|
|
||||||
export type Exam = ReadingExam | ListeningExam | WritingExam | SpeakingExam | LevelExam;
|
export type Exam = ReadingExam | ListeningExam | WritingExam | SpeakingExam | LevelExam;
|
||||||
export type Variant = "full" | "partial";
|
export type Variant = "full" | "partial";
|
||||||
|
export type InstructorGender = "male" | "female" | "varied";
|
||||||
|
|
||||||
export interface ReadingExam {
|
export interface ReadingExam {
|
||||||
parts: ReadingPart[];
|
parts: ReadingPart[];
|
||||||
@@ -82,6 +83,7 @@ export interface SpeakingExam {
|
|||||||
minTimer: number;
|
minTimer: number;
|
||||||
isDiagnostic: boolean;
|
isDiagnostic: boolean;
|
||||||
variant?: Variant;
|
variant?: Variant;
|
||||||
|
instructorGender: InstructorGender;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Exercise =
|
export type Exercise =
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {Module} from "@/interfaces";
|
import {Module} from "@/interfaces";
|
||||||
|
import {InstructorGender} from "./exam";
|
||||||
import {Stat} from "./user";
|
import {Stat} from "./user";
|
||||||
|
|
||||||
export type UserResults = {[key in Module]: ModuleResult};
|
export type UserResults = {[key in Module]: ModuleResult};
|
||||||
@@ -19,7 +20,8 @@ export interface Assignment {
|
|||||||
type: "academic" | "general";
|
type: "academic" | "general";
|
||||||
stats: Stat[];
|
stats: Stat[];
|
||||||
}[];
|
}[];
|
||||||
exams: {id: string; module: Module, assignee: string}[];
|
exams: {id: string; module: Module; assignee: string}[];
|
||||||
|
instructorGender?: InstructorGender;
|
||||||
startDate: Date;
|
startDate: Date;
|
||||||
endDate: Date;
|
endDate: Date;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {Module} from ".";
|
import {Module} from ".";
|
||||||
|
import {InstructorGender} from "./exam";
|
||||||
|
|
||||||
export type User = StudentUser | TeacherUser | CorporateUser | AgentUser | AdminUser | DeveloperUser;
|
export type User = StudentUser | TeacherUser | CorporateUser | AgentUser | AdminUser | DeveloperUser;
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ export interface BasicUser {
|
|||||||
|
|
||||||
export interface StudentUser extends BasicUser {
|
export interface StudentUser extends BasicUser {
|
||||||
type: "student";
|
type: "student";
|
||||||
|
preferredGender?: InstructorGender;
|
||||||
demographicInformation?: DemographicInformation;
|
demographicInformation?: DemographicInformation;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +50,7 @@ export interface AdminUser extends BasicUser {
|
|||||||
|
|
||||||
export interface DeveloperUser extends BasicUser {
|
export interface DeveloperUser extends BasicUser {
|
||||||
type: "developer";
|
type: "developer";
|
||||||
|
preferredGender?: InstructorGender;
|
||||||
demographicInformation?: DemographicInformation;
|
demographicInformation?: DemographicInformation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ export default function ExamPage({page}: Props) {
|
|||||||
setSessionId(shortUID.randomUUID(8));
|
setSessionId(shortUID.randomUUID(8));
|
||||||
}
|
}
|
||||||
}, [setSessionId, selectedModules, sessionId]);
|
}, [setSessionId, selectedModules, sessionId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user?.type === "developer") console.log(exam);
|
if (user?.type === "developer") console.log(exam);
|
||||||
}, [exam, user]);
|
}, [exam, user]);
|
||||||
@@ -160,7 +161,14 @@ 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, variant));
|
const examPromises = selectedModules.map((module) =>
|
||||||
|
getExam(
|
||||||
|
module,
|
||||||
|
avoidRepeated,
|
||||||
|
variant,
|
||||||
|
user?.type === "student" || user?.type === "developer" ? user.preferredGender : undefined,
|
||||||
|
),
|
||||||
|
);
|
||||||
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!));
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import Input from "@/components/Low/Input";
|
import Input from "@/components/Low/Input";
|
||||||
|
import Select from "@/components/Low/Select";
|
||||||
import {Exercise, InteractiveSpeakingExercise, SpeakingExam, SpeakingExercise} from "@/interfaces/exam";
|
import {Exercise, InteractiveSpeakingExercise, SpeakingExam, SpeakingExercise} from "@/interfaces/exam";
|
||||||
|
import {AVATARS} from "@/resources/speakingAvatars";
|
||||||
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,6 +9,7 @@ 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 {capitalize, sample, uniq} from "lodash";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import {useRouter} from "next/router";
|
import {useRouter} from "next/router";
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
@@ -15,6 +18,7 @@ import {toast} from "react-toastify";
|
|||||||
import {v4} from "uuid";
|
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 [gender, setGender] = useState<"male" | "female">("male");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const generate = () => {
|
const generate = () => {
|
||||||
@@ -39,17 +43,19 @@ const PartTab = ({part, index, setPart}: {part?: SpeakingPart; index: number; se
|
|||||||
if (!part) return toast.error("Please generate the first part before generating the video!");
|
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.");
|
toast.info("This will take quite a while, please do not leave this page or close the tab/window.");
|
||||||
|
|
||||||
|
const avatar = sample(AVATARS.filter((x) => x.gender === gender));
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const initialTime = moment();
|
const initialTime = moment();
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.post(`/api/exam/speaking/generate/speaking/generate_${index === 3 ? "interactive" : "speaking"}_video`, part)
|
.post(`/api/exam/speaking/generate/speaking/generate_${index === 3 ? "interactive" : "speaking"}_video`, {...part, avatar: avatar?.id})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
const isError = typeof result.data === "string" || moment().diff(initialTime, "seconds") < 60;
|
const isError = typeof result.data === "string" || moment().diff(initialTime, "seconds") < 60;
|
||||||
|
|
||||||
playSound(isError ? "error" : "check");
|
playSound(isError ? "error" : "check");
|
||||||
if (isError) return toast.error("Something went wrong, please try to generate the video again.");
|
if (isError) return toast.error("Something went wrong, please try to generate the video again.");
|
||||||
setPart({...part, result: result.data});
|
setPart({...part, result: result.data, gender, avatar});
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
toast.error("Something went wrong!");
|
toast.error("Something went wrong!");
|
||||||
@@ -60,6 +66,18 @@ const PartTab = ({part, index, setPart}: {part?: SpeakingPart; index: number; se
|
|||||||
|
|
||||||
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 flex-col gap-3 w-full">
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">Gender</label>
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{value: "male", label: "Male"},
|
||||||
|
{value: "female", label: "Female"},
|
||||||
|
]}
|
||||||
|
value={{value: gender, label: capitalize(gender)}}
|
||||||
|
onChange={(value) => (value ? setGender(value.value as typeof gender) : null)}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="flex gap-4 items-end">
|
<div className="flex gap-4 items-end">
|
||||||
<button
|
<button
|
||||||
onClick={generate}
|
onClick={generate}
|
||||||
@@ -128,6 +146,11 @@ const PartTab = ({part, index, setPart}: {part?: SpeakingPart; index: number; se
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{part.result && <span className="font-bold mt-4">Video Generated: ✅</span>}
|
{part.result && <span className="font-bold mt-4">Video Generated: ✅</span>}
|
||||||
|
{part.avatar && part.gender && (
|
||||||
|
<span>
|
||||||
|
<b>Instructor:</b> {part.avatar.name} - {capitalize(part.avatar.gender)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
@@ -140,6 +163,8 @@ interface SpeakingPart {
|
|||||||
questions?: string[];
|
questions?: string[];
|
||||||
topic: string;
|
topic: string;
|
||||||
result?: SpeakingExercise | InteractiveSpeakingExercise;
|
result?: SpeakingExercise | InteractiveSpeakingExercise;
|
||||||
|
gender?: "male" | "female";
|
||||||
|
avatar?: (typeof AVATARS)[number];
|
||||||
}
|
}
|
||||||
|
|
||||||
const SpeakingGeneration = () => {
|
const SpeakingGeneration = () => {
|
||||||
@@ -165,6 +190,8 @@ const SpeakingGeneration = () => {
|
|||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const genders = [part1?.gender, part2?.gender, part3?.gender].filter((x) => !!x);
|
||||||
|
|
||||||
const exam: SpeakingExam = {
|
const exam: SpeakingExam = {
|
||||||
id: v4(),
|
id: v4(),
|
||||||
isDiagnostic: false,
|
isDiagnostic: false,
|
||||||
@@ -172,6 +199,7 @@ const SpeakingGeneration = () => {
|
|||||||
minTimer,
|
minTimer,
|
||||||
variant: minTimer >= 14 ? "full" : "partial",
|
variant: minTimer >= 14 ? "full" : "partial",
|
||||||
module: "speaking",
|
module: "speaking",
|
||||||
|
instructorGender: genders.every((x) => x === "male") ? "male" : genders.every((x) => x === "female") ? "female" : "varied",
|
||||||
};
|
};
|
||||||
|
|
||||||
axios
|
axios
|
||||||
|
|||||||
@@ -1,22 +1,13 @@
|
|||||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
import type {NextApiRequest, NextApiResponse} from "next";
|
import type {NextApiRequest, NextApiResponse} from "next";
|
||||||
import {app} from "@/firebase";
|
import {app} from "@/firebase";
|
||||||
import {
|
import {getFirestore, collection, getDocs, query, where, setDoc, doc, getDoc} from "firebase/firestore";
|
||||||
getFirestore,
|
|
||||||
collection,
|
|
||||||
getDocs,
|
|
||||||
query,
|
|
||||||
where,
|
|
||||||
setDoc,
|
|
||||||
doc,
|
|
||||||
getDoc,
|
|
||||||
} 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 {uuidv4} from "@firebase/util";
|
import {uuidv4} from "@firebase/util";
|
||||||
import {Module} from "@/interfaces";
|
import {Module} from "@/interfaces";
|
||||||
import {getExams} from "@/utils/exams.be";
|
import {getExams} from "@/utils/exams.be";
|
||||||
import { Exam, Variant } from "@/interfaces/exam";
|
import {Exam, InstructorGender, Variant} from "@/interfaces/exam";
|
||||||
import {capitalize, flatten, uniqBy} from "lodash";
|
import {capitalize, flatten, uniqBy} from "lodash";
|
||||||
import {User} from "@/interfaces/user";
|
import {User} from "@/interfaces/user";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
@@ -66,20 +57,14 @@ const generateExams = async (
|
|||||||
selectedModules: Module[],
|
selectedModules: Module[],
|
||||||
assignees: string[],
|
assignees: string[],
|
||||||
variant?: Variant,
|
variant?: Variant,
|
||||||
|
instructorGender?: InstructorGender,
|
||||||
): Promise<ExamWithUser[]> => {
|
): Promise<ExamWithUser[]> => {
|
||||||
if (generateMultiple) {
|
if (generateMultiple) {
|
||||||
// for optimization purposes, it would be better to create a new endpoint that returned the answers for all users at once
|
// for optimization purposes, it would be better to create a new endpoint that returned the answers for all users at once
|
||||||
const allExams = assignees.map(async (assignee) => {
|
const allExams = assignees.map(async (assignee) => {
|
||||||
const selectedModulePromises = selectedModules.map(
|
const selectedModulePromises = selectedModules.map(async (module: Module) => {
|
||||||
async (module: Module) => {
|
|
||||||
try {
|
try {
|
||||||
const exams: Exam[] = await getExams(
|
const exams: Exam[] = await getExams(db, module, "true", assignee, variant, instructorGender);
|
||||||
db,
|
|
||||||
module,
|
|
||||||
"true",
|
|
||||||
assignee,
|
|
||||||
variant,
|
|
||||||
);
|
|
||||||
|
|
||||||
const exam = exams[getRandomIndex(exams)];
|
const exam = exams[getRandomIndex(exams)];
|
||||||
if (exam) {
|
if (exam) {
|
||||||
@@ -90,22 +75,18 @@ const generateExams = async (
|
|||||||
console.error(e);
|
console.error(e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
}, []);
|
||||||
[],
|
|
||||||
);
|
|
||||||
const newModules = await Promise.all(selectedModulePromises);
|
const newModules = await Promise.all(selectedModulePromises);
|
||||||
|
|
||||||
return newModules;
|
return newModules;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const exams = flatten(await Promise.all(allExams)).filter(
|
const exams = flatten(await Promise.all(allExams)).filter((x) => x !== null) as ExamWithUser[];
|
||||||
(x) => x !== null,
|
|
||||||
) as ExamWithUser[];
|
|
||||||
return exams;
|
return exams;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedModulePromises = selectedModules.map(async (module: Module) => {
|
const selectedModulePromises = selectedModules.map(async (module: Module) => {
|
||||||
const exams: Exam[] = await getExams(db, module, "false", undefined);
|
const exams: Exam[] = await getExams(db, module, "false", undefined, variant, instructorGender);
|
||||||
const exam = exams[getRandomIndex(exams)];
|
const exam = exams[getRandomIndex(exams)];
|
||||||
|
|
||||||
if (exam) {
|
if (exam) {
|
||||||
@@ -116,11 +97,7 @@ const generateExams = async (
|
|||||||
|
|
||||||
const exams = await Promise.all(selectedModulePromises);
|
const exams = await Promise.all(selectedModulePromises);
|
||||||
const examesFiltered = exams.filter((x) => x !== null) as ExamWithUser[];
|
const examesFiltered = exams.filter((x) => x !== null) as ExamWithUser[];
|
||||||
return flatten(
|
return flatten(assignees.map((assignee) => examesFiltered.map((exam) => ({...exam, assignee}))));
|
||||||
assignees.map((assignee) =>
|
|
||||||
examesFiltered.map((exam) => ({ ...exam, assignee })),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async function POST(req: NextApiRequest, res: NextApiResponse) {
|
async function POST(req: NextApiRequest, res: NextApiResponse) {
|
||||||
@@ -131,6 +108,7 @@ async function POST(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
// false would generate the same exam for all users
|
// false would generate the same exam for all users
|
||||||
generateMultiple = false,
|
generateMultiple = false,
|
||||||
variant,
|
variant,
|
||||||
|
instructorGender,
|
||||||
...body
|
...body
|
||||||
} = req.body as {
|
} = req.body as {
|
||||||
selectedModules: Module[];
|
selectedModules: Module[];
|
||||||
@@ -140,19 +118,13 @@ async function POST(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
startDate: string;
|
startDate: string;
|
||||||
endDate: string;
|
endDate: string;
|
||||||
variant?: Variant;
|
variant?: Variant;
|
||||||
|
instructorGender?: InstructorGender;
|
||||||
};
|
};
|
||||||
|
|
||||||
const exams: ExamWithUser[] = await generateExams(
|
const exams: ExamWithUser[] = await generateExams(generateMultiple, selectedModules, assignees, variant, instructorGender);
|
||||||
generateMultiple,
|
|
||||||
selectedModules,
|
|
||||||
assignees,
|
|
||||||
variant,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (exams.length === 0) {
|
if (exams.length === 0) {
|
||||||
res
|
res.status(400).json({ok: false, error: "No exams found for the selected modules"});
|
||||||
.status(400)
|
|
||||||
.json({ ok: false, error: "No exams found for the selected modules" });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,6 +133,7 @@ async function POST(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
assignees,
|
assignees,
|
||||||
results: [],
|
results: [],
|
||||||
exams,
|
exams,
|
||||||
|
instructorGender,
|
||||||
...body,
|
...body,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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, Variant} from "@/interfaces/exam";
|
import {Exam, InstructorGender, 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,9 +23,14 @@ async function GET(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {module, avoidRepeated, variant} = req.query as {module: string; avoidRepeated: string; variant?: Variant};
|
const {module, avoidRepeated, variant, instructorGender} = req.query as {
|
||||||
|
module: string;
|
||||||
|
avoidRepeated: string;
|
||||||
|
variant?: Variant;
|
||||||
|
instructorGender?: InstructorGender;
|
||||||
|
};
|
||||||
|
|
||||||
const exams: Exam[] = await getExams(db, module, avoidRepeated, req.session.user.id, variant);
|
const exams: Exam[] = await getExams(db, module, avoidRepeated, req.session.user.id, variant, instructorGender);
|
||||||
res.status(200).json(exams);
|
res.status(200).json(exams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ import TimezoneSelect from "@/components/Low/TImezoneSelect";
|
|||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import {Module} from "@/interfaces";
|
import {Module} from "@/interfaces";
|
||||||
import ModuleLevelSelector from "@/components/Medium/ModuleLevelSelector";
|
import ModuleLevelSelector from "@/components/Medium/ModuleLevelSelector";
|
||||||
|
import Select from "@/components/Low/Select";
|
||||||
|
import {InstructorGender} from "@/interfaces/exam";
|
||||||
|
import {capitalize} from "lodash";
|
||||||
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
|
|
||||||
@@ -83,6 +86,11 @@ function UserProfile({user, mutateUser}: Props) {
|
|||||||
user.type === "corporate" ? undefined : user.demographicInformation?.employment,
|
user.type === "corporate" ? undefined : user.demographicInformation?.employment,
|
||||||
);
|
);
|
||||||
const [passport_id, setPassportID] = useState<string | undefined>(user.type === "student" ? user.demographicInformation?.passport_id : undefined);
|
const [passport_id, setPassportID] = useState<string | undefined>(user.type === "student" ? user.demographicInformation?.passport_id : undefined);
|
||||||
|
|
||||||
|
const [preferredGender, setPreferredGender] = useState<InstructorGender | undefined>(
|
||||||
|
user.type === "student" || user.type === "developer" ? user.preferredGender || "varied" : undefined,
|
||||||
|
);
|
||||||
|
|
||||||
const [position, setPosition] = useState<string | undefined>(user.type === "corporate" ? user.demographicInformation?.position : undefined);
|
const [position, setPosition] = useState<string | undefined>(user.type === "corporate" ? user.demographicInformation?.position : undefined);
|
||||||
const [corporateInformation, setCorporateInformation] = useState(user.type === "corporate" ? user.corporateInformation : undefined);
|
const [corporateInformation, setCorporateInformation] = useState(user.type === "corporate" ? user.corporateInformation : undefined);
|
||||||
const [companyName, setCompanyName] = useState<string | undefined>(user.type === "agent" ? user.agentInformation?.companyName : undefined);
|
const [companyName, setCompanyName] = useState<string | undefined>(user.type === "agent" ? user.agentInformation?.companyName : undefined);
|
||||||
@@ -90,6 +98,7 @@ function UserProfile({user, mutateUser}: Props) {
|
|||||||
user.type === "agent" ? user.agentInformation?.commercialRegistration : undefined,
|
user.type === "agent" ? user.agentInformation?.commercialRegistration : undefined,
|
||||||
);
|
);
|
||||||
const [timezone, setTimezone] = useState<string>(user.demographicInformation?.timezone || moment.tz.guess());
|
const [timezone, setTimezone] = useState<string>(user.demographicInformation?.timezone || moment.tz.guess());
|
||||||
|
|
||||||
const {groups} = useGroups();
|
const {groups} = useGroups();
|
||||||
const {users} = useUsers();
|
const {users} = useUsers();
|
||||||
|
|
||||||
@@ -146,6 +155,7 @@ function UserProfile({user, mutateUser}: Props) {
|
|||||||
newPassword,
|
newPassword,
|
||||||
profilePicture,
|
profilePicture,
|
||||||
desiredLevels,
|
desiredLevels,
|
||||||
|
preferredGender,
|
||||||
demographicInformation: {
|
demographicInformation: {
|
||||||
phone,
|
phone,
|
||||||
country,
|
country,
|
||||||
@@ -337,6 +347,24 @@ function UserProfile({user, mutateUser}: Props) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{preferredGender && ["developer", "student"].includes(user.type) && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<div className="flex flex-col gap-3 w-full">
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">Speaking Instructor's Gender</label>
|
||||||
|
<Select
|
||||||
|
value={{value: preferredGender, label: capitalize(preferredGender)}}
|
||||||
|
onChange={(value) => (value ? setPreferredGender(value.value as InstructorGender) : null)}
|
||||||
|
options={[
|
||||||
|
{value: "male", label: "Male"},
|
||||||
|
{value: "female", label: "Female"},
|
||||||
|
{value: "varied", label: "Varied"},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
{user.type === "corporate" && (
|
{user.type === "corporate" && (
|
||||||
|
|||||||
37
src/resources/speakingAvatars.ts
Normal file
37
src/resources/speakingAvatars.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
export const AVATARS = [
|
||||||
|
{
|
||||||
|
name: "Matthew Noah",
|
||||||
|
id: "5912afa7c77c47d3883af3d874047aaf",
|
||||||
|
gender: "male",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Vera Cerise",
|
||||||
|
id: "9e58d96a383e4568a7f1e49df549e0e4",
|
||||||
|
gender: "female",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Edward Tony",
|
||||||
|
id: "d2cdd9c0379a4d06ae2afb6e5039bd0c",
|
||||||
|
gender: "male",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Tanya Molly",
|
||||||
|
id: "045cb5dcd00042b3a1e4f3bc1c12176b",
|
||||||
|
gender: "female",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Kayla Abbi",
|
||||||
|
id: "1ae1e5396cc444bfad332155fdb7a934",
|
||||||
|
gender: "female",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Jerome Ryan",
|
||||||
|
id: "0ee6aa7cc1084063a630ae514fccaa31",
|
||||||
|
gender: "male",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Tyler Christopher",
|
||||||
|
id: "5772cff935844516ad7eeff21f839e43",
|
||||||
|
gender: "male",
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import {collection, getDocs, query, where, setDoc, doc, Firestore} from "firebase/firestore";
|
import {collection, getDocs, query, where, setDoc, doc, Firestore} from "firebase/firestore";
|
||||||
import {shuffle} from "lodash";
|
import {shuffle} from "lodash";
|
||||||
import {Exam, Variant} from "@/interfaces/exam";
|
import {Exam, InstructorGender, Variant} from "@/interfaces/exam";
|
||||||
import {Stat} from "@/interfaces/user";
|
import {Stat} from "@/interfaces/user";
|
||||||
|
|
||||||
export const getExams = async (
|
export const getExams = async (
|
||||||
@@ -12,22 +12,23 @@ export const getExams = async (
|
|||||||
// by the teacher that performed the request
|
// by the teacher that performed the request
|
||||||
userId: string | undefined,
|
userId: string | undefined,
|
||||||
variant?: Variant,
|
variant?: Variant,
|
||||||
|
instructorGender?: InstructorGender,
|
||||||
): 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[] = filterByVariant(
|
const allExams = shuffle(
|
||||||
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,
|
|
||||||
);
|
const variantExams: Exam[] = filterByVariant(allExams, variant);
|
||||||
|
const genderedExams: Exam[] = filterByInstructorGender(variantExams, instructorGender);
|
||||||
|
|
||||||
if (avoidRepeated === "true") {
|
if (avoidRepeated === "true") {
|
||||||
const statsQ = query(collection(db, "stats"), where("user", "==", userId));
|
const statsQ = query(collection(db, "stats"), where("user", "==", userId));
|
||||||
@@ -37,12 +38,18 @@ 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((x) => !stats.map((s) => s.exam).includes(x.id));
|
const filteredExams = genderedExams.filter((x) => !stats.map((s) => s.exam).includes(x.id));
|
||||||
|
|
||||||
return filteredExams.length > 0 ? filteredExams : exams;
|
return filteredExams.length > 0 ? filteredExams : genderedExams;
|
||||||
}
|
}
|
||||||
|
|
||||||
return exams;
|
return genderedExams;
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterByInstructorGender = (exams: Exam[], instructorGender?: InstructorGender) => {
|
||||||
|
if (!instructorGender || instructorGender === "varied") return exams;
|
||||||
|
|
||||||
|
return exams.filter((e) => (e.module === "speaking" ? e.instructorGender === instructorGender : true));
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterByVariant = (exams: Exam[], variant?: Variant) => {
|
const filterByVariant = (exams: Exam[], variant?: Variant) => {
|
||||||
|
|||||||
@@ -1,11 +1,29 @@
|
|||||||
import {Module} from "@/interfaces";
|
import {Module} from "@/interfaces";
|
||||||
import {Exam, ReadingExam, ListeningExam, WritingExam, SpeakingExam, Exercise, UserSolution, LevelExam, Variant} from "@/interfaces/exam";
|
import {
|
||||||
|
Exam,
|
||||||
|
ReadingExam,
|
||||||
|
ListeningExam,
|
||||||
|
WritingExam,
|
||||||
|
SpeakingExam,
|
||||||
|
Exercise,
|
||||||
|
UserSolution,
|
||||||
|
LevelExam,
|
||||||
|
Variant,
|
||||||
|
InstructorGender,
|
||||||
|
} from "@/interfaces/exam";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
export const getExam = async (module: Module, avoidRepeated: boolean, variant?: Variant): Promise<Exam | undefined> => {
|
export const getExam = async (
|
||||||
|
module: Module,
|
||||||
|
avoidRepeated: boolean,
|
||||||
|
variant?: Variant,
|
||||||
|
instructorGender?: InstructorGender,
|
||||||
|
): Promise<Exam | undefined> => {
|
||||||
const url = new URLSearchParams();
|
const url = new URLSearchParams();
|
||||||
url.append("avoidRepeated", avoidRepeated.toString());
|
url.append("avoidRepeated", avoidRepeated.toString());
|
||||||
|
|
||||||
if (variant) url.append("variant", variant);
|
if (variant) url.append("variant", variant);
|
||||||
|
if (module === "speaking" && instructorGender) url.append("instructorGender", instructorGender);
|
||||||
|
|
||||||
const examRequest = await axios<Exam[]>(`/api/exam/${module}?${url.toString()}`);
|
const examRequest = await axios<Exam[]>(`/api/exam/${module}?${url.toString()}`);
|
||||||
if (examRequest.status !== 200) {
|
if (examRequest.status !== 200) {
|
||||||
|
|||||||
Reference in New Issue
Block a user