Skip to content

Commit 87b98b7

Browse files
authored
complete flows and emails (#981)
1 parent b4ba110 commit 87b98b7

File tree

34 files changed

+442
-190
lines changed

34 files changed

+442
-190
lines changed

netlify/functions-src/functions/common/interfaces/user.interface.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
import type { ObjectId } from 'mongodb'
22

3+
export enum ChannelName {
4+
EMAIL = 'email',
5+
SLACK = 'slack',
6+
LINKED = 'linkedin',
7+
FACEBOOK = 'facebook',
8+
TWITTER = 'twitter',
9+
GITHUB = 'github',
10+
WEBSITE = 'website',
11+
}
12+
13+
export interface Channel extends Document {
14+
readonly type: ChannelName;
15+
readonly id: string;
16+
}
17+
318
export interface User {
419
readonly _id: ObjectId;
520
available?: boolean;
@@ -12,14 +27,15 @@ export interface User {
1227
image?: {
1328
filename: string;
1429
};
15-
channels?: any[];
30+
channels?: Channel[];
1631
createdAt: Date;
1732
spokenLanguages?: string[];
1833
tags?: string[];
1934
}
2035

2136
export type ApplicationUser = User & {
2237
email_verified: boolean;
38+
picture: string;
2339
}
2440

2541
export enum Role {

netlify/functions-src/functions/data/mentors.ts

+13-4
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,30 @@ type ApproveApplicationResult = {
2828
user: User;
2929
}
3030

31-
export const approveApplication = async (applicationId: string): Promise<ApproveApplicationResult> => {
31+
export const respondToApplication = async (
32+
applicationId: string,
33+
status: Application['status'],
34+
reason?: string
35+
): Promise<Application> => {
3236
const applicationsCollection = getCollection<Application>('applications');
33-
const usersCollection = getCollection<User>('users');
3437

35-
// Update the application status to "Approved"
3638
const updatedApplication = await applicationsCollection.findOneAndUpdate(
3739
{ _id: new ObjectId(applicationId) },
38-
{ $set: { status: 'Approved' } },
40+
{ $set: { status, reason } },
3941
{ returnDocument: 'after' }
4042
);
4143

4244
if (!updatedApplication) {
4345
throw new Error(`Application ${applicationId} not found`);
4446
}
4547

48+
return updatedApplication;
49+
}
50+
51+
export const approveApplication = async (applicationId: string): Promise<ApproveApplicationResult> => {
52+
const updatedApplication = await respondToApplication(applicationId, 'Approved');
53+
const usersCollection = getCollection<User>('users');
54+
4655
const updatedUser = await usersCollection.findOneAndUpdate(
4756
{ _id: updatedApplication.user },
4857
{ $addToSet: { roles: 'Mentor' } },

netlify/functions-src/functions/data/mentorships.ts

+91-7
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,84 @@
1-
import { ObjectId } from 'mongodb';
1+
import { ObjectId, type Filter } from 'mongodb';
22
import { getCollection } from '../utils/db'
33
import { Status, type Mentorship } from '../interfaces/mentorship';
4-
import { DataError } from './errors';
54
import { upsertEntity } from './utils';
65
import type { EntityPayload } from './types';
6+
import { ChannelName, type User } from '../common/interfaces/user.interface';
7+
import { buildMailToURL, buildSlackURL } from '../utils/contactUrl';
8+
9+
export const getMentorships = async (query: Record<string, string | undefined>): Promise<any[]> => {
10+
const mentorshipsCollection = getCollection('mentorships');
11+
12+
const userId = query.userId;
13+
if (!userId) {
14+
throw new Error('User ID is required');
15+
}
16+
17+
// TODO: - validate that either it's admin or the userId is the same as the one in the token
18+
const filter: Filter<Document> = {
19+
$or: [
20+
{ mentor: new ObjectId(userId) },
21+
{ mentee: new ObjectId(userId) },
22+
],
23+
};
24+
25+
if (query.id) {
26+
try {
27+
const objectId = new ObjectId(query.id); // Convert the ID to ObjectId
28+
filter._id = objectId;
29+
} catch {
30+
throw new Error('Invalid mentorship ID');
31+
}
32+
}
33+
34+
if (query.from) {
35+
filter.createdAt = { $gte: new Date(query.from) };
36+
}
37+
38+
// Create an aggregation pipeline that includes the full user objects
39+
return mentorshipsCollection.aggregate([
40+
{ $match: filter },
41+
{
42+
$lookup: {
43+
from: 'users',
44+
localField: 'mentor',
45+
foreignField: '_id',
46+
as: 'mentorData'
47+
}
48+
},
49+
{
50+
$lookup: {
51+
from: 'users',
52+
localField: 'mentee',
53+
foreignField: '_id',
54+
as: 'menteeData'
55+
}
56+
},
57+
{
58+
$addFields: {
59+
mentor: { $arrayElemAt: ['$mentorData', 0] },
60+
mentee: { $arrayElemAt: ['$menteeData', 0] },
61+
// Add isMine field that checks if mentee._id equals userId
62+
isMine: {
63+
$eq: [
64+
{ $toString: { $arrayElemAt: ['$menteeData._id', 0] } },
65+
userId
66+
]
67+
},
68+
}
69+
},
70+
]).toArray();
71+
};
772

873
export const findMentorship = async (mentorId: ObjectId, userId: ObjectId) => {
9-
const mentorship = getCollection('mentorships')
74+
const mentorship = getCollection<Mentorship>('mentorships')
1075
.find({
11-
mentor: mentorId,
12-
mentee: userId
76+
mentor: new ObjectId(mentorId),
77+
mentee: new ObjectId(userId),
1378
});
1479

15-
return mentorship[0];
80+
const foundMentorship = await mentorship.toArray();
81+
return foundMentorship[0];
1682
}
1783

1884
export const getOpenRequestsCount = async (userId: ObjectId) => {
@@ -30,4 +96,22 @@ export const getOpenRequestsCount = async (userId: ObjectId) => {
3096
export const upsertMentorship = async (mentorship: EntityPayload<Mentorship>) => {
3197
const upsertedMentorship = upsertEntity<Mentorship>('mentorships', mentorship);
3298
return upsertedMentorship;
33-
}
99+
}
100+
101+
export const getMentorContactURL = (mentor: User) => {
102+
// slack if exists, otherwise email
103+
const slackId = mentor.channels?.find(channel => channel.type === ChannelName.SLACK)?.id;
104+
return buildSlackURL(slackId) || buildMailToURL(mentor.email);
105+
}
106+
107+
export const getMenteeOpenRequestsCount = async (menteeId: ObjectId) => {
108+
const openRequests = await getCollection('mentorships')
109+
.countDocuments({
110+
mentee: menteeId,
111+
status: {
112+
$in: [Status.NEW, Status.VIEWED]
113+
}
114+
});
115+
116+
return openRequests;
117+
}

netlify/functions-src/functions/email/emails.ts

+82-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,28 @@
11
import { send } from './client';
22

3+
export const sendEmailVerification = async ({ name, email, link }: { name: string; email: string; link: string; }) => {
4+
return send({
5+
name: 'email-verification',
6+
to: email,
7+
subject: 'Verify your email address',
8+
data: {
9+
name,
10+
link,
11+
},
12+
});
13+
}
14+
15+
export const sendMentorApplicationReceived = async ({ name, email }: { name: string; email: string; }) => {
16+
return send({
17+
name: 'mentor-application-received',
18+
to: email,
19+
subject: 'Mentor Application Received',
20+
data: {
21+
name,
22+
},
23+
});
24+
}
25+
326
export const sendApplicationApprovedEmail = async ({ name, email }: { name: string; email: string; }) => {
427
return send({
528
name: 'mentor-application-approved',
@@ -11,7 +34,7 @@ export const sendApplicationApprovedEmail = async ({ name, email }: { name: stri
1134
});
1235
}
1336

14-
export const sendApplocationDeclinedEmail = async ({ name, email, reason }: { name: string; email: string; reason: string; }) => {
37+
export const sendApplicationDeclinedEmail = async ({ name, email, reason }: { name: string; email: string; reason: string; }) => {
1538
return send({
1639
name: 'mentor-application-declined',
1740
to: email,
@@ -21,4 +44,61 @@ export const sendApplocationDeclinedEmail = async ({ name, email, reason }: { na
2144
reason,
2245
},
2346
});
24-
}
47+
}
48+
49+
export const sendMentorshipRequest = async ({ menteeName, mentorName, email, background, expectation, menteeEmail, message }: { menteeName: string; mentorName: string; email: string; background: string; expectation: string; menteeEmail: string; message: string; }) => {
50+
return send({
51+
name: 'mentorship-requested',
52+
to: email,
53+
subject: 'Mentorship Request',
54+
data: {
55+
menteeName,
56+
mentorName,
57+
background,
58+
expectation,
59+
menteeEmail,
60+
message,
61+
},
62+
});
63+
}
64+
65+
export const sendMentorshipAccepted = async ({ menteeName, mentorName, email, contactURL, openRequests }: { menteeName: string; mentorName: string; email: string; contactURL: string; openRequests: number; }) => {
66+
return send({
67+
name: 'mentorship-accepted',
68+
to: email,
69+
subject: 'Mentorship Accepted',
70+
data: {
71+
menteeName,
72+
mentorName,
73+
contactURL,
74+
openRequests,
75+
},
76+
});
77+
}
78+
79+
export const sendMentorshipDeclined = async ({ menteeName, mentorName, email, bySystem, reason }: { menteeName: string; mentorName: string; email: string; bySystem: boolean; reason: string; }) => {
80+
return send({
81+
name: 'mentorship-declined',
82+
to: email,
83+
subject: 'Mentorship Declined',
84+
data: {
85+
menteeName,
86+
mentorName,
87+
bySystem,
88+
reason,
89+
},
90+
});
91+
}
92+
93+
export const sendMentorshipRequestCancelled = async ({ menteeName, mentorName, email, reason }: { menteeName: string; mentorName: string; email: string; reason: string; }) => {
94+
return send({
95+
name: 'mentorship-cancelled',
96+
to: email,
97+
subject: 'Mentorship Request Cancelled',
98+
data: {
99+
menteeName,
100+
mentorName,
101+
reason,
102+
},
103+
});
104+
}

netlify/functions-src/functions/email/templates/show.js

+10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
const express = require('express');
22
const { compile } = require('ejs');
33
const fs = require('fs');
4+
const path = require('path');
5+
const { marked } = require('marked');
46

57
const app = express();
68
const port = 3003;
@@ -18,6 +20,14 @@ function injectData(template, data) {
1820
});
1921
}
2022

23+
app.get('/', function (req, res) {
24+
// Return README.md content if templateName is empty
25+
const readmePath = path.join(__dirname, 'README.md');
26+
const readmeContent = fs.readFileSync(readmePath, { encoding: 'utf8' });
27+
const htmlContent = marked(readmeContent);
28+
return res.send(htmlContent);
29+
});
30+
2131
app.get('/:templateName', function (req, res) {
2232
const { templateName } = req.params;
2333
if (templateName.includes('.')) return;
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { withDB } from './hof/withDB';
22
import { withAuth } from './utils/auth';
33
import { withRouter } from './hof/withRouter';
4-
import { handler as mentorshipsRequestsHandler, updateRequestHandler } from './modules/mentorships/requests'
4+
import { handler as mentorshipsRequestsHandler, updateMentorshipRequestHandler } from './modules/mentorships/requests'
55
import { handler as getAllMentorshipsHandler } from './modules/mentorships/get-all'
66
import { handler as applyForMentorshipHandler } from './modules/mentorships/apply';
77
import { Role } from './common/interfaces/user.interface';
@@ -11,7 +11,11 @@ export const handler: ApiHandler = withDB(
1111
withRouter([
1212
['/', 'GET', withAuth(getAllMentorshipsHandler, { role: Role.ADMIN })],
1313
['/:userId/requests', 'GET', withAuth(mentorshipsRequestsHandler)],
14-
['/:userId/requests/:mentorshipId', 'PUT', withAuth(updateRequestHandler)],
15-
['/:mentorId/apply', 'POST', withAuth(applyForMentorshipHandler)],
14+
['/:userId/requests/:mentorshipId', 'PUT', withAuth(updateMentorshipRequestHandler, {
15+
includeFullUser: true,
16+
})],
17+
['/:mentorId/apply', 'POST', withAuth(applyForMentorshipHandler, {
18+
includeFullUser: true,
19+
})],
1620
])
1721
);
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
import type { User } from '../../../common/interfaces/user.interface';
22
import { upsertApplication } from '../../../data/mentors';
3+
import { sendMentorApplicationReceived } from '../../../email/emails';
34
import type { ApiHandler } from '../../../types';
45
import { success } from '../../../utils/response';
56
import type { Application } from '../types';
67

8+
// create / update application by user
79
export const handler: ApiHandler<Application, User> = async (event, context) => {
810
const application = event.parsedBody!;
911
const { data, isNew } = await upsertApplication({
1012
...application,
1113
user: context.user._id,
1214
status: 'Pending',
1315
});
16+
17+
if (isNew) {
18+
sendMentorApplicationReceived({
19+
name: context.user.name,
20+
email: context.user.email,
21+
});
22+
}
23+
1424
return success({ data }, isNew ? 201 : 200);
1525
}

0 commit comments

Comments
 (0)