) => {
29 | const template = await getTemplateContent(name);
30 | const layout = await getLayout();
31 | const content = compile(template)({
32 | ...SYSTEM_DATA,
33 | ...data
34 | });
35 | return compile(layout)({ content });
36 | }
37 |
38 | const getLayout = async () => {
39 | return getTemplateContent('layout');
40 | }
41 |
42 | const getTemplateContent = async (name: string) => {
43 | const templatesDir = path.resolve(__dirname, 'email/templates');
44 | const templatePath = `${templatesDir}/${name}.html`;
45 | try {
46 | return promises.readFile(templatePath, {
47 | encoding: 'utf8',
48 | });
49 | } catch (error) {
50 | // eslint-disable-next-line no-console
51 | console.error('Error reading template file:', error);
52 | throw new Error(`Template file not found: ${templatePath}`);
53 | }
54 | }
--------------------------------------------------------------------------------
/netlify/functions-src/functions/email/sendgrid.ts:
--------------------------------------------------------------------------------
1 | import sgMail from '@sendgrid/mail';
2 | import type { SendData } from './interfaces/email.interface';
3 | import Config from '../config';
4 |
5 | sgMail.setApiKey(Config.sendGrid.API_KEY);
6 |
7 | export const sendEmail = async (payload: SendData) => {
8 | try {
9 | const msg = {
10 | to: payload.to,
11 | from: Config.email.FROM,
12 | subject: payload.subject,
13 | html: payload.html,
14 | };
15 |
16 | console.log('Sending email:', msg.to, msg.subject);
17 | const result = await sgMail.send(msg);
18 | console.log('Email sent:', msg.to, msg.subject, result[0].statusCode);
19 | return result;
20 | } catch (error) {
21 | console.error('Error sending email:', error);
22 | throw new Error('Failed to send email');
23 | }
24 | }
--------------------------------------------------------------------------------
/netlify/functions-src/functions/email/templates/email-verification.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
12 |
13 |
14 |
15 |
Hey <%= name %>
16 |
24 | You're almost there!
25 |
26 |
Please click the link below to verify your email
27 |
28 | Verify
49 |
50 |
51 |
52 | (Or copy and paste this url
53 | <%= link %> into your browser)
54 |
55 |
56 |
--------------------------------------------------------------------------------
/netlify/functions-src/functions/email/templates/mentor-application-admin-notification.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
12 |
13 |
14 |
15 |
16 | Hello, Admin!
17 |
18 |
21 | New Mentor Application Received
22 |
23 |
24 | A new mentor application has been submitted by <%= name %>. Here are the details:
25 |
26 |
27 |
Name: <%= name %>
28 |
Email: <%= email %>
29 | <% if (title) { %>
30 |
Title: <%= title %>
31 | <% } %>
32 | <% if (tags && tags.length > 0) { %>
33 |
Tags: <%= tags.join(', ') %>
34 | <% } %>
35 | <% if (country) { %>
36 |
Country: <%= country %>
37 | <% } %>
38 | <% if (spokenLanguages && spokenLanguages.length > 0) { %>
39 |
Spoken Languages: <%= spokenLanguages.join(', ') %>
40 | <% } %>
41 |
42 |
43 |
44 | Please review this application as soon as possible. View Application
45 |
46 |
47 | Thank you for your attention.
48 |
49 |
--------------------------------------------------------------------------------
/netlify/functions-src/functions/email/templates/mentor-application-declined.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
12 |
13 |
14 |
15 |
16 | Hello, <%= name %>!
17 |
18 |
21 | Sorry but we can't approve your application yet
22 |
23 |
24 | Unfortunately, we can't approve your application yet due to the reason
25 | below. If you feel your application should be accepted as is, please send an
26 | email to
27 | admin@codingcoach.io.
30 | Once you fix the application, please submit it again.
31 |
32 |
33 | <%= reason %>
34 |
35 |
47 |
48 |
--------------------------------------------------------------------------------
/netlify/functions-src/functions/email/templates/mentor-application-received.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
12 |
13 |
14 |
15 |
16 | Hello, <%= name %>!
17 |
18 |
21 | Mentor Application Received
22 |
23 |
24 | Thank you so much for applying to become a mentor here at Coding Coach. We are reviewing your application, and will let you know when we have completed our review.
25 |
26 |
27 |
28 | Until then, have a look at this super helpful document to get yourself ready to be a mentor!
29 |
30 |
42 |
43 |
--------------------------------------------------------------------------------
/netlify/functions-src/functions/email/templates/mentorship-accepted.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
12 |
13 |
14 |
15 |
16 | Congratulations, <%= menteeName %>!
17 |
18 |
21 | Mentorship Request Accepted
22 |
23 |
24 | Your request for mentorship with
25 | <%= mentorName %>
26 | has been approved.
27 |
28 |
29 | <%= mentorName %> asks that you contact them at
30 | <%= contactURL %> in order to get started.
31 |
32 | <% if (openRequests) { %>
33 |
34 | 👉 Note that you have <%= openRequests %> open mentorship requests.
35 | Once the mentorship is actually started, please cancel your other similar requests via your Backoffice .
41 |
42 | <% } %>
43 |
44 |
--------------------------------------------------------------------------------
/netlify/functions-src/functions/email/templates/mentorship-cancelled.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
12 |
13 |
14 |
15 |
Hello, <%= mentorName %>!
16 |
24 | Mentorship Cancelled
25 |
26 |
27 |
28 | Thank you for considering mentoring <%= menteeName %> . Now they asked to
29 | withdraw the request because
30 |
31 |
32 |
<%= reason %>
33 |
34 |
35 | Although this one didn't work out, we're sure you'll get more requests soon.
36 |
37 |
38 |
--------------------------------------------------------------------------------
/netlify/functions-src/functions/email/templates/mentorship-declined.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
12 |
13 |
14 |
15 |
16 | Hello, <%= menteeName %>!
17 |
18 |
26 | Mentorship Request Not Accepted
27 |
28 |
29 | <% if (typeof(bySystem) !="undefined" && bySystem) { %>
30 |
31 | Unfortunately, your request for mentorship with
32 | <%= mentorName %>
33 | has been declined by our system because
34 | <%= mentorName %>
35 | seems to be unavailable at the moment.
36 |
37 | <% } else { %>
38 |
39 | Unfortunately, your request for mentorship with
40 | <%= mentorName %>
41 | was not accepted.
42 |
43 |
44 | They provided the reason below, which we hope will help you find your next
45 | mentor:
46 |
47 |
<%= reason %>
48 | <% } %>
49 |
50 |
51 | Although this one didn't work out, there are many other mentors at
52 | CodingCoach looking to mentor
53 | someone like you.
54 |
55 |
56 |
--------------------------------------------------------------------------------
/netlify/functions-src/functions/email/templates/nodemon-emails.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["."],
3 | "ext": "html,js",
4 | "exec": "node netlify/functions-src/functions/email/templates/show.js"
5 | }
6 |
--------------------------------------------------------------------------------
/netlify/functions-src/functions/email/templates/show.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const { compile } = require('ejs');
3 | const fs = require('fs');
4 | const path = require('path');
5 | const { marked } = require('marked');
6 |
7 | const app = express();
8 | const port = 3003;
9 | const layout = fs.readFileSync(`${__dirname}/layout.html`, {
10 | encoding: 'utf8',
11 | });
12 |
13 | function injectData(template, data) {
14 | const content = compile(template)({
15 | ...data,
16 | baseUrl: 'https://example.com',
17 | });
18 | return compile(layout)({
19 | content,
20 | });
21 | }
22 |
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 |
31 | app.get('/:templateName', function (req, res) {
32 | const { templateName } = req.params;
33 | if (templateName.includes('.')) return;
34 | const { data } = req.query;
35 | const template = fs.readFileSync(
36 | `${__dirname}/${templateName}.html`,
37 | { encoding: 'utf8' },
38 | );
39 | const content = injectData(
40 | template,
41 | JSON.parse(data || '{}'),
42 | );
43 | res.send(content);
44 | });
45 |
46 | app.listen(port, () => {
47 | console.log(`Example app listening at http://localhost:${port}`);
48 | });
49 |
--------------------------------------------------------------------------------
/netlify/functions-src/functions/favorites.ts:
--------------------------------------------------------------------------------
1 | import { withDB } from './hof/withDB';
2 | import { withRouter } from './hof/withRouter';
3 | import { getFavoritesHandler } from './modules/favorites/get';
4 | import type { ApiHandler } from './types';
5 | import { withAuth } from './utils/auth';
6 | import { toggleFavoriteHandler } from './modules/favorites/post'
7 |
8 | export const handler: ApiHandler = withDB(
9 | withRouter([
10 | ['/', 'GET', withAuth(getFavoritesHandler, {
11 | authRequired: true,
12 | includeFullUser: true,
13 | })],
14 | ['/:mentorId', 'POST', withAuth(toggleFavoriteHandler, {
15 | authRequired: true,
16 | includeFullUser: true,
17 | })],
18 | ])
19 | )
--------------------------------------------------------------------------------
/netlify/functions-src/functions/hof/withDB.ts:
--------------------------------------------------------------------------------
1 | import type { ApiHandler } from '../types';
2 | import { connectToDatabase } from '../utils/db';
3 |
4 | export const withDB = (handler: ApiHandler): ApiHandler => {
5 | return async (event, context) => {
6 | await connectToDatabase()
7 | return handler(event, context)
8 | }
9 | }
--------------------------------------------------------------------------------
/netlify/functions-src/functions/interfaces/mentorship.ts:
--------------------------------------------------------------------------------
1 | import { ObjectId } from 'mongodb';
2 |
3 | export enum Status {
4 | NEW = 'New',
5 | VIEWED = 'Viewed',
6 | APPROVED = 'Approved',
7 | REJECTED = 'Rejected',
8 | CANCELLED = 'Cancelled',
9 | TERMINATED = 'Terminated',
10 | }
11 |
12 | export interface Mentorship {
13 | readonly _id: ObjectId;
14 | readonly mentor: ObjectId;
15 | readonly mentee: ObjectId;
16 | status: Status;
17 | readonly message: string;
18 | readonly goals: string[];
19 | readonly expectation: string;
20 | readonly background: string;
21 | reason?: string;
22 | readonly createdAt: Date;
23 | readonly updatedAt: Date;
24 | readonly reminderSentAt?: Date;
25 | }
26 |
--------------------------------------------------------------------------------
/netlify/functions-src/functions/mentors.ts:
--------------------------------------------------------------------------------
1 | import type { ApiHandler } from './types';
2 | import { withDB } from './hof/withDB';
3 | import { withAuth } from './utils/auth';
4 | import { withRouter } from './hof/withRouter';
5 | import { handler as getMentorsHanler } from './modules/mentors/get';
6 | import { handler as getApplicationsHandler } from './modules/mentors/applications/get';
7 | import { handler as upsertApplicationHandler } from './modules/mentors/applications/post';
8 | import { handler as updateApplicationHandler } from './modules/mentors/applications/put';
9 | import { Role } from './common/interfaces/user.interface';
10 |
11 | export const handler: ApiHandler = withDB(
12 | withRouter([
13 | ['/', 'GET', withAuth(getMentorsHanler, { authRequired: false })],
14 | ['/applications', 'GET', withAuth(getApplicationsHandler, { includeFullUser: true, role: Role.ADMIN })],
15 | ['/applications/:applicationId', 'PUT', withAuth(updateApplicationHandler, { includeFullUser: true, role: Role.ADMIN })],
16 | ['/applications', 'POST', withAuth(upsertApplicationHandler, { includeFullUser: true, })],
17 | // TODO: find out if needed
18 | // ['/:userId/applications', 'GET', withAuth(getUserApplicationsHandler, { returnUser: true })],
19 | ])
20 | );
21 |
--------------------------------------------------------------------------------
/netlify/functions-src/functions/mentorships.ts:
--------------------------------------------------------------------------------
1 | import { withDB } from './hof/withDB';
2 | import { withAuth } from './utils/auth';
3 | import { withRouter } from './hof/withRouter';
4 | import { handler as mentorshipsRequestsHandler, updateMentorshipRequestHandler } from './modules/mentorships/requests'
5 | import { handler as getAllMentorshipsHandler } from './modules/mentorships/get-all'
6 | import { handler as applyForMentorshipHandler } from './modules/mentorships/apply';
7 | import { Role } from './common/interfaces/user.interface';
8 | import type { ApiHandler } from './types';
9 |
10 | export const handler: ApiHandler = withDB(
11 | withRouter([
12 | ['/', 'GET', withAuth(getAllMentorshipsHandler, { role: Role.ADMIN })],
13 | ['/:userId/requests', 'GET', withAuth(mentorshipsRequestsHandler)],
14 | ['/:userId/requests/:mentorshipId', 'PUT', withAuth(updateMentorshipRequestHandler, {
15 | includeFullUser: true,
16 | })],
17 | ['/:mentorId/apply', 'POST', withAuth(applyForMentorshipHandler, {
18 | includeFullUser: true,
19 | })],
20 | ])
21 | );
22 |
--------------------------------------------------------------------------------
/netlify/functions-src/functions/modules/favorites/get.ts:
--------------------------------------------------------------------------------
1 | import type { User } from '../../common/interfaces/user.interface';
2 | import { getFavorites } from '../../data/favorites';
3 | import type { ApiHandler } from '../../types';
4 | import { success } from '../../utils/response';
5 |
6 | export const getFavoritesHandler: ApiHandler = async (_event, context) => {
7 | const userId = context.user._id;
8 | const mentorIds = await getFavorites(userId);
9 |
10 | return success({
11 | data: {
12 | mentorIds,
13 | },
14 | });
15 | };
16 |
--------------------------------------------------------------------------------
/netlify/functions-src/functions/modules/favorites/post.ts:
--------------------------------------------------------------------------------
1 | import type { ApiHandler } from '../../types';
2 | import { error, success } from '../../utils/response';
3 | import { toggleFavorite } from '../../data/favorites';
4 | import { DataError } from '../../data/errors';
5 |
6 | export const toggleFavoriteHandler: ApiHandler = async (event, context) => {
7 | try {
8 | const mentorId = event.queryStringParameters?.mentorId;
9 | if (!mentorId) {
10 | return error('Mentor ID is required', 400);
11 | }
12 |
13 | const mentorIds = await toggleFavorite(context.user._id, mentorId);
14 | return success({
15 | data: {
16 | mentorIds,
17 | },
18 | });
19 | } catch (err) {
20 | if (err instanceof DataError) {
21 | return error(err.message, err.statusCode);
22 | }
23 | return error('Internal server error', 500);
24 | }
25 | };
--------------------------------------------------------------------------------
/netlify/functions-src/functions/modules/mentors/applications/get.ts:
--------------------------------------------------------------------------------
1 | import { getApplications } from '../../../data/mentors';
2 | import type { ApiHandler } from '../../../types'
3 | import { success } from '../../../utils/response';
4 |
5 | export const handler: ApiHandler = async (event) => {
6 | const status = event.queryStringParameters?.status;
7 | const applications = await getApplications(status);
8 |
9 | return success({ data: applications });
10 | }
--------------------------------------------------------------------------------
/netlify/functions-src/functions/modules/mentors/applications/post.ts:
--------------------------------------------------------------------------------
1 | import type { User } from '../../../common/interfaces/user.interface';
2 | import { upsertApplication } from '../../../data/mentors';
3 | import { sendMentorApplicationReceived, sendMentorApplicationAdminNotification } from '../../../email/emails';
4 | import type { ApiHandler } from '../../../types';
5 | import { success } from '../../../utils/response';
6 | import type { Application } from '../types';
7 |
8 | // create / update application by user
9 | export const handler: ApiHandler = async (event, context) => {
10 | const application = event.parsedBody!;
11 | const { data, isNew } = await upsertApplication({
12 | ...application,
13 | user: context.user._id,
14 | status: 'Pending',
15 | });
16 |
17 | if (isNew) {
18 | console.log('Sending mentor application received email:', context.user._id);
19 | try {
20 | await sendMentorApplicationReceived({
21 | name: context.user.name,
22 | email: context.user.email,
23 | });
24 | await sendMentorApplicationAdminNotification(context.user);
25 | } catch (error) {
26 | console.error('Error sending mentor application received email:', error);
27 | }
28 | }
29 |
30 | return success({ data }, isNew ? 201 : 200);
31 | }
--------------------------------------------------------------------------------
/netlify/functions-src/functions/modules/mentors/applications/put.ts:
--------------------------------------------------------------------------------
1 | import type { User } from '../../../common/interfaces/user.interface';
2 | import { approveApplication, respondToApplication } from '../../../data/mentors';
3 | import type { ApiHandler } from '../../../types';
4 | import type { Application } from '../types';
5 | import { error, success } from '../../../utils/response';
6 | import { sendApplicationApprovedEmail, sendApplicationDeclinedEmail } from '../../../email/emails';
7 | import { getUserBy } from '../../../data/users';
8 |
9 | // update application by admin
10 | export const handler: ApiHandler, User> = async (event, context) => {
11 | const { applicationId } = event.queryStringParameters || {};
12 | const { status, reason } = event.parsedBody || {};
13 |
14 | if (!applicationId || !status) {
15 | return { statusCode: 400, body: 'Bad request' };
16 | }
17 |
18 | if (status === 'Approved') {
19 | try {
20 | const { user, application } = await approveApplication(applicationId);
21 | await sendApplicationApprovedEmail({ name: user.name, email: user.email });
22 |
23 | return success({
24 | data: application,
25 | });
26 | } catch (e) {
27 | return error(e.message, 500);
28 | }
29 | }
30 |
31 | if (status === 'Rejected') {
32 | const application = await respondToApplication(applicationId, status, reason);
33 | const user = await getUserBy('_id', application.user);
34 | if (user) {
35 | await sendApplicationDeclinedEmail({ name: user.name, email: user.email, reason: application.reason! });
36 | }
37 |
38 | return success({
39 | data: application,
40 | });
41 | }
42 |
43 | return error(`Invalid status ${status}`, 400);
44 | }
45 |
--------------------------------------------------------------------------------
/netlify/functions-src/functions/modules/mentors/types.ts:
--------------------------------------------------------------------------------
1 | import type { ObjectId, OptionalId } from 'mongodb'
2 | import type { PaginationParams } from '../../types'
3 |
4 | export interface Mentor {
5 | _id: string
6 | name: string
7 | email: string
8 | title?: string
9 | tags?: string[]
10 | country?: string
11 | spokenLanguages?: string[]
12 | avatar?: string
13 | }
14 |
15 | export type ApplicationStatus = 'Pending' | 'Approved' | 'Rejected';
16 | export type Application = OptionalId<{
17 | user: ObjectId;
18 | status: ApplicationStatus;
19 | reason?: string;
20 | }>;
21 |
22 | export interface GetMentorsQuery {
23 | available?: boolean
24 | tags?: string | string[]
25 | country?: string
26 | spokenLanguages?: string | string[]
27 | page?: string
28 | limit?: string
29 | }
30 |
31 | export interface GetMentorsResponse {
32 | data: Mentor[]
33 | filters: any[]
34 | pagination: PaginationParams;
35 | }
36 |
--------------------------------------------------------------------------------
/netlify/functions-src/functions/modules/mentorships/get-all.ts:
--------------------------------------------------------------------------------
1 | import { HandlerEvent } from '@netlify/functions';
2 | import { getCollection } from '../../utils/db';
3 | import { withErrorHandling, error, success } from '../../utils/response';
4 | import { withAuth } from '../../utils/auth';
5 | import { Role } from '../../common/interfaces/user.interface';
6 |
7 | const getMentorships = async (query: any): Promise => {
8 | const mentorshipsCollection = getCollection('mentorships');
9 |
10 | const {from, to} = query;
11 |
12 | const filter: any = {};
13 |
14 | if (from) {
15 | filter.createdAt = { $gte: new Date(query.from) };
16 | }
17 | if (to) {
18 | filter.createdAt = { $lte: new Date(query.to) };
19 | }
20 |
21 | return mentorshipsCollection.aggregate([
22 | { $match: filter },
23 | {
24 | $lookup: {
25 | from: 'users',
26 | localField: 'mentor',
27 | foreignField: '_id',
28 | as: 'mentor'
29 | }
30 | },
31 | {
32 | $lookup: {
33 | from: 'users',
34 | localField: 'mentee',
35 | foreignField: '_id',
36 | as: 'mentee'
37 | }
38 | },
39 | { $unwind: { path: '$mentor' } },
40 | { $unwind: { path: '$mentee' } }
41 | ]).toArray();
42 | };
43 |
44 | const getAllMentorshipsHandler = async (event: HandlerEvent) => {
45 | try {
46 | const query = event.queryStringParameters || {};
47 | const mentorships = await getMentorships(query);
48 | return success({ data: mentorships });
49 | } catch (err) {
50 | return error(err.message, 400);
51 | }
52 | };
53 |
54 | export const handler = getAllMentorshipsHandler;
--------------------------------------------------------------------------------
/netlify/functions-src/functions/modules/users/delete.ts:
--------------------------------------------------------------------------------
1 | import type { User } from '../../common/interfaces/user.interface';
2 | import type { ApiHandler } from '../../types';
3 | import { deleteUser } from '../../data/users';
4 | import { deleteUser as deleteUserFromAuth0 } from '../../admin/delete';
5 | import { success } from '../../utils/response';
6 |
7 | export const handler: ApiHandler = async (event, context) => {
8 | const {_id, auth0Id } = context.user;
9 | const result = await deleteUser(_id);
10 | deleteUserFromAuth0(auth0Id)
11 | .then(result => {
12 | // eslint-disable-next-line no-console
13 | console.log('User deleted from Auth0:', result);
14 | })
15 | .catch(error => {
16 | // eslint-disable-next-line no-console
17 | console.error('Error deleting user from Auth0:', error);
18 | }
19 | );
20 | return success({ data: result }, 204);
21 | }
22 |
--------------------------------------------------------------------------------
/netlify/functions-src/functions/modules/users/userInfo.ts:
--------------------------------------------------------------------------------
1 | import type { ApiHandler } from '../../types';
2 | import { error, success } from '../../utils/response';
3 | import { getUserById, upsertUser } from '../../data/users';
4 | import type { User } from '../../common/interfaces/user.interface';
5 |
6 | export const handler: ApiHandler = async (event, context) => {
7 | const userId = event.queryStringParameters?.userId;
8 | const currentUserAuth0Id = context.user?.auth0Id;
9 |
10 | if (!userId) {
11 | return {
12 | statusCode: 400,
13 | body: 'userId is required',
14 | };
15 | }
16 | const user = await getUserById(userId, currentUserAuth0Id);
17 |
18 | if (!user) {
19 | console.error(`User id: ${userId} not found`);
20 | return error('User not found', 404);
21 | }
22 |
23 | return success({
24 | data: user,
25 | });
26 | }
27 |
28 | export const updateUserInfoHandler: ApiHandler = async (event, context) => {
29 | try {
30 | const user = event.parsedBody;
31 | if (!user) {
32 | return error('Invalid request body', 400);
33 | }
34 |
35 | if (user.auth0Id !== context.user?.auth0Id) {
36 | return error('Unauthorized', 401);
37 | }
38 |
39 | const upsertedUser = await upsertUser(user);
40 | return success({
41 | data: upsertedUser,
42 | });
43 | } catch (e) {
44 | return error(e.message, 400);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/netlify/functions-src/functions/modules/users/verify.ts:
--------------------------------------------------------------------------------
1 | import { auth0Service } from '../../common/auth0.service';
2 | import type { User } from '../../common/interfaces/user.interface';
3 | import { sendEmailVerification } from '../../email/emails';
4 | import type { ApiHandler } from '../../types';
5 | import { error, success } from '../../utils/response';
6 |
7 | export const handler: ApiHandler = async (_event, context) => {
8 | try {
9 | const { auth0Id, name, email } = context.user;
10 | const { ticket } = await auth0Service.createVerificationEmailTicket(auth0Id);
11 | await sendEmailVerification({
12 | name,
13 | email,
14 | link: ticket,
15 | });
16 |
17 | return success({ data: { message: 'Verification email sent successfully' } });
18 | } catch (e) {
19 | console.error('Error sending verification email:', e);
20 | return error('Error sending verification email', 500);
21 | }
22 | }
--------------------------------------------------------------------------------
/netlify/functions-src/functions/types/index.ts:
--------------------------------------------------------------------------------
1 | import { HandlerContext, HandlerEvent, HandlerResponse } from '@netlify/functions'
2 | import type { WithId } from 'mongodb'
3 | import type { User } from '../common/interfaces/user.interface';
4 |
5 | export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
6 |
7 | export interface AuthUser {
8 | auth0Id: string
9 | }
10 |
11 | export interface AuthContext extends HandlerContext {
12 | user: U
13 | }
14 |
15 | export interface BaseResponse {
16 | statusCode: number
17 | body: string
18 | headers?: { [key: string]: string | number | boolean }
19 | }
20 |
21 | export interface ErrorResponse extends BaseResponse {
22 | statusCode: number
23 | body: string
24 | }
25 |
26 | export interface SuccessResponse extends BaseResponse {
27 | statusCode: number
28 | body: string
29 | }
30 |
31 | export type ApiResponse = Promise
32 |
33 | export interface PaginationParams {
34 | page: number
35 | total: number
36 | hasMore: boolean
37 | }
38 |
39 | export interface FilterParams {
40 | [key: string]: string | string[] | undefined
41 | }
42 |
43 | export type HandlerEventWithBody = HandlerEvent & { parsedBody?: T }
44 |
45 | export type ApiHandler = (event: HandlerEventWithBody, context: AuthContext) => Promise
46 |
47 | export type CreateRequest> = Omit
--------------------------------------------------------------------------------
/netlify/functions-src/functions/users.ts:
--------------------------------------------------------------------------------
1 | import type { ApiHandler } from './types';
2 | import { handler as usersCurrentHandler } from './modules/users/current'
3 | import { handler as getUserInfoHandler, updateUserInfoHandler } from './modules/users/userInfo'
4 | import { handler as deleteUser } from './modules/users/delete'
5 | import { handler as verifyUserHandler } from './modules/users/verify'
6 | import { withRouter } from './hof/withRouter';
7 | import { withDB } from './hof/withDB';
8 | import { withAuth } from './utils/auth';
9 |
10 | export const handler: ApiHandler = withDB(
11 | withRouter([
12 | ['/', 'PUT', withAuth(updateUserInfoHandler)],
13 | ['/', 'DELETE', withAuth(deleteUser, {
14 | includeFullUser: true,
15 | })],
16 | ['/current', 'GET', usersCurrentHandler],
17 | ['/verify', 'POST', withAuth(verifyUserHandler, {
18 | emailVerificationRequired: false,
19 | includeFullUser: true,
20 | })],
21 | ['/:userId', 'GET', withAuth(getUserInfoHandler, {
22 | authRequired: false,
23 | })],
24 | ])
25 | )
--------------------------------------------------------------------------------
/netlify/functions-src/functions/utils/contactUrl.ts:
--------------------------------------------------------------------------------
1 | export const buildSlackURL = (slackId: string | undefined) => {
2 | if (!slackId) {
3 | return null;
4 | }
5 | return `https://coding-coach.slack.com/team/${slackId}`;
6 | }
7 |
8 | export const buildMailToURL = (email: string) => {
9 | return `mailto:${email}`;
10 | }
11 |
--------------------------------------------------------------------------------
/netlify/functions-src/functions/utils/db.ts:
--------------------------------------------------------------------------------
1 | import { MongoClient, Db, Document } from 'mongodb'
2 | import type { CollectionName } from '../data/types'
3 |
4 | let cachedDb: Db | null = null
5 | let client: MongoClient | null = null
6 |
7 | export async function connectToDatabase(): Promise {
8 | if (cachedDb) {
9 | return cachedDb
10 | }
11 |
12 | if (!process.env.MONGODB_URI) {
13 | throw new Error('Please define the MONGODB_URI environment variable')
14 | }
15 |
16 | if (!process.env.MONGODB_DB) {
17 | throw new Error('Please define the MONGODB_DB environment variable')
18 | }
19 |
20 | if (!client) {
21 | client = new MongoClient(process.env.MONGODB_URI)
22 | await client.connect()
23 | }
24 |
25 | const db = client.db(process.env.MONGODB_DB)
26 | cachedDb = db
27 |
28 | return db
29 | }
30 |
31 | // can't run transactions on a sharded cluster
32 | // export const startSession = () => {
33 | // if (!client) {
34 | // throw new Error('Database client not connected. Have you remembered to call connectToDatabase()?')
35 | // }
36 | // return client.startSession()
37 | // }
38 |
39 | // Helper function to get a collection with proper typing
40 | export const getCollection = (collectionName: CollectionName) => {
41 | if (!cachedDb) {
42 | throw new Error('Database not connected. Have you remembered to wrap your function with withDB?.')
43 | }
44 | return cachedDb.collection(collectionName)
45 | }
--------------------------------------------------------------------------------
/netlify/functions-src/functions/utils/response.ts:
--------------------------------------------------------------------------------
1 | import type { ErrorCodes } from '../../../../api-types/errorCodes';
2 | import { ErrorResponse, SuccessResponse, ApiHandler } from '../types'
3 |
4 | type SuccessPayload = {
5 | [key: string]: any;
6 | data: T;
7 | }
8 |
9 | export function success(data: SuccessPayload, statusCode = 200): SuccessResponse {
10 | return {
11 | statusCode,
12 | headers: { 'Content-Type': 'application/json' },
13 | body: JSON.stringify({ success: true, ...data })
14 | }
15 | }
16 |
17 | export function error(message: string, statusCode = 400, errorCode?: ErrorCodes): ErrorResponse {
18 | if (process.env.CONTEXT !== 'production') {
19 | console.error('===== error ======', message);
20 | }
21 |
22 | const response = {
23 | statusCode,
24 | headers: {
25 | 'Content-Type': 'application/json',
26 | 'Cache-Control': 'no-store',
27 | },
28 | body: JSON.stringify({ success: false, message, errorCode })
29 | }
30 | return response;
31 | }
32 |
33 | export function withErrorHandling(handler: ApiHandler): ApiHandler {
34 | return async (event, context) => {
35 | try {
36 | return await handler(event, context)
37 | } catch (err) {
38 | console.error('Error:', err)
39 | return error(err instanceof Error ? err.message : 'Internal server error', 500)
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/netlify/functions-src/lists/dto/list.dto.ts:
--------------------------------------------------------------------------------
1 | export class ListDto {
2 | name: string
3 | isFavorite: boolean
4 | user: string
5 | mentors: string[]
6 |
7 | constructor(partial: Partial) {
8 | Object.assign(this, partial)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/netlify/functions-src/mongo-scripts/approve-applications.mongodb.js:
--------------------------------------------------------------------------------
1 | // MongoDB Playground
2 | // Use Ctrl+Space inside a snippet or a string literal to trigger completions.
3 |
4 | const { stat } = require('fs');
5 | const { ObjectId } = require('mongodb');
6 |
7 | // The current database to use.
8 | use('codingcoach');
9 |
10 | // Create a new document in the collection.
11 | db.getCollection('applications').findOneAndUpdate(
12 | {_id: new ObjectId('67e99efa8eb43562ce98b410')},
13 | { $set: { status: 'Pending' } },
14 | { returnDocument: 'after' }
15 | );
--------------------------------------------------------------------------------
/netlify/functions-src/mongo-scripts/create-user.mongodb.js:
--------------------------------------------------------------------------------
1 | // MongoDB Playground
2 | // Use Ctrl+Space inside a snippet or a string literal to trigger completions.
3 |
4 | // The current database to use.
5 | use('codingcoach');
6 |
7 | // Create a new document in the collection.
8 | db.getCollection('users').insertOne({
9 | auth0Id: 'auth0|123456789',
10 | email: 'user@example.com',
11 | available: true,
12 | name: 'John Doe',
13 | avatar: 'avatar.png',
14 | image: {
15 | fieldname: 'avatar',
16 | originalname: 'avatar.png',
17 | encoding: '7bit',
18 | mimetype: 'image/png',
19 | destination: '/uploads/',
20 | filename: 'avatar.png',
21 | path: '/uploads/avatar.png',
22 | size: 12345,
23 | },
24 | title: 'Software Engineer',
25 | description: 'Experienced software engineer with expertise in web development.',
26 | country: 'US',
27 | spokenLanguages: ['en', 'es'],
28 | tags: ['JavaScript', 'Node.js', 'MongoDB'],
29 | roles: ['mentor'],
30 | channels: [
31 | {
32 | type: 'email',
33 | id: 'user@example.com',
34 | },
35 | ],
36 | });
37 |
--------------------------------------------------------------------------------
/netlify/functions-src/mongo-scripts/delete-mentorship.mongodb.js:
--------------------------------------------------------------------------------
1 | // MongoDB Playground
2 | // Use Ctrl+Space inside a snippet or a string literal to trigger completions.
3 |
4 | const { ObjectId } = require('mongodb');
5 |
6 | // The current database to use.
7 | use('codingcoach');
8 |
9 | // Create a new document in the collection.
10 | // db.getCollection('mentorships').find(
11 | // { _id: new ObjectId('67e049568a3938d0aac4a216') },
12 | // );
13 | db.getCollection('mentorships').deleteOne(
14 | { _id: new ObjectId('680c9355f0bf77449b54551e') },
15 | );
--------------------------------------------------------------------------------
/netlify/functions-src/mongo-scripts/delete-user.mongodb.js:
--------------------------------------------------------------------------------
1 | // MongoDB Playground
2 | // Use Ctrl+Space inside a snippet or a string literal to trigger completions.
3 |
4 | // The current database to use.
5 | use('codingcoach');
6 |
7 | // Create a new document in the collection.
8 | db.getCollection('users').deleteOne(
9 | { _id: new ObjectId('6803e5702de92c770092fc6b') },
10 | );
--------------------------------------------------------------------------------
/netlify/functions-src/mongo-scripts/find-mentorships.mongodb.js:
--------------------------------------------------------------------------------
1 | // MongoDB Playground
2 | // Use Ctrl+Space inside a snippet or a string literal to trigger completions.
3 |
4 | const { ObjectId } = require('mongodb');
5 |
6 | // The current database to use.
7 | use('codingcoach');
8 |
9 | // Create a new document in the collection.
10 | // db.getCollection('mentorships').find(
11 | // { _id: new ObjectId('67e049568a3938d0aac4a216') },
12 | // );
13 | db.getCollection('mentorships').find({}).sort({ createdAt: 1 });
14 |
--------------------------------------------------------------------------------
/netlify/functions-src/mongo-scripts/get-all-users.mongodb.js:
--------------------------------------------------------------------------------
1 | // MongoDB Playground
2 | // Use Ctrl+Space inside a snippet or a string literal to trigger completions.
3 |
4 | // The current database to use.
5 | use("codingcoach");
6 |
7 | db.users.find({});
8 |
9 |
--------------------------------------------------------------------------------
/netlify/functions-src/mongo-scripts/get-mentors.mongodb.js:
--------------------------------------------------------------------------------
1 | /* global use, db */
2 | // MongoDB Playground
3 | // Use Ctrl+Space inside a snippet or a string literal to trigger completions.
4 |
5 | // The current database to use.
6 | use('codingcoach');
7 |
8 | // Search for documents in the current collection.
9 | db.getCollection('users')
10 | .find(
11 | {
12 | roles: { $in: ['Mentor'] },
13 | },
14 | {
15 | /*
16 | * Projection
17 | * _id: 0, // exclude _id
18 | * fieldA: 1 // include field
19 | */
20 | }
21 | )
22 | .sort({
23 | /*
24 | * fieldA: 1 // ascending
25 | * fieldB: -1 // descending
26 | */
27 | });
28 |
--------------------------------------------------------------------------------
/netlify/functions-src/mongo-scripts/get-panding-applications.mongodb.js:
--------------------------------------------------------------------------------
1 | // MongoDB Playground
2 | // Use Ctrl+Space inside a snippet or a string literal to trigger completions.
3 |
4 | const { stat } = require('fs');
5 |
6 | // The current database to use.
7 | use('codingcoach');
8 |
9 | // Create a new document in the collection.
10 | db.getCollection('applications').find({status: 'Pending'});
--------------------------------------------------------------------------------
/netlify/functions-src/mongo-scripts/update-mentorship.mongodb.js:
--------------------------------------------------------------------------------
1 | // MongoDB Playground
2 | // Use Ctrl+Space inside a snippet or a string literal to trigger completions.
3 |
4 | const { ObjectId } = require('mongodb');
5 |
6 | // The current database to use.
7 | use('codingcoach');
8 |
9 | // Create a new document in the collection.
10 | // db.getCollection('mentorships').find(
11 | // { _id: new ObjectId('67e049568a3938d0aac4a216') },
12 | // );
13 | db.getCollection('mentorships').updateOne(
14 | { _id: new ObjectId('67e9a7023ce1a19ad81bd5b7') },
15 | {
16 | $set: {
17 | status: 'New',
18 | },
19 | },
20 | );
--------------------------------------------------------------------------------
/netlify/functions-src/mongo-scripts/update-user.mongodb.js:
--------------------------------------------------------------------------------
1 | // MongoDB Playground
2 | // Use Ctrl+Space inside a snippet or a string literal to trigger completions.
3 |
4 | // The current database to use.
5 | use('codingcoach');
6 |
7 | // Create a new document in the collection.
8 | db.getCollection('users').updateOne(
9 | { email: 'moshfeu@gmail.com' },
10 | {
11 | $set: {
12 | // roles: ['Member', 'Mentor', 'Admin'],
13 | name: 'The Mentor'
14 | },
15 | },
16 | );
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {import('next').NextConfig}
3 | */
4 | const nextConfig = {
5 | typescript: {
6 | // !! WARN !!
7 | // Dangerously allow production builds to successfully complete even if
8 | // your project has type errors.
9 | // !! WARN !!
10 | ignoreBuildErrors: true,
11 | tsconfigPath: './tsconfig.json',
12 | },
13 | eslint: {
14 | // Warning: This allows production builds to successfully complete even if
15 | // your project has ESLint errors.
16 | ignoreDuringBuilds: true,
17 | },
18 | webpack5: true,
19 | webpack: (config) => {
20 | config.resolve.fallback = {
21 | fs: false,
22 | path: false,
23 | os: false,
24 | module: false,
25 | };
26 |
27 | config.module.rules.push({
28 | test: /\.svg$/,
29 | use: [
30 | {
31 | loader: '@svgr/webpack',
32 | options: {
33 | svgo: false,
34 | },
35 | },
36 | ],
37 | });
38 |
39 | return config;
40 | },
41 | };
42 |
43 | module.exports = nextConfig;
44 |
--------------------------------------------------------------------------------
/pages/404.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PageNotFound from '../src/PageNotFound';
3 |
4 | function FourOhFour() {
5 | return ;
6 | }
7 |
8 | export default FourOhFour;
9 |
--------------------------------------------------------------------------------
/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Document, { Html, Head, Main, NextScript } from 'next/document';
3 | import { ServerStyleSheet } from 'styled-components';
4 |
5 | export default class MyDocument extends Document {
6 | static async getInitialProps(ctx) {
7 | const sheet = new ServerStyleSheet();
8 | const originalRenderPage = ctx.renderPage;
9 |
10 | try {
11 | ctx.renderPage = () =>
12 | originalRenderPage({
13 | enhanceApp: (App) => (props) =>
14 | sheet.collectStyles( ),
15 | });
16 |
17 | const initialProps = await Document.getInitialProps(ctx);
18 | return {
19 | ...initialProps,
20 | styles: (
21 | <>
22 |
28 |
32 | {initialProps.styles}
33 | {sheet.getStyleElement()}
34 | >
35 | ),
36 | };
37 | } finally {
38 | sheet.seal();
39 | }
40 | }
41 |
42 | render() {
43 | return (
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import App from '../src/components/layouts/App';
2 | import MentorsList from '../src/components/MentorsList/MentorsList';
3 |
4 | function HomePage() {
5 | return (
6 |
7 |
8 |
9 | );
10 | }
11 |
12 | export default HomePage;
13 |
--------------------------------------------------------------------------------
/pages/me/admin.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Layout from '../../src/Me/Me'
3 | import Admin from '../../src/Me/Routes/Admin'
4 |
5 | export default function Index() {
6 | return (
7 |
8 |
9 |
10 | )
11 | }
--------------------------------------------------------------------------------
/pages/me/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Layout from '../../src/Me/Me'
3 | import Home from '../../src/Me/Routes/Home'
4 |
5 | export default function Index() {
6 | return (
7 |
8 |
9 |
10 | )
11 | }
--------------------------------------------------------------------------------
/pages/me/requests.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Layout from '../../src/Me/Me'
3 | import Requests from '../../src/Me/MentorshipRequests'
4 |
5 | export default function Index() {
6 | return (
7 |
8 |
9 |
10 | )
11 | }
--------------------------------------------------------------------------------
/pages/sitemap.xml/index.tsx:
--------------------------------------------------------------------------------
1 | import { GetServerSideProps } from 'next/types';
2 | import { buildSitemap } from '../../src/utils/sitemapGenerator';
3 |
4 | export default function Index() {
5 | return null;
6 | }
7 |
8 | export const getServerSideProps: GetServerSideProps = async ({ res }) => {
9 | res.setHeader('Content-Type', 'text/xml');
10 | const xml = await buildSitemap();
11 | res.write(xml);
12 |
13 | res.end();
14 |
15 | // Empty since we don't render anything
16 | return {
17 | props: {},
18 | };
19 | };
20 |
--------------------------------------------------------------------------------
/pages/u/[id].tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { GetServerSideProps } from 'next';
3 | import ApiService from '../../src/api';
4 | import App from '../../src/components/layouts/App';
5 | import { UserProfile } from '../../src/components/UserProfile/UserProfile';
6 | import { User } from '../../src/types/models';
7 |
8 | type UserPageProps = {
9 | user: User;
10 | };
11 |
12 | function UserPage({ user }: UserPageProps) {
13 | return (
14 |
15 |
16 |
17 | );
18 | }
19 |
20 | export default UserPage;
21 |
22 | export const getServerSideProps: GetServerSideProps = async (
23 | context
24 | ) => {
25 | const { id } = context.query;
26 | // TODO - should mock ApiService on SSR more generally
27 | const api = new ApiService({
28 | getIdToken: () => '',
29 | });
30 | const user = await api.getUser(Array.isArray(id) ? id[0] : id!);
31 | if (!user) {
32 | return {
33 | notFound: true,
34 | };
35 | }
36 |
37 | return {
38 | props: {
39 | user,
40 | },
41 | };
42 | };
43 |
--------------------------------------------------------------------------------
/public/CNAME:
--------------------------------------------------------------------------------
1 | mentors.codingcoach.io
2 |
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coding-Coach/find-a-mentor/0bec6315fa6e716e5b8d1e74f1d92d1952c36777/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #20293a
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/public/codingcoach-logo-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coding-Coach/find-a-mentor/0bec6315fa6e716e5b8d1e74f1d92d1952c36777/public/codingcoach-logo-16.png
--------------------------------------------------------------------------------
/public/codingcoach-logo-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coding-Coach/find-a-mentor/0bec6315fa6e716e5b8d1e74f1d92d1952c36777/public/codingcoach-logo-192.png
--------------------------------------------------------------------------------
/public/codingcoach-logo-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coding-Coach/find-a-mentor/0bec6315fa6e716e5b8d1e74f1d92d1952c36777/public/codingcoach-logo-32.png
--------------------------------------------------------------------------------
/public/codingcoach-logo-384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coding-Coach/find-a-mentor/0bec6315fa6e716e5b8d1e74f1d92d1952c36777/public/codingcoach-logo-384.png
--------------------------------------------------------------------------------
/public/codingcoach-logo-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coding-Coach/find-a-mentor/0bec6315fa6e716e5b8d1e74f1d92d1952c36777/public/codingcoach-logo-512.png
--------------------------------------------------------------------------------
/public/codingcoach-logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coding-Coach/find-a-mentor/0bec6315fa6e716e5b8d1e74f1d92d1952c36777/public/codingcoach-logo.jpg
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coding-Coach/find-a-mentor/0bec6315fa6e716e5b8d1e74f1d92d1952c36777/public/favicon.ico
--------------------------------------------------------------------------------
/public/images/coding-coach-patron-button.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coding-Coach/find-a-mentor/0bec6315fa6e716e5b8d1e74f1d92d1952c36777/public/images/coding-coach-patron-button.jpg
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Mentors",
3 | "name": "Mentors - CodingCoach",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "codingcoach-logo-192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "codingcoach-logo-384.png",
17 | "type": "image/png",
18 | "sizes": "384x384"
19 | },
20 | {
21 | "src": "codingcoach-logo-512.png",
22 | "type": "image/png",
23 | "sizes": "512x512"
24 | }
25 | ],
26 | "start_url": ".",
27 | "display": "standalone",
28 | "theme_color": "#20293a",
29 | "background_color": "#20293a"
30 | }
31 |
--------------------------------------------------------------------------------
/public/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Coding-Coach/find-a-mentor/0bec6315fa6e716e5b8d1e74f1d92d1952c36777/public/mstile-150x150.png
--------------------------------------------------------------------------------
/public/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 | Created by potrace 1.11, written by Peter Selinger 2001-2013
9 |
10 |
12 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/scripts/ignore-step.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo "VERCEL_ENV: $VERCEL_ENV"
4 |
5 | if [[ "$VERCEL_ENV" == "production" ]] ; then
6 | # Proceed with the build
7 | echo "✅ - Build can proceed"
8 | exit 1;
9 |
10 | else
11 | # Don't build
12 | echo "🛑 - Build cancelled"
13 | exit 0;
14 | fi
15 |
16 | # https://vercel.com/support/articles/how-do-i-use-the-ignored-build-step-field-on-vercel
--------------------------------------------------------------------------------
/src/Me/Header/Header.test.tsx:
--------------------------------------------------------------------------------
1 | import { render } from '@testing-library/react';
2 | import Header from './Header';
3 |
4 | describe('Header', () => {
5 | test('Header renders', () => {
6 | const { getByText } = render();
7 | expect(getByText('Home')).toBeTruthy();
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/src/Me/Header/Header.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { desktop } from '../styles/shared/devices';
4 | import Logo from '../../assets/me/logo.svg';
5 |
6 | const HeaderContainer = styled.div`
7 | height: 243px;
8 | width: 100%;
9 | background: radial-gradient(circle, #a5fcdb 0%, #12c395 100%);
10 | display: flex;
11 | justify-content: space-between;
12 |
13 | @media ${desktop} {
14 | height: 268px;
15 | }
16 | `;
17 |
18 | const Home = styled.div`
19 | height: 34px;
20 | width: 76px;
21 | color: #fff;
22 | font-family: Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif;
23 | font-size: 28px;
24 | font-weight: 700;
25 | line-height: 34px;
26 | padding-top: 43px;
27 | padding-left: 16px;
28 |
29 | @media ${desktop} {
30 | color: #fff;
31 | padding-top: 39px;
32 | padding-left: 152px;
33 | }
34 | `;
35 |
36 | const LogoContainer = styled.div`
37 | padding-top: 43px;
38 | padding-right: 16px;
39 | height: 30px;
40 | padding-right: 1rem;
41 |
42 | @media ${desktop} {
43 | display: none;
44 | }
45 | `;
46 |
47 | type HeaderProps = {
48 | title: string;
49 | };
50 |
51 | const Header = ({ title }: HeaderProps) => {
52 | return (
53 |
54 | {title}
55 |
56 |
57 |
58 |
59 | );
60 | };
61 |
62 | export default Header;
63 |
--------------------------------------------------------------------------------
/src/Me/Main.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 | import styled from 'styled-components/macro';
3 | import { desktop, mobile } from './styles/shared/devices';
4 | import { mobileNavHeight } from './Navigation/Navbar';
5 | import { useUser } from '../context/userContext/UserContext';
6 | import { CardContainer } from './components/Card';
7 |
8 | const Main: FC = ({ children }) => {
9 | const { currentUser } = useUser();
10 | if (typeof window === 'undefined' && !currentUser) {
11 | return null;
12 | }
13 | return {children} ;
14 | };
15 |
16 | export default Main;
17 |
18 | const Content = styled.div`
19 | gap: 10px;
20 | display: flex;
21 | flex-wrap: wrap;
22 | padding: 0 16px;
23 | margin-top: -50px;
24 | justify-content: center;
25 |
26 | @media ${desktop} {
27 | margin-right: auto;
28 | margin-left: auto;
29 | padding-bottom: 10px;
30 |
31 | ${CardContainer}:not(.wide) {
32 | max-width: 400px;
33 | }
34 | }
35 |
36 | @media ${mobile} {
37 | padding-bottom: ${mobileNavHeight + 8}px;
38 | flex-direction: column;
39 | gap: 20px;
40 | }
41 | `;
42 |
--------------------------------------------------------------------------------
/src/Me/Me.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ToastContainer } from 'react-toastify';
3 | import 'react-toastify/dist/ReactToastify.css';
4 | import styled from 'styled-components/macro';
5 | import Head from 'next/head'
6 | import { useRouter } from 'next/router';
7 |
8 | import Header from './Header/Header';
9 | import Main from './Main';
10 | import Navbar from './Navigation/Navbar';
11 | import { GlobalStyle } from './styles/global';
12 | import { desktop } from './styles/shared/devices';
13 | import { isSsr } from '../helpers/ssr';
14 | import { useUser } from '../context/userContext/UserContext';
15 | import { useAuth } from '../context/authContext/AuthContext';
16 | import { useRoutes } from '../hooks/useRoutes';
17 |
18 | const Me = (props: any) => {
19 | const { children, title } = props;
20 | const { pathname, push } = useRouter();
21 | const routes = useRoutes();
22 | const { currentUser, isLoading } = useUser();
23 | const auth = useAuth();
24 |
25 | React.useEffect(() => {
26 | if (!isLoading && !currentUser) {
27 | auth.login(pathname);
28 | }
29 | }, [currentUser, auth, pathname, isLoading]);
30 |
31 | if (isSsr()) {
32 | return null;
33 | }
34 |
35 | if (!currentUser) {
36 | return null;
37 | }
38 |
39 | if (!currentUser.email_verified) {
40 | push(routes.root.get());
41 | return Email not verified, redirecting...
;
42 | }
43 |
44 | return (
45 |
46 | <>
47 |
48 | {title} | CodingCoach
49 |
50 |
51 |
52 |
53 | {children}
54 | >
55 |
56 |
57 |
58 | );
59 | };
60 |
61 | export default Me;
62 |
63 | const Container = styled.div`
64 | min-height: 100vh;
65 | background-color: #f8f8f8;
66 |
67 | @media ${desktop} {
68 | padding-left: 75px;
69 | }
70 | `;
71 |
--------------------------------------------------------------------------------
/src/Me/MentorshipRequests/index.js:
--------------------------------------------------------------------------------
1 | import MentorshipRequests from './MentorshipRequests';
2 | export { default as ReqContent } from './ReqContent';
3 | export default MentorshipRequests;
4 |
--------------------------------------------------------------------------------
/src/Me/Modals/MentorshipRequestModals/AcceptModal.tsx:
--------------------------------------------------------------------------------
1 | import Body from './style';
2 | import { Modal } from '../Modal';
3 | import MentorshipSvg from '../../../assets/me/mentorship.svg';
4 | import { links } from '../../../config/constants';
5 | import { report } from '../../../ga';
6 |
7 | type AcceptModalProps = {
8 | username: string;
9 | menteeEmail: string;
10 | onClose(): void;
11 | };
12 |
13 | const AcceptModal = ({ username, menteeEmail, onClose }: AcceptModalProps) => {
14 | return (
15 |
16 |
17 |
18 |
19 | Awesome! You are now Mentoring {username}! Please make sure to
20 | follow our{' '}
21 |
26 | Guidelines
27 | {' '}
28 | and our{' '}
29 |
34 | Code of Conduct
35 |
36 | .
37 |
38 | What's next?
39 |
40 | We just sent an email to {username} to inform them the happy
41 | news. In this email we also included one of your contact channels. At
42 | this point they also have access to your channels so they probably
43 | will contact you soon.
44 |
45 |
46 | Have a question? Send an email to{' '}
47 | report('Member Area', 'Send Email', 'Mentorship')}
49 | href={`mailto:${menteeEmail}`}
50 | >
51 | {menteeEmail}
52 |
53 |
54 |
55 |
56 | );
57 | };
58 |
59 | export default AcceptModal;
60 |
--------------------------------------------------------------------------------
/src/Me/Modals/MentorshipRequestModals/DeclineModal.tsx:
--------------------------------------------------------------------------------
1 | import BodyStyle from './style';
2 | import { useRef, useState } from 'react';
3 | import { Modal } from '../Modal';
4 | import TextArea from '../../components/Textarea';
5 | import styled from 'styled-components';
6 | import FormField from '../../components/FormField';
7 |
8 | const Body = styled(BodyStyle)`
9 | justify-content: flex-start;
10 | p {
11 | text-align: left;
12 | }
13 | `;
14 |
15 | type DeclineModalProps = {
16 | username: string;
17 | onSave(message: string | null): void;
18 | onClose(): void;
19 | };
20 |
21 | const DeclineModal = ({ username, onSave, onClose }: DeclineModalProps) => {
22 | const [loadingState, setLoadingState] = useState(false);
23 | const message = useRef(null);
24 |
25 | return (
26 | {
33 | setLoadingState(true);
34 | onSave(message.current);
35 | }}
36 | >
37 |
38 |
39 |
40 | You have declined {username} and that’s ok, now is not a good
41 | time!
42 |
43 |
44 | As a courtesy, please let {username} know why you are declining the
45 | mentorship.
46 |
47 |
48 |
57 |
58 |
59 |
60 | );
61 | };
62 |
63 | export default DeclineModal;
64 |
--------------------------------------------------------------------------------
/src/Me/Modals/MentorshipRequestModals/index.ts:
--------------------------------------------------------------------------------
1 | export { default as DeclineModal } from './DeclineModal';
2 | export { default as MentorshipRequest } from './MentorshipRequest';
3 | export { default as AcceptModal } from './AcceptModal';
4 | export { default as CancelModal } from './CancelModal';
5 |
--------------------------------------------------------------------------------
/src/Me/Modals/MentorshipRequestModals/stories/AcceptModal.stories.tsx:
--------------------------------------------------------------------------------
1 | import AcceptModal from '../AcceptModal';
2 | import { action } from '@storybook/addon-actions';
3 | import { StoriesContainer } from '../../../../stories/StoriesContainer';
4 |
5 | export default {
6 | title: 'Mentorship/Approved',
7 | component: AcceptModal,
8 | };
9 |
10 | const onClose = action('onClose');
11 |
12 | export const Default = () => (
13 |
14 |
15 |
16 | );
17 |
--------------------------------------------------------------------------------
/src/Me/Modals/MentorshipRequestModals/stories/CancelModal.stories.tsx:
--------------------------------------------------------------------------------
1 | import CancelModal from '../CancelModal';
2 | import { action } from '@storybook/addon-actions';
3 | import { StoriesContainer } from '../../../../stories/StoriesContainer';
4 |
5 | export default {
6 | title: 'Mentorship/Canceled',
7 | component: CancelModal,
8 | };
9 |
10 | const onSave = action('onSave');
11 | const onClose = action('onClose');
12 |
13 | export const Default = () => (
14 |
15 |
16 |
17 | );
18 |
--------------------------------------------------------------------------------
/src/Me/Modals/MentorshipRequestModals/stories/DeclineModal.stories.tsx:
--------------------------------------------------------------------------------
1 | import DeclineModal from '../DeclineModal';
2 | import { action } from '@storybook/addon-actions';
3 | import { StoriesContainer } from '../../../../stories/StoriesContainer';
4 |
5 | export default {
6 | title: 'Mentorship/Decline',
7 | component: DeclineModal,
8 | };
9 |
10 | const onClose = action('onClose');
11 | const onSave = action('onSave');
12 |
13 | export const Default = () => (
14 |
15 |
16 |
17 | );
18 |
--------------------------------------------------------------------------------
/src/Me/Modals/MentorshipRequestModals/stories/MentorshipRequest.stories.tsx:
--------------------------------------------------------------------------------
1 | import MentorshipRequest from '../MentorshipRequest';
2 | import { StoriesContainer } from '../../../../stories/StoriesContainer';
3 |
4 | export default {
5 | title: 'Mentorship/Request',
6 | component: MentorshipRequest,
7 | };
8 |
9 | export const Default = () => (
10 |
11 |
12 |
13 | );
14 |
--------------------------------------------------------------------------------
/src/Me/Modals/MentorshipRequestModals/style.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components/macro';
2 | import { desktop } from '../../styles/shared/devices';
3 | import { StyledTextarea } from '../../components/Textarea/Textarea';
4 |
5 | export default styled.div`
6 | min-height: 415px;
7 | display: flex;
8 | flex-direction: column;
9 | align-items: stretch;
10 | justify-content: center;
11 | color: #4f4f4f;
12 | margin: 0 auto;
13 | overflow-y: auto;
14 |
15 | @media ${desktop} {
16 | max-width: 500px;
17 | }
18 |
19 | #menteeEmailLink {
20 | text-align: left;
21 | }
22 |
23 | p {
24 | text-align: center;
25 | }
26 |
27 | ul {
28 | margin: 0;
29 | padding: 0;
30 | list-style: none;
31 | display: grid;
32 |
33 | li {
34 | display: grid;
35 | grid-template-columns: 0 1fr;
36 | grid-gap: 1.75em;
37 | align-items: baseline;
38 |
39 | &:before {
40 | content: '👉';
41 | }
42 | }
43 | }
44 |
45 | ${StyledTextarea} {
46 | width: 100%;
47 | }
48 |
49 | svg {
50 | align-self: center;
51 | }
52 | `;
53 |
--------------------------------------------------------------------------------
/src/Me/Modals/RedirectToGravatar.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Modal } from './Modal';
3 | import styled from 'styled-components';
4 |
5 | export const RedirectToGravatar = () => {
6 | const [isModalOpen, setModalOpen] = useState(false);
7 | const openModal = (e: React.MouseEvent) => {
8 | e.preventDefault();
9 | setModalOpen(true);
10 | };
11 |
12 | const closeModal = () => {
13 | setModalOpen(false);
14 | };
15 |
16 | const handleProceed = () => {
17 | window.open('https://gravatar.com', '_blank', 'noopener,noreferrer');
18 | closeModal();
19 | };
20 |
21 | return (
22 | <>
23 |
29 | Gravatar
30 |
31 | {isModalOpen && (
32 |
39 |
40 | We’ll take you to Gravatar.com to update your avatar
41 |
42 | Don’t have an account? You're a developer, you'll manage 😉
43 |
44 | Once you make a change, your new look will show up here once
45 | Gravatar's cache expires
46 |
47 |
48 | )}
49 | >
50 | );
51 | };
52 |
53 | const Content = styled.div`
54 | font-size: 1.2rem;
55 | line-height: 1.3;
56 | `;
57 |
--------------------------------------------------------------------------------
/src/Me/Navigation/Navbar.test.tsx:
--------------------------------------------------------------------------------
1 | import { render } from '@testing-library/react';
2 | import Navbar from './Navbar';
3 | import { UserProvider } from '../../context/userContext/UserContext';
4 |
5 | describe('Navbar', () => {
6 | test('Navbar renders', () => {
7 | const { getByText } = render(
8 |
9 |
10 |
11 | );
12 | expect(getByText('Home')).toBeTruthy();
13 | expect(getByText('Mentors')).toBeTruthy();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/Me/Routes/Home.tsx:
--------------------------------------------------------------------------------
1 | import Avatar from './Home/Avatar/Avatar';
2 | import Profile from '../../Me/Profile/Profile';
3 |
4 | export const Home = () => {
5 | return (
6 | <>
7 |
8 |
9 | >
10 | );
11 | };
12 |
13 | export default Home;
14 |
--------------------------------------------------------------------------------
/src/Me/components/Button/Button.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, fireEvent } from '@testing-library/react';
2 | import Button from './';
3 |
4 | describe('Button component', () => {
5 | it('passes down other props to the button tag', () => {
6 | const { getByTestId } = render(
7 | {}}>
8 | click me
9 |
10 | );
11 | expect(getByTestId('test-button')).toBeInTheDocument();
12 | });
13 |
14 | it(`fires 'prop.onClick' upon user click`, () => {
15 | const onClick = jest.fn(() => {});
16 | const { getByText } = render(Click Me );
17 |
18 | const btn = getByText('Click Me');
19 |
20 | fireEvent.click(btn);
21 |
22 | expect(onClick).toHaveBeenCalledTimes(1);
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/Me/components/Button/Button.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import styled from 'styled-components';
3 | import { Loader } from '../../../components/Loader';
4 |
5 | type Skin = 'primary' | 'secondary' | 'danger';
6 | type ButtonProps = Pick<
7 | React.DetailedHTMLProps<
8 | React.ButtonHTMLAttributes,
9 | HTMLButtonElement
10 | >,
11 | | 'onClick'
12 | | 'id'
13 | | 'disabled'
14 | | 'type'
15 | | 'name'
16 | | 'children'
17 | | 'title'
18 | | 'autoFocus'
19 | > & {
20 | skin?: Skin;
21 | isLoading?: boolean;
22 | };
23 |
24 | const StyledButton = styled.button`
25 | height: 30px;
26 | cursor: pointer;
27 | font-size: 14px;
28 | line-height: 17px;
29 | user-select: none;
30 | text-align: center;
31 | border-radius: 3px;
32 | box-sizing: border-box;
33 | font-family: Lato, sans-serif;
34 | transition: box-shadow 0.1s ease-in-out;
35 |
36 | &:hover {
37 | box-shadow: inset 0 0 100px 0 #00000010;
38 | }
39 |
40 | &:disabled {
41 | opacity: 0.5;
42 | }
43 | `;
44 |
45 | const PrimaryButton = styled(StyledButton)`
46 | background-color: #69d5b1;
47 | color: #fff;
48 | `;
49 |
50 | const SecondaryButton = styled(StyledButton)`
51 | background-color: #fff;
52 | border: 2px solid;
53 | color: #69d5b1;
54 | `;
55 |
56 | const DangerButton = styled(StyledButton)`
57 | background-color: #d56969;
58 | color: #fff;
59 | `;
60 |
61 | const getComponentBySkin = (skin: Skin) => {
62 | switch (skin) {
63 | case 'primary':
64 | default:
65 | return PrimaryButton;
66 | case 'secondary':
67 | return SecondaryButton;
68 | case 'danger':
69 | return DangerButton;
70 | }
71 | };
72 |
73 | export const Button: FC = ({
74 | skin = 'primary',
75 | isLoading = false,
76 | children,
77 | ...props
78 | }) => {
79 | const ThemedButton = getComponentBySkin(skin);
80 | return (
81 | {isLoading ? : children}
82 | );
83 | };
84 |
--------------------------------------------------------------------------------
/src/Me/components/Button/IconButton.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components/macro';
2 | import classNames from 'classnames';
3 |
4 | type IconButtonProps = {
5 | icon: string;
6 | onClick: () => void;
7 | buttonProps?: React.ButtonHTMLAttributes;
8 | color?: string;
9 | size?: 'sm' | 'lg' | '2x' | '3x';
10 | };
11 |
12 | const Button = styled.button`
13 | background: none;
14 | cursor: pointer;
15 | `;
16 |
17 | export const IconButton = ({ onClick, icon, size, color, buttonProps }: IconButtonProps) => {
18 | return (
19 |
20 | {typeof icon === 'string' && (
21 |
25 | )}
26 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/src/Me/components/Button/index.ts:
--------------------------------------------------------------------------------
1 | export { Button as default } from './Button';
2 |
--------------------------------------------------------------------------------
/src/Me/components/Checkbox/index.ts:
--------------------------------------------------------------------------------
1 | export { Checkbox as default } from './Checkbox';
2 |
--------------------------------------------------------------------------------
/src/Me/components/FormField/FormField.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import FormField from './';
3 | import { render } from '@testing-library/react';
4 | import { formFieldContext } from './formContext';
5 |
6 | describe('FormField component', () => {
7 | it('renders properly', () => {
8 | const { container } = render(
9 |
10 | );
11 | expect(container.firstChild).toMatchSnapshot();
12 | });
13 | it('renders the label when it is defined', () => {
14 | const { getByText } = render( );
15 | expect(getByText('test label')).toBeInTheDocument();
16 | });
17 | it('passes down the id', () => {
18 | let formFieldId;
19 | render(
20 |
21 |
22 | {(id) => (formFieldId = id)}
23 |
24 |
25 | );
26 | expect(formFieldId).toMatch('form-field-');
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/src/Me/components/FormField/FormField.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useState } from 'react';
2 | import styled from 'styled-components';
3 | import uniqueId from 'lodash/uniqueId';
4 | import { formFieldContext } from './formContext';
5 |
6 | const InlineHelpText = styled.span`
7 | color: #5c5c5c;
8 | font-weight: normal;
9 |
10 | &:before {
11 | content: '(';
12 | }
13 |
14 | &:after {
15 | content: ')';
16 | }
17 | `;
18 |
19 | const FormFieldContainer = styled.div`
20 | display: flex;
21 | flex-direction: column;
22 | font-family: Lato, sans-serif;
23 | margin-bottom: 30px;
24 | `;
25 |
26 | const Label = styled.label`
27 | font-weight: bold;
28 | font-size: 14px;
29 | line-height: 17px;
30 | margin-bottom: 6px;
31 | `;
32 |
33 | type FormFieldProps = {
34 | label?: string;
35 | className?: string;
36 | helpText?: string;
37 | };
38 |
39 | export const FormField: FC = ({
40 | label,
41 | className,
42 | children,
43 | helpText,
44 | }) => {
45 | const [id] = useState(() => uniqueId('form-field-'));
46 | return (
47 |
48 | {label && (
49 |
50 | {label} {helpText && {helpText} }
51 |
52 | )}
53 |
54 | {children}
55 |
56 |
57 | );
58 | };
59 |
--------------------------------------------------------------------------------
/src/Me/components/FormField/__snapshots__/FormField.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`FormField component renders properly 1`] = `
4 | .c2 {
5 | color: #5c5c5c;
6 | font-weight: normal;
7 | }
8 |
9 | .c2:before {
10 | content: '(';
11 | }
12 |
13 | .c2:after {
14 | content: ')';
15 | }
16 |
17 | .c0 {
18 | display: -webkit-box;
19 | display: -webkit-flex;
20 | display: -ms-flexbox;
21 | display: flex;
22 | -webkit-flex-direction: column;
23 | -ms-flex-direction: column;
24 | flex-direction: column;
25 | font-family: Lato,sans-serif;
26 | margin-bottom: 30px;
27 | }
28 |
29 | .c1 {
30 | font-weight: bold;
31 | font-size: 14px;
32 | line-height: 17px;
33 | margin-bottom: 6px;
34 | }
35 |
36 |
39 |
43 | test label
44 |
45 |
48 | help text
49 |
50 |
51 |
52 | `;
53 |
--------------------------------------------------------------------------------
/src/Me/components/FormField/formContext.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const formFieldContext = React.createContext();
4 |
--------------------------------------------------------------------------------
/src/Me/components/FormField/index.js:
--------------------------------------------------------------------------------
1 | export { FormField as default } from './FormField';
2 |
--------------------------------------------------------------------------------
/src/Me/components/Input/Input.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Input from './';
3 | import { render } from '@testing-library/react';
4 |
5 | describe('Input component', () => {
6 | it('renders properly', () => {
7 | const { container } = render( );
8 | expect(container.firstChild).toMatchSnapshot();
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/src/Me/components/Input/Input.tsx:
--------------------------------------------------------------------------------
1 | import { InputHTMLAttributes, useContext } from 'react';
2 | import styled from 'styled-components';
3 | import { formFieldContext } from '../FormField/formContext';
4 |
5 | const StyledInput = styled.input`
6 | font-family: Lato, sans-serif;
7 | font-size: 14px;
8 | line-height: 17px;
9 | border-radius: 3px;
10 | background-color: #fff;
11 | border: 1px solid #bfbfbf;
12 | padding: 7px 12px 6px 8px;
13 | color: #4f4f4f;
14 | ::placeholder {
15 | color: #898889;
16 | }
17 | :disabled {
18 | background-color: #dadada;
19 | }
20 | `;
21 |
22 | export const Input = (props: InputHTMLAttributes) => {
23 | const id = useContext(formFieldContext);
24 | return ;
25 | };
26 |
--------------------------------------------------------------------------------
/src/Me/components/Input/__snapshots__/Input.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Input component renders properly 1`] = `
4 | .c0 {
5 | font-family: Lato,sans-serif;
6 | font-size: 14px;
7 | line-height: 17px;
8 | border-radius: 3px;
9 | background-color: #fff;
10 | border: 1px solid #bfbfbf;
11 | padding: 7px 12px 6px 8px;
12 | color: #4f4f4f;
13 | }
14 |
15 | .c0::-webkit-input-placeholder {
16 | color: #898889;
17 | }
18 |
19 | .c0::-moz-placeholder {
20 | color: #898889;
21 | }
22 |
23 | .c0:-ms-input-placeholder {
24 | color: #898889;
25 | }
26 |
27 | .c0::placeholder {
28 | color: #898889;
29 | }
30 |
31 | .c0:disabled {
32 | background-color: #dadada;
33 | }
34 |
35 |
39 | `;
40 |
--------------------------------------------------------------------------------
/src/Me/components/Input/index.ts:
--------------------------------------------------------------------------------
1 | export { Input as default } from './Input';
2 |
--------------------------------------------------------------------------------
/src/Me/components/List/List.test.tsx:
--------------------------------------------------------------------------------
1 | import { render } from '@testing-library/react';
2 | import List from './';
3 | import { ListItemProps } from './ListItem';
4 |
5 | describe('List component', () => {
6 | const items: ListItemProps[] = [
7 | {
8 | type: 'email',
9 | value: 'myemail@codecoache.com',
10 | },
11 | {
12 | type: 'title',
13 | value: 'front-end',
14 | },
15 | ];
16 |
17 | it('Should render children as ListItem', () => {
18 | const itemsComp = [
19 | ,
20 | ,
21 | ];
22 | const { getByTestId, getByText, rerender } = render(
23 | {itemsComp[0]}
24 | );
25 | getByTestId(items[0].type);
26 | getByText(items[0].value);
27 |
28 | rerender(
29 |
30 | {itemsComp[0]}
31 | {itemsComp[1]}
32 |
33 | );
34 |
35 | getByTestId(items[1].type);
36 | getByText(items[1].value);
37 | });
38 |
39 | it('Should render props.items as ListItem', async () => {
40 | const { findAllByText } = render(
);
41 | const rgx = new RegExp(`(${items[0].value}|${items[1].value})`);
42 | const els = await findAllByText(rgx);
43 |
44 | expect(els.length).toEqual(2);
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/src/Me/components/List/List.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 | import styled from 'styled-components';
3 | import ListItem, { ListItemProps } from './ListItem';
4 |
5 | type ListPropsWithChildren = {
6 | items?: never;
7 | };
8 |
9 | type ListPropsWithItems = {
10 | children?: never;
11 | items: ListItemProps[];
12 | };
13 |
14 | type FCList = FC & {
15 | Item: typeof ListItem;
16 | };
17 |
18 | const Style = {
19 | List: styled.ul`
20 | padding: 0;
21 | `,
22 | };
23 |
24 | const renderItems = (item: ListItemProps) => (
25 |
26 | );
27 |
28 | export const List: FCList = ({ items, children }) => {
29 | const listItems = items?.map(renderItems) ?? children;
30 | return {listItems} ;
31 | };
32 |
33 | //This will allow us to use ListItem as
34 | List.Item = ListItem;
35 |
36 | export default List;
37 |
--------------------------------------------------------------------------------
/src/Me/components/List/ListItem.tsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import styled from 'styled-components';
3 | import AvailableIcon from '../../../assets/me/icon-available.svg';
4 | import CountryIcon from '../../../assets/me/icon-country.svg';
5 | import DescriptionIcon from '../../../assets/me/icon-description.svg';
6 | import EmailIcon from '../../../assets/me/icon-email.svg';
7 | import SpokenLanguagesIcon from '../../../assets/me/icon-spokenLanguages.svg';
8 | import TagsIcon from '../../../assets/me/icon-tags.svg';
9 | import TitleIcon from '../../../assets/me/icon-title.svg';
10 | import UnavailableIcon from '../../../assets/me/icon-unavailable.svg';
11 |
12 | export type ListItemProps = {
13 | type: keyof typeof icons;
14 | value: string;
15 | };
16 |
17 | const icons = {
18 | email: EmailIcon,
19 | spokenLanguages: SpokenLanguagesIcon,
20 | country: CountryIcon,
21 | title: TitleIcon,
22 | tags: TagsIcon,
23 | available: AvailableIcon,
24 | unavailable: UnavailableIcon,
25 | description: DescriptionIcon,
26 | };
27 |
28 | const ItemRow = styled.div`
29 | display: grid;
30 | grid-template-columns: 40px auto;
31 | margin-top: 5px;
32 | `;
33 |
34 | const ItemIcon = styled.div`
35 | width: 24px;
36 | height: 100%;
37 | margin-right: 20px;
38 | `;
39 |
40 | const ItemText = styled.div`
41 | padding-top: 4px;
42 | `;
43 |
44 | const ListItem = ({ type, value }: ListItemProps) => {
45 | const Icon = icons[type];
46 | return (
47 |
48 | {Icon && (
49 |
50 |
51 |
52 | )}
53 | {value}
54 |
55 | );
56 | };
57 |
58 | ListItem.propTypes = {
59 | value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
60 | type: PropTypes.oneOf([
61 | 'email',
62 | 'spokenLanguages',
63 | 'country',
64 | 'title',
65 | 'tags',
66 | 'available',
67 | 'unavailable',
68 | 'description',
69 | ]),
70 | };
71 |
72 | export default ListItem;
73 |
--------------------------------------------------------------------------------
/src/Me/components/List/index.ts:
--------------------------------------------------------------------------------
1 | export { List as default } from './List';
2 |
--------------------------------------------------------------------------------
/src/Me/components/RadioButton/RadioButtonContext.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | type RadioButtonContextProps =
4 | | undefined
5 | | {
6 | onChange(newValue: string): void;
7 | groupValue: string;
8 | };
9 |
10 | export const RadioButtonContext = createContext(
11 | undefined
12 | );
13 |
--------------------------------------------------------------------------------
/src/Me/components/RadioButton/RadioButtonGroup.test.tsx:
--------------------------------------------------------------------------------
1 | import { render } from '@testing-library/react';
2 | import RadioButton from '.';
3 | import RadioButtonGroup from './RadioButtonGroup';
4 |
5 | describe('RadioButtonGroup', () => {
6 | it('should call onChange when check an option', async () => {
7 | const onChange = jest.fn();
8 |
9 | const { findByText } = render(
10 |
11 |
12 |
13 |
14 |
15 | );
16 | (await findByText('Option 3')).click();
17 |
18 | expect(onChange).toHaveBeenCalledWith('3');
19 | });
20 |
21 | it('should match snapshot', () => {
22 | const { container } = render(
23 | {}}>
24 |
25 |
26 |
27 |
28 | );
29 |
30 | expect(container.firstChild).toMatchSnapshot();
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/src/Me/components/RadioButton/RadioButtonGroup.tsx:
--------------------------------------------------------------------------------
1 | import { PropsWithChildren, useState } from 'react';
2 | import styled from 'styled-components';
3 | import { RadioButtonContainer } from './RadioButton';
4 | import { RadioButtonContext } from './RadioButtonContext';
5 |
6 | const StyledRadioButtonGroup = styled.div`
7 | ${RadioButtonContainer} + ${RadioButtonContainer} {
8 | margin-top: 10px;
9 | }
10 | `;
11 |
12 | export type RadioButtonGroupProps = {
13 | value: T;
14 | onChange(value: T): void;
15 | };
16 |
17 | const RadioButtonGroup = ({
18 | value: defaultValue,
19 | onChange,
20 | children,
21 | }: PropsWithChildren>) => {
22 | const [value, setValue] = useState(defaultValue);
23 |
24 | const onRadioButtonChange = (newValue: T) => {
25 | setValue(newValue);
26 | onChange(newValue);
27 | };
28 |
29 | return (
30 |
33 | {children}
34 |
35 | );
36 | };
37 |
38 | export default RadioButtonGroup;
39 |
--------------------------------------------------------------------------------
/src/Me/components/RadioButton/index.ts:
--------------------------------------------------------------------------------
1 | export { RadioButton as default } from './RadioButton';
2 | export { default as RadioButtonGroup } from './RadioButtonGroup';
3 |
--------------------------------------------------------------------------------
/src/Me/components/RichList/ReachItemTypes.d.ts:
--------------------------------------------------------------------------------
1 | export type RichItemTagTheme =
2 | | 'primary'
3 | | 'secondary'
4 | | 'danger'
5 | | 'checked'
6 | | 'disabled'
7 | | 'cancelled';
8 |
9 | export type RichItemProps = {
10 | id: string;
11 | userId: string;
12 | avatar: string;
13 | title: string;
14 | subtitle: string;
15 | tag: {
16 | value: string;
17 | theme: RichItemTagTheme;
18 | };
19 | info: string;
20 | expand: boolean;
21 | onClick: (id: string) => void;
22 | };
23 |
--------------------------------------------------------------------------------
/src/Me/components/RichList/RichList.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useState } from 'react';
2 | import { UnstyledList } from '../../../components/common';
3 |
4 | export const useExpendableRichItems = () => {
5 | const [expandId, setExpandId] = useState('');
6 |
7 | const onSelect = (id: string) => {
8 | setExpandId(expandId === id ? '' : id);
9 | };
10 |
11 | return { expandId, onSelect };
12 | };
13 |
14 | export const RichList: FC = ({ children }) => {
15 | return {children} ;
16 | };
17 |
18 | export default RichList;
19 |
--------------------------------------------------------------------------------
/src/Me/components/RichList/index.ts:
--------------------------------------------------------------------------------
1 | export { default as RichItem } from './RichItem';
2 | export { default as RichList } from './RichList';
3 |
--------------------------------------------------------------------------------
/src/Me/components/Select/Select.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Select from './';
3 | import { render } from '@testing-library/react';
4 |
5 | const options = [
6 | { value: 1, label: 'one' },
7 | { value: 2, label: 'two' },
8 | { value: 3, label: 'three' },
9 | ];
10 | const noop = () => {};
11 |
12 | describe('Select component', () => {
13 | it('renders properly', () => {
14 | const options = [
15 | { value: 1, label: 'one' },
16 | { value: 2, label: 'two' },
17 | { value: 3, label: 'three' },
18 | ];
19 | const { container } = render(
20 |
28 | );
29 |
30 | expect(container.firstChild).toMatchSnapshot();
31 | });
32 | it('does not allow to select more items than specified by maxSelections', () => {
33 | const maxItems = 2;
34 |
35 | const { getByText } = render(
36 |
46 | );
47 |
48 | expect(getByText('one')).toBeInTheDocument();
49 | expect(getByText('two')).toBeInTheDocument();
50 | expect(getByText('Reached max items')).toBeInTheDocument();
51 | });
52 | });
53 |
--------------------------------------------------------------------------------
/src/Me/components/Select/index.ts:
--------------------------------------------------------------------------------
1 | export { Select as default } from './Select';
2 |
--------------------------------------------------------------------------------
/src/Me/components/Textarea/Textarea.test.tsx:
--------------------------------------------------------------------------------
1 | import Textarea from './';
2 | import { render } from '@testing-library/react';
3 |
4 | describe('Textarea component', () => {
5 | it('renders properly', () => {
6 | const { container } = render();
7 | expect(container.firstChild).toMatchSnapshot();
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/src/Me/components/Textarea/Textarea.tsx:
--------------------------------------------------------------------------------
1 | import React, { TextareaHTMLAttributes, useContext } from 'react';
2 | import styled from 'styled-components/macro';
3 | import { formFieldContext } from '../FormField/formContext';
4 |
5 | export const StyledTextarea = styled.textarea<{ invalid?: boolean }>`
6 | font-family: Lato, sans-serif;
7 | font-size: 14px;
8 | line-height: 17px;
9 | border-radius: 3px;
10 | background-color: #fff;
11 | border: 1px solid
12 | ${props => (props.invalid ? 'var(--form-text-invalid)' : '#bfbfbf')};
13 | padding: 7px 12px 6px 8px;
14 | color: #4f4f4f;
15 | min-height: 75px;
16 |
17 | ::placeholder {
18 | color: ${props =>
19 | props.invalid
20 | ? 'var(--form-text-invalid)'
21 | : 'var(--form-text-placeholder)'};
22 | }
23 |
24 | :disabled {
25 | opacity: 0.7;
26 | }
27 | `;
28 |
29 | export const Textarea = (
30 | props: TextareaHTMLAttributes
31 | ) => {
32 | const id = useContext(formFieldContext);
33 | return ;
34 | };
35 |
--------------------------------------------------------------------------------
/src/Me/components/Textarea/__snapshots__/Textarea.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Textarea component renders properly 1`] = `
4 | .c0 {
5 | font-family: Lato,sans-serif;
6 | font-size: 14px;
7 | line-height: 17px;
8 | border-radius: 3px;
9 | background-color: #fff;
10 | border: 1px solid #bfbfbf;
11 | padding: 7px 12px 6px 8px;
12 | color: #4f4f4f;
13 | min-height: 75px;
14 | }
15 |
16 | .c0::-webkit-input-placeholder {
17 | color: var(--form-text-placeholder);
18 | }
19 |
20 | .c0::-moz-placeholder {
21 | color: var(--form-text-placeholder);
22 | }
23 |
24 | .c0:-ms-input-placeholder {
25 | color: var(--form-text-placeholder);
26 | }
27 |
28 | .c0::placeholder {
29 | color: var(--form-text-placeholder);
30 | }
31 |
32 | .c0:disabled {
33 | opacity: 0.7;
34 | }
35 |
36 |
40 | `;
41 |
--------------------------------------------------------------------------------
/src/Me/components/Textarea/index.ts:
--------------------------------------------------------------------------------
1 | export { Textarea as default } from './Textarea';
2 |
--------------------------------------------------------------------------------
/src/Me/styles/global.ts:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from 'styled-components';
2 |
3 | export const GlobalStyle = createGlobalStyle`
4 | body {
5 | &.has-modal {
6 | overflow: hidden;
7 | }
8 | }`;
9 |
--------------------------------------------------------------------------------
/src/Me/styles/shared/devices.ts:
--------------------------------------------------------------------------------
1 | const desktop = `(min-width: 801px)`;
2 | const mobile = `(max-width: 800px)`;
3 |
4 | export { desktop, mobile };
5 |
--------------------------------------------------------------------------------
/src/__tests__/App.test.js:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom';
2 | import nock from 'nock';
3 | import { act } from 'react-dom/test-utils';
4 |
5 | import App from '../components/layouts/App';
6 | import { UserProvider } from '../context/userContext/UserContext';
7 |
8 | const API_BASE_URL = 'https://api.codingcoach.io';
9 |
10 | it('renders without crashing', async () => {
11 | nock(API_BASE_URL)
12 | .options('/mentors')
13 | .query(true)
14 | .reply(200)
15 | .get('/mentors')
16 | .reply(() => [])
17 | .get('/current')
18 | .reply(() => ({}));
19 |
20 | const div = document.createElement('div');
21 |
22 | act(() => {
23 | ReactDOM.render(
24 |
25 |
26 | ,
27 | div
28 | );
29 | });
30 | expect(div.querySelector('.app')).toBeDefined();
31 | await act(() => Promise.resolve());
32 | });
33 |
--------------------------------------------------------------------------------
/src/__tests__/Card.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Card from '../Me/components/Card';
3 | import { render, fireEvent } from '@testing-library/react';
4 |
5 | describe('Card component', () => {
6 | it('render card just title and children', () => {
7 | const title = 'mentor';
8 | const { getByText } = render(
9 |
10 | I'm a children
11 |
12 | );
13 | expect(getByText('mentor')).toBeInTheDocument();
14 | expect(getByText("I'm a children")).toBeInTheDocument();
15 | });
16 |
17 | it('render card with Edit button', () => {
18 | const title = 'mentor';
19 | const editFn = jest.fn();
20 | const { getByText } = render( );
21 | expect(getByText('mentor')).toBeInTheDocument();
22 | const editButton = getByText('Edit');
23 | expect(editButton).toBeInTheDocument();
24 | fireEvent.click(editButton);
25 | expect(editFn).toHaveBeenCalled();
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/src/__tests__/titleGenerator.spec.js:
--------------------------------------------------------------------------------
1 | import { generate, prefix } from '../titleGenerator';
2 |
3 | const USA = 'United States';
4 | jest.mock('svg-country-flags/countries.json', () => ({
5 | USA: 'United States',
6 | }));
7 |
8 | const tag = 'javascript';
9 | const name = 'John Doe';
10 | const country = 'USA';
11 |
12 | describe.only('title generator', () => {
13 | it('should be only prefix by default', () => {
14 | const title = generate({});
15 |
16 | expect(title).toBe(`${prefix}`);
17 | });
18 |
19 | it('should be mentor name if supplied', () => {
20 | const title = generate({
21 | tag,
22 | name,
23 | country,
24 | });
25 |
26 | expect(title).toBe(`${prefix} | ${name}`);
27 | });
28 |
29 | it(`should be country's mentors if country supplied`, () => {
30 | const title = generate({
31 | country,
32 | });
33 |
34 | expect(title).toBe(`${prefix} | mentors from ${USA}`);
35 | });
36 |
37 | it(`should be tag's mentors if country supplied`, () => {
38 | const title = generate({
39 | tag,
40 | });
41 |
42 | expect(title).toBe(`${prefix} | ${tag} mentors`);
43 | });
44 |
45 | it(`should country's mentors if country supplied`, () => {
46 | const title = generate({
47 | country,
48 | tag,
49 | });
50 |
51 | expect(title).toBe(`${prefix} | ${tag} mentors from ${USA}`);
52 | });
53 | });
54 |
--------------------------------------------------------------------------------
/src/api/admin.ts:
--------------------------------------------------------------------------------
1 | import ApiService, { paths } from '.';
2 | import { MentorshipRequest, UserRecord } from '../types/models';
3 |
4 | export function getAllMentorshipRequests(apiService: ApiService, numMonthAgo: number = 1) {
5 | const monthAgo = new Date();
6 | monthAgo.setMonth(monthAgo.getMonth() - numMonthAgo);
7 | return apiService.makeApiCall(`${paths.MENTORSHIP}`, {
8 | from: monthAgo,
9 | });
10 | }
11 |
12 | export async function sendStaledRequestEmail(apiService: any, mentorshipId: string) {
13 | await apiService.makeApiCall(
14 | `${paths.MENTORSHIP}/requests/${mentorshipId}/reminder`,
15 | null,
16 | 'PUT'
17 | );
18 | }
19 |
20 | export async function sendMentorNotActive(apiService: ApiService, mentorId: string) {
21 | const response = await apiService.makeApiCall(
22 | `${paths.ADMIN}/mentor/${mentorId}/notActive`,
23 | null,
24 | 'PUT'
25 | );
26 | if (response?.success) {
27 | return response.data;
28 | }
29 | }
30 |
31 | export async function freezeMentor(apiService: any, mentorId: string) {
32 | await apiService.makeApiCall(`${paths.ADMIN}/mentor/${mentorId}/freeze`, null, 'PUT');
33 | }
34 |
35 | export function getUserRecords(apiService: ApiService, userId: string) {
36 | return apiService.makeApiCall(`${paths.USERS}/${userId}/records`);
37 | }
--------------------------------------------------------------------------------
/src/assets/me/camera.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/me/close.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/me/home.svg:
--------------------------------------------------------------------------------
1 |
5 |
9 |
13 |
--------------------------------------------------------------------------------
/src/assets/me/icon-available.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/me/icon-country.svg:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
--------------------------------------------------------------------------------
/src/assets/me/icon-description.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/me/icon-door-exit.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/me/icon-email.svg:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
--------------------------------------------------------------------------------
/src/assets/me/icon-spokenLanguages.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/me/icon-survey.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/me/icon-tags.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/me/icon-title.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/me/icon-unavailable.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/me/icon-user-remove.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/me/logo.svg:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/assets/me/mentors.svg:
--------------------------------------------------------------------------------
1 |
5 |
9 |
13 |
--------------------------------------------------------------------------------
/src/channelProvider.js:
--------------------------------------------------------------------------------
1 | export const providers = {
2 | slack: {
3 | icon: 'slack',
4 | url: 'https://coding-coach.slack.com/team/{id}',
5 | },
6 | email: {
7 | icon: 'at',
8 | url: 'mailto:{id}',
9 | },
10 | linkedin: {
11 | icon: 'linkedin',
12 | inputIcon: 'linkedin-square',
13 | url: 'https://www.linkedin.com/in/{id}',
14 | },
15 | facebook: {
16 | icon: 'facebook',
17 | inputIcon: 'facebook-square',
18 | url: 'https://www.facebook.com/{id}',
19 | },
20 | twitter: {
21 | icon: 'twitter',
22 | inputIcon: 'twitter-square',
23 | url: 'https://twitter.com/{id}',
24 | },
25 | github: {
26 | icon: 'github',
27 | url: 'https://github.com/{id}',
28 | },
29 | website: {
30 | icon: 'globe',
31 | url: 'https://{id}',
32 | },
33 | };
34 |
35 | export function getChannelInfo(channel) {
36 | const { type, id } = channel;
37 | const { icon, url: providerUrl } = providers[type];
38 | const idPh = '{id}';
39 | return {
40 | icon,
41 | url: providerUrl.replace(idPh, id),
42 | };
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/AutoComplete/AutoComplete.css:
--------------------------------------------------------------------------------
1 | .ac .highlight {
2 | background: rgba(105, 213, 177, 0.3);
3 | }
4 |
5 | .ac .ac-menu {
6 | position: absolute;
7 | z-index: 1;
8 | left: var(--filter-input-offset);
9 | width: calc(100% - var(--filter-input-offset));
10 | max-height: 150px;
11 | overflow: auto;
12 | background: #fff;
13 | border: 1px solid rgba(155, 155, 155, 0.5);
14 | border-top: 0;
15 | }
16 |
17 | .ac .clear-btn {
18 | position: absolute;
19 | top: 50%;
20 | right: 10px;
21 | transform: translateY(-50%);
22 | }
23 |
24 | @media all and (max-width: 576px) {
25 | .ac .ac-menu {
26 | width: calc(100% - var(--filter-input-offset) * 2);
27 | }
28 | .ac .clear-btn {
29 | right: 20px;
30 | }
31 | }
32 |
33 | .ac .ac-item {
34 | cursor: pointer;
35 | padding: 5px;
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/Card/Card.types.ts:
--------------------------------------------------------------------------------
1 | import { TooltipProps } from 'react-tippy';
2 | import { Mentor } from '../../types/models';
3 |
4 | export type CTAButtonProps = {
5 | tooltipProps: TooltipProps;
6 | onClick?: () => void;
7 | text: string;
8 | link?: string;
9 | };
10 |
11 | export type CardProps = {
12 | mentor: Mentor;
13 | isFav: boolean;
14 | onAvatarClick?(): void;
15 | onFavMentor(mentor: Mentor): void;
16 | appearance: 'extended' | 'compact';
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/Card/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'react-obfuscate';
2 |
--------------------------------------------------------------------------------
/src/components/Content/Content.css:
--------------------------------------------------------------------------------
1 | .modal-container {
2 | background: rgba(100,100,100,0.8);
3 | width: 100%;
4 | height: 100%;
5 | position: fixed;
6 | top: 0;
7 | left: 0;
8 | z-index: -1;
9 | opacity: 0;
10 | pointer-events: none;
11 | transition: all 0.2s ease-in-out;
12 | }
13 |
14 | .modal-container.active {
15 | opacity: 1;
16 | z-index: 999;
17 | pointer-events: auto;
18 | }
19 |
20 | .modal-container .modal-box {
21 | display: flex;
22 | flex-direction: column;
23 | background: #fff;
24 | min-height: 100px;
25 | border-radius: 5px;
26 | margin: 300px auto;
27 | opacity: 0;
28 | padding: 25px;
29 | position: relative;
30 | transition: all 0.4s ease-in-out;
31 | }
32 |
33 | .modal-container.active .modal-box {
34 | margin: 0 auto;
35 | opacity: 1;
36 | }
37 |
38 | .modal-container .modal-box .close {
39 | border: none;
40 | background: #ddd;
41 | position: absolute;
42 | top: 8px;
43 | right: 8px;
44 | border-radius: 50%;
45 | width: 24px;
46 | height: 24px;
47 | color: #000;
48 | font-size: 15px;
49 | cursor: pointer;
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/Content/Content.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import cookiesPolicy from '../../contents/cookiesPolicy';
3 | import termsAndConditions from '../../contents/termsAndConditions';
4 | import privacyPolicy from '../../contents/privacyPolicy';
5 | import codeOfConduct from '../../contents/codeOfConduct.js';
6 |
7 | const Contents = {
8 | 'cookies-policy': cookiesPolicy,
9 | 'terms-conditions': termsAndConditions,
10 | 'privacy-policy': privacyPolicy,
11 | 'code-conduct': codeOfConduct,
12 | };
13 |
14 | export default class Content extends Component {
15 | render() {
16 | const { topic } = this.props;
17 | const html = Contents[topic] || `Cannot find the content!
`;
18 |
19 | return (
20 |
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/Filter/Filter.css:
--------------------------------------------------------------------------------
1 | #mentorCount {
2 | font-size: 0.65em;
3 | padding-left: 0.5em;
4 | font-weight: 400;
5 | }
6 |
7 | #filter {
8 | background-color: rgba(105, 213, 177, 0.3);
9 | color: #4a4a4a;
10 | font-size: 1.5em;
11 | text-transform: uppercase;
12 | padding: 15px 0 15px 30px;
13 | margin: 0;
14 | }
15 | @media screen and (max-height: 677px) {
16 | #filter {
17 | padding: 10px 0 10px 30px;
18 | }
19 | }
20 |
21 | .toggle-filter {
22 | float: right;
23 | margin-right: 10px;
24 | background: none;
25 | border: none;
26 | }
27 |
28 | .toggle-filter i {
29 | transition: transform 0.15s ease;
30 | }
31 |
32 | .toggle-filter i.show-filters {
33 | transform: rotateZ(90deg);
34 | }
35 |
36 | @media all and (max-width: 800px) {
37 | .inputs {
38 | left: 0;
39 | opacity: 1;
40 | padding: 0 10px;
41 | position: absolute;
42 | width: 100%;
43 | z-index: 1;
44 | }
45 |
46 | .inputs[aria-expanded='false'] {
47 | pointer-events: none;
48 | opacity: 0;
49 | }
50 |
51 | .inputs[aria-expanded='true'] {
52 | transition: opacity 0.5s ease-in-out;
53 | }
54 |
55 | .filter-wrapper {
56 | order: 2;
57 | }
58 | }
59 |
60 | @media all and (min-width: 800px) {
61 | .toggle-filter {
62 | display: none;
63 | }
64 |
65 | .sidebar {
66 | max-width: var(--filter-width);
67 | position: fixed;
68 | }
69 | }
70 |
71 | @media screen and (max-width: 1280px) {
72 | #filter {
73 | padding: 5px 0 5px 30px;
74 | }
75 | }
76 |
77 | .fav-filter {
78 | position: relative;
79 | margin-top: 20px;
80 | padding-left: 18px;
81 | background-color: #fff;
82 | text-transform: uppercase;
83 | color: #4a4a4a;
84 | font-weight: bold;
85 | }
86 |
87 | @media all and (max-width: 576px) {
88 | .fav-filter {
89 | padding: 0 15px;
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/components/FilterClear/FilterClear.css:
--------------------------------------------------------------------------------
1 | .clear {
2 | font-size: 12px;
3 | padding: 5px;
4 | border: none;
5 | background: none;
6 | }
7 |
8 | .clear:hover {
9 | color: var(--tag-color);
10 | cursor: pointer;
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/FilterClear/FilterClear.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const FilterClear = ({ onClear }) => {
4 | return (
5 | <>
6 |
7 | clear
8 |
9 | >
10 | );
11 | };
12 |
13 | export default FilterClear;
14 |
--------------------------------------------------------------------------------
/src/components/Input/Input.css:
--------------------------------------------------------------------------------
1 | .label {
2 | position: absolute;
3 | top: -10px;
4 | left: 25px;
5 | background-color: #fff;
6 | text-transform: uppercase;
7 | color: #4a4a4a;
8 | font-weight: bold;
9 | }
10 |
11 | .input-container {
12 | position: relative;
13 | margin-top: 20px;
14 | }
15 |
16 | .input {
17 | width: 250px;
18 | height: 40px;
19 | background: none;
20 | border: none;
21 | border: 1px solid rgba(155, 155, 155, 0.5);
22 | border-radius: 2px;
23 | margin-left: var(--filter-input-offset);
24 | padding-left: 10px;
25 | color: var(--tag-color);
26 | }
27 |
28 | .input.input-extended {
29 | width: 100%;
30 | }
31 |
32 | @media all and (max-width: 800px) {
33 | .input {
34 | width: calc(100% - var(--filter-input-offset) * 2);
35 | font-size: 16px;
36 | }
37 | }
--------------------------------------------------------------------------------
/src/components/Input/Input.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Input = ({ id, label, children }) => (
4 |
5 |
6 | {label}
7 |
8 | {children}
9 |
10 | );
11 |
12 | export default Input;
13 |
--------------------------------------------------------------------------------
/src/components/Link/Link.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactElement } from 'react';
2 | import NextLink, { LinkProps as NextLinkProps } from 'next/link';
3 |
4 | type LinkProps = React.PropsWithChildren & {
5 | className?: string;
6 | };
7 |
8 | const isA = (element: React.ReactNode) => {
9 | return (
10 | React.isValidElement(element) &&
11 | (element.type === 'a' ||
12 | /* styled.a */ (element.type as any).target === 'a')
13 | );
14 | };
15 |
16 | const hasATag = (children: LinkProps['children']) => {
17 | if (typeof children !== 'object') {
18 | return false;
19 | }
20 | if (isA(children)) {
21 | return true;
22 | }
23 | return React.Children.toArray(
24 | (children as ReactElement).props?.children
25 | )?.some(hasATag);
26 | };
27 |
28 | const Link = (props: LinkProps) => {
29 | const { children, className, ...rest } = props;
30 | const hasA = hasATag(props.children);
31 |
32 | return (
33 |
34 | {
35 | // eslint-disable-next-line jsx-a11y/anchor-is-valid
36 | hasA ? children : {children}
37 | }
38 |
39 | );
40 | };
41 |
42 | export default Link;
43 |
--------------------------------------------------------------------------------
/src/components/Loader.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames';
2 | import styled from 'styled-components';
3 |
4 | const SrOnly = styled.span`
5 | position: absolute;
6 | width: 1px;
7 | height: 1px;
8 | padding: 0;
9 | margin: -1px;
10 | overflow: hidden;
11 | clip: rect(0, 0, 0, 0);
12 | white-space: nowrap;
13 | border: 0;
14 | `;
15 |
16 | type LoaderProps = {
17 | className?: string;
18 | size?: number;
19 | };
20 |
21 | export const Loader = ({ className, size = 1 }: LoaderProps) => {
22 | return (
23 |
30 | Loading...
31 |
32 | );
33 | };
34 |
--------------------------------------------------------------------------------
/src/components/LoginNavigation/LoginNavigation.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { useAuth } from '../../context/authContext/AuthContext';
3 | import { desktop, mobile } from '../../Me/styles/shared/devices';
4 |
5 | function LoginNavigation() {
6 | const auth = useAuth();
7 |
8 | return (
9 |
10 | auth.login()}>
11 | Login / Register
12 |
13 |
14 | );
15 | }
16 |
17 | const LoginAreaItem = styled.button`
18 | background: none;
19 | font-size: 16px;
20 | cursor: pointer;
21 | padding: 0;
22 |
23 | @media ${mobile} {
24 | color: #fff;
25 | text-align: start;
26 | }
27 |
28 | @media ${desktop} {
29 | color: #69d5b1;
30 |
31 | &:hover {
32 | color: #54aa8d;
33 | }
34 | }
35 | `;
36 |
37 | const LoginArea = styled.div`
38 | display: flex;
39 | flex-direction: column;
40 | margin: 0 40px;
41 |
42 | @media all and (min-width: 800px) {
43 | margin: 0 20px;
44 | flex-direction: row;
45 |
46 | * {
47 | margin-left: 20px;
48 | }
49 | }
50 | `;
51 |
52 | export default LoginNavigation;
53 |
--------------------------------------------------------------------------------
/src/components/Logo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Logo = () => {
4 | return (
5 |
11 |
12 |
20 |
21 |
22 |
23 |
24 |
29 |
34 |
35 |
36 |
37 |
38 | );
39 | };
40 |
41 | export default Logo;
42 |
--------------------------------------------------------------------------------
/src/components/MentorsList/MentorList.css:
--------------------------------------------------------------------------------
1 | @media all and (max-width: 800px) {
2 | main {
3 | flex-direction: column;
4 | }
5 |
6 | .mentors-wrapper {
7 | padding: 0 18px;
8 | background: #fff;
9 | position: relative;
10 | transform: translateY(0);
11 | transition: transform 0.3s ease;
12 | }
13 |
14 | .mentors-wrapper.active {
15 | transform: translateY(300px);
16 | margin-bottom: 50px;
17 | }
18 | }
19 |
20 | @media all and (min-width: 800px) {
21 | .mentors-cards {
22 | display: flex;
23 | flex-wrap: wrap;
24 | justify-content: center;
25 | }
26 | }
27 |
28 | .mentors-wrapper .loader {
29 | font-size: 1.5rem;
30 | margin-top: 15px;
31 | }
32 |
33 | .nothing-to-show {
34 | font-size: 20px;
35 | line-height: 1.6;
36 | padding: 8px;
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/MentorsList/Pager.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components/macro';
2 | import { useFilters } from '../../context/filtersContext/FiltersContext';
3 | import { scrollToTop } from '../../helpers/window';
4 | import { Button } from '../../Me/components/Button/Button';
5 | import { useFilterParams } from '../../utils/permaLinkService';
6 |
7 | type PagerProps = {
8 | page: number;
9 | hasNext: boolean;
10 | onPage: (page: number) => void;
11 | };
12 |
13 | const StyledPager = styled.div`
14 | gap: 5px;
15 | display: flex;
16 | margin: 20px 0 0;
17 | justify-content: center;
18 | `;
19 |
20 | export const Pager = ({ hasNext }: PagerProps) => {
21 | const [state, dispatch] = useFilters();
22 | const { setFilterParams } = useFilterParams();
23 | const { page } = state;
24 |
25 | const setPage = async (page: number) => {
26 | await scrollToTop(500);
27 | setFilterParams('page', page > 1 && page);
28 | dispatch({
29 | type: 'setPage',
30 | payload: page,
31 | });
32 | };
33 |
34 | return (
35 |
36 | {page > 1 && setPage(page - 1)}>Previous }
37 | {hasNext && setPage(page + 1)}>Next }
38 |
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/src/components/MobileNavigation/MobileNavigation.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import styled from 'styled-components';
3 | import Modal from '../Modal/Modal';
4 | import Navigation from '../Navigation/Navigation';
5 | import ModalContent from '../Modal/ModalContent';
6 | import LoginNavigation from '../LoginNavigation/LoginNavigation';
7 | import Vercel from '../../assets/powered-by-vercel.svg';
8 |
9 | function MobileNavigation(props) {
10 | const [modal, setModal] = useState({
11 | title: null,
12 | content: null,
13 | onClose: null,
14 | });
15 |
16 | const handleModal = ({ title, content, onClose }) => {
17 | setModal({ title, content, onClose });
18 | };
19 |
20 | return (
21 | <>
22 |
23 | {modal.content}
24 |
25 |
26 | handleModal({ title, content })}
29 | />
30 |
31 | {!props.isAuthenticated && }
32 |
33 |
34 | handleModal({ title, content })}
38 | />
39 | handleModal({ title, content })}
43 | />
44 | handleModal({ title, content })}
48 | />
49 | handleModal({ title, content })}
53 | />
54 |
55 | >
56 | );
57 | }
58 |
59 | const ContentWrapper = styled.ul`
60 | line-height: 2.5rem;
61 | font-size: 16px;
62 | list-style: none;
63 | position: absolute;
64 | bottom: 1rem;
65 | left: inherit;
66 | cursor: pointer;
67 | `;
68 |
69 | export default MobileNavigation;
70 |
--------------------------------------------------------------------------------
/src/components/Modal/Modal.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import classNames from 'classnames';
3 |
4 | export default class Modal extends Component {
5 | state = {
6 | isActive: this.props.isActive ?? false,
7 | };
8 |
9 | handleOpen = (children) => {
10 | this.setState({
11 | isActive: !!children,
12 | children,
13 | });
14 | };
15 |
16 | handleClose = () => {
17 | const { onClose } = this.props;
18 |
19 | this.setState({
20 | isActive: false,
21 | });
22 |
23 | if (typeof onClose === 'function') {
24 | onClose();
25 | }
26 | };
27 |
28 | UNSAFE_componentWillReceiveProps(nextProps) {
29 | if (nextProps.children !== this.props.children) {
30 | this.handleOpen(nextProps.children);
31 | }
32 | }
33 |
34 | onTransitionEnd = (e) => {
35 | if (!this.state.isActive) {
36 | this.setState({ children: null });
37 | }
38 | };
39 |
40 | render() {
41 | const { isActive, children } = this.state;
42 | const { title, size = '', showCloseButton = true } = this.props;
43 | return (
44 |
48 |
49 | {
50 | showCloseButton && (
51 |
52 |
53 |
54 | )
55 | }
56 | {
57 | title && (
58 |
59 |
{title}
60 |
61 | )
62 | }
63 |
64 |
65 | {children
66 | ? React.cloneElement(children, { onClose: this.handleClose })
67 | : ''}
68 |
69 |
70 |
71 |
72 | );
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/components/Modal/ModalContent.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import Content from '../Content/Content';
3 |
4 | export default class ModalContent extends Component {
5 | state = {
6 | modal: {
7 | title: null,
8 | content: null,
9 | onClose: null,
10 | },
11 | };
12 |
13 | render() {
14 | const { content, onClose, policyTitle, handleModal } = this.props;
15 | return (
16 | {
18 | this.setState(
19 | {
20 | modal: {
21 | title: policyTitle,
22 | content: ,
23 | onClose,
24 | },
25 | },
26 | () => {
27 | handleModal &&
28 | handleModal(policyTitle, , onClose);
29 | }
30 | );
31 | }}
32 | >
33 | {policyTitle}
34 |
35 | );
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/SocialLinks/SocialLinks.css:
--------------------------------------------------------------------------------
1 | .social-link {
2 | margin: 0 10px;
3 | }
4 |
5 | .social-link svg path {
6 | transition: fill 255ms ease;
7 | }
8 |
9 | .social-link:hover svg path {
10 | fill: #65d6af;
11 | }
12 |
13 | .social-wrapper {
14 | align-self: center;
15 | margin-top: 30px;
16 | }
17 |
18 | @media all and (max-width: 800px) {
19 | .social-wrapper {
20 | margin-block: 10px;
21 | }
22 |
23 | .social-link {
24 | margin: 0 5px;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/Switch/Switch.css:
--------------------------------------------------------------------------------
1 | .switch-container {
2 | display: flex;
3 | justify-content: space-between;
4 | }
5 |
6 | .switch-input {
7 | height: 20px;
8 | }
9 |
10 | .switch-input input[type=checkbox] {
11 | height: 0;
12 | width: 0;
13 | visibility: hidden;
14 | }
15 |
16 | .switch-input label {
17 | cursor: pointer;
18 | text-indent: -9999px;
19 | width: 60px;
20 | height: 30px;
21 | background-color: grey;
22 | display: block;
23 | border-radius: 100px;
24 | position: relative;
25 | top: -25px;
26 | transition: background-color 0.35s ease-in-out;
27 | }
28 |
29 | .switch-input.small label {
30 | width: 25px;
31 | height: 15px;
32 | top: -15px;
33 | margin-left: 5px;
34 | }
35 |
36 | .switch-input label:after {
37 | content: '';
38 | position: absolute;
39 | top: 5px;
40 | left: 5px;
41 | width: 20px;
42 | height: 20px;
43 | background: #fff;
44 | border-radius: 20px;
45 | transition: left 0.2s ease-in-out, transform 0.2s ease-in-out;
46 | }
47 |
48 | .switch-input.small label:after {
49 | top: 2.5px;
50 | left: 2.5px;
51 | width: 10px;
52 | height: 10px;
53 | border-radius: 10px;
54 | }
55 |
56 | .switch-input input:checked + label {
57 | background-color: var(--tag-color);
58 | }
59 |
60 | .switch-input.dark input:checked + label {
61 | background-color: var(--button-dark-bg);
62 | }
63 |
64 | .switch-input input:checked + label:after {
65 | left: calc(100% - 5px);
66 | transform: translateX(-100%);
67 | }
68 |
69 | .switch-input.small input:checked label:after {
70 | left: calc(100% - 2.5px);
71 | }
72 |
--------------------------------------------------------------------------------
/src/components/Switch/Switch.test.tsx:
--------------------------------------------------------------------------------
1 | import { render } from '@testing-library/react';
2 | import Switch from './Switch';
3 |
4 | describe('Switch component', () => {
5 | it('renders properly', () => {
6 | const { container } = render(
7 | {}} isChecked={false} />
8 | );
9 | expect(container.firstChild).toMatchSnapshot();
10 | });
11 |
12 | it('should call onToggle on click', () => {
13 | const onToggle = jest.fn();
14 |
15 | const { getByLabelText } = render(
16 |
17 | );
18 | getByLabelText('Toggle').click();
19 | expect(onToggle).toHaveBeenCalledWith(true);
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/components/Switch/__snapshots__/Switch.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Switch component renders properly 1`] = `
4 | .c0 {
5 | display: -webkit-box;
6 | display: -webkit-flex;
7 | display: -ms-flexbox;
8 | display: flex;
9 | -webkit-box-pack: justify;
10 | -webkit-justify-content: space-between;
11 | -ms-flex-pack: justify;
12 | justify-content: space-between;
13 | }
14 |
15 | .c1 {
16 | height: 20px;
17 | }
18 |
19 | .c1 input[type='checkbox'] {
20 | height: 0;
21 | width: 0;
22 | visibility: hidden;
23 | }
24 |
25 | .c1 input[type='checkbox']:checked + label {
26 | background-color: var(--tag-color);
27 | }
28 |
29 | .c1 input[type='checkbox']:checked + label:after {
30 | -webkit-transform: translateX(-100%);
31 | -ms-transform: translateX(-100%);
32 | transform: translateX(-100%);
33 | left: calc(100% - 5px);
34 | }
35 |
36 | .c1 label {
37 | display: block;
38 | cursor: pointer;
39 | position: relative;
40 | text-indent: -9999px;
41 | border-radius: 100px;
42 | background-color: grey;
43 | -webkit-transition: background-color 0.35s ease-in-out;
44 | transition: background-color 0.35s ease-in-out;
45 | top: -25px;
46 | width: 60px;
47 | height: 30px;
48 | }
49 |
50 | .c1 label:after {
51 | content: '';
52 | position: absolute;
53 | background: #fff;
54 | -webkit-transition: left 0.2s ease-in-out,-webkit-transform 0.2s ease-in-out;
55 | -webkit-transition: left 0.2s ease-in-out,transform 0.2s ease-in-out;
56 | transition: left 0.2s ease-in-out,transform 0.2s ease-in-out;
57 | top: 5px;
58 | left: 5px;
59 | width: 20px;
60 | height: 20px;
61 | border-radius: 20px;
62 | }
63 |
64 |
67 |
70 | label
71 |
72 |
75 |
79 |
83 | Toggle
84 |
85 |
86 |
87 | `;
88 |
--------------------------------------------------------------------------------
/src/components/common.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components/macro';
2 |
3 | export const UnstyledList = styled.ul`
4 | padding: 0;
5 | margin: 0;
6 | list-style: none;
7 | `;
8 |
--------------------------------------------------------------------------------
/src/components/layouts/App/ActionsHandler.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useEffect } from 'react';
2 | import { useRouter } from 'next/router';
3 |
4 | import auth from '../../../utils/auth';
5 |
6 | export const ActionsHandler: FC = ({ children }) => {
7 | const router = useRouter();
8 | const {query} = router;
9 | const redirectTo = query.redirectTo || '';
10 |
11 | useEffect(() => {
12 | const redirectedFrom = query.from;
13 | if (redirectedFrom) {
14 | auth.login(redirectedFrom as string);
15 | }
16 | }, [query]);
17 |
18 | if (redirectTo) {
19 | router.push(redirectTo as string);
20 | return Redirecting to {redirectTo}
;
21 | }
22 |
23 | return <>{children}>;
24 | };
25 |
--------------------------------------------------------------------------------
/src/components/layouts/App/App.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: 'Lato', sans-serif;
3 | overflow-x: hidden;
4 | -webkit-font-smoothing: antialiased;
5 | margin: 0;
6 | }
7 |
8 | .app {
9 | padding-bottom: 50px;
10 | }
11 |
12 | .main-header {
13 | padding: 30px;
14 | margin: 0 auto;
15 | display: flex;
16 | justify-content: space-between;
17 | align-items: center;
18 | }
19 |
20 | .logo {
21 | margin: 30px 30px;
22 | }
23 |
24 | .main-header h1 {
25 | font-size: 48px;
26 | }
27 |
28 | .social {
29 | margin-top: 10px;
30 | }
31 |
32 | @media screen and (max-height: 650px) {
33 | .social-wrapper {
34 | margin-top: 10px !important;
35 | }
36 | }
37 | @media screen and (max-height: 677px) {
38 | .logo {
39 | margin: 5px 30px;
40 | }
41 | }
42 |
43 | @media all and (max-width: 800px) {
44 | .sidebar-nav {
45 | position: relative;
46 | display: none;
47 | padding: 0;
48 | justify-content: space-evenly;
49 | margin-top: 30px;
50 | }
51 | }
52 |
53 | @media all and (max-width: 800px) {
54 | .logo {
55 | margin: 10px;
56 | }
57 |
58 | .logo svg {
59 | height: 80px;
60 | }
61 |
62 | .patreon-link {
63 | margin: 5px auto 10px;
64 | width: 130px;
65 | }
66 |
67 | .patreon-link img {
68 | display: block;
69 | }
70 | }
71 |
72 | @media screen and (max-width: 1280px) {
73 | .logo {
74 | margin: 10px auto;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/components/layouts/App/VerificationModal.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import styled from 'styled-components/macro';
3 | import { useApi } from '../../../context/apiContext/ApiContext';
4 | import { useUser } from '../../../context/userContext/UserContext';
5 | import Button from '../../../Me/components/Button';
6 | import { maskEmail } from '../../../utils/maskSansitiveString';
7 |
8 | type VerificationModalProps = {
9 | onSuccess: () => void;
10 | email: string;
11 | };
12 |
13 | const ModalText = styled.p`
14 | text-align: center;
15 | font-size: 16px;
16 | line-height: 1.5;
17 | `;
18 |
19 | export const VerificationModal = ({ onSuccess }: VerificationModalProps) => {
20 | const [loading, setLoading] = useState(false);
21 | const { emailVerifiedInfo } = useUser();
22 | const api = useApi();
23 |
24 | if (emailVerifiedInfo.isVerified === true) {
25 | // eslint-disable-next-line no-console
26 | console.warn('email is verified');
27 | return;
28 | }
29 |
30 | const send = async () => {
31 | setLoading(true);
32 | try {
33 | const result = await api.resendVerificationEmail();
34 | if (result.success) {
35 | onSuccess();
36 | }
37 | } catch {}
38 | setLoading(false);
39 | };
40 |
41 | return (
42 | <>
43 |
44 | Psst, we believe that you are who you say you are.
45 |
46 | Just to make sure, we need you to verify your email.
47 | Recognize {maskEmail(emailVerifiedInfo.email)}?
48 | {emailVerifiedInfo.isRegisteredRecently ? (
49 | <>
50 | This is the address we sent a verification email to.
51 |
52 | Can't find it? Hit the button
53 | >
54 | ) : (
55 | <>Hit the button to send a verification email right to your inbox>
56 | )}
57 |
58 |
59 | Resend verification email
60 |
61 |
62 |
63 | >
64 | );
65 | };
66 |
--------------------------------------------------------------------------------
/src/components/layouts/App/index.tsx:
--------------------------------------------------------------------------------
1 | import App from './App'
2 | export default App
--------------------------------------------------------------------------------
/src/config/constants.ts:
--------------------------------------------------------------------------------
1 | export const auth = {
2 | DOMAIN: process.env.REACT_APP_AUTH_DOMAIN,
3 | CLIENT_ID: process.env.REACT_APP_AUTH_CLIENT_ID,
4 | CALLBACK_URL: process.env.REACT_APP_AUTH_CALLBACK,
5 | };
6 |
7 | export const links = {
8 | MENTORSHIP_GUIDELINES:
9 | 'https://codingcoach.io/guidelines/mentorship-guidelines',
10 | };
11 |
--------------------------------------------------------------------------------
/src/config/experiments.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | /**
3 | * @typedef {'newBackoffice'} Experiments
4 | */
5 |
6 | const experiments = {};
7 |
8 | /**
9 | * @param {string} source
10 | */
11 | function addSource(source) {
12 | if (source && source.length) {
13 | source.split(',').forEach((exp) => {
14 | experiments[exp] = true;
15 | });
16 | }
17 | }
18 |
19 | addSource(process.env.NEXT_PUBLIC_EXPERIMENTS);
20 | addSource(new URLSearchParams(window.location.search).get('experiments'));
21 |
22 | if (Object.keys(experiments).length) {
23 | localStorage.setItem('experiments', JSON.stringify(experiments));
24 | }
25 |
26 | /**
27 | * @param {Experiments} name
28 | */
29 | export const isOpen = (name) => {
30 | const openExperiments = JSON.parse(
31 | localStorage.getItem('experiments') || '{}'
32 | );
33 | return openExperiments[name];
34 | };
35 |
--------------------------------------------------------------------------------
/src/contents/privacyPolicy.js:
--------------------------------------------------------------------------------
1 | const privacyPolicy = `Effective date: October 03, 2018
2 |
3 | Coding Coach ("us", "we", or "our") operates the website and the Coding Coach mobile application (the "Service").
4 |
5 | This page informs you of our policies regarding the collection, use, and disclosure ofn personal data when you use our Service and the choices you have associated with that data. Our Privacy Policy for Coding Coach is managed through Free Privacy Policy.
6 |
7 | We use your data to provide and improve the Service. By using the Service, you agree to the collection and use of information in accordance with this policy. Unless otherwise defined in this Privacy Policy, terms used in this Privacy Policy have the same meanings as in our Terms and Conditions.
8 | `;
9 |
10 | export default privacyPolicy;
11 |
--------------------------------------------------------------------------------
/src/context/apiContext/ApiContext.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, FC, useMemo } from 'react';
2 | import { AuthContext } from '../authContext/AuthContext';
3 | import ApiService from '../../api';
4 |
5 | export const ApiContext = createContext(null);
6 |
7 | export const ApiProvider: FC = (props: any) => {
8 | const { children } = props;
9 | const auth = useContext(AuthContext);
10 | const api = useMemo(() => new ApiService(auth), [auth]);
11 | return {children} ;
12 | };
13 |
14 | export function useApi(): ApiService {
15 | const api = useContext(ApiContext);
16 | if (!api) {
17 | throw new Error(`"useApi" has to be called inside ApiProvider`);
18 | }
19 | return api;
20 | }
21 |
--------------------------------------------------------------------------------
/src/context/modalContext/ModalContext.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, FC } from 'react';
2 | import { ModalProvider, useModal as rmhUseModal } from 'react-modal-hook';
3 |
4 | export const ModalContext = createContext<{
5 | closeModal(): void;
6 | }>({closeModal: () => {}});
7 |
8 | export const ModalHookProvider: FC = ({ children }) => (
9 | {children}
10 | );
11 |
12 | export function useModal(modal: JSX.Element, deps?: any[]): [openModal: () => void, closeModal: () => void] {
13 | const [openModal, closeModal] = rmhUseModal(() => {
14 | const { type: ModalComponent, props, key } = modal;
15 | return (
16 |
17 |
21 |
22 | );
23 | }, deps);
24 |
25 | return [openModal, closeModal];
26 | }
27 |
--------------------------------------------------------------------------------
/src/external-types.d.ts:
--------------------------------------------------------------------------------
1 | import { TawkAPI } from './utils/tawk';
2 |
3 | export declare global {
4 | interface Window {
5 | Tawk_API: TawkAPI;
6 | gtag(command: 'config', targetId: string, config?: Record): void;
7 | gtag(command: 'event', eventName: string, params?: Record): void;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/favoriteManager.js:
--------------------------------------------------------------------------------
1 | import { report } from './ga';
2 | const LOCAL_FAV_KEY = 'favs';
3 |
4 | export function toggleFavMentor(api, mentor, favs) {
5 | const favIndex = favs.indexOf(mentor._id);
6 |
7 | if (favIndex > -1) {
8 | favs.splice(favIndex, 1);
9 | } else {
10 | favs.push(mentor._id);
11 | }
12 | api.addMentorToFavorites(mentor._id);
13 | return favs;
14 | }
15 |
16 | export function get(api) {
17 | return api.getFavorites();
18 | }
19 |
20 | export function readFavMentorsFromLocalStorage() {
21 | let favMentorsFromLocalStorage = [];
22 | const favsFromLocal = window.localStorage.getItem(LOCAL_FAV_KEY);
23 | if (favsFromLocal) {
24 | favMentorsFromLocalStorage = JSON.parse(favsFromLocal);
25 | window.localStorage.removeItem(LOCAL_FAV_KEY);
26 | report('Favorite', 'sync');
27 | }
28 | return favMentorsFromLocalStorage;
29 | }
30 |
31 | export function updateFavMentorsForUser(api, mentors) {
32 | let timeOut = 0;
33 | mentors.forEach((mentorId) => {
34 | setTimeout(
35 | (mentorId) => {
36 | api.addMentorToFavorites(mentorId);
37 | },
38 | timeOut,
39 | mentorId
40 | );
41 | timeOut += 300;
42 | });
43 | }
44 |
--------------------------------------------------------------------------------
/src/ga.ts:
--------------------------------------------------------------------------------
1 | import { isSsr } from './helpers/ssr';
2 |
3 | export function report(category: string, action: string, label?: string) {
4 | if (!shouldReport()) {
5 | // eslint-disable-next-line no-console
6 | console.log('Fake report: ');
7 | // eslint-disable-next-line no-console
8 | console.log({ category, action, label });
9 | return;
10 | }
11 | window.gtag('event', action, {
12 | event_category: category,
13 | event_label: label,
14 | });
15 | }
16 |
17 | export function reportPageView() {
18 | if (!shouldReport()) {
19 | return;
20 | }
21 | window.gtag('event', 'page_view', {
22 | page_path: window.location.pathname,
23 | page_title: document.title,
24 | });
25 | }
26 |
27 | export function reportError(category: string, label: string) {
28 | report(category, 'Error', label);
29 | }
30 |
31 | function shouldReport() {
32 | if (isSsr()) {
33 | return false;
34 | }
35 | return !window.location.host.includes('localhost');
36 | }
37 |
--------------------------------------------------------------------------------
/src/helpers/avatar.js:
--------------------------------------------------------------------------------
1 | export const getAvatarUrl = (avatar) => {
2 | if (avatar?.startsWith('/avatars/')) {
3 | return `${process.env.NEXT_PUBLIC_API_ENDPOINT}${avatar}`;
4 | }
5 | return avatar;
6 | };
7 |
--------------------------------------------------------------------------------
/src/helpers/getTitleTags.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * It has to be a function that returns an array of tags otherwise the tags would rendered in both app and page level
3 | */
4 | export const getTitleTags = (title: string) => {
5 | return [
6 | {title} ,
7 | ,
8 | ,
9 | ];
10 | };
11 |
--------------------------------------------------------------------------------
/src/helpers/languages.ts:
--------------------------------------------------------------------------------
1 | import ISO6391 from 'iso-639-1';
2 |
3 | export const languages = ISO6391.getLanguages(ISO6391.getAllCodes());
4 | export const languageName = (languageCode: string) =>
5 | languages.find(({ code }) => code === languageCode)?.name;
6 |
--------------------------------------------------------------------------------
/src/helpers/mentorship.ts:
--------------------------------------------------------------------------------
1 | export const STATUS = {
2 | approved: 'Approved',
3 | cancelled: 'Cancelled',
4 | new: 'New',
5 | rejected: 'Rejected',
6 | viewed: 'Viewed',
7 | } as const;
8 |
9 | export type Status = typeof STATUS[keyof typeof STATUS];
10 |
--------------------------------------------------------------------------------
/src/helpers/ssr.ts:
--------------------------------------------------------------------------------
1 | export function isSsr() {
2 | return typeof window === 'undefined';
3 | }
--------------------------------------------------------------------------------
/src/helpers/time.ts:
--------------------------------------------------------------------------------
1 | const DAY = 86400e3;
2 |
3 | export function daysAgo(timestamp: number | Date | string) {
4 | if (timestamp instanceof Date) {
5 | timestamp = timestamp.getTime();
6 | }
7 | if (typeof timestamp === 'string') {
8 | timestamp = Date.parse(timestamp);
9 | }
10 | const now = Date.now();
11 | const time = (now - timestamp) / DAY;
12 | return time;
13 | }
14 |
15 | export function formatTimeAgo(timestamp: number | Date | string) {
16 | const time = daysAgo(timestamp);
17 |
18 | if (time < 0.2) return 'Just now';
19 | if (time < 1) return `${Math.floor(time * 24)} Hours Ago`;
20 | if (time < 30) return `${Math.floor(time)} Days Ago`;
21 | if (time < 365) return `${Math.floor(time / 30)} Months Ago`;
22 |
23 | return `${Math.floor(time / 365)} Year Ago`;
24 | }
25 |
--------------------------------------------------------------------------------
/src/helpers/user.ts:
--------------------------------------------------------------------------------
1 | import ISO6391 from 'iso-639-1';
2 | import countries from 'svg-country-flags/countries.json';
3 | import { Country, User, UserRole } from '../types/models';
4 | import { overwriteProfileDefaults } from '../utils/overwriteProfileDefaults';
5 |
6 | type LabelValue = {
7 | label: string;
8 | value: T;
9 | };
10 |
11 | export type UserVM = Omit<
12 | User,
13 | 'tags' | 'country' | 'spokenLanguages' | 'avatar'
14 | > & {
15 | tags: LabelValue[];
16 | country: LabelValue;
17 | spokenLanguages: LabelValue[];
18 | };
19 |
20 | function userHasRule(role: UserRole, user?: User) {
21 | return user?.roles.includes(role);
22 | }
23 |
24 | export function isMentor(user?: User) {
25 | return userHasRule('Mentor', user);
26 | }
27 |
28 | export function isAdmin(user?: User) {
29 | return userHasRule('Admin', user);
30 | }
31 |
32 | export function fromVMtoM(user: UserVM): User {
33 | return {
34 | ...user,
35 | description: user.description,
36 | tags: user.tags.map(i => i.value),
37 | spokenLanguages: user.spokenLanguages.map(i => i.value),
38 | country: user.country.value as Country,
39 | };
40 | }
41 |
42 | export function fromMtoVM(user: User): UserVM {
43 | return {
44 | ...user,
45 | ...(isMentor(user) ? {} : overwriteProfileDefaults(user)),
46 | country: user.country
47 | ? { label: countries[user.country], value: user.country }
48 | : { label: '', value: '' },
49 | spokenLanguages: user.spokenLanguages
50 | ? user.spokenLanguages.map(i => ({
51 | label: ISO6391.getName(i),
52 | value: i,
53 | }))
54 | : [],
55 | tags: user.tags ? user.tags.map(i => ({ label: i, value: i })) : [],
56 | title: user.title ?? '',
57 | channels: user.channels ?? [],
58 | };
59 | }
60 |
--------------------------------------------------------------------------------
/src/helpers/window.ts:
--------------------------------------------------------------------------------
1 | export function scrollToTop(scrollDuration: number = 200) {
2 | return new Promise((resolve) => {
3 | const scrollStep = -window.scrollY / (scrollDuration / 15),
4 | scrollInterval = setInterval(function () {
5 | if (window.scrollY !== 0) {
6 | window.scrollBy(0, scrollStep);
7 | } else {
8 | clearInterval(scrollInterval);
9 | resolve();
10 | }
11 | }, 15);
12 | });
13 | }
14 |
--------------------------------------------------------------------------------
/src/hooks/useDeviceType.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { desktop } from '../Me/styles/shared/devices';
3 |
4 | export function useDeviceType() {
5 | // Default isDesktop to true because we don't know the device dimensions on the server
6 | // TODO: Detect device dimensions on server
7 | const [isDesktop, setIsDesktop] = useState(true);
8 | const handleMatcher = ({ matches }: { matches: boolean }) => setIsDesktop(matches);
9 |
10 | useEffect(() => {
11 | if (typeof window !== 'object') {
12 | return;
13 | }
14 | const matcher = window.matchMedia(desktop);
15 | handleMatcher(matcher);
16 |
17 | matcher.addEventListener('change', handleMatcher);
18 | }, []);
19 |
20 | return { isDesktop, isMobile: !isDesktop };
21 | }
22 |
--------------------------------------------------------------------------------
/src/hooks/useRoutes.ts:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import { User } from '../types/models';
3 |
4 | export const useRoutes = () => {
5 | const { asPath } = useRouter();
6 | const getUrlWithFilterParams = (url: string) => {
7 | const queryParamsOnly =
8 | /(.*)(?\?.*)/.exec(asPath)?.groups.query ?? '';
9 | return url + queryParamsOnly;
10 | };
11 |
12 | return {
13 | root: {
14 | get: () => getUrlWithFilterParams('/')
15 | },
16 | user: {
17 | get: (userOrUserId: User | string) => {
18 | return getUrlWithFilterParams(
19 | `/u/${
20 | typeof userOrUserId === 'string' ? userOrUserId : userOrUserId._id
21 | }`
22 | );
23 | }
24 | },
25 | me: {
26 | get: () => '/me',
27 | requests: {
28 | get: () => '/me/requests',
29 | },
30 | admin: {
31 | get: () => '/me/admin',
32 | },
33 | },
34 | };
35 | };
36 |
--------------------------------------------------------------------------------
/src/listsGenerator.js:
--------------------------------------------------------------------------------
1 | import countries from 'svg-country-flags/countries.json';
2 | import ISO6391 from 'iso-639-1';
3 |
4 | function mapToItem(label, value) {
5 | return {
6 | label,
7 | value: (typeof value === 'string' && value) || label,
8 | };
9 | }
10 |
11 | function sortByLabel(a, b) {
12 | return a.label < b.label ? -1 : 1;
13 | }
14 |
15 | export function generateLists(mentors) {
16 | const json = {
17 | tags: [],
18 | countries: [],
19 | names: [],
20 | languages: [],
21 | };
22 |
23 | for (let i = 0; i < mentors.length; i++) {
24 | json.tags.push(...(mentors[i].tags || []));
25 | json.countries.push(mentors[i].country);
26 | json.names.push(mentors[i].name);
27 | json.languages.push(...(mentors[i].spokenLanguages || []));
28 | }
29 |
30 | json.names = [...new Set(json.names)].map(mapToItem).sort(sortByLabel);
31 | json.tags = [...new Set(json.tags.map((tag) => tag.toLowerCase()))]
32 | .map(mapToItem)
33 | .sort(sortByLabel);
34 | json.countries = [...new Set(json.countries)]
35 | .map((country) => mapToItem(countries[country], country))
36 | .sort(sortByLabel);
37 | json.languages = [...new Set(json.languages)]
38 | .map((language) => mapToItem(ISO6391.getName(language), language))
39 | .sort(sortByLabel);
40 | return json;
41 | }
42 |
--------------------------------------------------------------------------------
/src/messages.js:
--------------------------------------------------------------------------------
1 | export default {
2 | GENERIC_ERROR: 'Something went wrong. Please contact us',
3 | EDIT_DETAILS_MENTOR_SUCCESS: 'Your details updated successfully',
4 | EDIT_DETAILS_APPLICATION_SUBMITTED:
5 | "Thanks for joining us! We'll approve your application ASAP.",
6 | EDIT_DETAILS_DELETE_ACCOUNT_CONFIRM:
7 | 'Are you sure you want to delete your account?',
8 | CARD_APPLY_TOOLTIP:
9 | "Click here to Login / Register to get the mentor's details",
10 | CARD_ANONYMOUS_LIKE_TOOLTIP:
11 | 'Click here to Login / Register to add this mentor to favorites',
12 | LOGOUT: 'Logout',
13 | CARD_APPLY_REQUEST_TOOLTIP: 'Click here to send a mentorship request.',
14 | CARD_APPLY_REQUEST_SUCCESS: 'Your mentorship request has been sent.',
15 | };
16 |
--------------------------------------------------------------------------------
/src/persistData/index.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-syntax */
2 | import { isSsr } from '../helpers/ssr';
3 | import type { MentorshipRequest, User } from '../types/models';
4 |
5 | type PersistDataKeyMap = {
6 | user: User;
7 | 'mentorship-request': Pick;
8 | }
9 |
10 | type PersistDataKey = keyof PersistDataKeyMap;
11 |
12 | export const getPersistData = (key: T): PersistDataKeyMap[T] | null => {
13 | if (isSsr()) {
14 | return null;
15 | }
16 | const item = localStorage.getItem(key);
17 | if (!item) {
18 | return null;
19 | }
20 | try {
21 | return JSON.parse(item);
22 | } catch (error) {
23 | console.error(`Error parsing value from localStorage for key "${key}":`, error);
24 | return null;
25 | }
26 | }
27 | export const setPersistData = (key: T, value: PersistDataKeyMap[T]): void => {
28 | try {
29 | const serializedValue = JSON.stringify(value);
30 | localStorage.setItem(key, serializedValue);
31 | } catch (error) {
32 | console.error(`Error setting value to localStorage for key "${key}":`, error);
33 | }
34 | }
35 | export const removeFromLocalStorage = (key: PersistDataKey): void => {
36 | try {
37 | localStorage.removeItem(key);
38 | } catch (error) {
39 | console.error(`Error removing value from localStorage for key "${key}":`, error);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/setup-tests.js:
--------------------------------------------------------------------------------
1 | import { configure } from '@testing-library/react';
2 |
3 | configure({ computedStyleSupportsPseudoElements: true });
4 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // https://create-react-app.dev/docs/running-tests/#initializing-test-environment
2 | import '@testing-library/jest-dom/extend-expect';
3 | import 'jest-styled-components';
4 |
5 | // https://github.com/facebook/create-react-app/issues/10126#issuecomment-735272763
6 | window.matchMedia = (query) => ({
7 | matches: false,
8 | media: query,
9 | onchange: null,
10 | addListener: jest.fn(), // Deprecated
11 | removeListener: jest.fn(), // Deprecated
12 | addEventListener: jest.fn(),
13 | removeEventListener: jest.fn(),
14 | dispatchEvent: jest.fn(),
15 | });
16 |
--------------------------------------------------------------------------------
/src/stories/StoriesContainer.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const StoriesContainer = styled.div`
4 | display: flex;
5 | flex-direction: column;
6 | padding: 10vh 20vh;
7 | align-items: center;
8 | justify-content: center;
9 | height: 100vh;
10 | `;
11 |
--------------------------------------------------------------------------------
/src/stories/checkbox.stories.mdx:
--------------------------------------------------------------------------------
1 | import Checkbox from '../Me/components/Checkbox';
2 | import { Meta, Story, Canvas } from '@storybook/addon-docs/blocks';
3 |
4 |
5 |
6 | export const Template = args =>
7 |
8 | # Checkbox
9 |
10 | The `Checkbox` component is a specialized component that brings cross-browser consistency to the appearance of the Checkbox
11 | instead of letting each browser do as they please, visually. To accomplish this, we've wrapped some custom HTML and SVG
12 | around a primitive ` ` element, and then hide the underlying checkbox.
13 |
14 | With the exception of a `LabelComponent` prop, all props are forwarded to the underlying `input` primitive.
15 |
16 | ## Checked
17 |
18 |
19 |
20 |
25 | Checked label with bold text !
26 |
27 | }
28 | />
29 |
30 |
31 |
32 | ## Unchecked
33 |
34 |
35 |
36 |
41 | Unchecked label with bold text !
42 |
43 | }
44 | />
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/stories/input.stories.mdx:
--------------------------------------------------------------------------------
1 | import Input from '../Me/components/Input';
2 | import FormField from '../Me/components/FormField';
3 | import { Meta, Story, Canvas } from '@storybook/addon-docs/blocks';
4 |
5 |
]}
10 | />
11 |
12 | export const Template = args =>
13 |
14 | # Input
15 |
16 | The ` ` component is a styled component that wraps the primitive html element `input`. All props
17 | are forwarded to the underlying `input` element. To render an `Input` with a label you need to use a `FormField`
18 | component.
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | ## With Help Message
29 |
30 | The `Input` component can be shown with an optional help message to the right of its label by passing along a
31 | `helpText` prop to the `FormField` component.
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | ## Without Label
42 |
43 |
44 | The `Input` component can be shown without a label by omitting the `label` prop on the `FormField` component that wraps it.
45 | It's important to still use a `FormField` even when you don't want the label to show.
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/src/stories/liststorydata.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "type": "email",
4 | "value": "myemail@codecoache.com"
5 | },
6 | {
7 | "type": "spokenLanguages",
8 | "value": "EN"
9 | },
10 | {
11 | "type": "country",
12 | "value": "US"
13 | },
14 | {
15 | "type": "title",
16 | "value": "Sharing of Knowledge, regular weekly meetings"
17 | },
18 | {
19 | "type": "tags",
20 | "value": "front-end, reactjs, css, html, Ninja"
21 | },
22 | {
23 | "type": "unavailable",
24 | "value": "unavailable"
25 | },
26 | {
27 | "type": "description",
28 | "value": "If you are up for more than once a week exploration."
29 | }
30 | ]
31 |
--------------------------------------------------------------------------------
/src/stories/mentor-card.stories.mdx:
--------------------------------------------------------------------------------
1 | import Card from '../components/Card/Card';
2 | import { UserProvider } from '../context/userContext/UserContext';
3 | import { Meta, Story, Canvas } from '@storybook/addon-docs/blocks';
4 |
5 |
10 |
11 | export const Template = args => ;
12 |
13 |
14 |
15 |
16 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/stories/profile-card.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import Card from '../Me/components/Card';
4 |
5 | export default { title: 'Profile Card' };
6 |
7 | const CardContainer = styled.div`
8 | width: 375px;
9 | padding: 15px;
10 | `;
11 |
12 | // eslint-disable-next-line no-console
13 | const action = () => console.log('Clicked');
14 |
15 | export const empty = () => (
16 |
17 |
18 |
19 | );
20 | export const withChildren = () => (
21 |
22 |
23 |
24 |
This is an example of a list inside a card
25 |
26 | First item
27 | 2nd item
28 | 3rd item
29 | 4th item
30 |
31 |
32 |
33 |
34 | );
35 |
--------------------------------------------------------------------------------
/src/stories/select.stories.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import 'styled-components/macro';
3 | import Select from '../Me/components/Select';
4 | import FormField from '../Me/components/FormField';
5 |
6 | import { StoriesContainer } from './StoriesContainer';
7 |
8 | export default { title: 'Select' };
9 | const options = [
10 | { value: 1, label: 'one' },
11 | { value: 2, label: 'two' },
12 | { value: 3, label: 'three' },
13 | ];
14 |
15 | export const SingleSelect = () => {
16 | const [selectedValues, setSelectedValues] = useState();
17 |
18 | return (
19 |
20 |
21 | {
24 | setSelectedValues(selected);
25 | }}
26 | options={options}
27 | placeholder="Select a number"
28 | />
29 |
30 |
31 | );
32 | };
33 |
34 | export const MultiSelect = () => {
35 | const [selectedValues, setSelectedValues] = useState();
36 |
37 | return (
38 |
39 |
40 | {
44 | setSelectedValues(selected);
45 | }}
46 | options={options}
47 | placeholder="Select a number"
48 | />
49 |
50 |
51 | );
52 | };
53 |
54 | export const MultiSelectWithMaxItems = () => {
55 | const [selectedValues, setSelectedValues] = useState();
56 |
57 | return (
58 |
59 |
60 | {
64 | setSelectedValues(selected);
65 | }}
66 | options={options}
67 | maxSelections={2}
68 | placeholder="Select a number"
69 | />
70 |
71 |
72 | );
73 | };
74 |
--------------------------------------------------------------------------------
/src/stories/textarea.stories.mdx:
--------------------------------------------------------------------------------
1 | import Textarea from '../Me/components/Textarea';
2 | import FormField from '../Me/components/FormField';
3 | import { Meta, Story, Canvas } from '@storybook/addon-docs/blocks';
4 |
5 |
]}
10 | />
11 |
12 | export const Template = args =>
13 |
14 | # Textarea
15 |
16 | The `Textarea` component is a styled component that wraps the primitive html element `textarea`. All props are
17 | forwarded to the underlying `textarea` element. To render a `Textarea` with a label you need to use a `FormField`
18 | component.
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | ## With Help Message
29 |
30 | The `Textarea` component can be shown with an optional help message to the right of its label by passing along a
31 | `helpText` prop to the `FormField` component.
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | ## Without Label
42 |
43 | The `Textarea` component can be shown without a label by omitting the `label` prop on the `FormField` component
44 | that wraps it. It's important to still use a `FormField` even when you don't want the label to show.
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/src/titleGenerator.js:
--------------------------------------------------------------------------------
1 | import countries from 'svg-country-flags/countries.json';
2 | import ISO6391 from 'iso-639-1';
3 |
4 | export const prefix = 'Coding Coach';
5 |
6 | export function set({ tag, country, name, language }) {
7 | document.title = generate({ tag, country, name, language });
8 | }
9 |
10 | export function generate({ tag, country, name, language }) {
11 | let title = prefix;
12 | if (name || country || tag || language) {
13 | title += ' | ';
14 |
15 | if (name) {
16 | title += name;
17 | } else {
18 | if (language) {
19 | title += ` ${ISO6391.getName(language)} speaking `;
20 | }
21 | if (tag) {
22 | title += `${tag} `;
23 | }
24 | title += 'mentors';
25 | if (country) {
26 | title += ` from ${countries[country]}`;
27 | }
28 | }
29 | }
30 | return title;
31 | }
32 |
--------------------------------------------------------------------------------
/src/types/global.d.ts:
--------------------------------------------------------------------------------
1 | export type AnyFixMe = any;
2 |
--------------------------------------------------------------------------------
/src/types/models.d.ts:
--------------------------------------------------------------------------------
1 | import countries from 'svg-country-flags/countries.json';
2 | import { Status } from '../helpers/mentorship';
3 |
4 | type BaseDBObject = {
5 | _id: string;
6 | createdAt: string;
7 | };
8 |
9 | type Country = keyof typeof countries;
10 | type UserRole = 'Admin' | 'Mentor' | 'User';
11 | type Channel = {
12 | id: string;
13 | type: string;
14 | };
15 |
16 | export type User = BaseDBObject & {
17 | name: string;
18 | title: string;
19 | email: string;
20 | email_verified: boolean;
21 | tags: string[];
22 | avatar?: string;
23 | country: Country;
24 | roles: UserRole[];
25 | available: boolean;
26 | description?: string;
27 | spokenLanguages: string[];
28 | channels: Channel[];
29 | createdAt: string;
30 | };
31 | export type Mentor = User & {};
32 | type ApplicationStatus = 'Pending' | 'Approved' | 'Rejected';
33 | export type Application = BaseDBObject & {
34 | status: ApplicationStatus;
35 | reason?: string;
36 | };
37 |
38 | export type MentorshipUser = Pick;
39 | export type MentorshipRequest = {
40 | _id: string;
41 | status: Status;
42 | createdAt: string;
43 | message: string;
44 | background: string;
45 | expectation: string;
46 | isMine: boolean;
47 | mentor: MentorshipUser;
48 | mentee: MentorshipUser;
49 | readonly reminderSentAt?: string;
50 | };
51 |
52 | export enum UserRecordType {
53 | MentorNotResponding = 1
54 | }
55 |
56 | export type UserRecord = BaseDBObject & {
57 | userId: string;
58 | type: UserRecordType;
59 | }
--------------------------------------------------------------------------------
/src/utils/isDeep.ts:
--------------------------------------------------------------------------------
1 | export const isDeep = () => {
2 | if (typeof window === 'undefined') {
3 | return false;
4 | }
5 | return new URLSearchParams(window.location.search).get('deep') === 'deep';
6 | }
7 |
--------------------------------------------------------------------------------
/src/utils/maskSansitiveString.ts:
--------------------------------------------------------------------------------
1 | const replaceWithAsterisk = (str: string) => {
2 | return str.replace(/./g, '*');
3 | }
4 |
5 | export const maskEmail = (email: string) => {
6 | return email.replace(/(.)(.*)(.@.)(.*)(.\..*)/, (...[, g1l1, g1Rest, at, g2Rest, sufix]) => {
7 | return `${g1l1}${replaceWithAsterisk(g1Rest)}${at}${replaceWithAsterisk(g2Rest)}${sufix}`;
8 | });
9 | }
10 |
--------------------------------------------------------------------------------
/src/utils/overwriteProfileDefaults.ts:
--------------------------------------------------------------------------------
1 | import type { User } from '../types/models';
2 |
3 | export function overwriteProfileDefaults({
4 | email,
5 | name,
6 | avatar,
7 | }: Pick) {
8 | const [emailName] = email.split('@');
9 |
10 | return {
11 | name: emailName === name ? '' : name,
12 | };
13 | }
14 |
--------------------------------------------------------------------------------
/src/utils/permaLinkService.js:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 |
3 | export const getFilterParams = (search) => {
4 | const params = new URLSearchParams(search);
5 | return {
6 | tag: params.get('technology') || '',
7 | country: params.get('country') || '',
8 | name: params.get('name') || '',
9 | language: params.get('language') || '',
10 | page: Number(params.get('page') || '1'),
11 | };
12 | };
13 |
14 | export const useFilterParams = () => {
15 | const router = useRouter();
16 |
17 | return {
18 | setFilterParams: (param, value) => {
19 | const params = new URLSearchParams(router.asPath.split('?')[1]);
20 | if (value) {
21 | if (params.get(param) !== value) {
22 | params.set(param, value);
23 | }
24 | } else if (params.get(param)) {
25 | params.delete(param);
26 | }
27 | if (param !== 'page') {
28 | params.delete('page');
29 | }
30 | router.push({
31 | pathname: '/',
32 | search: new URLSearchParams(params).toString(),
33 | });
34 | },
35 | };
36 | };
37 |
--------------------------------------------------------------------------------
/src/utils/sitemapGenerator.tsx:
--------------------------------------------------------------------------------
1 | const getPath = (key?: string, value?: string) => {
2 | if (!key) {
3 | return '';
4 | }
5 | if (key === 'id') {
6 | return `u/${value}`;
7 | }
8 | return `?${key}=${encodeURIComponent(value)}`;
9 | };
10 |
11 | const createUrl = (key?: string, value?: string) => {
12 | return `https://mentors.codingcoach.io/${getPath(
13 | key,
14 | value
15 | )} `;
16 | };
17 |
18 | export const buildSitemap = async () => {
19 | const mentors = await fetch(`https://api.codingcoach.io/mentors?limit=1400`)
20 | .then((data) => data.json())
21 | .then((res) => res.data);
22 |
23 | const json = {
24 | technology: [],
25 | country: [],
26 | id: [],
27 | language: [],
28 | };
29 |
30 | for (let i = 0; i < mentors.length; i++) {
31 | json.technology.push(...(mentors[i].tags || []));
32 | json.country.push(mentors[i].country);
33 | json.id.push(mentors[i]._id);
34 | json.language.push(...(mentors[i].spokenLanguages || []));
35 | }
36 |
37 | json.technology = [...new Set(json.technology)];
38 | json.country = [...new Set(json.country)];
39 | json.language = [...new Set(json.language)];
40 |
41 | const lineBreak = '\n\t';
42 | const URLs = Object.keys(json)
43 | .map((key) => [...json[key]].map((value) => createUrl(key, value)))
44 | .flat();
45 | const xml = `
46 |
47 | ${createUrl()}
48 | ${URLs.join(lineBreak)}
49 |
50 | `;
51 | return xml;
52 | };
53 |
--------------------------------------------------------------------------------
/src/utils/tawk.ts:
--------------------------------------------------------------------------------
1 | import { isSsr } from '../helpers/ssr';
2 | import { UserRole } from '../types/models';
3 |
4 | type Visitor = {
5 | name: string;
6 | email: string;
7 | roles: UserRole[];
8 | };
9 |
10 | export type TawkAPI = {
11 | visitor: Visitor;
12 | onLoad(): void;
13 | addTags(tags: UserRole[], callback?: () => void): void;
14 | };
15 |
16 | function init() {
17 | if (process.env.NODE_ENV === 'development' || isSsr()) {
18 | return;
19 | }
20 | (function() {
21 | const s1 = document.createElement('script');
22 | s1.async = true;
23 | s1.src = 'https://embed.tawk.to/60a117b2185beb22b30dae86/1f5qk953t';
24 | s1.setAttribute('crossorigin', '*');
25 | document.head.prepend(s1);
26 | })();
27 |
28 | window.Tawk_API = window.Tawk_API || {};
29 | window.Tawk_API.onLoad = function() {
30 | window.Tawk_API.addTags(['Mentor', 'User']);
31 | };
32 | }
33 |
34 | export function setVisitor(visitor: Visitor): void {
35 | if (!window.Tawk_API) {
36 | return;
37 | }
38 | window.Tawk_API.visitor = visitor;
39 | }
40 |
41 | init();
42 |
--------------------------------------------------------------------------------
/tsconfig.functions.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2018",
4 | "module": "commonjs",
5 | "outDir": "netlify/functions",
6 | "rootDir": "netlify/functions-src",
7 | "strict": true,
8 | "esModuleInterop": true,
9 | "skipLibCheck": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "lib": ["es2018", "esnext.asynciterable"],
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "experimentalDecorators": true,
15 | "emitDecoratorMetadata": true
16 | },
17 | "include": ["netlify/functions-src/**/*"],
18 | "exclude": ["node_modules", "**/*.test.ts"]
19 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": false,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "preserve",
22 | "incremental": true,
23 | "downlevelIteration": true,
24 | },
25 | "include": [
26 | "src"
27 | ],
28 | "exclude": [
29 | "node_modules"
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------