├── .babelrc ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── main.yml ├── .gitignore ├── .gitpod.yml ├── .nvmrc ├── .prettierignore ├── .prettierrc.json ├── .storybook ├── main.js ├── preview-head.html └── preview.js ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── api-types └── errorCodes.ts ├── babel-plugin-macros.config.js ├── cypress.json ├── cypress ├── builders │ ├── mentorship-requests │ │ └── index.ts │ ├── response.ts │ └── users │ │ └── current │ │ └── get.ts ├── fixtures │ ├── example.json │ ├── favorites │ │ └── get.json │ ├── mentors │ │ └── get.json │ └── mentorship-requests │ │ ├── get.json │ │ └── put.json ├── integration │ ├── components │ │ └── Button.spec.js │ ├── login.spec.js │ ├── me │ │ ├── edit-profile.spec.ts │ │ ├── home.spec.ts │ │ └── mentorship-requests.spec.js │ └── mentors.js ├── plugins │ └── index.js ├── support │ ├── commands.ts │ └── index.ts └── tsconfig.json ├── docs └── auth.md ├── netlify.toml ├── netlify └── functions-src │ ├── functions │ ├── admin │ │ └── delete.ts │ ├── common │ │ ├── auth0.service.ts │ │ ├── dto │ │ │ └── user.dto.ts │ │ └── interfaces │ │ │ └── user.interface.ts │ ├── config │ │ └── index.ts │ ├── data │ │ ├── errors.ts │ │ ├── favorites.ts │ │ ├── mentors.ts │ │ ├── mentorships.ts │ │ ├── types.ts │ │ ├── users.ts │ │ └── utils.ts │ ├── email.ts │ ├── email │ │ ├── client.ts │ │ ├── emails.ts │ │ ├── interfaces │ │ │ └── email.interface.ts │ │ ├── sendgrid.ts │ │ └── templates │ │ │ ├── README.md │ │ │ ├── email-verification.html │ │ │ ├── layout.html │ │ │ ├── mentor-application-admin-notification.html │ │ │ ├── mentor-application-approved.html │ │ │ ├── mentor-application-declined.html │ │ │ ├── mentor-application-received.html │ │ │ ├── mentor-freeze.html │ │ │ ├── mentor-not-active.html │ │ │ ├── mentorship-accepted.html │ │ │ ├── mentorship-cancelled.html │ │ │ ├── mentorship-declined.html │ │ │ ├── mentorship-reminder.html │ │ │ ├── mentorship-requested.html │ │ │ ├── nodemon-emails.json │ │ │ ├── show.js │ │ │ └── welcome.html │ ├── favorites.ts │ ├── hof │ │ ├── withDB.ts │ │ └── withRouter.ts │ ├── interfaces │ │ └── mentorship.ts │ ├── mentors.ts │ ├── mentorships.ts │ ├── modules │ │ ├── favorites │ │ │ ├── get.ts │ │ │ └── post.ts │ │ ├── mentors │ │ │ ├── applications │ │ │ │ ├── get.ts │ │ │ │ ├── post.ts │ │ │ │ └── put.ts │ │ │ ├── get.ts │ │ │ └── types.ts │ │ ├── mentorships │ │ │ ├── apply.ts │ │ │ ├── get-all.ts │ │ │ └── requests.ts │ │ └── users │ │ │ ├── current.ts │ │ │ ├── delete.ts │ │ │ ├── userInfo.ts │ │ │ └── verify.ts │ ├── types │ │ └── index.ts │ ├── users.ts │ └── utils │ │ ├── auth.ts │ │ ├── contactUrl.ts │ │ ├── db.ts │ │ └── response.ts │ ├── lists │ └── dto │ │ └── list.dto.ts │ └── mongo-scripts │ ├── approve-applications.mongodb.js │ ├── create-user.mongodb.js │ ├── delete-mentorship.mongodb.js │ ├── delete-user.mongodb.js │ ├── find-mentorships.mongodb.js │ ├── get-all-users.mongodb.js │ ├── get-mentors.mongodb.js │ ├── get-panding-applications.mongodb.js │ ├── update-mentorship.mongodb.js │ └── update-user.mongodb.js ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages ├── 404.tsx ├── _app.tsx ├── _document.tsx ├── head.tsx ├── index.tsx ├── me │ ├── admin.tsx │ ├── index.tsx │ └── requests.tsx ├── sitemap.xml │ └── index.tsx └── u │ └── [id].tsx ├── public ├── CNAME ├── apple-touch-icon.png ├── browserconfig.xml ├── codingcoach-logo-16.png ├── codingcoach-logo-192.png ├── codingcoach-logo-32.png ├── codingcoach-logo-384.png ├── codingcoach-logo-512.png ├── codingcoach-logo.jpg ├── favicon.ico ├── images │ └── coding-coach-patron-button.jpg ├── manifest.json ├── mstile-150x150.png └── safari-pinned-tab.svg ├── scripts └── ignore-step.sh ├── src ├── Me │ ├── Header │ │ ├── Header.test.tsx │ │ └── Header.tsx │ ├── Main.tsx │ ├── Me.tsx │ ├── MentorshipRequests │ │ ├── MentorshipRequests.js │ │ ├── MentorshipRequests.test.js │ │ ├── ReqContent.js │ │ ├── UsersList.tsx │ │ ├── index.js │ │ └── test-setup.js │ ├── Modals │ │ ├── EditMentorDetails.tsx │ │ ├── MentorshipRequestModals │ │ │ ├── AcceptModal.tsx │ │ │ ├── CancelModal.tsx │ │ │ ├── DeclineModal.tsx │ │ │ ├── MentorshipRequest.js │ │ │ ├── index.ts │ │ │ ├── stories │ │ │ │ ├── AcceptModal.stories.tsx │ │ │ │ ├── CancelModal.stories.tsx │ │ │ │ ├── DeclineModal.stories.tsx │ │ │ │ └── MentorshipRequest.stories.tsx │ │ │ └── style.ts │ │ ├── Modal.tsx │ │ ├── RedirectToGravatar.tsx │ │ └── model.tsx │ ├── Navigation │ │ ├── Navbar.test.tsx │ │ └── Navbar.tsx │ ├── Profile │ │ └── Profile.tsx │ ├── Routes │ │ ├── Admin.tsx │ │ ├── Home.tsx │ │ └── Home │ │ │ └── Avatar │ │ │ └── Avatar.tsx │ ├── components │ │ ├── Button │ │ │ ├── Button.test.tsx │ │ │ ├── Button.tsx │ │ │ ├── IconButton.tsx │ │ │ └── index.ts │ │ ├── Card │ │ │ └── index.tsx │ │ ├── Checkbox │ │ │ ├── Checkbox.tsx │ │ │ └── index.ts │ │ ├── FormField │ │ │ ├── FormField.test.js │ │ │ ├── FormField.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── FormField.test.js.snap │ │ │ ├── formContext.js │ │ │ └── index.js │ │ ├── Input │ │ │ ├── Input.test.tsx │ │ │ ├── Input.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── Input.test.tsx.snap │ │ │ └── index.ts │ │ ├── List │ │ │ ├── List.test.tsx │ │ │ ├── List.tsx │ │ │ ├── ListItem.tsx │ │ │ └── index.ts │ │ ├── RadioButton │ │ │ ├── RadioButton.tsx │ │ │ ├── RadioButtonContext.tsx │ │ │ ├── RadioButtonGroup.test.tsx │ │ │ ├── RadioButtonGroup.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── RadioButtonGroup.test.tsx.snap │ │ │ └── index.ts │ │ ├── RichList │ │ │ ├── ReachItemTypes.d.ts │ │ │ ├── RichItem.test.tsx │ │ │ ├── RichItem.tsx │ │ │ ├── RichList.test.tsx │ │ │ ├── RichList.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── RichItem.test.tsx.snap │ │ │ └── index.ts │ │ ├── Select │ │ │ ├── Select.test.tsx │ │ │ ├── Select.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── Select.test.tsx.snap │ │ │ └── index.ts │ │ └── Textarea │ │ │ ├── Textarea.test.tsx │ │ │ ├── Textarea.tsx │ │ │ ├── __snapshots__ │ │ │ └── Textarea.test.tsx.snap │ │ │ └── index.ts │ └── styles │ │ ├── global.ts │ │ └── shared │ │ └── devices.ts ├── PageNotFound.tsx ├── __tests__ │ ├── App.test.js │ ├── Card.test.js │ └── titleGenerator.spec.js ├── api │ ├── admin.ts │ └── index.ts ├── assets │ ├── 404.svg │ ├── me │ │ ├── camera.svg │ │ ├── close.svg │ │ ├── home.svg │ │ ├── icon-available.svg │ │ ├── icon-country.svg │ │ ├── icon-description.svg │ │ ├── icon-door-exit.svg │ │ ├── icon-email.svg │ │ ├── icon-spokenLanguages.svg │ │ ├── icon-survey.svg │ │ ├── icon-tags.svg │ │ ├── icon-title.svg │ │ ├── icon-unavailable.svg │ │ ├── icon-user-remove.svg │ │ ├── logo.svg │ │ ├── mentee.svg │ │ ├── mentors.svg │ │ └── mentorship.svg │ ├── mentorshipRequestSuccess.svg │ └── powered-by-vercel.svg ├── channelProvider.js ├── components │ ├── AutoComplete │ │ ├── AutoComplete.css │ │ └── AutoComplete.js │ ├── Card │ │ ├── Card.css.ts │ │ ├── Card.tsx │ │ ├── Card.types.ts │ │ └── index.d.ts │ ├── Content │ │ ├── Content.css │ │ └── Content.js │ ├── Filter │ │ ├── Filter.css │ │ └── Filter.js │ ├── FilterClear │ │ ├── FilterClear.css │ │ └── FilterClear.js │ ├── Header │ │ └── Header.js │ ├── Input │ │ ├── Input.css │ │ └── Input.js │ ├── Link │ │ └── Link.tsx │ ├── Loader.tsx │ ├── LoginNavigation │ │ └── LoginNavigation.js │ ├── Logo.js │ ├── MemberArea │ │ ├── EditProfile.css │ │ ├── EditProfile.js │ │ ├── MemberArea.js │ │ ├── PendingApplications.js │ │ └── model.js │ ├── MentorsList │ │ ├── MentorList.css │ │ ├── MentorsList.js │ │ └── Pager.tsx │ ├── MobileNavigation │ │ └── MobileNavigation.js │ ├── Modal │ │ ├── Modal.css │ │ ├── Modal.js │ │ └── ModalContent.js │ ├── Navigation │ │ └── Navigation.js │ ├── Sidebar │ │ └── Sidebar.tsx │ ├── SiteTitle.js │ ├── SocialLinks │ │ ├── SocialLinks.css │ │ └── SocialLinks.js │ ├── Switch │ │ ├── Switch.css │ │ ├── Switch.test.tsx │ │ ├── Switch.tsx │ │ └── __snapshots__ │ │ │ └── Switch.test.tsx.snap │ ├── UserProfile │ │ └── UserProfile.tsx │ ├── common.ts │ └── layouts │ │ └── App │ │ ├── ActionsHandler.tsx │ │ ├── App.css │ │ ├── App.js │ │ ├── VerificationModal.tsx │ │ └── index.tsx ├── config │ ├── constants.ts │ └── experiments.js ├── contents │ ├── codeOfConduct.js │ ├── cookiesPolicy.js │ ├── privacyPolicy.js │ └── termsAndConditions.js ├── context │ ├── apiContext │ │ └── ApiContext.tsx │ ├── authContext │ │ └── AuthContext.tsx │ ├── filtersContext │ │ └── FiltersContext.tsx │ ├── mentorsContext │ │ └── MentorsContext.js │ ├── modalContext │ │ └── ModalContext.tsx │ └── userContext │ │ └── UserContext.tsx ├── external-types.d.ts ├── favoriteManager.js ├── ga.ts ├── helpers │ ├── avatar.js │ ├── getTitleTags.tsx │ ├── languages.ts │ ├── mentorship.ts │ ├── ssr.ts │ ├── time.ts │ ├── user.ts │ └── window.ts ├── hooks │ ├── useDeviceType.ts │ └── useRoutes.ts ├── index.css ├── listsGenerator.js ├── messages.js ├── persistData │ └── index.ts ├── react-app-env.d.ts ├── serviceWorker.js ├── setup-tests.js ├── setupTests.js ├── stories │ ├── StoriesContainer.js │ ├── button.stories.mdx │ ├── checkbox.stories.mdx │ ├── icons.stories.js │ ├── input.stories.mdx │ ├── liststorydata.json │ ├── mentor-card.stories.mdx │ ├── modal.stories.js │ ├── profile-card.stories.js │ ├── profile-list.stories.mdx │ ├── radiobutton.stories.tsx │ ├── select.stories.js │ └── textarea.stories.mdx ├── titleGenerator.js ├── types │ ├── global.d.ts │ └── models.d.ts └── utils │ ├── auth.js │ ├── isDeep.ts │ ├── maskSansitiveString.ts │ ├── overwriteProfileDefaults.ts │ ├── permaLinkService.js │ ├── sitemapGenerator.tsx │ ├── tags.js │ └── tawk.ts ├── tsconfig.functions.json ├── tsconfig.json ├── tsconfig.tsbuildinfo └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": [ 4 | "babel-plugin-macros", 5 | ["babel-plugin-styled-components", { "ssr": true, "displayName": true, "preprocess": false }] 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # codingcoach_io 5 | open_collective: codingcoach-io 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug, help wanted 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS, any] 28 | - Browser [e.g. chrome, safari, any] 29 | 30 | **Smartphone (please complete the following information):** 31 | - Device: [e.g. iPhone6, any] 32 | - OS: [e.g. iOS8.1, any] 33 | - Browser [e.g. stock browser, safari, any] 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Tests 4 | 5 | on: 6 | pull_request: 7 | branches: 8 | - master 9 | push: 10 | branches: 11 | - master 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | build: 16 | 17 | runs-on: ubuntu-latest 18 | 19 | strategy: 20 | matrix: 21 | node-version: [18.x] 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v1 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | - run: yarn 30 | - run: npm run build --if-present 31 | # - run: npm test TODO: Enable tests again 32 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: yarn 3 | command: yarn start 4 | ports: 5 | - port: 3000 6 | onOpen: open-preview 7 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next 2 | build 3 | tsconfig.json 4 | netlify -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "quoteProps": "as-needed" 7 | } 8 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['../src/**/*.stories.@(js|mdx|json|tsx)'], 3 | addons: ['@storybook/preset-create-react-app', '@storybook/addon-essentials'], 4 | typescript: { 5 | reactDocgen: 'react-docgen', 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 8 | 14 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import '../src/index.css'; 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | install: 3 | - yarn 4 | node_js: 5 | - v12.9.1 6 | cache: 7 | directories: 8 | - "$HOME/.yarn-cache" 9 | - "$HOME/travis/.cache/Cypress" 10 | script: 11 | - NODE_ENV=production yarn predeploy 12 | - cypress install 13 | - yarn test 14 | addons: 15 | apt: 16 | packages: 17 | - libgconf-2-4 18 | env: 19 | - PUBLIC_URL=https://mentors.codingcoach.io 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thank you so much for your willing to help! We and the community appreciate you 🖖 2 | 3 | We're using (from certain point, at least) the following tools. Please make sure you're using them too. 4 | 5 | ## Typescript 6 | 7 | [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) 8 | 9 | We still have some `.js` files but please don't add more. 10 | For files with JSX syntax, use `.tsx` files, otherwise, use `.ts`. 11 | 12 | ## Styled Components 💅 13 | 14 | [![style: styled-components](https://img.shields.io/badge/style-%F0%9F%92%85%20styled--components-orange.svg?colorB=daa357&colorA=db748e)](https://github.com/styled-components/styled-components) 15 | We need to migrate some of the existing code. 16 | 17 | Please don't use css in any other kind (global, css modules etc.) 18 | Currently, we don't have common styled-components (components that can be used in more than one component). 19 | If there is a case for this, consult us and we'll decide together. 20 | 21 | 22 | ## Storybook 📖 23 | 24 | [![storybook](https://cdn.jsdelivr.net/gh/storybookjs/brand@master/badge/badge-storybook.svg)](https://mentors.codingcoach.io/sb/) 25 | If you're creating a reusable component (like Card or Modal), please create a storybook for it. 26 | We'll probably mention it in the issue. 27 | 28 | 29 | ## Tests 🕵️‍♂️ 30 | 31 | We're using [cypress](https://www.cypress.io/). The tests are not blocker but please, do your best. 32 | 33 | ## Timing ⏳ 34 | 35 | We all are contributing our time to the community and that's great. In the other hand, we want to deliver the product to our users. 36 | When you ask for a task, please add to the comment, your time estimation and try to make it. 37 | If you see that you can't make it, please let us know so we could assign it to someone else. 38 | 39 | Don't hesitate to ask [Mosh](https://coding-coach.slack.com/team/UFL4N8ETE), [Crysfel](https://coding-coach.slack.com/team/UCQ9GL8UB) or [Brent](UHZHPRDFD) anything and we will do our best to help you. 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Coding-Coach 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /api-types/errorCodes.ts: -------------------------------------------------------------------------------- 1 | export enum ErrorCodes { 2 | EmailNotVerified = 1, 3 | } 4 | -------------------------------------------------------------------------------- /babel-plugin-macros.config.js: -------------------------------------------------------------------------------- 1 | const isDev = process.env.NODE_ENV !== 'production'; 2 | 3 | module.exports = { 4 | styledComponents: { 5 | fileName: isDev, 6 | displayName: isDev, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:3000", 3 | "videoUploadOnPasses": false, 4 | "defaultCommandTimeout": 30000, 5 | "responseTimeout": 30000, 6 | "supportFile": "cypress/support/index.ts" 7 | } 8 | -------------------------------------------------------------------------------- /cypress/builders/mentorship-requests/index.ts: -------------------------------------------------------------------------------- 1 | import { MentorshipRequest } from '../../../src/types/models'; 2 | import { userBuilder } from '../users/current/get'; 3 | 4 | const defaultMentorshipRequest: MentorshipRequest = { 5 | id: '123', 6 | mentor: userBuilder({ 7 | name: 'Mentor', 8 | }), 9 | mentee: userBuilder(), 10 | status: 'Approved', 11 | date: '1609748339000', 12 | message: 'hi', 13 | background: 'yes', 14 | expectation: 'the world', 15 | isMine: false, 16 | }; 17 | 18 | export const mentorshipRequestBuilder = ( 19 | overrides: Partial 20 | ) => ({ 21 | ...defaultMentorshipRequest, 22 | ...overrides, 23 | }); 24 | -------------------------------------------------------------------------------- /cypress/builders/response.ts: -------------------------------------------------------------------------------- 1 | export const withSuccess = (data?: T) => ({ 2 | success: true, 3 | data, 4 | }); 5 | 6 | export const withError = (errors: string[]) => ({ 7 | success: false, 8 | errors, 9 | }); 10 | -------------------------------------------------------------------------------- /cypress/builders/users/current/get.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../../../../src/types/models'; 2 | 3 | const defaults: User = { 4 | _id: '1', 5 | title: 'One of the best', 6 | name: 'Brent M Clark', 7 | email: 'brentmclark@gmail.com', 8 | roles: ['Mentor'], 9 | avatar: 10 | 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC0AAAAtBAMAAADINP+pAAAAG1BMVEXMzMyWlpa+vr6xsbG3t7fFxcWqqqqjo6OcnJzvcxCxAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAANElEQVQ4jWNgGAUjGDCZpcBJZMBsgiCRgat5M4OJAohEBYlCigzhYBINGCcD1YPIUTCsAADs9gb4p4yG2QAAAABJRU5ErkJggg==', 11 | spokenLanguages: ['en', 'he'], 12 | country: 'US', 13 | tags: ['cypress', 'react', 'typescript'], 14 | available: false, 15 | createdAt: '2021-01-01T00:00:00.000Z', 16 | channels: [ 17 | { 18 | id: 'email@codingcoach.io', 19 | type: 'email', 20 | }, 21 | ], 22 | }; 23 | 24 | export const userBuilder = (user: Partial = {}) => { 25 | return { 26 | ...defaults, 27 | ...user, 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /cypress/fixtures/favorites/get.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /cypress/fixtures/mentorship-requests/put.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "mentorship": { 4 | "status": "Viewed" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /cypress/integration/components/Button.spec.js: -------------------------------------------------------------------------------- 1 | const { cy } = global; 2 | 3 | describe('Buttons', () => { 4 | it('renders the primary button', () => { 5 | cy.visit('localhost:6006/iframe.html?id=button--primary'); 6 | cy.get('#root').within(() => { 7 | cy.get('button').should('have.text', 'Primary'); 8 | }); 9 | }); 10 | it('renders the secondary button', () => { 11 | cy.visit('localhost:6006/iframe.html?id=button--secondary'); 12 | cy.get('#root').within(() => { 13 | cy.get('button').should('have.text', 'Secondary'); 14 | }); 15 | }); 16 | it('renders the danger button', () => { 17 | cy.visit('localhost:6006/iframe.html?id=button--danger'); 18 | cy.get('#root').within(() => { 19 | cy.get('button').should('have.text', 'Danger'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /cypress/integration/login.spec.js: -------------------------------------------------------------------------------- 1 | import { withSuccess } from '../builders/response'; 2 | import { userBuilder } from '../builders/users/current/get'; 3 | const { cy } = global; 4 | 5 | describe('login', () => { 6 | it('should be able to login a user', () => { 7 | const user = userBuilder(); 8 | cy.login(); 9 | cy.intercept('GET', '/users/current', withSuccess(user)); 10 | cy.intercept('GET', '/mentors?limit=*', { fixture: 'mentors/get' }); 11 | cy.intercept('GET', `/users/${user._id}/favorites`, { 12 | fixture: 'favorites/get', 13 | }); 14 | cy.visit('/'); 15 | cy.getByTestId('user-avatar').getByAltText('brentmclark@gmail.com').click(); 16 | cy.getByText('Logout').should('have.length', 1); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /cypress/integration/me/edit-profile.spec.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../../../src/types/models'; 2 | import { withSuccess } from '../../builders/response'; 3 | import { userBuilder } from '../../builders/users/current/get'; 4 | 5 | const driverFactory = () => { 6 | let user: Partial = {}; 7 | 8 | return { 9 | given: { 10 | user: (_user: Partial) => { 11 | user = _user; 12 | }, 13 | }, 14 | init() { 15 | cy.intercept('GET', '/users/current', withSuccess(userBuilder(user))); 16 | cy.intercept('PUT', '/users/1', withSuccess()); 17 | cy.login(); 18 | cy.visit('/me'); 19 | openEditModal(); 20 | }, 21 | }; 22 | }; 23 | 24 | const openEditModal = () => { 25 | cy.contains('Edit').click(); 26 | }; 27 | 28 | describe('Me / edit profile', () => { 29 | let driver: ReturnType; 30 | 31 | beforeEach(() => { 32 | driver = driverFactory(); 33 | }); 34 | 35 | it('should form has the user details', () => { 36 | driver.init(); 37 | cy.get('input[name="name"]').should('have.value', 'Brent M Clark'); 38 | }); 39 | 40 | it('should show validation message when missing required fields', () => { 41 | driver.given.user({ title: '' }); 42 | driver.init(); 43 | 44 | cy.contains('button', 'Save').click(); 45 | 46 | cy.get('div').should( 47 | 'include.text', 48 | 'The following fields is missing or invalid' 49 | ); 50 | }); 51 | 52 | it('should show success toast upon successful submit', () => { 53 | driver.init(); 54 | 55 | cy.contains('button', 'Save').click(); 56 | 57 | cy.get('.Toastify').should( 58 | 'include.text', 59 | 'Your details updated successfully' 60 | ); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /cypress/integration/me/home.spec.ts: -------------------------------------------------------------------------------- 1 | import { withSuccess } from '../../builders/response'; 2 | import { userBuilder } from '../../builders/users/current/get'; 3 | 4 | describe('Me / home', () => { 5 | before(() => { 6 | cy.intercept('GET', '/users/current', withSuccess(userBuilder())); 7 | cy.login(); 8 | cy.visit('/me'); 9 | }); 10 | 11 | describe('Avatar', () => { 12 | it('should present user avatar', () => { 13 | cy.get(`img[alt="brentmclark@gmail.com"]`); 14 | }); 15 | 16 | it('should allow the user to click on share and get an input with their profile url', () => { 17 | cy.get('[aria-label="Share your profile"]').click(); 18 | cy.get('input[readonly]').should('have.value', 'http://localhost:3000/u/1'); 19 | }); 20 | }); 21 | 22 | describe('Details', () => { 23 | it('should present user details', () => { 24 | cy.getByTestId('email-label').should( 25 | 'have.text', 26 | 'brentmclark@gmail.com' 27 | ); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | // This function is called when a project is opened or re-opened (e.g. due to 2 | // the project's config changing) 3 | 4 | module.exports = (on, config) => { 5 | // `on` is used to hook into various events Cypress emits 6 | // `config` is the resolved Cypress config 7 | }; 8 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/cypress/add-commands'; 2 | 3 | Cypress.Commands.add('filterByName', name => { 4 | cy.getByTestId('name-filter-autocomplete').type(name); 5 | 6 | cy.get('.ac-menu').click(); 7 | }); 8 | 9 | Cypress.Commands.add('clearNameFilter', () => { 10 | cy.getByText('clear').click(); 11 | }); 12 | 13 | Cypress.Commands.add('login', () => { 14 | window.localStorage.setItem( 15 | 'auth-data', 16 | JSON.stringify({ expiresAt: 1887058578000 }) 17 | ); 18 | }); 19 | 20 | Cypress.Commands.add('getByTestId', function(testId: string) { 21 | return cy.get(`[data-testid="${testId}"]`); 22 | }); 23 | 24 | Cypress.Commands.add('getAllByTestId', function(testId: string) { 25 | return cy.get(`[data-testid="${testId}"]`); 26 | }); 27 | 28 | Cypress.Commands.add('getByAltText', function(alt: string) { 29 | return cy.get(`[alt="${alt}"]`); 30 | }); 31 | 32 | Cypress.Commands.add('getByText', function(text: string) { 33 | return cy.contains(text); 34 | }); 35 | -------------------------------------------------------------------------------- /cypress/support/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import './commands'; 4 | 5 | declare global { 6 | namespace Cypress { 7 | interface Chainable { 8 | login(): Chainable; 9 | getByTestId(testId: string): Chainable; 10 | getByText(text: string): Chainable; 11 | getAllByText(text: string): Chainable; 12 | } 13 | } 14 | } 15 | 16 | let polyfill: string; 17 | 18 | // Polyfill window.fetch because there is no native support from Cypress yet 19 | // Adapted from: https://github.com/cypress-io/cypress/issues/95#issuecomment-517594737 20 | before(() => { 21 | cy.readFile('node_modules/whatwg-fetch/dist/fetch.umd.js').then(contents => { 22 | polyfill = contents; 23 | }); 24 | }); 25 | 26 | // use `Cypress` instead of `cy` so this persists across all tests 27 | Cypress.on('window:before:load', win => { 28 | // @ts-ignore:next-line 29 | delete win.fetch; 30 | win.eval(polyfill); 31 | }); 32 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "isolatedModules": true, 6 | // be explicit about types included 7 | // to avoid clashing with Jest types 8 | "types": ["cypress"] 9 | }, 10 | "include": [ 11 | "../node_modules/cypress", 12 | "./**/*.ts", 13 | "../src/types/models.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /docs/auth.md: -------------------------------------------------------------------------------- 1 | # Authentication System 2 | 3 | We're using Auth0 to handle authentication in our application. This document outlines the authentication flow and how to set up your environment for development. 4 | 5 | ## Registration 6 | 7 | 1. User clicks on the "Sign Up" button. 8 | 2. User is redirected to the Auth0 login page. 9 | 3. User enters their email and password. 10 | 4. Auth0 sends a verification email, which we leverage for the welcome email. 11 | 5. User clicks on the verification link in the email. 12 | 6. User is redirected to the application with a verification token. For more information about the redirection see the [docs](https://auth0.com/docs/customize/email/email-templates#configure-template-fields) - open the "Redirect the URL" section. 13 | > **ℹ️ Info** 14 | > Remember. The application.callback_domain variable will contain the origin of the first URL listed in the application's Allowed Callback URL list 15 | 16 | ## Authentication 17 | 18 | 1. User clicks on the "Log In" button. 19 | 2. User is redirected to the Auth0 login page. 20 | 3. User enters their email and password. 21 | 4. Auth0 verifies the credentials and redirects the user back to the application with an access token which includes auth0id and an `email_verified` and `picture` added by a [custom action](https://manage.auth0.com/dashboard/eu/codingcoach/actions/library/details/e425e5f6-fcd0-4ec9-a3b5-68b3ef8eca75). `email_verified` is enabled by the `email` scope and `picture` is enabled by the `profile` scope. -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "yarn build" 3 | publish = ".next" 4 | 5 | [functions] 6 | directory = "netlify/functions-src/functions" 7 | included_files = ["netlify/functions-src/functions/email/templates/**.html"] 8 | 9 | [dev] 10 | command = "yarn start" 11 | 12 | [build.environment] 13 | NEXT_TELEMETRY_DISABLED = "1" 14 | NODE_VERSION = "18" 15 | 16 | [[plugins]] 17 | package = "@netlify/plugin-nextjs" 18 | -------------------------------------------------------------------------------- /netlify/functions-src/functions/admin/delete.ts: -------------------------------------------------------------------------------- 1 | import Config from '../config'; 2 | 3 | const auth0Domain = Config.auth0.backend.DOMAIN; 4 | 5 | const getAdminAccessToken = async (): Promise => { 6 | try { 7 | 8 | const response = await fetch(`https://${auth0Domain}/oauth/token`, { 9 | method: 'POST', 10 | headers: { 11 | 'Content-Type': 'application/json', 12 | }, 13 | body: JSON.stringify({ 14 | client_id: Config.auth0.backend.CLIENT_ID, 15 | client_secret: Config.auth0.backend.CLIENT_SECRET, 16 | audience: Config.auth0.backend.AUDIENCE, 17 | grant_type: 'client_credentials', 18 | }), 19 | }); 20 | const data = await response.json(); 21 | return data.access_token; 22 | } catch (error) { 23 | // eslint-disable-next-line no-console 24 | console.error('Error fetching admin access token:', error); 25 | throw new Error('Failed to fetch access token'); 26 | } 27 | }; 28 | 29 | 30 | export const deleteUser = async (userId: string): Promise => { 31 | const accessToken = await getAdminAccessToken(); 32 | if (!accessToken) { 33 | throw new Error('Failed to get access token'); 34 | } 35 | 36 | const response = await fetch(`https://${auth0Domain}/api/v2/users/${userId}`, { 37 | method: 'DELETE', 38 | headers: { 39 | Authorization: `Bearer ${accessToken}`, 40 | }, 41 | }); 42 | 43 | if (!response.ok) { 44 | throw new Error(`Failed to delete user: ${response.statusText}`); 45 | } 46 | 47 | return response.json(); 48 | } -------------------------------------------------------------------------------- /netlify/functions-src/functions/common/dto/user.dto.ts: -------------------------------------------------------------------------------- 1 | import type { ObjectId } from 'mongodb' 2 | import { Role } from '../interfaces/user.interface' 3 | 4 | export class UserDto { 5 | _id: ObjectId 6 | auth0Id: string 7 | email: string 8 | name: string 9 | avatar?: string 10 | roles: Role[] 11 | 12 | constructor(partial: Partial) { 13 | Object.assign(this, partial) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /netlify/functions-src/functions/common/interfaces/user.interface.ts: -------------------------------------------------------------------------------- 1 | import type { ObjectId } from 'mongodb' 2 | 3 | export enum ChannelName { 4 | EMAIL = 'email', 5 | SLACK = 'slack', 6 | LINKED = 'linkedin', 7 | FACEBOOK = 'facebook', 8 | TWITTER = 'twitter', 9 | GITHUB = 'github', 10 | WEBSITE = 'website', 11 | } 12 | 13 | export interface Channel extends Document { 14 | readonly type: ChannelName; 15 | readonly id: string; 16 | } 17 | 18 | export interface User { 19 | readonly _id: ObjectId; 20 | available?: boolean; 21 | auth0Id: string; 22 | email: string; 23 | name: string; 24 | avatar?: string; 25 | roles: Role[]; 26 | country?: string; 27 | image?: { 28 | filename: string; 29 | }; 30 | channels?: Channel[]; 31 | createdAt: Date; 32 | spokenLanguages?: string[]; 33 | tags?: string[]; 34 | } 35 | 36 | export type ApplicationUser = User & { 37 | email_verified: boolean; 38 | picture: string; 39 | } 40 | 41 | export enum Role { 42 | ADMIN = 'Admin', 43 | MEMBER = 'Member', 44 | MENTOR = 'Mentor' 45 | } 46 | -------------------------------------------------------------------------------- /netlify/functions-src/functions/config/index.ts: -------------------------------------------------------------------------------- 1 | const config = { 2 | mongo: { 3 | url: process.env.MONGO_DATABASE_URL, 4 | }, 5 | auth0: { 6 | // to decode the token 7 | frontend: { 8 | CLIENT_ID: process.env.AUTH0_FRONTEND_CLIENT_ID, 9 | CLIENT_SECRET: process.env.AUTH0_FRONTEND_CLIENT_SECRET, 10 | DOMAIN: process.env.AUTH0_DOMAIN, 11 | }, 12 | // To get access to auth0 admin features 13 | backend: { 14 | CLIENT_ID: process.env.AUTH0_BACKEND_CLIENT_ID, 15 | CLIENT_SECRET: process.env.AUTH0_BACKEND_CLIENT_SECRET!, 16 | DOMAIN: process.env.AUTH0_DOMAIN!, 17 | AUDIENCE: process.env.AUTH0_AUDIENCE!, 18 | }, 19 | }, 20 | sendGrid: { 21 | API_KEY: process.env.SENDGRID_API_KEY!, 22 | }, 23 | sentry: { 24 | DSN: process.env.SENTRY_DSN, 25 | }, 26 | email: { 27 | FROM: 'Coding Coach ', 28 | }, 29 | files: { 30 | public: process.env.PUBLIC_FOLDER, 31 | avatars: `avatars`, 32 | }, 33 | pagination: { 34 | limit: 20, 35 | }, 36 | urls: { 37 | CLIENT_BASE_URL: process.env.CLIENT_BASE_URL, 38 | }, 39 | }; 40 | 41 | export default config; 42 | -------------------------------------------------------------------------------- /netlify/functions-src/functions/data/errors.ts: -------------------------------------------------------------------------------- 1 | export class DataError extends Error { 2 | constructor(public statusCode: number, public message: string = `Date Error: ${statusCode}`) { 3 | super(message); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /netlify/functions-src/functions/data/favorites.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId } from 'mongodb'; 2 | import { getCollection } from '../utils/db'; 3 | import type { User } from '../common/interfaces/user.interface'; 4 | import { DataError } from './errors'; 5 | 6 | export interface Favorite { 7 | _id: ObjectId; 8 | userId: ObjectId; 9 | mentorId: ObjectId; 10 | createdAt: Date; 11 | } 12 | 13 | export const getFavorites = async (userId: ObjectId): Promise => { 14 | const favoritesCollection = getCollection('favorites'); 15 | const favorites = await favoritesCollection.find( 16 | { userId }, 17 | { projection: { mentorId: 1 } } 18 | ).toArray(); 19 | 20 | return favorites.map(fav => fav.mentorId.toString()); 21 | }; 22 | 23 | export const toggleFavorite = async (userId: ObjectId, mentorId: string): Promise => { 24 | const favoritesCollection = getCollection('favorites'); 25 | const mentorsCollection = getCollection('users'); 26 | 27 | // First, check if mentor exists 28 | const mentor = await mentorsCollection.findOne({ _id: new ObjectId(mentorId) }); 29 | if (!mentor) { 30 | throw new DataError(404, 'Mentor not found'); 31 | } 32 | 33 | // Try to find and remove the favorite 34 | const existingFavorite = await favoritesCollection.findOneAndDelete({ 35 | userId, 36 | mentorId: new ObjectId(mentorId) 37 | }); 38 | 39 | // If no favorite was found, create a new one 40 | if (!existingFavorite) { 41 | const favorite: Favorite = { 42 | _id: new ObjectId(), 43 | userId, 44 | mentorId: new ObjectId(mentorId), 45 | createdAt: new Date() 46 | }; 47 | await favoritesCollection.insertOne(favorite); 48 | } 49 | 50 | // Return updated list of favorites 51 | return getFavorites(userId); 52 | }; -------------------------------------------------------------------------------- /netlify/functions-src/functions/data/types.ts: -------------------------------------------------------------------------------- 1 | import type { OptionalId, WithId } from 'mongodb'; 2 | 3 | export type BaseExistingEntity = WithId<{ 4 | createdAt: Date; 5 | }> 6 | 7 | type UpdateEntityPayload = WithId>; 8 | export type CreateEntityPayload = OptionalId; 9 | export type EntityPayload = CreateEntityPayload | UpdateEntityPayload; 10 | export type CollectionName = 'users' | 'applications' | 'mentorships' | 'favorites'; 11 | 12 | export type UpsertResult = Promise<{ 13 | data: WithId; 14 | isNew: boolean; 15 | }> 16 | -------------------------------------------------------------------------------- /netlify/functions-src/functions/data/utils.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId, type Filter, type MatchKeysAndValues, type OptionalId, type OptionalUnlessRequiredId, type WithId } from 'mongodb'; 2 | import { getCollection } from '../utils/db'; 3 | import type { BaseExistingEntity, CollectionName, EntityPayload, UpsertResult } from './types'; 4 | import { DataError } from './errors'; 5 | 6 | export const upsertEntityByCondition = async >( 7 | collectionName: CollectionName, 8 | condition: Filter, 9 | entity: MatchKeysAndValues, 10 | ): UpsertResult => { 11 | const collection = getCollection(collectionName); 12 | const { value: upsertedEntity, lastErrorObject } = await collection.findOneAndUpdate( 13 | condition, 14 | { $set: entity }, 15 | { upsert: true, returnDocument: 'after', includeResultMetadata: true } 16 | ); 17 | const isNew = lastErrorObject?.updatedExisting === false; 18 | if (!upsertedEntity) { 19 | throw new Error('Failed to upsert application'); 20 | } 21 | return { 22 | data: upsertedEntity, 23 | isNew, 24 | }; 25 | } 26 | 27 | export async function upsertEntity(collectionName: CollectionName, entity: EntityPayload): Promise> { 28 | const collection = getCollection(collectionName); 29 | const { _id: entityId, ...entityData } = entity; 30 | 31 | if (entityId) { 32 | const updatedEntity = await collection.findOneAndUpdate( 33 | { _id: new ObjectId(entityId) as Filter }, 34 | { 35 | $set: entityData as Partial, 36 | }, 37 | { returnDocument: "after" } 38 | ); 39 | if (!updatedEntity) { 40 | throw new DataError(404, `${collectionName}: entitiy id: '${entity._id}' not found`); 41 | } 42 | return updatedEntity; 43 | } else { 44 | const entityPayload = { 45 | ...entity as OptionalUnlessRequiredId, 46 | createdAt: new Date(), 47 | }; 48 | const result = await collection.insertOne(entityPayload); 49 | return { ...entityPayload, _id: result.insertedId } as WithId; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /netlify/functions-src/functions/email.ts: -------------------------------------------------------------------------------- 1 | import type { Handler } from '@netlify/functions'; 2 | import { send } from './email/client'; 3 | 4 | export const handler: Handler = async (event) => { 5 | const { to, subject } = event.queryStringParameters || {}; 6 | 7 | if (!to || !subject) { 8 | return { 9 | statusCode: 400, 10 | body: JSON.stringify({ error: 'Missing required fields' }), 11 | }; 12 | } 13 | 14 | try { 15 | await send({ to, subject, name: 'welcome', data: { name: 'John Doe' } }); 16 | 17 | return { 18 | statusCode: 200, 19 | body: JSON.stringify({ message: 'Email sent successfully' }), 20 | }; 21 | } catch (error) { 22 | // eslint-disable-next-line no-console 23 | console.error('Error sending email:', error); 24 | return { 25 | statusCode: 500, 26 | body: JSON.stringify({ error: 'Failed to send email' }), 27 | }; 28 | } 29 | } -------------------------------------------------------------------------------- /netlify/functions-src/functions/email/client.ts: -------------------------------------------------------------------------------- 1 | import { promises } from 'fs'; 2 | import path from 'path'; 3 | import { compile } from 'ejs'; 4 | import type { EmailParams } from './interfaces/email.interface'; 5 | import { sendEmail } from './sendgrid'; 6 | 7 | const SYSTEM_DATA = { 8 | baseUrl: process.env.CLIENT_BASE_URL, 9 | } 10 | 11 | export const send = async (params: EmailParams) => { 12 | const { to, subject, data = {}, name } = params; 13 | 14 | const content = await injectData(name, data); 15 | try { 16 | return sendEmail({ 17 | to: to as string, 18 | subject, 19 | html: content, 20 | }); 21 | } catch (error) { 22 | // eslint-disable-next-line no-console 23 | console.error('Send email error', params, JSON.stringify(error, null, 2)); 24 | throw new Error('Failed to send email'); 25 | } 26 | } 27 | 28 | const injectData = async (name: string, data: Record) => { 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 | 12 | 13 | 14 |
6 | Illustration 11 |
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 | 12 | 13 | 14 |
6 | Illustration 11 |
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 | 12 | 13 | 14 |
6 | Illustration 11 |
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 | 36 | 37 | 45 | 46 |
38 | Fix my profile 44 |
47 |
48 | -------------------------------------------------------------------------------- /netlify/functions-src/functions/email/templates/mentor-application-received.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 12 | 13 | 14 |
6 | Illustration 11 |
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 | 31 | 32 | 40 | 41 |
33 | View mentorship guidelines 39 |
42 |
43 | -------------------------------------------------------------------------------- /netlify/functions-src/functions/email/templates/mentorship-accepted.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 12 | 13 | 14 |
6 | Illustration 11 |
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 | 12 | 13 | 14 |
6 | Illustration 11 |
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 | 12 | 13 | 14 |
6 | Illustration 11 |
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 | 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 |