Added a new type of e-mail to send to students when a new assignment is created
This commit is contained in:
@@ -34,8 +34,12 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
|
|||||||
const [assignees, setAssignees] = useState<string[]>(assignment?.assignees || []);
|
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 [name, setName] = useState(assignment?.name || generate({minLength: 6, maxLength: 8, min: 2, max: 3, join: " ", formatter: capitalize}));
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [startDate, setStartDate] = useState<Date | null>(assignment ? moment(assignment.startDate).toDate() : moment().add(1, "day").toDate());
|
const [startDate, setStartDate] = useState<Date | null>(
|
||||||
const [endDate, setEndDate] = useState<Date | null>(assignment ? moment(assignment.endDate).toDate() : moment().add(8, "day").toDate());
|
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
|
// 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);
|
||||||
|
|
||||||
@@ -51,23 +55,16 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
|
|||||||
const createAssignment = () => {
|
const createAssignment = () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
(assignment ? axios.patch : axios.post)(
|
(assignment ? axios.patch : axios.post)(`/api/assignments${assignment ? `/${assignment.id}` : ""}`, {
|
||||||
`/api/assignments${assignment ? `/${assignment.id}` : ""}`,
|
|
||||||
{
|
|
||||||
assignees,
|
assignees,
|
||||||
name,
|
name,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
selectedModules,
|
selectedModules,
|
||||||
generateMultiple,
|
generateMultiple,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(
|
toast.success(`The assignment "${name}" has been ${assignment ? "updated" : "created"} successfully!`);
|
||||||
`The assignment "${name}" has been ${
|
|
||||||
assignment ? "updated" : "created"
|
|
||||||
} successfully!`
|
|
||||||
);
|
|
||||||
cancelCreation();
|
cancelCreation();
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
@@ -285,7 +282,7 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div className="flex gap-4 w-full justify-end">
|
<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
|
Generate different exams
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,3 +40,15 @@ export function prepareMailOptions(context: object, to: string[], subject: strin
|
|||||||
context,
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,17 @@
|
|||||||
// 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 {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 {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} from "@/interfaces/exam";
|
import {Exam} from "@/interfaces/exam";
|
||||||
import { flatten } from "lodash";
|
import {capitalize, flatten} from "lodash";
|
||||||
|
import {User} from "@/interfaces/user";
|
||||||
|
import moment from "moment";
|
||||||
|
import {sendEmail} from "@/email";
|
||||||
|
|
||||||
const db = getFirestore(app);
|
const db = getFirestore(app);
|
||||||
|
|
||||||
@@ -49,16 +52,11 @@ function getRandomIndex(arr: any[]): number {
|
|||||||
return randomIndex;
|
return randomIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateExams = async (
|
const generateExams = async (generateMultiple: Boolean, selectedModules: Module[], assignees: string[]): Promise<ExamWithUser[]> => {
|
||||||
generateMultiple: Boolean,
|
|
||||||
selectedModules: Module[],
|
|
||||||
assignees: string[]
|
|
||||||
): 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 = await assignees.map(async (assignee) => {
|
const allExams = await assignees.map(async (assignee) => {
|
||||||
const selectedModulePromises = await selectedModules.map(
|
const selectedModulePromises = await selectedModules.map(async (module: Module) => {
|
||||||
async (module: Module) => {
|
|
||||||
try {
|
try {
|
||||||
const exams: Exam[] = await getExams(db, module, "true", assignee);
|
const exams: Exam[] = await getExams(db, module, "true", assignee);
|
||||||
|
|
||||||
@@ -71,22 +69,17 @@ 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 = await selectedModules.map(
|
const selectedModulePromises = await selectedModules.map(async (module: Module) => {
|
||||||
async (module: Module) => {
|
|
||||||
const exams: Exam[] = await getExams(db, module, "false", undefined);
|
const exams: Exam[] = await getExams(db, module, "false", undefined);
|
||||||
const exam = exams[getRandomIndex(exams)];
|
const exam = exams[getRandomIndex(exams)];
|
||||||
|
|
||||||
@@ -94,41 +87,34 @@ const generateExams = async (
|
|||||||
return {module: exam.module, id: exam.id};
|
return {module: exam.module, id: exam.id};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
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) {
|
||||||
const {
|
const {
|
||||||
selectedModules,
|
selectedModules,
|
||||||
assignees,
|
assignees,
|
||||||
// Generarte multiple true would generate an unique exam for eacah user
|
// Generate multiple true would generate an unique exam for each user
|
||||||
// false would generate the same exam for all usersa
|
// false would generate the same exam for all users
|
||||||
generateMultiple = false,
|
generateMultiple = false,
|
||||||
...body
|
...body
|
||||||
} = req.body as {
|
} = req.body as {
|
||||||
selectedModules: Module[];
|
selectedModules: Module[];
|
||||||
assignees: string[];
|
assignees: string[];
|
||||||
generateMultiple: Boolean;
|
generateMultiple: Boolean;
|
||||||
|
name: string;
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const exams: ExamWithUser[] = await generateExams(
|
const exams: ExamWithUser[] = await generateExams(generateMultiple, selectedModules, assignees);
|
||||||
generateMultiple,
|
|
||||||
selectedModules,
|
|
||||||
assignees
|
|
||||||
);
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,4 +127,23 @@ async function POST(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
|
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 {app, adminApp} from "@/firebase";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import {withIronSessionApiRoute} from "iron-session/next";
|
import {withIronSessionApiRoute} from "iron-session/next";
|
||||||
import {prepareMailer, prepareMailOptions} from "@/email";
|
import {prepareMailer, prepareMailOptions, sendEmail} from "@/email";
|
||||||
import ShortUniqueId from "short-unique-id";
|
import ShortUniqueId from "short-unique-id";
|
||||||
|
|
||||||
export default withIronSessionApiRoute(sendVerification, sessionOptions);
|
export default withIronSessionApiRoute(sendVerification, sessionOptions);
|
||||||
@@ -13,8 +13,8 @@ async function sendVerification(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const short = new ShortUniqueId();
|
const short = new ShortUniqueId();
|
||||||
|
|
||||||
if (req.session.user) {
|
if (req.session.user) {
|
||||||
const transport = prepareMailer("verification");
|
const ok = await sendEmail(
|
||||||
const mailOptions = prepareMailOptions(
|
"verification",
|
||||||
{
|
{
|
||||||
name: req.session.user.name,
|
name: req.session.user.name,
|
||||||
code: short.randomUUID(6),
|
code: short.randomUUID(6),
|
||||||
@@ -22,12 +22,9 @@ async function sendVerification(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
},
|
},
|
||||||
[req.session.user.email],
|
[req.session.user.email],
|
||||||
"EnCoach Verification",
|
"EnCoach Verification",
|
||||||
"verification",
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await transport.sendMail(mailOptions);
|
res.status(200).json({ok});
|
||||||
|
|
||||||
res.status(200).json({ok: true});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.status(404).json({ok: false});
|
res.status(404).json({ok: false});
|
||||||
|
|||||||
Reference in New Issue
Block a user