Merge branch 'develop'
This commit is contained in:
@@ -4,7 +4,7 @@ import {BsArrowRepeat} from "react-icons/bs";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
color?: "rose" | "purple" | "red" | "green" | "gray";
|
||||
color?: "rose" | "purple" | "red" | "green" | "gray" | "pink";
|
||||
variant?: "outline" | "solid";
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
@@ -49,6 +49,11 @@ export default function Button({
|
||||
outline:
|
||||
"bg-transparent text-mti-rose-light border border-mti-rose-light hover:bg-mti-rose-light disabled:text-mti-rose disabled:bg-mti-rose-ultralight disabled:border-none selection:bg-mti-rose-dark hover:text-white selection:text-white",
|
||||
},
|
||||
pink: {
|
||||
solid: "bg-ielts-speaking text-white border border-ielts-speaking hover:bg-ielts-speaking disabled:text-ielts-speaking disabled:bg-ielts-speaking-transparent selection:bg-ielts-speaking",
|
||||
outline:
|
||||
"bg-transparent text-ielts-speaking border border-ielts-speaking hover:bg-ielts-speaking disabled:text-ielts-speaking disabled:bg-ielts-speaking-transparent disabled:border-none selection:bg-ielts-speaking hover:text-white selection:text-white",
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -8,6 +8,8 @@ import axios from "axios";
|
||||
import {speakingReverseMarking} from "@/utils/score";
|
||||
import {Tab} from "@headlessui/react";
|
||||
import clsx from "clsx";
|
||||
import Modal from "../Modal";
|
||||
import ReactDiffViewer, {DiffMethod} from "react-diff-viewer";
|
||||
|
||||
const Waveform = dynamic(() => import("../Waveform"), {ssr: false});
|
||||
|
||||
@@ -22,6 +24,7 @@ export default function InteractiveSpeaking({
|
||||
onBack,
|
||||
}: InteractiveSpeakingExercise & CommonProps) {
|
||||
const [solutionsURL, setSolutionsURL] = useState<string[]>([]);
|
||||
const [diffNumber, setDiffNumber] = useState<0 | 1 | 2 | 3>(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (userSolutions && userSolutions.length > 0) {
|
||||
@@ -42,6 +45,44 @@ export default function InteractiveSpeaking({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal title={`Correction (Prompt ${diffNumber})`} isOpen={diffNumber !== 0} onClose={() => setDiffNumber(0)}>
|
||||
<>
|
||||
{userSolutions &&
|
||||
userSolutions.length > 0 &&
|
||||
diffNumber !== 0 &&
|
||||
userSolutions[0].evaluation &&
|
||||
userSolutions[0].evaluation[`transcript_${diffNumber}`] &&
|
||||
userSolutions[0].evaluation[`fixed_text_${diffNumber}`] && (
|
||||
<div className="w-full h-full rounded-xl overflow-hidden flex flex-col mt-4">
|
||||
<div className="w-full grid grid-cols-2 bg-neutral-100">
|
||||
<span className="p-3 font-medium text-lg border-r border-r-neutral-200">Transcript</span>
|
||||
<span className="p-3 font-medium text-lg border-l border-l-neutral-200">Recommended Improvements</span>
|
||||
</div>
|
||||
<ReactDiffViewer
|
||||
styles={{
|
||||
contentText: {
|
||||
fontFamily: '"Open Sans", system-ui, -apple-system, "Helvetica Neue", sans-serif',
|
||||
padding: "32px 28px",
|
||||
},
|
||||
marker: {display: "none"},
|
||||
diffRemoved: {padding: "32px 28px"},
|
||||
diffAdded: {padding: "32px 28px"},
|
||||
|
||||
wordRemoved: {padding: "0px", display: "initial"},
|
||||
wordAdded: {padding: "0px", display: "initial"},
|
||||
wordDiff: {padding: "0px", display: "initial"},
|
||||
}}
|
||||
oldValue={userSolutions[0].evaluation[`transcript_${diffNumber}`]?.replaceAll("\\n", "\n")}
|
||||
newValue={userSolutions[0].evaluation[`fixed_text_${diffNumber}`]?.replaceAll("\\n", "\n")}
|
||||
splitView
|
||||
hideLineNumbers
|
||||
showDiffOnly={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</Modal>
|
||||
|
||||
<div className="flex flex-col h-full w-full gap-8 mb-20">
|
||||
<div className="flex flex-col w-full gap-8 bg-mti-gray-smoke rounded-xl py-8 pb-12 px-16">
|
||||
<div className="flex flex-col gap-3">
|
||||
@@ -67,10 +108,23 @@ export default function InteractiveSpeaking({
|
||||
{solutionsURL.map((x, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="w-full p-4 px-8 bg-transparent border-2 border-mti-gray-platinum rounded-2xl flex-col gap-8 items-center">
|
||||
className="w-full p-4 px-8 bg-transparent border-2 border-mti-gray-platinum rounded-2xl flex flex-col gap-4">
|
||||
<div className="flex gap-8 items-center justify-center py-8">
|
||||
<Waveform audio={x} waveColor="#FCDDEC" progressColor="#EF5DA8" />
|
||||
</div>
|
||||
{userSolutions &&
|
||||
userSolutions.length > 0 &&
|
||||
userSolutions[0].evaluation &&
|
||||
userSolutions[0].evaluation[`transcript_${(index + 1) as 1 | 2 | 3}`] &&
|
||||
userSolutions[0].evaluation[`fixed_text_${(index + 1) as 1 | 2 | 3}`] && (
|
||||
<Button
|
||||
className="w-full max-w-[180px] !py-2 self-center"
|
||||
color="pink"
|
||||
variant="outline"
|
||||
onClick={() => setDiffNumber((index + 1) as 1 | 2 | 3)}>
|
||||
View Correction
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -8,11 +8,15 @@ import axios from "axios";
|
||||
import {speakingReverseMarking} from "@/utils/score";
|
||||
import {Tab} from "@headlessui/react";
|
||||
import clsx from "clsx";
|
||||
import Modal from "../Modal";
|
||||
import {BsQuestionCircleFill} from "react-icons/bs";
|
||||
import ReactDiffViewer, {DiffMethod} from "react-diff-viewer";
|
||||
|
||||
const Waveform = dynamic(() => import("../Waveform"), {ssr: false});
|
||||
|
||||
export default function Speaking({id, type, title, video_url, text, prompts, userSolutions, onNext, onBack}: SpeakingExercise & CommonProps) {
|
||||
const [solutionURL, setSolutionURL] = useState<string>();
|
||||
const [showDiff, setShowDiff] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (userSolutions && userSolutions.length > 0) {
|
||||
@@ -25,8 +29,48 @@ export default function Speaking({id, type, title, video_url, text, prompts, use
|
||||
}
|
||||
}, [userSolutions]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(userSolutions);
|
||||
}, [userSolutions]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal title="Correction" isOpen={showDiff} onClose={() => setShowDiff(false)}>
|
||||
<>
|
||||
{userSolutions &&
|
||||
userSolutions.length > 0 &&
|
||||
userSolutions[0].evaluation?.transcript_1 &&
|
||||
userSolutions[0].evaluation?.fixed_text_1 && (
|
||||
<div className="w-full h-full rounded-xl overflow-hidden flex flex-col mt-4">
|
||||
<div className="w-full grid grid-cols-2 bg-neutral-100">
|
||||
<span className="p-3 font-medium text-lg border-r border-r-neutral-200">Transcript</span>
|
||||
<span className="p-3 font-medium text-lg border-l border-l-neutral-200">Recommended Improvements</span>
|
||||
</div>
|
||||
<ReactDiffViewer
|
||||
styles={{
|
||||
contentText: {
|
||||
fontFamily: '"Open Sans", system-ui, -apple-system, "Helvetica Neue", sans-serif',
|
||||
padding: "32px 28px",
|
||||
},
|
||||
marker: {display: "none"},
|
||||
diffRemoved: {padding: "32px 28px"},
|
||||
diffAdded: {padding: "32px 28px"},
|
||||
|
||||
wordRemoved: {padding: "0px", display: "initial"},
|
||||
wordAdded: {padding: "0px", display: "initial"},
|
||||
wordDiff: {padding: "0px", display: "initial"},
|
||||
}}
|
||||
oldValue={userSolutions[0].evaluation.transcript_1.replaceAll("\\n", "\n")}
|
||||
newValue={userSolutions[0].evaluation.fixed_text_1.replaceAll("\\n", "\n")}
|
||||
splitView
|
||||
hideLineNumbers
|
||||
showDiffOnly={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</Modal>
|
||||
|
||||
<div className="flex flex-col h-full w-full gap-8 mb-20">
|
||||
<div className="flex flex-col w-full gap-2 bg-mti-gray-smoke rounded-xl py-8 px-16">
|
||||
<div className="flex flex-col gap-3">
|
||||
@@ -65,10 +109,19 @@ export default function Speaking({id, type, title, video_url, text, prompts, use
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full h-full flex flex-col gap-8">
|
||||
<div className="w-full p-4 px-8 bg-transparent border-2 border-mti-gray-platinum rounded-2xl flex-col gap-8 items-center">
|
||||
<div className="w-full h-full flex flex-col gap-8 relative">
|
||||
<div className="w-full p-4 px-8 bg-transparent border-2 border-mti-gray-platinum rounded-2xl flex-col gap-8 items-center relative">
|
||||
<div className="flex gap-8 items-center justify-center py-8">
|
||||
{solutionURL && <Waveform audio={solutionURL} waveColor="#FCDDEC" progressColor="#EF5DA8" />}
|
||||
|
||||
{userSolutions &&
|
||||
userSolutions.length > 0 &&
|
||||
userSolutions[0].evaluation?.transcript_1 &&
|
||||
userSolutions[0].evaluation?.fixed_text_1 && (
|
||||
<Button className="w-full max-w-[180px] !py-2" color="pink" variant="outline" onClick={() => setShowDiff(true)}>
|
||||
View Correction
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{userSolutions && userSolutions.length > 0 && userSolutions[0].evaluation && typeof userSolutions[0].evaluation !== "string" && (
|
||||
|
||||
@@ -34,8 +34,12 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
|
||||
const [assignees, setAssignees] = useState<string[]>(assignment?.assignees || []);
|
||||
const [name, setName] = useState(assignment?.name || generate({minLength: 6, maxLength: 8, min: 2, max: 3, join: " ", formatter: capitalize}));
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [startDate, setStartDate] = useState<Date | null>(assignment ? moment(assignment.startDate).toDate() : moment().add(1, "day").toDate());
|
||||
const [endDate, setEndDate] = useState<Date | null>(assignment ? moment(assignment.endDate).toDate() : moment().add(8, "day").toDate());
|
||||
const [startDate, setStartDate] = useState<Date | null>(
|
||||
assignment ? moment(assignment.startDate).toDate() : moment().hours(0).minutes(0).add(1, "day").toDate(),
|
||||
);
|
||||
const [endDate, setEndDate] = useState<Date | null>(
|
||||
assignment ? moment(assignment.endDate).toDate() : moment().hours(23).minutes(59).add(8, "day").toDate(),
|
||||
);
|
||||
// creates a new exam for each assignee or just one exam for all assignees
|
||||
const [generateMultiple, setGenerateMultiple] = useState<boolean>(false);
|
||||
|
||||
@@ -49,33 +53,26 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
|
||||
};
|
||||
|
||||
const createAssignment = () => {
|
||||
setIsLoading(true);
|
||||
setIsLoading(true);
|
||||
|
||||
(assignment ? axios.patch : axios.post)(
|
||||
`/api/assignments${assignment ? `/${assignment.id}` : ""}`,
|
||||
{
|
||||
assignees,
|
||||
name,
|
||||
startDate,
|
||||
endDate,
|
||||
selectedModules,
|
||||
generateMultiple,
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
toast.success(
|
||||
`The assignment "${name}" has been ${
|
||||
assignment ? "updated" : "created"
|
||||
} successfully!`
|
||||
);
|
||||
cancelCreation();
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
toast.error("Something went wrong, please try again later!");
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
};
|
||||
(assignment ? axios.patch : axios.post)(`/api/assignments${assignment ? `/${assignment.id}` : ""}`, {
|
||||
assignees,
|
||||
name,
|
||||
startDate,
|
||||
endDate,
|
||||
selectedModules,
|
||||
generateMultiple,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(`The assignment "${name}" has been ${assignment ? "updated" : "created"} successfully!`);
|
||||
cancelCreation();
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
toast.error("Something went wrong, please try again later!");
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
};
|
||||
|
||||
const deleteAssignment = () => {
|
||||
if (assignment) {
|
||||
@@ -285,7 +282,7 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
|
||||
</div>
|
||||
</section>
|
||||
<div className="flex gap-4 w-full justify-end">
|
||||
<Checkbox isChecked={generateMultiple} onChange={() => setGenerateMultiple(d => !d)}>
|
||||
<Checkbox isChecked={generateMultiple} onChange={() => setGenerateMultiple((d) => !d)}>
|
||||
Generate different exams
|
||||
</Checkbox>
|
||||
</div>
|
||||
|
||||
@@ -40,3 +40,15 @@ export function prepareMailOptions(context: object, to: string[], subject: strin
|
||||
context,
|
||||
};
|
||||
}
|
||||
|
||||
export async function sendEmail(template: string, context: object, to: string[], subject: string): Promise<boolean> {
|
||||
try {
|
||||
const transport = prepareMailer(template);
|
||||
const mailOptions = prepareMailOptions(context, to, subject, template);
|
||||
|
||||
await transport.sendMail(mailOptions);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
28
src/email/templates/assignment.handlebars
Normal file
28
src/email/templates/assignment.handlebars
Normal file
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<div>
|
||||
<p>Hello {{user.name}},</p>
|
||||
<br />
|
||||
<p>You have just been given the assignment <b>"{{assignment.name}}"</b> by your teacher {{assignment.assigner}}!</p>
|
||||
<br />
|
||||
<p>It's start date will be on <b>{{assignment.startDate}}</b> and will only last until <b>{{assignment.endDate}}</b>
|
||||
</p>
|
||||
<br />
|
||||
<p>For this assignment, you've been tasked with completing exams of the following modules:
|
||||
<b>{{assignment.modules}}</b>.
|
||||
</p>
|
||||
<br />
|
||||
<p>Don't forget to do it before its end date!</p>
|
||||
<p>Click <b><a href="https://platform.encoach.com">here</a></b> to open the EnCoach Platform!</p>
|
||||
<br />
|
||||
<p>Thanks,</p>
|
||||
<p>Your EnCoach team</p>
|
||||
</div>
|
||||
|
||||
</html>
|
||||
13
src/email/templates/assignment.handlebars.json
Normal file
13
src/email/templates/assignment.handlebars.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"user": {
|
||||
"name": "Tiago Ribeiro"
|
||||
},
|
||||
"assignment": {
|
||||
"name": "Final Exam",
|
||||
"assigner": "Teacher",
|
||||
"assignees": [],
|
||||
"modules": "Reading and Writing",
|
||||
"startDate": "24/12/2023",
|
||||
"endDate": "27/01/2024"
|
||||
}
|
||||
}
|
||||
@@ -103,13 +103,24 @@ export interface Evaluation {
|
||||
|
||||
interface InteractiveSpeakingEvaluation extends Evaluation {
|
||||
perfect_answer_1?: string;
|
||||
transcript_1?: string;
|
||||
fixed_text_1?: string;
|
||||
perfect_answer_2?: string;
|
||||
transcript_2?: string;
|
||||
fixed_text_2?: string;
|
||||
perfect_answer_3?: string;
|
||||
transcript_3?: string;
|
||||
fixed_text_3?: string;
|
||||
}
|
||||
|
||||
interface SpeakingEvaluation extends CommonEvaluation {
|
||||
perfect_answer_1?: string;
|
||||
transcript_1?: string;
|
||||
fixed_text_1?: string;
|
||||
}
|
||||
|
||||
interface CommonEvaluation extends Evaluation {
|
||||
perfect_answer?: string;
|
||||
perfect_answer_1?: string;
|
||||
fixed_text?: string;
|
||||
}
|
||||
|
||||
@@ -141,7 +152,7 @@ export interface SpeakingExercise {
|
||||
userSolutions: {
|
||||
id: string;
|
||||
solution: string;
|
||||
evaluation?: CommonEvaluation;
|
||||
evaluation?: SpeakingEvaluation;
|
||||
}[];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
// 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} from "firebase/firestore";
|
||||
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 } from "@/interfaces/exam";
|
||||
import { flatten } from "lodash";
|
||||
import {Module} from "@/interfaces";
|
||||
import {getExams} from "@/utils/exams.be";
|
||||
import {Exam} from "@/interfaces/exam";
|
||||
import {capitalize, flatten} from "lodash";
|
||||
import {User} from "@/interfaces/user";
|
||||
import moment from "moment";
|
||||
import {sendEmail} from "@/email";
|
||||
|
||||
const db = getFirestore(app);
|
||||
|
||||
@@ -39,106 +42,108 @@ async function GET(req: NextApiRequest, res: NextApiResponse) {
|
||||
}
|
||||
|
||||
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[]
|
||||
): 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 = await assignees.map(async (assignee) => {
|
||||
const selectedModulePromises = await selectedModules.map(
|
||||
async (module: Module) => {
|
||||
try {
|
||||
const exams: Exam[] = await getExams(db, module, "true", assignee);
|
||||
const generateExams = async (generateMultiple: Boolean, selectedModules: Module[], assignees: string[]): 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 = await assignees.map(async (assignee) => {
|
||||
const selectedModulePromises = await selectedModules.map(async (module: Module) => {
|
||||
try {
|
||||
const exams: Exam[] = await getExams(db, module, "true", assignee);
|
||||
|
||||
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 = await selectedModules.map(
|
||||
async (module: Module) => {
|
||||
const exams: Exam[] = await getExams(db, module, "false", undefined);
|
||||
const exam = exams[getRandomIndex(exams)];
|
||||
const selectedModulePromises = await 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,
|
||||
// Generarte multiple true would generate an unique exam for eacah user
|
||||
// false would generate the same exam for all usersa
|
||||
generateMultiple = false,
|
||||
...body
|
||||
} = req.body as {
|
||||
selectedModules: Module[];
|
||||
assignees: string[];
|
||||
generateMultiple: Boolean;
|
||||
};
|
||||
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,
|
||||
...body
|
||||
} = req.body as {
|
||||
selectedModules: Module[];
|
||||
assignees: string[];
|
||||
generateMultiple: Boolean;
|
||||
name: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
};
|
||||
|
||||
const exams: ExamWithUser[] = await generateExams(
|
||||
generateMultiple,
|
||||
selectedModules,
|
||||
assignees
|
||||
);
|
||||
if (exams.length === 0) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ ok: false, error: "No exams found for the selected modules" });
|
||||
return;
|
||||
}
|
||||
const exams: ExamWithUser[] = await generateExams(generateMultiple, selectedModules, assignees);
|
||||
|
||||
await setDoc(doc(db, "assignments", uuidv4()), {
|
||||
assigner: req.session.user?.id,
|
||||
assignees,
|
||||
results: [],
|
||||
exams,
|
||||
...body,
|
||||
});
|
||||
if (exams.length === 0) {
|
||||
res.status(400).json({ok: false, error: "No exams found for the selected modules"});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({ ok: true });
|
||||
await setDoc(doc(db, "assignments", uuidv4()), {
|
||||
assigner: req.session.user?.id,
|
||||
assignees,
|
||||
results: [],
|
||||
exams,
|
||||
...body,
|
||||
});
|
||||
|
||||
res.status(200).json({ok: true});
|
||||
|
||||
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 = exams.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!",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {getAuth as getAdminAuth, UserRecord} from "firebase-admin/auth";
|
||||
import {app, adminApp} from "@/firebase";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import {withIronSessionApiRoute} from "iron-session/next";
|
||||
import {prepareMailer, prepareMailOptions} from "@/email";
|
||||
import {prepareMailer, prepareMailOptions, sendEmail} from "@/email";
|
||||
import ShortUniqueId from "short-unique-id";
|
||||
|
||||
export default withIronSessionApiRoute(sendVerification, sessionOptions);
|
||||
@@ -13,8 +13,8 @@ async function sendVerification(req: NextApiRequest, res: NextApiResponse) {
|
||||
const short = new ShortUniqueId();
|
||||
|
||||
if (req.session.user) {
|
||||
const transport = prepareMailer("verification");
|
||||
const mailOptions = prepareMailOptions(
|
||||
const ok = await sendEmail(
|
||||
"verification",
|
||||
{
|
||||
name: req.session.user.name,
|
||||
code: short.randomUUID(6),
|
||||
@@ -22,12 +22,9 @@ async function sendVerification(req: NextApiRequest, res: NextApiResponse) {
|
||||
},
|
||||
[req.session.user.email],
|
||||
"EnCoach Verification",
|
||||
"verification",
|
||||
);
|
||||
|
||||
await transport.sendMail(mailOptions);
|
||||
|
||||
res.status(200).json({ok: true});
|
||||
res.status(200).json({ok});
|
||||
return;
|
||||
}
|
||||
res.status(404).json({ok: false});
|
||||
|
||||
Reference in New Issue
Block a user