- Added the ability for a student/developer to choose a gender for a speaking instructor;
- Made it so, if chosen, the user will only get speaking exams with their chosen gender; - Added the ability for speaking exams to select a gender when generating;
This commit is contained in:
@@ -123,6 +123,7 @@ export default function ExamPage({page}: Props) {
|
||||
setSessionId(shortUID.randomUUID(8));
|
||||
}
|
||||
}, [setSessionId, selectedModules, sessionId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.type === "developer") console.log(exam);
|
||||
}, [exam, user]);
|
||||
@@ -160,7 +161,14 @@ export default function ExamPage({page}: Props) {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
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) => {
|
||||
if (values.every((x) => !!x)) {
|
||||
setExams(values.map((x) => x!));
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import Input from "@/components/Low/Input";
|
||||
import Select from "@/components/Low/Select";
|
||||
import {Exercise, InteractiveSpeakingExercise, SpeakingExam, SpeakingExercise} from "@/interfaces/exam";
|
||||
import {AVATARS} from "@/resources/speakingAvatars";
|
||||
import useExamStore from "@/stores/examStore";
|
||||
import {getExamById} from "@/utils/exams";
|
||||
import {playSound} from "@/utils/sound";
|
||||
@@ -7,6 +9,7 @@ import {convertCamelCaseToReadable} from "@/utils/string";
|
||||
import {Tab} from "@headlessui/react";
|
||||
import axios from "axios";
|
||||
import clsx from "clsx";
|
||||
import {capitalize, sample, uniq} from "lodash";
|
||||
import moment from "moment";
|
||||
import {useRouter} from "next/router";
|
||||
import {useEffect, useState} from "react";
|
||||
@@ -15,6 +18,7 @@ import {toast} from "react-toastify";
|
||||
import {v4} from "uuid";
|
||||
|
||||
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 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!");
|
||||
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);
|
||||
const initialTime = moment();
|
||||
|
||||
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) => {
|
||||
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});
|
||||
setPart({...part, result: result.data, gender, avatar});
|
||||
})
|
||||
.catch((e) => {
|
||||
toast.error("Something went wrong!");
|
||||
@@ -60,6 +66,18 @@ const PartTab = ({part, index, setPart}: {part?: SpeakingPart; index: number; se
|
||||
|
||||
return (
|
||||
<Tab.Panel className="w-full bg-ielts-speaking/20 min-h-[600px] h-full rounded-xl p-6 flex flex-col gap-4">
|
||||
<div className="flex 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">
|
||||
<button
|
||||
onClick={generate}
|
||||
@@ -128,6 +146,11 @@ const PartTab = ({part, index, setPart}: {part?: SpeakingPart; index: number; se
|
||||
</div>
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
@@ -140,6 +163,8 @@ interface SpeakingPart {
|
||||
questions?: string[];
|
||||
topic: string;
|
||||
result?: SpeakingExercise | InteractiveSpeakingExercise;
|
||||
gender?: "male" | "female";
|
||||
avatar?: (typeof AVATARS)[number];
|
||||
}
|
||||
|
||||
const SpeakingGeneration = () => {
|
||||
@@ -165,6 +190,8 @@ const SpeakingGeneration = () => {
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const genders = [part1?.gender, part2?.gender, part3?.gender].filter((x) => !!x);
|
||||
|
||||
const exam: SpeakingExam = {
|
||||
id: v4(),
|
||||
isDiagnostic: false,
|
||||
@@ -172,7 +199,7 @@ const SpeakingGeneration = () => {
|
||||
minTimer,
|
||||
variant: minTimer >= 14 ? "full" : "partial",
|
||||
module: "speaking",
|
||||
instructorGender: "varied",
|
||||
instructorGender: genders.every((x) => x === "male") ? "male" : genders.every((x) => x === "female") ? "female" : "varied",
|
||||
};
|
||||
|
||||
axios
|
||||
|
||||
@@ -1,198 +1,170 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { app } from "@/firebase";
|
||||
import {
|
||||
getFirestore,
|
||||
collection,
|
||||
getDocs,
|
||||
query,
|
||||
where,
|
||||
setDoc,
|
||||
doc,
|
||||
getDoc,
|
||||
} from "firebase/firestore";
|
||||
import { withIronSessionApiRoute } from "iron-session/next";
|
||||
import { sessionOptions } from "@/lib/session";
|
||||
import { uuidv4 } from "@firebase/util";
|
||||
import { Module } from "@/interfaces";
|
||||
import { getExams } from "@/utils/exams.be";
|
||||
import { Exam, Variant } from "@/interfaces/exam";
|
||||
import { capitalize, flatten, uniqBy } from "lodash";
|
||||
import { User } from "@/interfaces/user";
|
||||
import type {NextApiRequest, NextApiResponse} from "next";
|
||||
import {app} from "@/firebase";
|
||||
import {getFirestore, collection, getDocs, query, where, setDoc, doc, getDoc} from "firebase/firestore";
|
||||
import {withIronSessionApiRoute} from "iron-session/next";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import {uuidv4} from "@firebase/util";
|
||||
import {Module} from "@/interfaces";
|
||||
import {getExams} from "@/utils/exams.be";
|
||||
import {Exam, InstructorGender, Variant} from "@/interfaces/exam";
|
||||
import {capitalize, flatten, uniqBy} from "lodash";
|
||||
import {User} from "@/interfaces/user";
|
||||
import moment from "moment";
|
||||
import { sendEmail } from "@/email";
|
||||
import {sendEmail} from "@/email";
|
||||
|
||||
const db = getFirestore(app);
|
||||
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!req.session.user) {
|
||||
res.status(401).json({ ok: false });
|
||||
return;
|
||||
}
|
||||
if (!req.session.user) {
|
||||
res.status(401).json({ok: false});
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === "GET") return GET(req, res);
|
||||
if (req.method === "POST") return POST(req, res);
|
||||
if (req.method === "GET") return GET(req, res);
|
||||
if (req.method === "POST") return POST(req, res);
|
||||
|
||||
res.status(404).json({ ok: false });
|
||||
res.status(404).json({ok: false});
|
||||
}
|
||||
|
||||
async function GET(req: NextApiRequest, res: NextApiResponse) {
|
||||
const q = query(collection(db, "assignments"));
|
||||
const snapshot = await getDocs(q);
|
||||
const q = query(collection(db, "assignments"));
|
||||
const snapshot = await getDocs(q);
|
||||
|
||||
const docs = snapshot.docs.map((doc) => ({
|
||||
id: doc.id,
|
||||
...doc.data(),
|
||||
}));
|
||||
const docs = snapshot.docs.map((doc) => ({
|
||||
id: doc.id,
|
||||
...doc.data(),
|
||||
}));
|
||||
|
||||
res.status(200).json(docs);
|
||||
res.status(200).json(docs);
|
||||
}
|
||||
|
||||
interface ExamWithUser {
|
||||
module: Module;
|
||||
id: string;
|
||||
assignee: string;
|
||||
module: Module;
|
||||
id: string;
|
||||
assignee: string;
|
||||
}
|
||||
|
||||
function getRandomIndex(arr: any[]): number {
|
||||
const randomIndex = Math.floor(Math.random() * arr.length);
|
||||
return randomIndex;
|
||||
const randomIndex = Math.floor(Math.random() * arr.length);
|
||||
return randomIndex;
|
||||
}
|
||||
|
||||
const generateExams = async (
|
||||
generateMultiple: Boolean,
|
||||
selectedModules: Module[],
|
||||
assignees: string[],
|
||||
variant?: Variant,
|
||||
generateMultiple: Boolean,
|
||||
selectedModules: Module[],
|
||||
assignees: string[],
|
||||
variant?: Variant,
|
||||
instructorGender?: InstructorGender,
|
||||
): Promise<ExamWithUser[]> => {
|
||||
if (generateMultiple) {
|
||||
// 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 selectedModulePromises = selectedModules.map(
|
||||
async (module: Module) => {
|
||||
try {
|
||||
const exams: Exam[] = await getExams(
|
||||
db,
|
||||
module,
|
||||
"true",
|
||||
assignee,
|
||||
variant,
|
||||
);
|
||||
if (generateMultiple) {
|
||||
// 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 selectedModulePromises = selectedModules.map(async (module: Module) => {
|
||||
try {
|
||||
const exams: Exam[] = await getExams(db, module, "true", assignee, variant, instructorGender);
|
||||
|
||||
const exam = exams[getRandomIndex(exams)];
|
||||
if (exam) {
|
||||
return { module: exam.module, id: exam.id, assignee };
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
const newModules = await Promise.all(selectedModulePromises);
|
||||
const exam = exams[getRandomIndex(exams)];
|
||||
if (exam) {
|
||||
return {module: exam.module, id: exam.id, assignee};
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
const newModules = await Promise.all(selectedModulePromises);
|
||||
|
||||
return newModules;
|
||||
}, []);
|
||||
return newModules;
|
||||
}, []);
|
||||
|
||||
const exams = flatten(await Promise.all(allExams)).filter(
|
||||
(x) => x !== null,
|
||||
) as ExamWithUser[];
|
||||
return exams;
|
||||
}
|
||||
const exams = flatten(await Promise.all(allExams)).filter((x) => x !== null) as ExamWithUser[];
|
||||
return exams;
|
||||
}
|
||||
|
||||
const selectedModulePromises = selectedModules.map(async (module: Module) => {
|
||||
const exams: Exam[] = await getExams(db, module, "false", undefined);
|
||||
const exam = exams[getRandomIndex(exams)];
|
||||
const selectedModulePromises = selectedModules.map(async (module: Module) => {
|
||||
const exams: Exam[] = await getExams(db, module, "false", undefined);
|
||||
const exam = exams[getRandomIndex(exams)];
|
||||
|
||||
if (exam) {
|
||||
return { module: exam.module, id: exam.id };
|
||||
}
|
||||
return null;
|
||||
});
|
||||
if (exam) {
|
||||
return {module: exam.module, id: exam.id};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const exams = await Promise.all(selectedModulePromises);
|
||||
const examesFiltered = exams.filter((x) => x !== null) as ExamWithUser[];
|
||||
return flatten(
|
||||
assignees.map((assignee) =>
|
||||
examesFiltered.map((exam) => ({ ...exam, assignee })),
|
||||
),
|
||||
);
|
||||
const exams = await Promise.all(selectedModulePromises);
|
||||
const examesFiltered = exams.filter((x) => x !== null) as ExamWithUser[];
|
||||
return flatten(assignees.map((assignee) => examesFiltered.map((exam) => ({...exam, assignee}))));
|
||||
};
|
||||
|
||||
async function POST(req: NextApiRequest, res: NextApiResponse) {
|
||||
const {
|
||||
selectedModules,
|
||||
assignees,
|
||||
// Generate multiple true would generate an unique exam for each user
|
||||
// false would generate the same exam for all users
|
||||
generateMultiple = false,
|
||||
variant,
|
||||
...body
|
||||
} = req.body as {
|
||||
selectedModules: Module[];
|
||||
assignees: string[];
|
||||
generateMultiple: Boolean;
|
||||
name: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
variant?: Variant;
|
||||
};
|
||||
const {
|
||||
selectedModules,
|
||||
assignees,
|
||||
// Generate multiple true would generate an unique exam for each user
|
||||
// false would generate the same exam for all users
|
||||
generateMultiple = false,
|
||||
variant,
|
||||
instructorGender,
|
||||
...body
|
||||
} = req.body as {
|
||||
selectedModules: Module[];
|
||||
assignees: string[];
|
||||
generateMultiple: Boolean;
|
||||
name: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
variant?: Variant;
|
||||
instructorGender?: InstructorGender;
|
||||
};
|
||||
|
||||
const exams: ExamWithUser[] = await generateExams(
|
||||
generateMultiple,
|
||||
selectedModules,
|
||||
assignees,
|
||||
variant,
|
||||
);
|
||||
const exams: ExamWithUser[] = await generateExams(generateMultiple, selectedModules, assignees, variant, instructorGender);
|
||||
|
||||
if (exams.length === 0) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ ok: false, error: "No exams found for the selected modules" });
|
||||
return;
|
||||
}
|
||||
if (exams.length === 0) {
|
||||
res.status(400).json({ok: false, error: "No exams found for the selected modules"});
|
||||
return;
|
||||
}
|
||||
|
||||
await setDoc(doc(db, "assignments", uuidv4()), {
|
||||
assigner: req.session.user?.id,
|
||||
assignees,
|
||||
results: [],
|
||||
exams,
|
||||
...body,
|
||||
});
|
||||
await setDoc(doc(db, "assignments", uuidv4()), {
|
||||
assigner: req.session.user?.id,
|
||||
assignees,
|
||||
results: [],
|
||||
exams,
|
||||
...body,
|
||||
});
|
||||
|
||||
res.status(200).json({ ok: true });
|
||||
res.status(200).json({ok: true});
|
||||
|
||||
for (const assigneeID of assignees) {
|
||||
const assigneeSnapshot = await getDoc(doc(db, "users", assigneeID));
|
||||
if (!assigneeSnapshot.exists()) continue;
|
||||
for (const assigneeID of assignees) {
|
||||
const assigneeSnapshot = await getDoc(doc(db, "users", assigneeID));
|
||||
if (!assigneeSnapshot.exists()) continue;
|
||||
|
||||
const assignee = { id: assigneeID, ...assigneeSnapshot.data() } as User;
|
||||
const name = body.name;
|
||||
const teacher = req.session.user!;
|
||||
const examModulesLabel = uniqBy(exams, (x) => x.module)
|
||||
.map((x) => capitalize(x.module))
|
||||
.join(", ");
|
||||
const startDate = moment(body.startDate).format("DD/MM/YYYY - HH:mm");
|
||||
const endDate = moment(body.endDate).format("DD/MM/YYYY - HH:mm");
|
||||
const assignee = {id: assigneeID, ...assigneeSnapshot.data()} as User;
|
||||
const name = body.name;
|
||||
const teacher = req.session.user!;
|
||||
const examModulesLabel = uniqBy(exams, (x) => x.module)
|
||||
.map((x) => capitalize(x.module))
|
||||
.join(", ");
|
||||
const startDate = moment(body.startDate).format("DD/MM/YYYY - HH:mm");
|
||||
const endDate = moment(body.endDate).format("DD/MM/YYYY - HH:mm");
|
||||
|
||||
await sendEmail(
|
||||
"assignment",
|
||||
{
|
||||
user: { name: assignee.name },
|
||||
assignment: {
|
||||
name,
|
||||
startDate,
|
||||
endDate,
|
||||
modules: examModulesLabel,
|
||||
assigner: teacher.name,
|
||||
},
|
||||
},
|
||||
[assignee.email],
|
||||
"EnCoach - New Assignment!",
|
||||
);
|
||||
}
|
||||
await sendEmail(
|
||||
"assignment",
|
||||
{
|
||||
user: {name: assignee.name},
|
||||
assignment: {
|
||||
name,
|
||||
startDate,
|
||||
endDate,
|
||||
modules: examModulesLabel,
|
||||
assigner: teacher.name,
|
||||
},
|
||||
},
|
||||
[assignee.email],
|
||||
"EnCoach - New Assignment!",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {app} from "@/firebase";
|
||||
import {getFirestore, setDoc, doc} from "firebase/firestore";
|
||||
import {withIronSessionApiRoute} from "iron-session/next";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import {Exam, Variant} from "@/interfaces/exam";
|
||||
import {Exam, InstructorGender, Variant} from "@/interfaces/exam";
|
||||
import {getExams} from "@/utils/exams.be";
|
||||
const db = getFirestore(app);
|
||||
|
||||
@@ -23,9 +23,14 @@ async function GET(req: NextApiRequest, res: NextApiResponse) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,9 @@ import TimezoneSelect from "@/components/Low/TImezoneSelect";
|
||||
import Modal from "@/components/Modal";
|
||||
import {Module} from "@/interfaces";
|
||||
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}) => {
|
||||
const user = req.session.user;
|
||||
|
||||
@@ -83,6 +86,11 @@ function UserProfile({user, mutateUser}: Props) {
|
||||
user.type === "corporate" ? undefined : user.demographicInformation?.employment,
|
||||
);
|
||||
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 [corporateInformation, setCorporateInformation] = useState(user.type === "corporate" ? user.corporateInformation : 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,
|
||||
);
|
||||
const [timezone, setTimezone] = useState<string>(user.demographicInformation?.timezone || moment.tz.guess());
|
||||
|
||||
const {groups} = useGroups();
|
||||
const {users} = useUsers();
|
||||
|
||||
@@ -146,6 +155,7 @@ function UserProfile({user, mutateUser}: Props) {
|
||||
newPassword,
|
||||
profilePicture,
|
||||
desiredLevels,
|
||||
preferredGender,
|
||||
demographicInformation: {
|
||||
phone,
|
||||
country,
|
||||
@@ -337,6 +347,24 @@ function UserProfile({user, mutateUser}: Props) {
|
||||
</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 />
|
||||
|
||||
{user.type === "corporate" && (
|
||||
|
||||
Reference in New Issue
Block a user