├── .eslintrc
├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ ├── add-edit-team-meeting-time.md
│ ├── add-remove-pm-admin-access.md
│ ├── address-warnings-for-rule-.md
│ ├── blank-issue.md
│ ├── bug-report.yml
│ └── update-package---name-of-package-.md
├── pull_request_template.md
└── workflows
│ ├── all-PRs.yaml
│ ├── all-merges.yaml
│ ├── aws-backend-deploy.yml
│ ├── aws-frontend-deploy.yml
│ ├── pr-instructions.yml
│ ├── waiting-to-merge.yaml
│ └── wr-pr-instructions.yml
├── .gitignore
├── .nvmrc
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── backend
├── .dockerignore
├── .env.example
├── .eslintrc.json
├── .prettierrc
├── Dockerfile.api
├── Dockerfile.dev
├── Dockerfile.prod
├── README.md
├── app.js
├── config
│ ├── auth.config.js
│ ├── auth.config.test.js
│ ├── database.config.js
│ └── index.js
├── controllers
│ ├── email.controller.js
│ ├── email.controller.test.js
│ ├── event.controller.js
│ ├── event.controller.test.js
│ ├── healthCheck.controller.js
│ ├── index.js
│ ├── project.controller.js
│ ├── project.controller.test.js
│ ├── recurringEvent.controller.js
│ ├── user.controller.js
│ └── user.controller.test.js
├── globalConfig.json
├── jest.config.js
├── jest.setup.js
├── middleware
│ ├── auth.middleware.js
│ ├── errorhandler.middleware.js
│ ├── index.js
│ ├── token.middleware.js
│ └── user.middleware.js
├── models
│ ├── checkIn.model.js
│ ├── event.model.js
│ ├── index.js
│ ├── project.model.js
│ ├── projectTeamMember.model.js
│ ├── question.model.js
│ ├── recurringEvent.model.js
│ ├── role.model.js
│ ├── timeTracker.model.js
│ └── user.model.js
├── package.json
├── routers
│ ├── auth.router.js
│ ├── checkIns.router.js
│ ├── checkUser.router.js
│ ├── checkUser.router.test.js
│ ├── events.router.js
│ ├── grantpermission.router.js
│ ├── healthCheck.router.js
│ ├── projectTeamMembers.router.js
│ ├── projects.router.js
│ ├── questions.router.js
│ ├── recurringEvents.router.js
│ ├── slack.router.js
│ ├── success.router.js
│ ├── users.router.js
│ └── users.router.test.js
├── server.js
├── setup-test.js
├── test
│ └── old-tests
│ │ ├── auth.router.test.js
│ │ ├── events.router.test.js
│ │ ├── projects.router.test.js
│ │ └── users.router.test.js
├── validators
│ ├── index.js
│ └── user.api.validator.js
├── workers
│ ├── closeCheckins.js
│ ├── createRecurringEvents.js
│ ├── createRecurringEvents.test.js
│ ├── lib
│ │ └── generateEventData.js
│ ├── openCheckins.js
│ └── slackbot.js
└── yarn.lock
├── client
├── .dockerignore
├── .env.example
├── .eslintrc.json
├── .gitignore
├── .prettierrc.json
├── Dockerfile.client
├── Dockerfile.dev
├── Dockerfile.prod
├── index.html
├── nginx
│ └── default.conf
├── package.json
├── public
│ ├── bg-image-pier.webp
│ ├── bg-image-skyline.webp
│ ├── bg-image-sunset.webp
│ ├── favicon.ico
│ ├── hflalogo.png
│ ├── logo180.png
│ ├── logo192.png
│ ├── logo310.png
│ ├── manifest.json
│ ├── projectleaderdashboard
│ │ ├── check.png
│ │ ├── github.png
│ │ ├── googledrive.png
│ │ └── slack.png
│ └── robots.txt
├── src
│ ├── App.jsx
│ ├── App.scss
│ ├── api
│ │ ├── EventsApiService.js
│ │ ├── ProjectApiService.js
│ │ ├── RecurringEventsApiService.js
│ │ ├── UserApiService.js
│ │ └── auth.js
│ ├── common
│ │ ├── datepicker
│ │ │ └── index.scss
│ │ └── tabs
│ │ │ ├── index.jsx
│ │ │ ├── index.scss
│ │ │ └── tab.jsx
│ ├── components
│ │ ├── ChangesModal.jsx
│ │ ├── DashboardUsers.jsx
│ │ ├── ErrorContainer.jsx
│ │ ├── Footer.jsx
│ │ ├── Form.jsx
│ │ ├── Header.jsx
│ │ ├── Leaderboard.jsx
│ │ ├── Leaderboard.test.jsx
│ │ ├── Navbar.jsx
│ │ ├── ProjectForm.jsx
│ │ ├── ReadyEvents.jsx
│ │ ├── __snapshots__
│ │ │ ├── Leaderboard.test.js.snap
│ │ │ └── Leaderboard.test.jsx.snap
│ │ ├── admin
│ │ │ ├── dashboard
│ │ │ │ ├── index.jsx
│ │ │ │ └── index.scss
│ │ │ ├── donutChart.jsx
│ │ │ ├── donutChartContainer.jsx
│ │ │ ├── donutChartLoading.jsx
│ │ │ ├── eventOverview.jsx
│ │ │ └── reports
│ │ │ │ ├── index.jsx
│ │ │ │ └── index.scss
│ │ ├── auth
│ │ │ ├── Auth.jsx
│ │ │ └── HandleAuth.jsx
│ │ ├── dashboard
│ │ │ ├── AddTeamMember.jsx
│ │ │ ├── AttendeeTable.jsx
│ │ │ ├── AttendeeTableRow.jsx
│ │ │ ├── DashboardButton.jsx
│ │ │ ├── ProjectInfo.jsx
│ │ │ ├── RosterTable.jsx
│ │ │ └── RosterTableRow.jsx
│ │ ├── data.js
│ │ ├── manageProjects
│ │ │ ├── addProject.jsx
│ │ │ ├── createNewEvent.jsx
│ │ │ ├── editMeetingTimes.jsx
│ │ │ ├── editProject.jsx
│ │ │ ├── editableField.jsx
│ │ │ ├── editableMeeting.jsx
│ │ │ ├── eventForm.jsx
│ │ │ ├── selectProject.jsx
│ │ │ └── utilities
│ │ │ │ ├── addDurationToTime.js
│ │ │ │ ├── findNextDayOccuranceOfDay.js
│ │ │ │ ├── readableEvent.js
│ │ │ │ ├── tests
│ │ │ │ └── addDurationToTime.test.js
│ │ │ │ ├── timeConvertFromForm.js
│ │ │ │ ├── validateEditableField.js
│ │ │ │ └── validateEventForm.js
│ │ ├── parts
│ │ │ ├── boxes
│ │ │ │ └── TitledBox.jsx
│ │ │ └── form
│ │ │ │ └── ValidatedTextField.jsx
│ │ ├── presentational
│ │ │ ├── CheckInButtons.jsx
│ │ │ ├── CreateNewProfileButton.jsx
│ │ │ ├── DashboardReport.jsx
│ │ │ ├── newUserForm.jsx
│ │ │ ├── profile
│ │ │ │ ├── ProfileOption.jsx
│ │ │ │ ├── UserEvents.jsx
│ │ │ │ ├── UserTable.jsx
│ │ │ │ └── UserTeams.jsx
│ │ │ ├── projectDashboardContainer.jsx
│ │ │ ├── returnUserForm.jsx
│ │ │ └── upcomingEvent.jsx
│ │ └── user-admin
│ │ │ ├── AddNewProject.jsx
│ │ │ ├── EditUsers.jsx
│ │ │ ├── UserManagement.jsx
│ │ │ └── UserPermissionSearch.jsx
│ ├── context
│ │ ├── authContext.jsx
│ │ ├── snackbarContext.jsx
│ │ └── userContext.jsx
│ ├── fonts
│ │ ├── aliseo-noncommercial-webfont.woff
│ │ └── aliseo-noncommercial-webfont.woff2
│ ├── hooks
│ │ ├── useAuth.js
│ │ └── withAuth.jsx
│ ├── index.jsx
│ ├── index.scss
│ ├── logo.svg
│ ├── pages
│ │ ├── CheckInForm.jsx
│ │ ├── EmailSent.jsx
│ │ ├── Event.jsx
│ │ ├── Events.jsx
│ │ ├── HealthCheck.jsx
│ │ ├── Home.jsx
│ │ ├── ManageProjects.jsx
│ │ ├── NewUser.jsx
│ │ ├── ProjectLeaderDashboard.jsx
│ │ ├── ProjectList.jsx
│ │ ├── ReturningUser.jsx
│ │ ├── SecretPassword.jsx
│ │ ├── Success.jsx
│ │ ├── UserAdmin.jsx
│ │ ├── UserDashboard.jsx
│ │ ├── UserPermission.jsx
│ │ ├── UserPermissionSearch.jsx
│ │ ├── UserProfile.jsx
│ │ ├── UserWelcome.jsx
│ │ └── Users.jsx
│ ├── sass
│ │ ├── AddNew.scss
│ │ ├── AddTeamMember.scss
│ │ ├── AdminLogin.scss
│ │ ├── CheckIn.scss
│ │ ├── Dashboard.scss
│ │ ├── DashboardUsers.scss
│ │ ├── ErrorContainer.scss
│ │ ├── Event.scss
│ │ ├── Events.scss
│ │ ├── Footer.scss
│ │ ├── Form.scss
│ │ ├── Headers.scss
│ │ ├── Home.scss
│ │ ├── MagicLink.scss
│ │ ├── ManageProjects.scss
│ │ ├── Navbar.scss
│ │ ├── ProjectLeaderDashboard.module.scss
│ │ ├── ReadyEvents.scss
│ │ ├── UserAdmin.scss
│ │ ├── UserProfile.scss
│ │ └── Users.scss
│ ├── serviceWorker.js
│ ├── services
│ │ ├── projectTeamMember-api-service.js
│ │ └── user.service.js
│ ├── setupProxy.js
│ ├── svg
│ │ ├── 22.gif
│ │ ├── Icon_Clock.svg
│ │ ├── Icon_Edit.svg
│ │ ├── Icon_Location.svg
│ │ ├── Icon_Plus.svg
│ │ ├── PlusIcon.svg
│ │ ├── hflalogo.png
│ │ ├── hflalogo.svg
│ │ └── hflalogo_white.png
│ ├── theme
│ │ ├── index.js
│ │ └── palette.js
│ └── utils
│ │ ├── authUtils.js
│ │ ├── blacklist.js
│ │ ├── createClockHours.js
│ │ ├── endpoints.js
│ │ ├── globalSettings.js
│ │ └── stringUtils.js
├── vite.config.mjs
└── yarn.lock
├── cypress.json
├── cypress
├── fixtures
│ └── example.json
├── integration
│ ├── admin_login.js
│ └── home_page.js
├── plugins
│ └── index.js
└── support
│ ├── commands.js
│ └── index.js
├── docker-compose.yml
├── github-actions
└── pr-instructions
│ ├── create-instruction.js
│ ├── post-comment.js
│ └── pr-instructions-template.md
├── nginx
├── .dockerignore
├── Dockerfile.nginx
└── default.conf
├── package.json
├── project-edit-info.md
├── utils
└── local_db_backup.sh
├── vrms.md
└── yarn.lock
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@babel/eslint-parser",
3 | "parserOptions": { "requireConfigFile": "false" },
4 | "babelOptions": { "configFile": "./.babelrc" }
5 | }
6 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and handle it automatically
2 | * text=auto
3 |
4 | # Perform LF normalization for .scss files due to resolve-url-loader issue
5 | *.scss text eol=lf
6 | *.scss text eol=lf
7 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/add-edit-team-meeting-time.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Add/edit team meeting time
3 | about: Use this template to request a meeting time change for your project
4 | title: Meeting time change request for [Project Name]
5 | labels: '1 week change request, role: Product, time-sensitive'
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### Overview
11 | We need to add/change the team meeting time for [YOUR PROJECT NAME HERE] so it will appear correctly on hackforla.org
12 |
13 | ### Is your team meeting team already displayed on hackforla.org?
14 | - [ ] yes
15 | - [ ] no
16 |
17 | ### List all of your meeting times (PST time ONLY) (Name of project, name of team meeting, day of week, start time am/pm, duration):
18 | EXAMPLE:
19 | - Home Unite US, Management, Saturday 12:30pm, 1 hour
20 | - Home Unite US, Team meeting, Tuesday 6pm, 2 hours
21 |
22 | Note: Please explicitly state if a meeting time that is currently displayed is to be deleted.
23 |
24 | ### What is your project page URL?
25 | https://www.hackforla.org/projects/vrms
26 |
27 | ### Who should we contact on your team (via Slack) for user acceptance testing
28 | Slack Channel #name:
29 | Slack @handle:
30 |
31 | ### Instructions
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/add-remove-pm-admin-access.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Add/Remove PM Admin Access
3 | about: VRMS admin team members are currently adding or removing product managers manually.
4 | This issue is how PMs can request changes to access on VRMS.
5 | title: 'Add/Remove PM Admin Access: [name of project]'
6 | labels: 'p-feature: Add/Remove PM access, role: Product, time-sensitive'
7 | assignees: ''
8 |
9 | ---
10 |
11 | Person who is staying on the team or leaving a team, person adds ticket.
12 |
13 | ### Required information
14 | - Name of PM requesting changes (your name):
15 | - Slack handle:
16 |
17 | #### People you are removing
18 | - Name:
19 | - Slack Handle:
20 | - Email:
21 |
22 | #### People you are adding
23 | - Name:
24 | - Slack Handle:
25 | - Email:
26 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/address-warnings-for-rule-.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 'Address warnings for rule:'
3 | about: Describe this issue template's purpose here.
4 | title: 'Address warnings for rule:'
5 | labels: 'role: Front End, size: 1pt'
6 | assignees: ''
7 |
8 | ---
9 |
10 | # v0.4 Setup Infrastructure
11 |
12 | This ticket addresses eslint warnings for rules copied from the v0.3 branch.
13 |
14 | # Instructions
15 | - [ ] Review the rule documentation for the rule (see reference pages below)
16 | - [ ] Navigate the cli to the `client` folder
17 | - [ ] Run the lint script (e.g. `npm run lint`)
18 | - [ ] Review the results, noting all instances where the rule is referenced.
19 | - [ ] Address each warning using one of the following options, according to your best judgement:
20 | - use the `npx eslint --fix --rule {rule to fix}` command (not available for all rules)
21 | - correct the code according to the recommendations listed in the rule documentation
22 | - disable the rule for the reported line (NOTE: only use the disable instruction for a single line. Do NOT use the more global options). Include a comment for why this decision was made.
23 | - raise the rule as a topic to discuss with the group in the next meeting. We can then decide whether to edit or drop the rule, add more global disable flags, etc.
24 |
25 | # Acceptance Criteria
26 | Running the lint script should result in an output that does not mention this rule (meaning all instances either corrected or marked for ignore).
27 |
28 | # Reference
29 | - [eslint rules](https://eslint.org/docs/rules/)
30 | - [eslint cli reference](https://eslint.org/docs/user-guide/command-line-interface)
31 | - [jest plugin](https://github.com/jest-community/eslint-plugin-jest#readme)
32 | - [prettier plugin](https://github.com/prettier/eslint-config-prettier)
33 | - [Air Bnb plugin](https://github.com/airbnb/javascript)
34 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/blank-issue.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Blank Issue
3 | about: Consistent formatting make Issues concise and easy to navigate
4 | title: ''
5 | labels: 'feature: missing, milestone: missing, role: Missing, size: missing'
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### Overview
11 | REPLACE THIS TEXT - Text here that clearly states the purpose of this issue in 2 sentences or less.
12 |
13 | ### Action Items
14 | REPLACE THIS TEXT - If this is the beginning of the task this is most likely something to be researched and documented.
15 |
16 | REPLACE THIS TEXT - If the issue has already been researched, and the course of action is clear, this will describe the steps. However, if the steps can be divided into tasks for more than one person, we recommend dividing it up into separate issues or assigning it as a pair programming task.
17 |
18 | ### Resources/Instructions
19 | REPLACE THIS TEXT - If there is a website that has documentation that helps with this issue provide the link(s) here.
20 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-report.yml:
--------------------------------------------------------------------------------
1 | name: 'Bug Report'
2 | description: 'Bug report HackforLA issue form.'
3 |
4 | body:
5 | - type: textarea
6 | id: description
7 | attributes:
8 | label: Describe the Bug
9 | description: Clearly state the bug
10 | validations:
11 | required: true
12 | - type: dropdown
13 | id: feature
14 | attributes:
15 | label: Feature
16 | description: Select a feature related to the bug
17 | options:
18 | - AWS
19 | - Brand
20 | - Login
21 | - Users
22 | - Agenda
23 | - Check in
24 | - Database
25 | - Dashboard
26 | - Marketing
27 | - Onboarding
28 | - Repo Update
29 | - Documentation
30 | - GitHub Actions
31 | - GitHub Hygiene
32 | - Infrastructure
33 | - Package Update
34 | - ESLint Warnings
35 | - Form validation
36 | - Recurring Events
37 | - Project Management
38 | - Account Setup Automation
39 | - Meeting Time Change Ticket
40 | - Add/Remove PM access
41 | validations:
42 | required: true
43 | - type: textarea
44 | id: replicate
45 | attributes:
46 | label: How to Replicate
47 | description: Clearly state how to replicate the bug
48 | validations:
49 | required: true
50 | - type: dropdown
51 | id: notification
52 | attributes:
53 | label: Request notification after bug squashing
54 | description: Request notification after a bug is squashed
55 | options:
56 | - Yes
57 | - No
58 | validations:
59 | required: true
60 | - type: textarea
61 | id: resource
62 | attributes:
63 | label: Resources/Information
64 | description: Include any important resources/information
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/update-package---name-of-package-.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 'Update package: [name of package]'
3 | about: Describe this issue template's purpose here.
4 | title: 'Update package: name of package'
5 | labels: 'housekeeping, role: Front End, size: 1pt'
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### Overview
11 | We need to update the [name of package] package that require major version changes to address security audit warnings
12 |
13 | ### Extra info
14 | We are doing this individually to manage scope of changes per PR
15 |
16 | ### Action Items
17 | - [ ] developer to update to the latest version of the package indicated
18 | - [ ] Ensure the project builds and runs
19 | - [ ] Ensure that all unit tests pass
20 | - [ ] Ensure that there are no lint errors
21 |
22 | ### Resources/Instructions
23 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | Fixes #replace_this_text_with_the_issue_number
2 |
3 | ### What changes did you make and why did you make them ?
4 |
5 | -
6 | -
7 | -
8 |
9 | ### Screenshots of Proposed Changes Of The Website (if any, please do not screen shot code changes)
10 |
11 |
12 |
13 |
14 | Visuals before changes are applied
15 |
16 | 
17 |
18 |
19 |
20 |
21 | Visuals after changes are applied
22 |
23 | 
24 |
25 |
26 |
--------------------------------------------------------------------------------
/.github/workflows/all-merges.yaml:
--------------------------------------------------------------------------------
1 | name: Build VMRS App
2 | on:
3 | pull_request:
4 | branches: [development]
5 | paths-ignore:
6 | - "*.md"
7 |
8 | jobs:
9 | push-docker-container:
10 | runs-on: ubuntu-latest
11 | if: github.event.pull_request.merged == true
12 | steps:
13 | - uses: actions/checkout@v4
14 | - name: Set up QEMU
15 | uses: docker/setup-qemu-action@v1
16 | - name: Set up Docker Buildx
17 | uses: docker/setup-buildx-action@v1
18 | - name: Login to DockerHub
19 | uses: docker/login-action@v1
20 | with:
21 | username: ${{ secrets.DOCKERHUB_USERNAME }}
22 | password: ${{ secrets.DOCKERHUB_TOKEN }}
23 | - name: Build and push client
24 | id: client_build
25 | uses: docker/build-push-action@v2
26 | with:
27 | context: ./client
28 | file: ./client/Dockerfile.client
29 | platforms: linux/amd64
30 | target: client-production
31 | stdin_open: true
32 | push: true
33 | tags: |
34 | vrmsdeploy/vrms:client
35 | - name: Client digest
36 | run: echo ${{ steps.client_build.outputs.digest }}
37 | - name: Build and push backend
38 | id: backend_build
39 | uses: docker/build-push-action@v2
40 | with:
41 | context: ./backend
42 | file: ./backend/Dockerfile.api
43 | platforms: linux/amd64
44 | target: api-production
45 | stdin_open: true
46 | push: true
47 | tags: |
48 | vrmsdeploy/vrms:backend
49 | - name: Backend digest
50 | run: echo ${{ steps.backend_build.outputs.digest }}
51 | - name: Build and push nginx
52 | uses: docker/build-push-action@v2
53 | id: nginx_build
54 | with:
55 | context: ./nginx
56 | file: ./nginx/Dockerfile.nginx
57 | platforms: linux/amd64
58 | push: true
59 | tags: |
60 | vrmsdeploy/vrms:nginx
61 | - name: Nginx digest
62 | run: echo ${{ steps.nginx_build.outputs.digest }}
63 |
--------------------------------------------------------------------------------
/.github/workflows/pr-instructions.yml:
--------------------------------------------------------------------------------
1 | name: Add Pull Request Instructions
2 | on:
3 | pull_request:
4 | types: [opened]
5 | branches:
6 | - 'development'
7 |
8 | jobs:
9 | Add-Pull-Request-Instructions:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v4
13 |
14 | # Create the message to post
15 | - name: Create Instruction
16 | uses: actions/github-script@v4
17 | id: instruction
18 | with:
19 | script: |
20 | const script = require('./github-actions/pr-instructions/create-instruction.js')
21 | const instruction = script({g: github, c: context})
22 | return JSON.stringify({ instruction: instruction, issueNum: context.payload.number })
23 |
24 | # Create an artifact with the message
25 | - name: Create Artifacts
26 | run: |
27 | mkdir -p addingPrInstructions/artifact
28 | echo ${{ steps.instruction.outputs.result }} > addingPrInstructions/artifact/artifact.txt
29 |
30 | - name: Upload Artifacts
31 | uses: actions/upload-artifact@v3
32 | with:
33 | name: adding-pr-instructions-artifact
34 | path: addingPrInstructions/artifact/
35 |
--------------------------------------------------------------------------------
/.github/workflows/waiting-to-merge.yaml:
--------------------------------------------------------------------------------
1 | name: Waiting to Merge
2 |
3 | on:
4 | pull_request:
5 | types: [synchronize, opened, reopened, labeled, unlabeled]
6 |
7 | jobs:
8 | do-not-merge:
9 | if: ${{ contains(github.event.*.labels.*.name, 'waiting to merge') }}
10 | name: Prevent Merging
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Check for label
14 | run: |
15 | echo "Pull request is labeled as 'waiting to merge'"
16 | echo "This workflow fails so that the pull request cannot be merged"
17 | exit 1
18 |
--------------------------------------------------------------------------------
/.github/workflows/wr-pr-instructions.yml:
--------------------------------------------------------------------------------
1 | name: WR Add Pull Request Instructions
2 | on:
3 | workflow_run:
4 | workflows: ["Add Pull Request Instructions"]
5 | types: [completed]
6 |
7 | jobs:
8 | Last-Workflow-Success:
9 | runs-on: ubuntu-latest
10 | if: ${{ github.event.workflow_run.conclusion == 'success' }}
11 | steps:
12 | - name: Download artifact
13 | uses: actions/github-script@v4
14 | with:
15 | script: |
16 | // Retrieve metadata about the artifacts of the last workflow
17 | // https://octokit.github.io/rest.js/v18#actions-list-workflow-run-artifacts
18 | const artifacts = await github.actions.listWorkflowRunArtifacts({
19 | owner: context.repo.owner,
20 | repo: context.repo.repo,
21 | run_id: context.payload.workflow_run.id,
22 | });
23 | const artifactData = artifacts.data.artifacts[0]
24 |
25 | // Download artifact with GET API
26 | // https://octokit.github.io/rest.js/v18#actions-download-artifact
27 | var download = await github.actions.downloadArtifact({
28 | owner: context.repo.owner,
29 | repo: context.repo.repo,
30 | artifact_id: artifactData.id,
31 | archive_format: 'zip',
32 | });
33 | const fs = require('fs');
34 | fs.writeFileSync('${{github.workspace}}/artifact.zip', Buffer.from(download.data));
35 | - run: unzip artifact.zip
36 |
37 | - uses: actions/github-script@v4
38 | id: artifact
39 | with:
40 | script: |
41 | // Retrieve pull request and issue number from downloaded artifact
42 | const fs = require('fs')
43 | const artifact = fs.readFileSync('artifact.txt')
44 | const artifactJSON = JSON.parse(artifact);
45 | return artifactJSON
46 |
47 | - uses: actions/checkout@v4
48 | # Create the message to post
49 | - name: Post Comment
50 | uses: actions/github-script@v4
51 | with:
52 | script: |
53 | const artifact = ${{ steps.artifact.outputs.result }};
54 |
55 | const script = require('./github-actions/pr-instructions/post-comment.js')
56 | script({g: github, c: context}, artifact)
57 |
58 | Last-Workflow-Failure:
59 | runs-on: ubuntu-latest
60 | if: ${{ github.event.workflow_run.conclusion == 'failure' }}
61 | steps:
62 | - name: Failed Run
63 | run: echo "The previous GitHub Action failed. Please check the logs for the previous action."
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | node_modules/
3 | npm-debug.log
4 | .DS_Store
5 | /*.env
6 | client/.env
7 | .env
8 | /.idea
9 | test.db
10 | videos/
11 | screenshots/
12 | vrms.code-workspace
13 | .vscode/
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 18
2 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | VRMS is governed by the [Hack for LA Code of Conduct](https://www.hackforla.org/code-of-conduct/) which applies to any interaction on our VRMS slack channel (inside the HackforLA Slack workspace), direct slack messages, github org or repository, or any other communication medium.
2 |
--------------------------------------------------------------------------------
/backend/.dockerignore:
--------------------------------------------------------------------------------
1 | .git
2 | .dockerignore
3 | .npm
4 | docker-compose*.yml
5 | npm-debug.log*
6 | Dockerfile.*
7 | node_modules
8 | *.sh
--------------------------------------------------------------------------------
/backend/.env.example:
--------------------------------------------------------------------------------
1 | # Contact the VRMS team for the below values
2 | DATABASE_URL=
3 | CUSTOM_REQUEST_HEADER=
4 | SLACK_OAUTH_TOKEN=
5 | SLACK_BOT_TOKEN=
6 | SLACK_TEAM_ID=
7 | SLACK_CHANNEL_ID=
8 | SLACK_CLIENT_ID=
9 | SLACK_CLIENT_SECRET=
10 | SLACK_SIGNING_SECRET=
11 | BACKEND_PORT=4000
12 | REACT_APP_PROXY=http://localhost:${BACKEND_PORT}
13 | GMAIL_CLIENT_ID=
14 | GMAIL_SECRET_ID=
15 | GMAIL_REFRESH_TOKEN=
16 | GMAIL_EMAIL=vrms.signup@gmail.com
17 | MAILHOG_PORT=1025
18 | MAILHOG_USER=user
19 | MAILHOG_PASSWORD=
20 | NODE_ENV=test
21 |
--------------------------------------------------------------------------------
/backend/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@babel/eslint-parser",
3 | "parserOptions": {
4 | "sourceType": "module",
5 | "allowImportExportEverywhere": false,
6 | "codeFrame": false
7 | },
8 | "plugins": ["@babel"],
9 | "extends": ["airbnb", "prettier"],
10 | "env": {
11 | "browser": true,
12 | "jest": true
13 | },
14 | "rules": {
15 | "max-len": ["error", { "code": 100 }],
16 | "prefer-promise-reject-errors": ["off"],
17 | "react/jsx-filename-extension": ["off"],
18 | "react/prop-types": ["warn"],
19 | "no-return-assign": ["off"]
20 | },
21 | "settings": {
22 | "react": {
23 | "version": "999.999.999"
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/backend/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "trailingComma": "all",
4 | "tabWidth": 2,
5 | "semi": true,
6 | "singleQuote": true
7 | }
8 |
--------------------------------------------------------------------------------
/backend/Dockerfile.api:
--------------------------------------------------------------------------------
1 | FROM node:18.12.0 AS api-development
2 | RUN mkdir /srv/backend
3 | WORKDIR /srv/backend
4 | RUN mkdir -p node_modules
5 | COPY package.json yarn.lock ./
6 | RUN yarn install --pure-lockfile
7 | COPY . .
8 |
9 | FROM node:18.12.0 AS api-test
10 | RUN mkdir /srv/backend
11 | WORKDIR /srv/backend
12 | COPY package.json yarn.lock ./
13 | RUN yarn install --silent
14 | RUN mkdir -p node_modules
15 |
16 | FROM node:18.12.0-slim AS api-production
17 | EXPOSE 4000
18 | USER node
19 | WORKDIR /srv/backend
20 | COPY --from=api-development /srv/backend/node_modules ./node_modules
21 | COPY . .
22 | CMD ["npm", "run", "dev"]
23 |
24 |
--------------------------------------------------------------------------------
/backend/Dockerfile.dev:
--------------------------------------------------------------------------------
1 | FROM node:18.12.0 AS api-development
2 | RUN mkdir /srv/backend
3 | WORKDIR /srv/backend
4 | RUN mkdir -p node_modules
5 | COPY package.json yarn.lock ./
6 | RUN yarn install --pure-lockfile
7 | COPY . .
8 |
--------------------------------------------------------------------------------
/backend/Dockerfile.prod:
--------------------------------------------------------------------------------
1 | FROM node:18.12.0 AS api-builder
2 | RUN mkdir /srv/backend
3 | WORKDIR /srv/backend
4 | RUN mkdir -p node_modules
5 | COPY package.json yarn.lock ./
6 | RUN yarn install --pure-lockfile
7 | COPY . .
8 |
9 | FROM node:18.12.0-slim AS api-production
10 | EXPOSE 4000
11 | USER node
12 | WORKDIR /srv/backend
13 | COPY --from=api-builder /srv/backend/node_modules ./node_modules
14 | COPY . .
15 | CMD ["npm", "run", "start"]
16 |
--------------------------------------------------------------------------------
/backend/README.md:
--------------------------------------------------------------------------------
1 | # Backend
2 |
3 | ## Running Tests
4 |
5 | To maintain consistency across the front end and back end development, we are using Jest
6 | as a test runner.
7 |
8 | ### Execute Tests
9 |
10 | You will need to be in the backend directory for this to work.
11 |
12 | 1. Run all tests: `npm test`
13 | 1. Run a single test: `npm test `
14 |
15 | ### Writing Tests
16 |
17 | To maintain idempotent tests, we have opted to use in memory test databases. Jest, like
18 | most test runners, has hooks or methods for you to call before or after tests. We can
19 | can setup our db and tear it down by importing the `setupDB` module.
20 |
21 | ```js
22 | // You will need to require the db-handler file.
23 | const { setupDB } = require("../setup-test");
24 |
25 | // You will need to name the in memory DB for this test.
26 | setupDB("api-auth");
27 | ```
28 |
29 | If you are unsure of where to start, then find a test that does something similar to your
30 | aims. Copy, tweak, and run that test until you have your desired outcome. Also make sure
31 | to give your test it's own name.
32 |
33 | ### Unit Tests
34 |
35 | Unit tests are tests to write around a single unit. These can be tests around validation
36 | of a Model, or testing the boundaries on a class.
37 |
38 | ### Integration Tests
39 |
40 | Integration Tests are tests that verify that differing components work together. A common
41 | example of an integration test is to verify that data is saved correctly in the database
42 | based on the use of the API.
--------------------------------------------------------------------------------
/backend/config/auth.config.js:
--------------------------------------------------------------------------------
1 | /*eslint-disable */
2 | module.exports = {
3 | SECRET:
4 | 'c0d7d0716e4cecffe9dcc77ff90476d98f5aace08ea40f5516bd982b06401021191f0f24cd6759f7d8ca41b64f68d0b3ad19417453bddfd1dbe8fcb197245079',
5 | CUSTOM_REQUEST_HEADER: process.env.CUSTOM_REQUEST_HEADER,
6 | TOKEN_EXPIRATION_SEC: 900,
7 | };
8 | /* eslint-enable */
--------------------------------------------------------------------------------
/backend/config/auth.config.test.js:
--------------------------------------------------------------------------------
1 | test.skip('Environment variables are working as expected', () => {
2 | const backendUrl = process.env.REACT_APP_PROXY;
3 | expect(backendUrl).toBe(`http://localhost:${process.env.BACKEND_PORT}`);
4 | });
5 |
--------------------------------------------------------------------------------
/backend/config/database.config.js:
--------------------------------------------------------------------------------
1 | exports.PORT = process.env.BACKEND_PORT;
2 | exports.DATABASE_URL = process.env.DATABASE_URL;
3 |
--------------------------------------------------------------------------------
/backend/config/index.js:
--------------------------------------------------------------------------------
1 | const CONFIG_AUTH = require('./auth.config');
2 | const CONFIG_DB = require('./database.config');
3 |
4 | module.exports = {
5 | CONFIG_AUTH,
6 | CONFIG_DB,
7 | };
8 |
--------------------------------------------------------------------------------
/backend/controllers/email.controller.test.js:
--------------------------------------------------------------------------------
1 | const EmailController = require('./email.controller');
2 |
3 | test('Can import the email controller', async () => {
4 | expect(EmailController).not.toBeUndefined();
5 | });
6 |
--------------------------------------------------------------------------------
/backend/controllers/event.controller.js:
--------------------------------------------------------------------------------
1 | const { Event } = require('../models');
2 |
3 | const EventController = {};
4 |
5 | EventController.event_list = async function (req, res) {
6 | const { query } = req;
7 |
8 | try {
9 | const events = await Event.find(query).populate('project');
10 | return res.status(200).send(events);
11 | } catch (err) {
12 | return res.sendStatus(400);
13 | }
14 | };
15 |
16 | EventController.event_by_id = async function (req, res) {
17 | const { EventId } = req.params;
18 |
19 | try {
20 | const events = await Event.findById(EventId).populate('project');
21 | return res.status(200).send(events);
22 | } catch (err) {
23 | return res.sendStatus(400);
24 | }
25 | };
26 |
27 | EventController.create = async function (req, res) {
28 | const { body } = req;
29 |
30 | try {
31 | const event = await Event.create(body);
32 | return res.status(201).send(event);
33 | } catch (err) {
34 | return res.sendStatus(400);
35 | }
36 | };
37 |
38 | EventController.destroy = async function (req, res) {
39 | const { EventId } = req.params;
40 |
41 | try {
42 | const event = await Event.findByIdAndDelete(EventId);
43 | return res.status(200).send(event);
44 | } catch (err) {
45 | return res.sendStatus(400);
46 | }
47 | };
48 |
49 | EventController.update = async function (req, res) {
50 | const { EventId } = req.params;
51 |
52 | try {
53 | const event = await Event.findByIdAndUpdate(EventId, req.body);
54 | return res.status(200).send(event);
55 | } catch (err) {
56 | return res.sendStatus(400);
57 | }
58 | };
59 |
60 | module.exports = EventController;
61 |
--------------------------------------------------------------------------------
/backend/controllers/event.controller.test.js:
--------------------------------------------------------------------------------
1 | const EventController = require('./event.controller');
2 |
3 | test('Can import the email controller', async () => {
4 | expect(EventController).not.toBeUndefined();
5 | });
6 |
--------------------------------------------------------------------------------
/backend/controllers/healthCheck.controller.js:
--------------------------------------------------------------------------------
1 | const HealthCheckController = {};
2 |
3 | HealthCheckController.isAlive = (_, res) => {
4 | res.status(200).send("I'm Alive!");
5 | }
6 |
7 | module.exports = HealthCheckController;
8 |
--------------------------------------------------------------------------------
/backend/controllers/index.js:
--------------------------------------------------------------------------------
1 | const EmailController = require('./email.controller');
2 | const EventController = require('./event.controller');
3 | const UserController = require('./user.controller');
4 | const ProjectController = require('./project.controller');
5 | const HealthCheckController = require('./healthCheck.controller');
6 | const RecurringEventController = require('./recurringEvent.controller')
7 |
8 | module.exports = {
9 | EmailController,
10 | EventController,
11 | UserController,
12 | ProjectController,
13 | HealthCheckController,
14 | RecurringEventController
15 | };
16 |
--------------------------------------------------------------------------------
/backend/controllers/project.controller.js:
--------------------------------------------------------------------------------
1 | const { Project } = require('../models');
2 |
3 | const ProjectController = {};
4 |
5 | ProjectController.project_list = async function (req, res) {
6 | const { query } = req;
7 |
8 | try {
9 | const projects = await Project.find(query);
10 | return res.status(200).send(projects);
11 | } catch (err) {
12 | return res.sendStatus(400);
13 | }
14 | };
15 |
16 | ProjectController.pm_filtered_projects = async function (req, res) {
17 | try {
18 | const projectList = await Project.find({});
19 | const projects = projectList.filter((proj) => req.body.includes(proj._id.toString()));
20 | return res.status(200).send(projects);
21 | } catch (e) {
22 | return res.sendStatus(400);
23 | }
24 | };
25 |
26 | ProjectController.create = async function (req, res) {
27 | const { body } = req;
28 |
29 | try {
30 | const newProject = await Project.create(body);
31 | return res.status(201).send(newProject);
32 | } catch (err) {
33 | return res.sendStatus(400);
34 | }
35 | };
36 |
37 | ProjectController.project_by_id = async function (req, res) {
38 | const { ProjectId } = req.params;
39 |
40 | try {
41 | const project = await Project.findById(ProjectId);
42 | return res.status(200).send(project);
43 | } catch (err) {
44 | return res.sendStatus(400);
45 | }
46 | };
47 |
48 | ProjectController.update = async function (req, res) {
49 | const { ProjectId } = req.params;
50 | try {
51 | const project = await Project.findOneAndUpdate({ _id: ProjectId }, req.body, { new: true });
52 | return res.status(200).send(project);
53 | } catch (err) {
54 | return res.sendStatus(400);
55 | }
56 | };
57 |
58 | ProjectController.destroy = async function (req, res) {
59 | const { ProjectId } = req.params;
60 |
61 | try {
62 | const project = await Project.findByIdAndDelete(ProjectId);
63 | return res.status(200).send(project);
64 | } catch (err) {
65 | return res.sendStatus(400);
66 | }
67 | };
68 |
69 | module.exports = ProjectController;
70 |
--------------------------------------------------------------------------------
/backend/controllers/project.controller.test.js:
--------------------------------------------------------------------------------
1 | const ProjectController = require('./project.controller');
2 |
3 | test('Can import the project controller', async () => {
4 | expect(ProjectController).not.toBeUndefined();
5 | });
6 |
--------------------------------------------------------------------------------
/backend/controllers/recurringEvent.controller.js:
--------------------------------------------------------------------------------
1 | const { RecurringEvent } = require('../models');
2 | const expectedHeader = process.env.CUSTOM_REQUEST_HEADER;
3 |
4 | const RecurringEventController = {};
5 |
6 |
7 | // // Add User with POST
8 | RecurringEventController.create = async function (req, res) {
9 | const { headers } = req;
10 |
11 | if (headers['x-customrequired-header'] !== expectedHeader) {
12 | return res.sendStatus(403);
13 | }
14 |
15 | try {
16 | const rEvent = await RecurringEvent.create(req.body);
17 | return res.status(200).send(rEvent);
18 | } catch (err) {
19 | return res.sendStatus(400);
20 | }
21 | };
22 |
23 | // Update Recurring Event using PATCH
24 | RecurringEventController.update = async function (req, res) {
25 |
26 | const { headers } = req;
27 | const { RecurringEventId } = req.params;
28 |
29 | if (headers['x-customrequired-header'] !== expectedHeader) {
30 | return res.sendStatus(403);
31 | }
32 |
33 | const filter = {_id: RecurringEventId};
34 | const update = req.body;
35 |
36 | try {
37 | const uEvent = await RecurringEvent.findOneAndUpdate(filter, update, {new: true});
38 | return res.status(200).send(uEvent);
39 | } catch (err) {
40 | return res.sendStatus(400);
41 | }
42 | };
43 |
44 |
45 | // Delete Recurring Event
46 | RecurringEventController.destroy = async function (req, res) {
47 | const { RecurringEventId } = req.params;
48 |
49 | try {
50 | const rEvent = await RecurringEvent.findByIdAndDelete(RecurringEventId);
51 | return res.status(200).send(rEvent);
52 | } catch (err) {
53 | return res.sendStatus(400);
54 | }
55 | };
56 |
57 | module.exports = RecurringEventController;
--------------------------------------------------------------------------------
/backend/controllers/user.controller.test.js:
--------------------------------------------------------------------------------
1 | const userContoller = require('./user.controller');
2 |
3 | test('Can import the email controller', async () => {
4 | expect(userContoller).not.toBeUndefined();
5 | });
6 |
--------------------------------------------------------------------------------
/backend/globalConfig.json:
--------------------------------------------------------------------------------
1 | {"mongoUri":"mongodb://127.0.0.1:43943/jest?","mongoDBName":"jest"}
--------------------------------------------------------------------------------
/backend/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testEnvironment: 'node',
3 | setupFilesAfterEnv: ['./jest.setup.js'],
4 | watchPathIgnorePatterns: ['globalConfig'],
5 | testPathIgnorePatterns: ['/test/old-tests/'],
6 | };
7 |
--------------------------------------------------------------------------------
/backend/jest.setup.js:
--------------------------------------------------------------------------------
1 | // Be able to use Env variables in Github Actions
2 | const dotenv = require('dotenv');
3 | const dotenvExpand = require('dotenv-expand');
4 |
5 | const myEnv = dotenv.config();
6 | dotenvExpand(myEnv);
7 |
8 |
9 | jest.setTimeout(30000)
10 |
11 | // TODO: Refactor worker routes. These are setup to run cron jobs every time the app
12 | // is instantiated. These break any integration tests.
13 | jest.mock('./workers/openCheckins');
14 | jest.mock('./workers/closeCheckins');
15 | jest.mock('./workers/createRecurringEvents');
16 | jest.mock('./workers/slackbot');
17 |
--------------------------------------------------------------------------------
/backend/middleware/auth.middleware.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 | const { CONFIG_AUTH } = require('../config');
3 |
4 | function verifyToken(req, res, next) {
5 | // Allow users to set token
6 | // eslint-disable-next-line dot-notation
7 | let token = req.headers['x-access-token'] || req.headers['authorization'];
8 | if (token.startsWith('Bearer ')) {
9 | // Remove Bearer from string
10 | token = token.slice(7, token.length);
11 | }
12 | if (!token) {
13 | return res.sendStatus(403);
14 | }
15 |
16 | try {
17 | const decoded = jwt.verify(token, CONFIG_AUTH.SECRET);
18 | res.cookie('token', token, { httpOnly: true });
19 | req.userId = decoded.id;
20 | return next();
21 | } catch (err) {
22 | return res.sendStatus(401);
23 | }
24 | }
25 |
26 | function verifyCookie(req, res, next) {
27 | jwt.verify(req.cookies.token, CONFIG_AUTH.SECRET, (err, decoded) => {
28 | if (err) {
29 | return res.sendStatus(401);
30 | }
31 | req.userId = decoded.id;
32 | req.role = decoded.accessLevel;
33 |
34 | next();
35 | });
36 | }
37 |
38 | const AuthUtil = {
39 | verifyToken,
40 | verifyCookie,
41 | };
42 | module.exports = AuthUtil;
43 |
--------------------------------------------------------------------------------
/backend/middleware/errorhandler.middleware.js:
--------------------------------------------------------------------------------
1 | function errorHandler (error, req, res, next) {
2 | error.status = error.status || 500;
3 |
4 | res.status(error.status).json({
5 | error: {
6 | status: error.status,
7 | message: error.message || 'Internal Server Error',
8 | stack: error.stack || ''
9 | }
10 | });
11 | }
12 |
13 | module.exports = errorHandler;
14 |
--------------------------------------------------------------------------------
/backend/middleware/index.js:
--------------------------------------------------------------------------------
1 | const AuthUtil = require('./auth.middleware');
2 | const verifyUser = require('./user.middleware');
3 | const verifyToken = require('./token.middleware');
4 |
5 | module.exports = {
6 | AuthUtil,
7 | verifyUser,
8 | verifyToken,
9 | };
10 |
--------------------------------------------------------------------------------
/backend/middleware/token.middleware.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 | const { CONFIG_AUTH } = require('../config');
3 |
4 | async function isTokenValid(req, res, next) {
5 | let token = req.headers['x-access-token'] || req.headers['authorization'];
6 | if (token.startsWith('Bearer ')) {
7 | // Remove Bearer from string
8 | token = token.slice(7, token.length);
9 | }
10 |
11 | if (!token) {
12 | return res.sendStatus(400);
13 | }
14 |
15 | try {
16 | jwt.verify(token, CONFIG_AUTH.SECRET);
17 | next();
18 | } catch (err) {
19 | return res.sendStatus(403);
20 | }
21 | }
22 |
23 | const verifyToken = {
24 | isTokenValid,
25 | };
26 |
27 | module.exports = verifyToken;
28 |
--------------------------------------------------------------------------------
/backend/middleware/user.middleware.js:
--------------------------------------------------------------------------------
1 | const { User } = require('../models');
2 |
3 | function checkDuplicateEmail(req, res, next) {
4 | User.findOne({ email: req.body.email }).then((user) => {
5 | if (user) {
6 | return res.sendStatus(400);
7 | }
8 | next();
9 | });
10 | }
11 |
12 | function isAdminByEmail(req, res, next) {
13 | User.findOne({ email: req.body.email }).then((user) => {
14 | if (!user) {
15 | return res.sendStatus(400);
16 | } else {
17 | const role = user.accessLevel;
18 | if (role === 'admin' || role === 'superadmin' || user.managedProjects.length > 0) {
19 | next();
20 | } else {
21 | next(res.sendStatus(401));
22 | }
23 | }
24 | });
25 | }
26 |
27 | const verifyUser = {
28 | checkDuplicateEmail,
29 | isAdminByEmail,
30 | };
31 |
32 | module.exports = verifyUser;
33 |
--------------------------------------------------------------------------------
/backend/models/checkIn.model.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | mongoose.Promise = global.Promise;
4 |
5 | const checkInSchema = mongoose.Schema({
6 | userId: { type: String },
7 | eventId: { type: String },
8 | checkedIn: { type: Boolean, default: true },
9 | createdDate: { type: Date, default: Date.now },
10 | });
11 |
12 | checkInSchema.methods.serialize = function () {
13 | return {
14 | id: this._id,
15 | userId: this.userId,
16 | eventId: this.eventId,
17 | checkedIn: this.checkedIn,
18 | createdDate: this.createdDate,
19 | };
20 | };
21 |
22 | const CheckIn = mongoose.model("CheckIn", checkInSchema);
23 |
24 | module.exports = { CheckIn };
25 |
--------------------------------------------------------------------------------
/backend/models/index.js:
--------------------------------------------------------------------------------
1 | const { CheckIn } = require('./checkIn.model');
2 | const { Event } = require('./event.model');
3 | const { Project } = require('./project.model');
4 | const { ProjectTeamMember } = require('./projectTeamMember.model');
5 | const { Question } = require('./question.model');
6 | const { RecurringEvent } = require('./recurringEvent.model');
7 | const { Role } = require('./role.model');
8 | const { User } = require('./user.model');
9 |
10 | const mongoose = require("mongoose");
11 | mongoose.Promise = global.Promise;
12 |
13 | module.exports = {
14 | CheckIn,
15 | Event,
16 | Project,
17 | ProjectTeamMember,
18 | Question,
19 | RecurringEvent,
20 | Role,
21 | User,
22 | };
23 |
--------------------------------------------------------------------------------
/backend/models/projectTeamMember.model.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | mongoose.Promise = global.Promise;
4 |
5 | /*
6 | ProjectTeamMember:
7 | - projectID
8 | - user ID
9 | - teamMemberStatus (active/inactive)
10 | - roleOnProject
11 | - joinedDate
12 | - leftDate
13 | - leftReason (project completed, project paused, switched projects, no-show, etc.)
14 | Idea for the future: numberGithubContributions (pull this from github?)
15 | */
16 |
17 | const projectTeamMemberSchema = mongoose.Schema({
18 | userId: { type: String }, // id of the user
19 | projectId: { type: String }, // id of the project
20 | teamMemberStatus: { type: String }, // Active or Inactive
21 | vrmsProjectAdmin: { type: Boolean }, // does this team member have admin rights to the project in VRMS?
22 | roleOnProject: { type: String }, // Developer, Project Manager, UX, Data Science
23 | joinedDate: { type: Date, default: Date.now }, // date/time joined project
24 | leftDate: { type: Date }, // only if Status = Inactive, date/time went inactive
25 | leftReason: { type: String }, // project completed, project paused, switched projects, no-show, other
26 | githubPermissionLevel: { type: String }, // Write, Triage, Read, Maintainer, or Admin; pull from Github API?
27 | onProjectGithub: { type: Boolean, default: false }, // added to the project team on github? pull from github api?
28 | onProjectGoogleDrive: { type: Boolean, default: false} // added to the project team's google drive folder?
29 | });
30 | projectTeamMemberSchema.methods.serialize = function() {
31 | return {
32 | id: this._id,
33 | userId: this.userId,
34 | projectId: this.projectId,
35 | teamMemberStatus: this.teamMemberStatus,
36 | vrmsProjectAdmin: this.vrmsProjectAdmin,
37 | roleOnProject: this.roleOnProject,
38 | joinedDate: this.joinedDate,
39 | leftDate: this.leftDate,
40 | leftReason: this.leftReason,
41 | githubPermissionLevel: this.githubPermissionLevel,
42 | onProjectGithub: this.onProjectGithub,
43 | onProjectGoogleDrive: this.onProjectGoogleDrive
44 | };
45 | };
46 |
47 | const ProjectTeamMember = mongoose.model('ProjectTeamMember', projectTeamMemberSchema);
48 |
49 | module.exports = { ProjectTeamMember };
50 |
--------------------------------------------------------------------------------
/backend/models/question.model.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | mongoose.Promise = global.Promise;
4 |
5 | const questionSchema = mongoose.Schema({
6 | questionText: { type: String },
7 | htmlName: { type: String },
8 | answers: {
9 | answerOneText: { type: String },
10 | answerTwoText: { type: String },
11 | answerThreeText: { type: String },
12 | answerFourText: { type: String }
13 | }
14 | });
15 |
16 | questionSchema.methods.serialize = function() {
17 | return {
18 | id: this._id,
19 | questionText: this.questionText,
20 | htmlName: this.htmlName,
21 | inputType: this.inputType,
22 | answers: {
23 | answerOneText: this.answers.answerOneText,
24 | answerTwoText: this.answers.answerTwoText,
25 | answerThreeText: this.answers.answerThreeText,
26 | answerFourText: this.answers.answerFourText
27 | }
28 | };
29 | };
30 |
31 | const Question = mongoose.model('Question', questionSchema);
32 |
33 | module.exports = { Question };
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/backend/models/recurringEvent.model.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | mongoose.Promise = global.Promise;
4 |
5 | const recurringEventSchema = mongoose.Schema({
6 | name: { type: String },
7 | location: { // should we include address here?
8 | city: { type: String },
9 | state: { type: String },
10 | country: { type: String }
11 | },
12 | hacknight: { type: String }, // DTLA, Westside, South LA, Online
13 | brigade: { type: String, default: "Hack for LA" },
14 | eventType: { type: String }, // Project Meeting, Orientation, Workshop
15 | description: { type: String },
16 | project: { // only needed if it's type = Project Meeting
17 | type: mongoose.Schema.Types.ObjectId,
18 | ref: 'Project'
19 | },
20 | date: { type: Date },
21 | startTime: { type: Date }, // start date and time of the event
22 | endTime: { type: Date }, // end date and time of the event
23 | hours: { type: Number }, // length of the event in hours
24 | createdDate: { type: Date, default: Date.now }, // date/time event was created
25 | updatedDate: { type: Date, default: Date.now }, // date/time event was last updated
26 | checkInReady: { type: Boolean, default: false }, // is the event open for check-ins?
27 | videoConferenceLink: { type: String }, // can be same or different from project
28 | owner: {
29 | ownerId: { type: String, default: '123456' } // id of user who created event
30 | }
31 | });
32 |
33 | recurringEventSchema.methods.serialize = function() {
34 | return {
35 | id: this._id,
36 | name: this.name,
37 | location: {
38 | city: this.location.city,
39 | state: this.location.state,
40 | country: this.location.country
41 | },
42 | hacknight: [this.hacknight],
43 | brigade: this.brigade,
44 | eventType: this.eventType,
45 | description: this.eventDescription,
46 | project: this.project,
47 | date: this.date,
48 | startTime: this.startTime,
49 | endTime: this.endTime,
50 | hours: this.hours,
51 | createdDate: this.createdDate,
52 | checkInReady: this.checkInReady,
53 | videoConferenceLink: this.videoConferenceLink,
54 | owner: {
55 | ownerId: this.owner.ownerId
56 | }
57 | };
58 | };
59 |
60 | const RecurringEvent = mongoose.model('RecurringEvent', recurringEventSchema);
61 |
62 | module.exports = { RecurringEvent };
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/backend/models/role.model.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | const Role = mongoose.model(
4 | "Role",
5 | new mongoose.Schema({
6 | name: { type: String },
7 | })
8 | );
9 |
10 | module.exports = { Role };
11 |
--------------------------------------------------------------------------------
/backend/models/timeTracker.model.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | mongoose.Promise = global.Promise;
4 |
5 | const timeTrackerSchema = mongoose.Schema({
6 | user: {
7 | type: mongoose.Schema.Types.ObjectId,
8 | ref: "User",
9 | },
10 | project: {
11 | type: mongoose.Schema.Types.ObjectId,
12 | ref: "Project",
13 | },
14 | category: { type: String }, // picklist with 4 options: Development, Design/UX, Product/Project Management, Other
15 | notes: { type: String },
16 | startDate: { type: Date },
17 | endDate: { type: Date },
18 | });
19 |
20 | timeTrackerSchema.methods.serialize = function () {
21 | return {
22 | id: this._id,
23 | user: {
24 | userId: this.user.userId,
25 | },
26 | selectedAnswer: this.selectedAnswer,
27 | startDate: this.startDate,
28 | endDate: this.endDate,
29 | };
30 | };
31 |
32 | const TimeTracker = mongoose.model("TimeTracker", timeTrackerSchema);
33 |
34 | module.exports = { TimeTracker };
35 |
--------------------------------------------------------------------------------
/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vrms-server",
3 | "version": "0.3.0",
4 | "description": "VRMS Backend",
5 | "main": "server.js",
6 | "scripts": {
7 | "lint": "eslint .",
8 | "format": "prettier --check .",
9 | "test": "jest",
10 | "test:watch": "jest --watch",
11 | "start": "node server.js",
12 | "dev": "nodemon server.js",
13 | "client": "npm run start --prefix client",
14 | "heroku-postbuild": "cd client && npm install && npm run build"
15 | },
16 | "author": "sarL3y",
17 | "license": "ISC",
18 | "devDependencies": {
19 | "@babel/core": "^7.15.0",
20 | "@babel/eslint-parser": "^7.15.0",
21 | "@babel/eslint-plugin": "^7.14.5",
22 | "concurrently": "^5.1.0",
23 | "debug": "^4.3.1",
24 | "eslint": "^7.9.0",
25 | "eslint-config-airbnb": "^18.2.0",
26 | "eslint-config-airbnb-base": "^14.2.0",
27 | "eslint-config-prettier": "^6.11.0",
28 | "eslint-plugin-import": "^2.22.0",
29 | "eslint-plugin-jsx-a11y": "^6.3.1",
30 | "eslint-plugin-react": "^7.20.6",
31 | "jest": "^26.4.0",
32 | "mockdate": "^3.0.5",
33 | "nodemon": "^2.0.2",
34 | "prettier": "^2.1.1",
35 | "pretty-quick": "^3.0.2",
36 | "supertest": "^4.0.2",
37 | "why-is-node-running": "^2.2.0"
38 | },
39 | "dependencies": {
40 | "@slack/bolt": "^2.2.3",
41 | "assert-env": "^0.6.0",
42 | "async": "^3.2.2",
43 | "cookie-parser": "^1.4.5",
44 | "cors": "^2.8.5",
45 | "dotenv": "^8.2.0",
46 | "dotenv-expand": "^5.1.0",
47 | "express": "^4.20.0",
48 | "express-validator": "^6.6.1",
49 | "googleapis": "^59.0.0",
50 | "helmet": "^3.22.0",
51 | "jsonwebtoken": "^8.5.1",
52 | "mongodb-memory-server": "^6.9.0",
53 | "mongoose": "^5.10.0",
54 | "morgan": "^1.10.0",
55 | "node-cron": "^2.0.3",
56 | "node-fetch": "^2.6.7",
57 | "nodemailer": "^6.6.1"
58 | },
59 | "directories": {
60 | "test": "test"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/backend/routers/auth.router.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const { AuthUtil, verifyUser, verifyToken } = require('../middleware');
3 | const { UserController } = require('../controllers/');
4 | const { authApiValidator } = require('../validators');
5 |
6 | const router = express.Router();
7 |
8 | // eslint-disable-next-line func-names
9 | router.use(function (req, res, next) {
10 | res.header('Access-Control-Allow-Headers', 'x-access-token, Origin, Content-Type, Accept');
11 | next();
12 | });
13 |
14 | // The root is /api/auth
15 | router.post(
16 | '/signup',
17 | [authApiValidator.validateCreateUserAPICall, verifyUser.checkDuplicateEmail],
18 | UserController.createUser,
19 | );
20 |
21 | router.post(
22 | '/signin',
23 | [authApiValidator.validateSigninUserAPICall, verifyUser.isAdminByEmail],
24 | UserController.signin,
25 | );
26 |
27 | router.post('/verify-signin', [verifyToken.isTokenValid], UserController.verifySignIn);
28 |
29 | router.post('/me', [AuthUtil.verifyCookie], UserController.verifyMe);
30 |
31 | router.post('/logout', [AuthUtil.verifyCookie], UserController.logout);
32 |
33 | module.exports = router;
34 |
--------------------------------------------------------------------------------
/backend/routers/checkIns.router.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const router = express.Router();
3 |
4 | const { CheckIn } = require('../models/checkIn.model');
5 |
6 | // GET /api/checkins/
7 | router.get('/', (req, res) => {
8 | CheckIn.find()
9 | .then((checkIns) => {
10 | res.status(200).send(checkIns);
11 | })
12 | .catch((err) => {
13 | console.log(err);
14 | res.sendStatus(400);
15 | });
16 | });
17 |
18 | router.get("/:id", (req, res) => {
19 | CheckIn.findById(req.params.id)
20 | .then((checkIn) => {
21 | res.status(200).send(checkIn);
22 | })
23 | .catch((err) => {
24 | console.log(err);
25 | res.sendStatus(400);
26 | });
27 | });
28 |
29 | router.get("/findEvent/:id", (req, res) => {
30 | CheckIn.find({ eventId: req.params.id, userId: { $ne: "undefined" } })
31 | .populate({
32 | path: "userId",
33 | model: "User",
34 | })
35 | .then((checkIns) => {
36 | res.status(200).send(checkIns);
37 | })
38 | .catch((err) => {
39 | console.log(err);
40 | res.sendStatus(400);
41 | });
42 | });
43 |
44 | router.post("/", (req, res) => {
45 | CheckIn.create(req.body)
46 | .then((checkIn) => {
47 | res.sendStatus(201);
48 | })
49 | .catch((err) => {
50 | console.log(err);
51 | res.sendStatus(400);
52 | });
53 | });
54 |
55 | module.exports = router;
56 |
--------------------------------------------------------------------------------
/backend/routers/checkUser.router.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 |
4 | const { User } = require('../models/user.model');
5 |
6 | // TODO: Refactor checkuser and test. Consider moving to auth.
7 |
8 | // GET /api/checkuser/
9 | router.post('/', (req, res) => {
10 | const { email, auth_origin } = req.body;
11 | console.log(email);
12 | console.log(`auth_origin: ${auth_origin}`);
13 |
14 | if (email === 'undefined') {
15 | return res.sendStatus(400);
16 | }
17 |
18 | if (email) {
19 | User.findOne({ email })
20 | .then((user) => {
21 | if (!user) {
22 | return res.sendStatus(400);
23 | } else {
24 | return res.status(200).send({ user: user, auth_origin: auth_origin });
25 | }
26 | })
27 | .catch((err) => {
28 | console.log(err);
29 |
30 | return res.sendStatus(400);
31 | });
32 | } else {
33 | // TODO: Refactor as path is not called, or tested.
34 | res.json({ message: 'Enter the email address you used to check-in last time.' });
35 | }
36 | });
37 |
38 | router.get('/:id', (req, res) => {
39 | // TODO: Refactor and test
40 | User.findById(req.params.id)
41 | .then((user) => {
42 | return res.status(200).send(user);
43 | })
44 | .catch((err) => {
45 | console.log(err);
46 | res.sendStatus(400);
47 | });
48 | });
49 |
50 | module.exports = router;
51 |
--------------------------------------------------------------------------------
/backend/routers/checkUser.router.test.js:
--------------------------------------------------------------------------------
1 | // Mock and import User Model
2 | jest.mock('../models/user.model');
3 | const { User } = require('../models');
4 |
5 | // Import checkUser router
6 | const express = require('express');
7 | const supertest = require('supertest');
8 | const checkUserRouter = require('./checkUser.router');
9 |
10 | // Create a new Express application for testing
11 | const testapp = express();
12 | // express.json() is a body parser needed for POST API requests
13 | testapp.use(express.json());
14 | testapp.use('/api/checkuser', checkUserRouter);
15 | const request = supertest(testapp);
16 |
17 | describe('Unit tests for checkUser router', () => {
18 | // Mock user for test
19 | const id = '123';
20 | const mockUser = {
21 | id,
22 | name: {
23 | firstName: 'mock',
24 | lastName: 'user',
25 | },
26 | accessLevel: 'user',
27 | skillsToMatch: [],
28 | projects: [],
29 | textingOk: false,
30 | managedProjects: [],
31 | isActive: true,
32 | email: 'mockuser@gmail.com',
33 | currentRole: 'Product Owner',
34 | desiredRole: 'Product Owner',
35 | newMember: false,
36 | firstAttended: 'NOV 2015',
37 | createdDate: '2020-01-14T02:14:22.407Z',
38 | attendanceReason: 'Civic Engagement',
39 | currentProject: 'Undebate',
40 | };
41 |
42 | const auth_origin = 'test-origin';
43 |
44 | // Clear all mocks after each test
45 | afterEach(() => {
46 | jest.clearAllMocks();
47 | });
48 |
49 | describe('CREATE', () => {
50 | it('should authenticate user with POST /api/checkuser', async (done) => {
51 | // Mock Mongoose method
52 | User.findOne.mockResolvedValue(mockUser);
53 |
54 | const response = await request
55 | .post('/api/checkuser')
56 | .send({ email: 'mockuser@gmail.com', auth_origin });
57 |
58 | // Tests
59 | expect(User.findOne).toHaveBeenCalledWith({ email: 'mockuser@gmail.com' });
60 | expect(response.status).toBe(200);
61 | expect(response.body).toEqual({ user: mockUser, auth_origin: auth_origin });
62 |
63 | // Marks completion of tests
64 | done();
65 | });
66 | });
67 |
68 | describe('READ', () => {
69 | it('should return a user by id with GET /api/checkuser/:id', async (done) => {
70 | // Mock Mongoose method
71 | User.findById.mockResolvedValue(mockUser);
72 |
73 | const response = await request.get(`/api/checkuser/${id}`);
74 |
75 | // Tests
76 | expect(User.findById).toHaveBeenCalledWith(id);
77 | expect(response.status).toBe(200);
78 | expect(response.body).toEqual(mockUser);
79 |
80 | // Marks completion of tests
81 | done();
82 | });
83 | });
84 | });
85 |
--------------------------------------------------------------------------------
/backend/routers/events.router.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const router = express.Router();
3 |
4 | const { Event } = require('../models/event.model');
5 | const { EventController } = require('../controllers');
6 |
7 | // The root is /api/events
8 | router.get('/', EventController.event_list);
9 |
10 | router.post('/', EventController.create);
11 |
12 | router.get('/:EventId', EventController.event_by_id);
13 |
14 | router.delete('/:EventId', EventController.destroy);
15 |
16 | router.patch('/:EventId', EventController.update);
17 |
18 | // TODO: Refactor and remove
19 | router.get("/nexteventbyproject/:id", (req, res) => {
20 | Event.find({ project: req.params.id })
21 | .populate("project")
22 | .then((events) => {
23 | res.status(200).json(events[events.length - 1]);
24 | })
25 | .catch((err) => {
26 | console.log(err);
27 | res.sendStatus(500);
28 | });
29 | });
30 |
31 | module.exports = router;
32 |
--------------------------------------------------------------------------------
/backend/routers/healthCheck.router.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const router = express.Router();
3 |
4 | const { HealthCheckController } = require('../controllers');
5 |
6 | // The root is /api/healthcheck
7 | router.get('/', HealthCheckController.isAlive);
8 |
9 | module.exports = router;
10 |
--------------------------------------------------------------------------------
/backend/routers/projectTeamMembers.router.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const router = express.Router();
3 |
4 | const { ProjectTeamMember } = require('../models/projectTeamMember.model');
5 |
6 | // GET /api/projectteammembers/
7 | router.get("/", (req, res) => {
8 | ProjectTeamMember.find()
9 | .populate("userId")
10 | .then((teamMembers) => {
11 | return res.status(200).send(teamMembers);
12 | })
13 | .catch((err) => {
14 | console.log(err);
15 | return res.sendStatus(400);
16 | });
17 | });
18 |
19 | router.get("/:id", (req, res) => {
20 | ProjectTeamMember.find({ projectId: req.params.id })
21 | .populate("userId")
22 | .then((teamMembers) => {
23 | return res.status(200).send(teamMembers);
24 | })
25 | .catch((err) => {
26 | console.log(err);
27 | return res.sendStatus(400);
28 | });
29 | });
30 |
31 | router.get("/project/:id/:userId", (req, res) => {
32 | ProjectTeamMember.find({
33 | projectId: req.params.id,
34 | userId: req.params.userId,
35 | })
36 | .populate("userId")
37 | .then((teamMember) => {
38 | if (!teamMember.length) {
39 | return res.sendStatus(400);
40 | } else {
41 | return res.status(200).send(teamMember);
42 | }
43 | })
44 | .catch((err) => {
45 | console.log(err);
46 | return res.sendStatus(400);
47 | });
48 | });
49 |
50 | router.get("/projectowner/:id", (req, res) => {
51 | const id = req.params.id;
52 |
53 | ProjectTeamMember.findOne({ userId: id })
54 | .populate("userId")
55 | .populate("projectId")
56 | .then((teamMember) => {
57 | teamMember.vrmsProjectAdmin === true
58 | ? res.status(200).send(teamMember)
59 | : res.status(200).send(false);
60 | })
61 | .catch((err) => {
62 | res.status(400).send(err);
63 | });
64 | });
65 |
66 | router.post("/", (req, res) => {
67 | ProjectTeamMember.create(req.body)
68 | .then((teamMember) => {
69 | return res.status(201).send(teamMember);
70 | })
71 | .catch((err) => {
72 | console.log(err);
73 | return res.sendStatus(400);
74 | });
75 | });
76 |
77 | router.patch("/:id", (req, res) => {
78 | ProjectTeamMember.findByIdAndUpdate(req.params.id, req.body)
79 | .then((edit) => res.json(edit))
80 | .catch((err) =>
81 | res.sendStatus(400));
82 | // };
83 | });
84 |
85 | module.exports = router;
86 |
--------------------------------------------------------------------------------
/backend/routers/projects.router.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 |
4 | const { ProjectController } = require('../controllers');
5 | const { AuthUtil } = require('../middleware');
6 |
7 | // The base is /api/projects
8 | router.get('/', ProjectController.project_list);
9 |
10 | // Its a put because we have to send the PM projects to be filtered here
11 | router.put('/', ProjectController.pm_filtered_projects);
12 |
13 | router.post('/', AuthUtil.verifyCookie, ProjectController.create);
14 |
15 | router.get('/:ProjectId', ProjectController.project_by_id);
16 |
17 | router.put('/:ProjectId', AuthUtil.verifyCookie, ProjectController.update);
18 |
19 | router.patch('/:ProjectId', AuthUtil.verifyCookie, ProjectController.update);
20 |
21 | module.exports = router;
22 |
--------------------------------------------------------------------------------
/backend/routers/questions.router.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 |
4 | const { Question } = require('../models/question.model');
5 |
6 |
7 | // GET /api/questions/
8 | router.get('/', (req, res) => {
9 | // const { query } = req;
10 |
11 | Question
12 | .find()
13 | .then(questions => {
14 | return res.status(200).send(questions);
15 | })
16 | .catch(err => {
17 | console.log(err);
18 | return res.sendStatus(400);
19 | });
20 | });
21 |
22 | router.post('/', (req, res) => {
23 |
24 | Question
25 | .create(req.body)
26 | .then(question => {
27 | return res.sendStatus(201);
28 | })
29 | .catch(err => {
30 | console.log(err);
31 | return res.sendStatus(400);
32 | });
33 | });
34 |
35 | router.get('/:id', (req, res) => {
36 |
37 | Question
38 | .findById(req.params.id)
39 | .then(event => {
40 | return res.status(200).send(event);
41 | })
42 | .catch(err => {
43 | console.log(err);
44 | return res.sendStatus(400);
45 | });
46 | });
47 |
48 | module.exports = router;
49 |
--------------------------------------------------------------------------------
/backend/routers/recurringEvents.router.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const cors = require('cors');
4 |
5 | const { RecurringEvent } = require('../models/recurringEvent.model');
6 | const { RecurringEventController } = require('../controllers/');
7 | const { AuthUtil } = require('../middleware');
8 |
9 | // GET /api/recurringevents/
10 | router.get('/', cors(), (req, res) => {
11 | // const { query } = req;
12 |
13 | RecurringEvent
14 | // .find(query.checkInReady === 'true' ? query : undefined)
15 | .find()
16 | // This will deselect the video conference link field
17 | .select("-videoConferenceLink")
18 | .populate('project')
19 | .then(recurringEvents => {
20 | return res.status(200).send(recurringEvents);
21 | })
22 | .catch(err => {
23 | console.log(err);
24 | return res.sendStatus(400);
25 | });
26 | });
27 |
28 | router.get("/internal", (req, res) => {
29 | RecurringEvent
30 | .find()
31 | .populate('project')
32 | .then(recurringEvents => {
33 | return res.status(200).send(recurringEvents)
34 | })
35 | .catch(err => {
36 | console.error(err)
37 | return res.status(400);
38 | })
39 | } )
40 |
41 | router.get('/:id', (req, res) => {
42 | RecurringEvent
43 | .findById(req.params.id)
44 | .then(recurringEvent => {
45 | return res.status(200).send(recurringEvent);
46 | })
47 | .catch(err => {
48 | console.log(err);
49 | return res.sendStatus(400);
50 | });
51 | });
52 |
53 | router.post('/', AuthUtil.verifyCookie, RecurringEventController.create);
54 |
55 | router.patch('/:RecurringEventId', AuthUtil.verifyCookie, RecurringEventController.update);
56 |
57 | router.delete('/:RecurringEventId', AuthUtil.verifyCookie, RecurringEventController.destroy);
58 |
59 | module.exports = router;
60 |
--------------------------------------------------------------------------------
/backend/routers/success.router.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const router = express.Router();
3 | const cors = require("cors");
4 |
5 | const { Event } = require('../models/event.model');
6 |
7 | // GET /api/recurringevents/
8 | router.get("/", cors(), (req, res) => {
9 | // const { query } = req;
10 |
11 | Event.find()
12 | .then((event) => {
13 | res.status(200).send(event);
14 | })
15 | .catch((err) => {
16 | console.log(err);
17 | res.sendStatus(400);
18 | });
19 |
20 | router.get("/:id", (req, res) => {
21 | Event.findById(req.params.id)
22 | .then((event) => {
23 | res.status(200).send(event);
24 | })
25 | .catch((err) => {
26 | console.log(err);
27 | res.sendStatus(400);
28 | });
29 | });
30 | });
31 |
32 | module.exports = router;
33 |
--------------------------------------------------------------------------------
/backend/routers/users.router.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 |
4 | const { UserController } = require('../controllers');
5 |
6 | // The base is /api/users
7 | router.get('/', UserController.user_list);
8 |
9 | router.get('/admins', UserController.admin_list);
10 |
11 | router.get('/projectManagers', UserController.projectLead_list);
12 |
13 | router.post('/', UserController.create);
14 |
15 | router.get('/:UserId', UserController.user_by_id);
16 |
17 | router.patch('/:UserId', UserController.update);
18 |
19 | router.delete('/:UserId', UserController.delete);
20 |
21 | module.exports = router;
22 |
--------------------------------------------------------------------------------
/backend/server.js:
--------------------------------------------------------------------------------
1 | const app = require("./app");
2 | const mongoose = require("mongoose");
3 |
4 | const { Role } = require("./models");
5 |
6 | // Load config variables
7 | const { CONFIG_DB } = require('./config/');
8 |
9 | // Required convention for mongoose - https://stackoverflow.com/a/51862948/5900471
10 | mongoose.Promise = global.Promise;
11 |
12 | let server;
13 | async function runServer(databaseUrl = CONFIG_DB.DATABASE_URL, port = CONFIG_DB.PORT) {
14 | await mongoose
15 | .connect(databaseUrl, {
16 | useNewUrlParser: true,
17 | useCreateIndex: true,
18 | useUnifiedTopology: true,
19 | useFindAndModify: false,
20 | })
21 | .catch((err) => err);
22 |
23 | server = app
24 | .listen(port, () => {
25 | console.log(
26 | `Mongoose connected from runServer() and is listening on ${port}`
27 | );
28 | })
29 | .on("error", (err) => {
30 | mongoose.disconnect();
31 | return err;
32 | });
33 | }
34 |
35 | async function closeServer() {
36 | await mongoose.disconnect().then(() => {
37 | return new Promise((resolve, reject) => {
38 | console.log("Closing Mongoose connection. Bye");
39 |
40 | server.close((err) => {
41 | if (err) {
42 | return reject(err);
43 | }
44 |
45 | resolve();
46 | });
47 | });
48 | });
49 | }
50 |
51 | function initial() {
52 | Role.collection.estimatedDocumentCount((err, count) => {
53 | if (!err && count === 0) {
54 | new Role({
55 | name: "APP_USER",
56 | }).save((err) => {
57 | if (err) {
58 | console.log("error", err);
59 | }
60 |
61 | console.log("added 'user' to roles collection");
62 | });
63 |
64 | new Role({
65 | name: "APP_ADMIN",
66 | }).save((err) => {
67 | if (err) {
68 | console.log("error", err);
69 | }
70 |
71 | console.log("added 'moderator' to roles collection");
72 | });
73 | }
74 | });
75 | }
76 |
77 | if (require.main === module) {
78 | runServer().catch((err) => console.error(err));
79 | initial();
80 | }
81 |
82 | module.exports = { app, runServer, closeServer };
83 |
--------------------------------------------------------------------------------
/backend/setup-test.js:
--------------------------------------------------------------------------------
1 | // test-setup.js
2 | const mongoose = require("mongoose");
3 | mongoose.set("useCreateIndex", true);
4 | mongoose.promise = global.Promise;
5 |
6 | const { MongoMemoryServer } = require("mongodb-memory-server");
7 |
8 | async function removeAllCollections() {
9 | const mongooseCollections = mongoose.connection.collections;
10 | const collections = Object.keys(mongooseCollections);
11 | for (const collectionName of collections) {
12 | const collection = mongoose.connection.collections[collectionName];
13 | collection.deleteMany();
14 | }
15 | }
16 |
17 | async function dropAllCollections() {
18 | const collections = Object.keys(mongoose.connection.collections);
19 | for (const collectionName of collections) {
20 | const collection = mongoose.connection.collections[collectionName];
21 | try {
22 | await collection.drop();
23 | } catch (error) {
24 | // Sometimes this error happens, but you can safely ignore it
25 | if (error.message === "ns not found") return;
26 | // This error occurs when you use it.todo. You can
27 | // safely ignore this error too
28 | if (error.message.includes("a background operation is currently running"))
29 | return;
30 | console.log(error.message);
31 | }
32 | }
33 | }
34 | let mongoServer;
35 | module.exports = {
36 | setupIntegrationDB(databaseName) {
37 | // Connect to Mongoose
38 | beforeAll(async () => {
39 | mongoServer = new MongoMemoryServer({
40 | instance: { dbName: databaseName },
41 | });
42 | const mongoUri = await mongoServer.getUri();
43 | const opts = {
44 | useNewUrlParser: true,
45 | useFindAndModify: false,
46 | useCreateIndex: true,
47 | useUnifiedTopology: true,
48 | };
49 | await mongoose.connect(mongoUri, opts, (err) => {
50 | if (err) console.error(err);
51 | });
52 | });
53 |
54 | // Disconnect Mongoose
55 | afterAll(async () => {
56 | await dropAllCollections();
57 | await mongoose.connection.close();
58 | await mongoServer.stop();
59 | });
60 | },
61 | };
62 |
--------------------------------------------------------------------------------
/backend/validators/index.js:
--------------------------------------------------------------------------------
1 | const authApiValidator = require('./user.api.validator');
2 |
3 | module.exports = {
4 | authApiValidator,
5 | };
6 |
--------------------------------------------------------------------------------
/backend/validators/user.api.validator.js:
--------------------------------------------------------------------------------
1 | const { body, validationResult } = require('express-validator');
2 |
3 | async function validateCreateUserAPICall(req, res, next) {
4 | await body('name.firstName').not().isEmpty().trim().escape().run(req);
5 | await body('name.lastName').not().isEmpty().trim().escape().run(req);
6 | await body('email', 'Invalid email')
7 | .exists()
8 | .isEmail()
9 | .normalizeEmail({ gmail_remove_dots: false })
10 | .run(req);
11 |
12 | // Finds the validation errors in this request and wraps them in an object with handy functions
13 | const errors = validationResult(req);
14 |
15 | if (!errors.isEmpty()) {
16 | return res.status(403).json({ errors: errors.array() });
17 | }
18 | return next();
19 | }
20 |
21 | async function validateSigninUserAPICall(req, res, next) {
22 | await body('email', 'Invalid email')
23 | .exists()
24 | .isEmail()
25 | .normalizeEmail({ gmail_remove_dots: false })
26 | .run(req);
27 |
28 | // Finds the validation errors in this request and wraps them in an object with handy functions
29 | const errors = validationResult(req);
30 |
31 | if (!errors.isEmpty()) {
32 | return res.status(403).json({ errors: errors.array() });
33 | }
34 | return next();
35 | }
36 |
37 | const authApiValidator = {
38 | validateCreateUserAPICall,
39 | validateSigninUserAPICall,
40 | };
41 |
42 | module.exports = authApiValidator;
43 |
--------------------------------------------------------------------------------
/backend/workers/lib/generateEventData.js:
--------------------------------------------------------------------------------
1 | function generateEventData(eventObj, TODAY_DATE = new Date()) {
2 | /**
3 | * Generates event data based on the provided event object and date.
4 | * In the cron job this function normally runs in, it is expected that eventObj.date is the same as TODAY_DATE.
5 | */
6 | const eventDate = new Date(eventObj.startTime);
7 | // Create new event
8 | const hours = eventDate.getHours();
9 | const minutes = eventDate.getMinutes();
10 | const seconds = eventDate.getSeconds();
11 | const milliseconds = eventDate.getMilliseconds();
12 |
13 | const yearToday = TODAY_DATE.getFullYear();
14 | const monthToday = TODAY_DATE.getMonth();
15 | const dateToday = TODAY_DATE.getDate();
16 |
17 | const newEventDate = new Date(yearToday, monthToday, dateToday, hours, minutes, seconds, milliseconds);
18 |
19 | const newEndTime = new Date(yearToday, monthToday, dateToday, hours + eventObj.hours, minutes, seconds, milliseconds)
20 |
21 | const eventToCreate = {
22 | name: eventObj.name && eventObj.name,
23 | hacknight: eventObj.hacknight && eventObj.hacknight,
24 | eventType: eventObj.eventType && eventObj.eventType,
25 | description: eventObj.eventDescription && eventObj.eventDescription,
26 | project: eventObj.project && eventObj.project,
27 | date: eventObj.date && newEventDate,
28 | startTime: eventObj.startTime && newEventDate,
29 | endTime: eventObj.endTime && newEndTime,
30 | hours: eventObj.hours && eventObj.hours
31 | }
32 |
33 | if (eventObj.hasOwnProperty("location")) {
34 | eventToCreate.location = {
35 | city: eventObj.location.city ? eventObj.location.city : 'REMOTE',
36 | state: eventObj.location.state ? eventObj.location.state : 'REMOTE',
37 | country: eventObj.location.country ? eventObj.location.country : 'REMOTE'
38 | };
39 | }
40 |
41 | return eventToCreate
42 | };
43 |
44 | module.exports = { generateEventData };
--------------------------------------------------------------------------------
/client/.dockerignore:
--------------------------------------------------------------------------------
1 | .git
2 | .dockerignore
3 | docker-compose*.yml
4 | npm-debug.log*
5 | Dockerfile.*
6 | node_modules
7 | *.sh
8 |
--------------------------------------------------------------------------------
/client/.env.example:
--------------------------------------------------------------------------------
1 | # Contact the VRMS team for the below values
2 | CLIENT_PORT=3000
3 | CLIENT_URL=http://localhost:${CLIENT_PORT}
4 | BACKEND_HOST=localhost
5 | BACKEND_PORT=4000
6 | REACT_APP_PROXY=http://${BACKEND_HOST}:${BACKEND_PORT}
7 | REACT_APP_CUSTOM_REQUEST_HEADER=
8 |
--------------------------------------------------------------------------------
/client/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2017": true
5 | },
6 | "extends": [
7 | "plugin:react/recommended",
8 | "airbnb",
9 | "airbnb-base",
10 | "plugin:prettier/recommended",
11 | "plugin:react-hooks/recommended"
12 | ],
13 | "parserOptions": {
14 | "ecmaFeatures": {
15 | "jsx": true
16 | },
17 | "ecmaVersion": 2017,
18 | "sourceType": "module"
19 | },
20 | "plugins": ["react"],
21 | "rules": {
22 | "react/jsx-filename-extension": [
23 | 1,
24 | {
25 | "extensions": [".js", ".jsx"]
26 | }
27 | ]
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 |
4 | # dependencies
5 | /node_modules
6 | /.pnp
7 | .pnp.js
8 |
9 | # testing
10 | /coverage
11 |
12 | # production
13 | /build
14 |
15 | # misc
16 | .DS_Store
17 | .env
18 | .env.local
19 | .env.development.local
20 | .env.test.local
21 | .env.production.local
22 |
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | package-lock.json
28 |
--------------------------------------------------------------------------------
/client/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true
3 | }
4 |
--------------------------------------------------------------------------------
/client/Dockerfile.client:
--------------------------------------------------------------------------------
1 | FROM node:20-alpine AS client-development
2 | RUN mkdir /srv/client && chown node:node /srv/client
3 | WORKDIR /srv/client
4 | USER node
5 | RUN mkdir -p node_modules
6 | COPY --chown=node:node package.json package.json ./
7 | RUN npm install --silent
8 |
9 | FROM node:20-alpine AS client-builder
10 | USER node
11 | WORKDIR /srv/client
12 | COPY --from=client-development /srv/client/node_modules node_modules
13 | COPY . .
14 | USER root
15 | RUN npm run build
16 |
17 | FROM nginx as client-production
18 | EXPOSE 3000
19 | COPY /nginx/default.conf /etc/nginx/conf.d/default.conf
20 | COPY --from=client-builder /srv/client/build /usr/share/nginx/html/
21 | CMD ["nginx", "-g", "daemon off;"]
22 |
--------------------------------------------------------------------------------
/client/Dockerfile.dev:
--------------------------------------------------------------------------------
1 | FROM node:18-alpine AS client-development
2 | RUN mkdir /srv/client && chown node:node /srv/client
3 | WORKDIR /srv/client
4 | USER node
5 | RUN mkdir -p node_modules
6 | COPY --chown=node:node package.json package.json ./
7 | RUN npm install --silent
8 |
--------------------------------------------------------------------------------
/client/Dockerfile.prod:
--------------------------------------------------------------------------------
1 | FROM node:20-alpine AS node-modules-install
2 | RUN mkdir /srv/client && chown node:node /srv/client
3 | WORKDIR /srv/client
4 | USER node
5 | RUN mkdir -p node_modules
6 | COPY --chown=node:node package.json package.json ./
7 | RUN npm install --no-update-notifier
8 |
9 | FROM node:20-alpine AS client-builder
10 | USER node
11 | WORKDIR /srv/client
12 | COPY --from=node-modules-install /srv/client/node_modules node_modules
13 | COPY . .
14 | USER root
15 | ARG CUSTOM_REQUEST_HEADER nAb3kY-S%qE#4!d
16 | ENV REACT_APP_CUSTOM_REQUEST_HEADER $CUSTOM_REQUEST_HEADER
17 | RUN npm run build
18 |
19 | FROM nginx as client-production
20 | EXPOSE 3000
21 | COPY /nginx/default.conf /etc/nginx/conf.d/default.conf
22 | COPY --from=client-builder /srv/client/build /usr/share/nginx/html/
23 | CMD ["nginx", "-g", "daemon off;"]
24 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
15 |
16 |
17 |
21 |
22 |
31 | VRMS
32 |
33 |
34 |
35 |
36 |
37 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/client/nginx/default.conf:
--------------------------------------------------------------------------------
1 | server {
2 |
3 | listen 3000;
4 | location / {
5 | root /usr/share/nginx/html;
6 | index index.html index.htm;
7 | try_files $uri $uri/ /index.html;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vrms-client",
3 | "version": "0.3.0",
4 | "description": "VRMS Client",
5 | "homepage": "https://www.vrms.io",
6 | "dependencies": {
7 | "@babel/plugin-transform-react-jsx-self": "^7.10.4",
8 | "@emotion/react": "^11.13.3",
9 | "@emotion/styled": "^11.13.0",
10 | "@mui/icons-material": "^5.14.19",
11 | "@mui/material": "^5.16.7",
12 | "@mui/x-date-pickers": "^7.21.0",
13 | "@vitejs/plugin-react": "^4.0.3",
14 | "@vitejs/plugin-react-swc": "^3.3.2",
15 | "classnames": "^2.2.6",
16 | "cross-env": "^7.0.2",
17 | "cross-var": "^1.1.0",
18 | "d3": "^5.15.1",
19 | "dotenv-cli": "^3.2.0",
20 | "http-proxy-middleware": "^2.0.9",
21 | "js-cookie": "^2.2.1",
22 | "local-storage": "^2.0.0",
23 | "mathjs": "^7.5.1",
24 | "minimist": "^1.2.6",
25 | "moment": "^2.29.2",
26 | "moment-recur": "^1.0.7",
27 | "react": "^18.2.0",
28 | "react-datepicker": "^4.16.0",
29 | "react-dom": "^18.2.0",
30 | "react-hook-form": "^7.44.3",
31 | "react-router-dom": "^5.1.2",
32 | "validator": "^13.7.0",
33 | "vite": "^4.5.14",
34 | "vite-plugin-svgr": "^3.2.0"
35 | },
36 | "scripts": {
37 | "vite": "vite",
38 | "dev": "vite",
39 | "start": "dotenv -e .env -e ../backend/.env -- cross-var cross-env PORT=%CLIENT_PORT% vite",
40 | "build": "vite build",
41 | "test": "vitest run",
42 | "preview": "vite preview",
43 | "heroku-postbuild": "npm run build"
44 | },
45 | "eslintConfig": {
46 | "extends": "react-app"
47 | },
48 | "browserslist": {
49 | "production": [
50 | ">0.2%",
51 | "not dead",
52 | "not op_mini all"
53 | ],
54 | "development": [
55 | "last 1 chrome version",
56 | "last 1 firefox version",
57 | "last 1 safari version"
58 | ]
59 | },
60 | "author": "sarL3y",
61 | "license": "ISC",
62 | "devDependencies": {
63 | "@testing-library/dom": "^10.3.2",
64 | "@testing-library/react": "^16.0.0",
65 | "eslint-config-airbnb": "^18.2.0",
66 | "eslint-config-airbnb-base": "^14.2.0",
67 | "eslint-config-prettier": "^6.11.0",
68 | "eslint-plugin-import": "^2.22.0",
69 | "eslint-plugin-jsx-a11y": "^6.3.1",
70 | "eslint-plugin-prettier": "^3.1.4",
71 | "eslint-plugin-react": "^7.20.6",
72 | "eslint-plugin-react-hooks": "^4.1.2",
73 | "jsdom": "^24.1.0",
74 | "prettier": "^2.1.1",
75 | "sass": "^1.49.7",
76 | "vitest": "^1.6.1"
77 | },
78 | "engines": {
79 | "node": "<=18.0.0"
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/client/public/bg-image-pier.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackforla/VRMS/85370ec62ebbb740c47dfb05b1f8b3cbf8830beb/client/public/bg-image-pier.webp
--------------------------------------------------------------------------------
/client/public/bg-image-skyline.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackforla/VRMS/85370ec62ebbb740c47dfb05b1f8b3cbf8830beb/client/public/bg-image-skyline.webp
--------------------------------------------------------------------------------
/client/public/bg-image-sunset.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackforla/VRMS/85370ec62ebbb740c47dfb05b1f8b3cbf8830beb/client/public/bg-image-sunset.webp
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackforla/VRMS/85370ec62ebbb740c47dfb05b1f8b3cbf8830beb/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/public/hflalogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackforla/VRMS/85370ec62ebbb740c47dfb05b1f8b3cbf8830beb/client/public/hflalogo.png
--------------------------------------------------------------------------------
/client/public/logo180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackforla/VRMS/85370ec62ebbb740c47dfb05b1f8b3cbf8830beb/client/public/logo180.png
--------------------------------------------------------------------------------
/client/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackforla/VRMS/85370ec62ebbb740c47dfb05b1f8b3cbf8830beb/client/public/logo192.png
--------------------------------------------------------------------------------
/client/public/logo310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackforla/VRMS/85370ec62ebbb740c47dfb05b1f8b3cbf8830beb/client/public/logo310.png
--------------------------------------------------------------------------------
/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "VRMS",
3 | "name": "Volunteer Relationship Management System",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo180.png",
12 | "type": "image/png",
13 | "sizes": "180x180"
14 | },
15 | {
16 | "src": "logo192.png",
17 | "type": "image/png",
18 | "sizes": "192x192"
19 | },
20 | {
21 | "src": "logo310.png",
22 | "type": "image/png",
23 | "sizes": "310x310"
24 | }
25 | ],
26 | "start_url": ".",
27 | "display": "standalone",
28 | "theme_color": "#000000",
29 | "background_color": "#ffffff"
30 | }
31 |
--------------------------------------------------------------------------------
/client/public/projectleaderdashboard/check.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackforla/VRMS/85370ec62ebbb740c47dfb05b1f8b3cbf8830beb/client/public/projectleaderdashboard/check.png
--------------------------------------------------------------------------------
/client/public/projectleaderdashboard/github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackforla/VRMS/85370ec62ebbb740c47dfb05b1f8b3cbf8830beb/client/public/projectleaderdashboard/github.png
--------------------------------------------------------------------------------
/client/public/projectleaderdashboard/googledrive.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackforla/VRMS/85370ec62ebbb740c47dfb05b1f8b3cbf8830beb/client/public/projectleaderdashboard/googledrive.png
--------------------------------------------------------------------------------
/client/public/projectleaderdashboard/slack.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackforla/VRMS/85370ec62ebbb740c47dfb05b1f8b3cbf8830beb/client/public/projectleaderdashboard/slack.png
--------------------------------------------------------------------------------
/client/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/client/src/App.scss:
--------------------------------------------------------------------------------
1 | .app {
2 | height: 100%;
3 | width: 100vw;
4 | display: flex;
5 | justify-content: center;
6 | align-content: center;
7 | overflow: hidden;
8 | max-height: 90vh;
9 | margin: 5vh 0;
10 | }
11 |
12 | .app-container {
13 | position: relative;
14 | max-width: 500px;
15 | width: 100%;
16 | background-color: white;
17 | overflow: hidden;
18 | border-radius: 10px;
19 | padding: 15px;
20 | }
21 |
22 | .main {
23 | height: calc(90vh - 160px);
24 | overflow-y: scroll;
25 | }
26 |
27 | .flexcenter-container {
28 | display: flex;
29 | flex-direction: column;
30 | justify-content: center;
31 | align-content: center;
32 | text-align: center;
33 | height: 100%;
34 | width: 100%;
35 | }
36 |
37 | .flex-container {
38 | display: flex;
39 | flex-direction: column;
40 | height: 100%;
41 | width: 100%;
42 | }
43 |
44 | @media (max-width: 500px) {
45 | .app-container {
46 | margin: 0 15px;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/client/src/api/EventsApiService.js:
--------------------------------------------------------------------------------
1 | import { REACT_APP_CUSTOM_REQUEST_HEADER } from '../utils/globalSettings';
2 |
3 | class EventsApiService {
4 | constructor() {
5 | this.headers = {
6 | 'Content-Type': 'application/json',
7 | 'x-customrequired-header': REACT_APP_CUSTOM_REQUEST_HEADER,
8 | };
9 | this.baseUrl = '/api/events/';
10 | }
11 |
12 | async fetchEvents() {
13 | try {
14 | const threeWeeksInMilliseconds = 3 * 7 * 24 * 60 * 60 * 1000; // 3 weeks in milliseconds
15 | const threeWeeksAgo = new Date();
16 | threeWeeksAgo.setTime(threeWeeksAgo.getTime() - threeWeeksInMilliseconds);
17 |
18 | const dateQuery = `?date[$gt]=${threeWeeksAgo.toISOString().split('.')[0]+"Z"}`;
19 |
20 | const res = await fetch(this.baseUrl + dateQuery, {
21 | headers: this.headers,
22 | });
23 | return await res.json();
24 | } catch (error) {
25 | console.log(`fetchEvents error: ${error}`);
26 | alert('Server not responding. Please refresh the page.');
27 | return [];
28 | }
29 | }
30 |
31 | async createNewEvent(eventToCreate) {
32 | const requestOptions = {
33 | method: 'POST',
34 | headers: this.headers,
35 | body: JSON.stringify(eventToCreate),
36 | };
37 |
38 | try {
39 | return await fetch(this.baseUrl, requestOptions);
40 | } catch (error) {
41 | console.error(`Add event error: `, error);
42 | alert('Server not responding. Please try again.');
43 | return undefined;
44 | }
45 | }
46 |
47 | async deleteEvent(recurringEventID) {
48 | const requestOptions = {
49 | method: 'DELETE',
50 | headers: this.headers,
51 | };
52 |
53 | try {
54 | return await fetch(`${this.baseUrl}/${recurringEventID}`, requestOptions);
55 | } catch (error) {
56 | console.error(`Delete event error: `, error);
57 | alert('Server not responding. Please try again.');
58 | return undefined;
59 | }
60 | }
61 |
62 | async updateEvent(eventToUpdate, eventID) {
63 | const requestOptions = {
64 | method: 'PATCH',
65 | headers: this.headers,
66 | body: JSON.stringify(eventToUpdate),
67 | };
68 | try {
69 | return await fetch(`${this.baseUrl}${eventID}`, requestOptions);
70 | } catch (error) {
71 | console.error(`Update event error: `, error);
72 | alert('Server not responding. Please try again.');
73 | return undefined;
74 | }
75 | }
76 | }
77 |
78 | export default EventsApiService;
79 |
--------------------------------------------------------------------------------
/client/src/api/RecurringEventsApiService.js:
--------------------------------------------------------------------------------
1 | import { REACT_APP_CUSTOM_REQUEST_HEADER } from '../utils/globalSettings';
2 |
3 | class RecurringEventsApiService {
4 | constructor() {
5 | this.headers = {
6 | 'Content-Type': 'application/json',
7 | 'x-customrequired-header': REACT_APP_CUSTOM_REQUEST_HEADER,
8 | };
9 | this.baseUrl = '/api/recurringEvents/';
10 | }
11 |
12 | async fetchRecurringEvents() {
13 | try {
14 | const res = await fetch(`${this.baseUrl}/internal`, {
15 | headers: this.headers,
16 | });
17 | return await res.json();
18 | } catch (error) {
19 | console.log(`fetchRecurringEvents error: ${error}`);
20 | alert('Server not responding. Please refresh the page.');
21 | return [];
22 | }
23 | }
24 |
25 | async createNewRecurringEvent(eventToCreate) {
26 | const requestOptions = {
27 | method: 'POST',
28 | headers: this.headers,
29 | body: JSON.stringify(eventToCreate),
30 | };
31 |
32 | try {
33 | return await fetch(this.baseUrl, requestOptions);
34 | } catch (error) {
35 | console.error(`Add recurring event error: `, error);
36 | alert('Server not responding. Please try again.');
37 | return undefined;
38 | }
39 | }
40 |
41 | async deleteRecurringEvent(recurringEventID) {
42 | const requestOptions = {
43 | method: 'DELETE',
44 | headers: this.headers,
45 | };
46 |
47 | try {
48 | return await fetch(`${this.baseUrl}/${recurringEventID}`, requestOptions);
49 | } catch (error) {
50 | console.error(`Delete recurring event error: `, error);
51 | alert('Server not responding. Please try again.');
52 | return undefined;
53 | }
54 | }
55 |
56 | async updateRecurringEvent(eventToUpdate, recurringEventID) {
57 | const requestOptions = {
58 | method: 'PATCH',
59 | headers: this.headers,
60 | body: JSON.stringify(eventToUpdate),
61 | };
62 | try {
63 | return await fetch(`${this.baseUrl}/${recurringEventID}`, requestOptions);
64 | } catch (error) {
65 | console.error(`Update recurring event error: `, error);
66 | alert('Server not responding. Please try again.');
67 | return undefined;
68 | }
69 | }
70 | }
71 |
72 | export default RecurringEventsApiService;
73 |
--------------------------------------------------------------------------------
/client/src/api/auth.js:
--------------------------------------------------------------------------------
1 | import { REACT_APP_CUSTOM_REQUEST_HEADER } from '../utils/globalSettings';
2 |
3 | const BASE_URL = '/api/auth';
4 |
5 | const DEFAULT_HEADERS = {
6 | 'x-customrequired-header': REACT_APP_CUSTOM_REQUEST_HEADER
7 | };
8 |
9 | export const fetchLogout = async () => {
10 | return await fetch(BASE_URL + '/logout', {
11 | method: 'POST',
12 | headers: DEFAULT_HEADERS,
13 | });
14 | }
15 |
--------------------------------------------------------------------------------
/client/src/common/datepicker/index.scss:
--------------------------------------------------------------------------------
1 | .datepicker-section {
2 | color: #000000;
3 | .datepicker-header {
4 | font-size: 15px;
5 | font-weight: bold;
6 | }
7 | }
8 |
9 | .datepicker-wrap {
10 | display: block;
11 | max-width: 100%;
12 | text-align: center;
13 |
14 | .datepicker-name {
15 | display: inline-flex;
16 | line-height: 1;
17 | width: 40px;
18 | max-width: 40px;
19 | }
20 |
21 | .react-datepicker-wrapper {
22 | .react-datepicker__input-container{
23 | input[type=text] {
24 | max-width: 242px;
25 | min-width: 242px;
26 | width: 242px;
27 | height: auto;
28 | color: #000000;
29 | font-size: 14px;
30 | font-weight: normal;
31 | line-height: 1;
32 | margin: 0;
33 | border-bottom: 1px solid #000000;
34 | text-align: center;
35 | &:focus {
36 | border-bottom: 1px solid #fa114f;
37 | }
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/client/src/common/tabs/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import "./index.scss";
3 |
4 | const TabsContainer = (props) => {
5 |
6 | const [activeTab, selectTab] = useState(props.active);
7 |
8 | function handleChange(index) {
9 | selectTab(index);
10 | }
11 |
12 | return (
13 |
14 |
15 | {props.children.map((elem, index) => {
16 | let style = index === activeTab ? "selected" : "";
17 | return (
18 | -
23 | {elem.props.title}
24 |
25 | );
26 | })}
27 |
28 |
{props.children[activeTab]}
29 |
30 | );
31 | };
32 |
33 | export default TabsContainer;
34 |
--------------------------------------------------------------------------------
/client/src/common/tabs/index.scss:
--------------------------------------------------------------------------------
1 | .tab-header{
2 | display: flex;
3 | justify-content: center;
4 | list-style: none;
5 | padding: 0;
6 | margin-bottom: 0;
7 | li{
8 | width: 50%;
9 | text-align: center;
10 | font-size: 15px;
11 | line-height: 1;
12 | margin-left: 0;
13 | padding: 10px;
14 | border-bottom: 2px solid #3D5A6C;
15 | transition: all .3s;
16 | font-weight: 300;
17 | cursor: pointer;
18 | &.selected{
19 | border-bottom: 2px solid #3D5A6C;
20 | color: #FFFFFF;
21 | font-weight: bold;
22 | background: #3D5A6C;
23 | border-radius: 10px 10px 0 0;
24 | }
25 | }
26 | }
27 |
28 | .tab{
29 | width: 100%;
30 | padding: 10px 0;
31 | font-family: 'Open Sans', sans-serif;
32 | color: #444;
33 | }
34 |
--------------------------------------------------------------------------------
/client/src/common/tabs/tab.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import "./index.scss";
3 |
4 | const Tab = (props) => {
5 | return(
6 | {props.children}
7 | )
8 | };
9 |
10 | export default Tab;
11 |
--------------------------------------------------------------------------------
/client/src/components/ChangesModal.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | Modal,
4 | Box,
5 | Typography,
6 | Grid
7 | } from '@mui/material'
8 | import { StyledButton } from './ProjectForm';
9 | import { Link } from 'react-router-dom';
10 | import WarningAmberRoundedIcon from '@mui/icons-material/WarningAmberRounded';
11 |
12 |
13 | const style = {
14 | position: 'absolute',
15 | top: '50%',
16 | left: '50%',
17 | transform: 'translate(-50%, -50%)',
18 | width: 400,
19 | bgcolor: 'background.paper',
20 | border: '2px solid #000',
21 | boxShadow: 24,
22 | p: 4,
23 | };
24 |
25 |
26 | export default function ChangesModal({open, onClose, handleClose, destination }) {
27 | return (
28 |
29 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | Wait! You made some changes.
42 |
43 |
44 | Are you sure you want to exit without saving?
45 |
46 |
47 |
48 |
55 | Yes
56 |
57 |
58 |
59 |
66 | No
67 |
68 |
69 |
70 |
71 |
72 | )
73 | }
--------------------------------------------------------------------------------
/client/src/components/DashboardUsers.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 |
3 | import '../sass/DashboardUsers.scss';
4 |
5 | const DashboardUsers = (props) => {
6 | const [users, setUsers] = useState(null);
7 |
8 | async function fetchData() {
9 | try {
10 | const res = await fetch("/api/users");
11 | const resJson = await res.json();
12 | setUsers(resJson);
13 | } catch(error) {
14 | alert(error);
15 | }
16 | console.log(users);
17 | }
18 |
19 | useEffect(() => {
20 | fetchData();
21 | }, []);
22 |
23 | return (
24 |
25 |
26 |
27 | {users !== null && users.map((user, index) => (
28 | -
29 |
30 |
31 |
{user.name.firstName} {user.name.lastName}
32 |
33 |
34 |
Current Role: {user.currentRole}
35 |
Desired Role: {user.desiredRole}
36 |
37 |
38 |
39 | ))}
40 |
41 |
42 |
43 | )
44 | };
45 |
46 | export default DashboardUsers;
47 |
--------------------------------------------------------------------------------
/client/src/components/ErrorContainer.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import "../sass/ErrorContainer.scss";
4 |
5 | export function ErrorContainer({ className, ...props }) {
6 | return {props.children}
;
7 | }
8 |
--------------------------------------------------------------------------------
/client/src/components/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Redirect } from 'react-router-dom';
3 |
4 | import pkg from '../../package.json';
5 | import useAuth from '../hooks/useAuth';
6 | import { Button, Box, Typography } from '@mui/material';
7 |
8 | import '../sass/Footer.scss';
9 |
10 | const Footer = () => {
11 | const { auth, logout } = useAuth();
12 |
13 | const handleLogout = async (e) => {
14 | e.preventDefault();
15 | await logout();
16 | return ;
17 | };
18 |
19 | return (
20 | //
21 |
26 | {`v${pkg.version} "Alpha"`}
30 |
31 | {auth?.user && (
32 |
43 | {`Hi ${auth.user.name.firstName}`}
52 |
59 |
60 | )}
61 |
62 | );
63 | };
64 |
65 | export default Footer;
66 |
--------------------------------------------------------------------------------
/client/src/components/Form.jsx:
--------------------------------------------------------------------------------
1 | // Form.jsx contains several unused components, including abstractions for button and form elements.
2 | // They are not currently being used in the codebase.
3 |
4 | import React from "react";
5 |
6 | import "../sass/Form.scss";
7 |
8 | /***********************************************
9 | * LABEL
10 | ***********************************************/
11 | export function Label({ className, isRadioParent, ...props }) {
12 | return ;
13 | }
14 |
15 | /***********************************************
16 | * INPUT TYPES
17 | ***********************************************/
18 | export const Input = React.forwardRef(({ className, ...props }, ref) => {
19 | if (props.type === "radio") {
20 | return ;
21 | } else {
22 | return (
23 |
29 | );
30 | }
31 | });
32 |
33 | export function Textarea({ className, ...props }) {
34 | return ;
35 | }
36 |
37 | export const Select = React.forwardRef(({ className, ...props }, ref) => {
38 | return (
39 |
45 | );
46 | });
47 |
48 | export const Option = React.forwardRef(({ className, ...props }, ref) => {
49 | return ;
50 | });
51 |
52 | export const OptionPlaceholder = React.forwardRef(
53 | ({ className, ...props }, ref) => {
54 | return (
55 |
63 | );
64 | }
65 | );
66 |
67 | /***********************************************
68 | * BUTTONS
69 | ***********************************************/
70 |
71 | export const SecondaryButton = React.forwardRef(
72 | ({ className, ...props }, ref) => {
73 | return ;
74 | }
75 | );
76 |
77 | export const AuxiliaryButton = React.forwardRef(
78 | ({ className, ...props }, ref) => {
79 | return (
80 |
86 | );
87 | }
88 | );
89 |
--------------------------------------------------------------------------------
/client/src/components/Header.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "../sass/Headers.scss";
3 |
4 | export function HeaderBarTextOnly({ className, children, ...props }) {
5 | return (
6 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/client/src/components/Leaderboard.test.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import LeaderBoard from "./Leaderboard";
3 | import { render } from "@testing-library/react";
4 | import { test, expect } from 'vitest';
5 |
6 | test("renders without crashing", () => {
7 | const { container } = render();
8 | expect(container).toMatchSnapshot();
9 | });
--------------------------------------------------------------------------------
/client/src/components/ReadyEvents.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { REACT_APP_CUSTOM_REQUEST_HEADER as headerToSend} from "../utils/globalSettings";
4 |
5 | import '../sass/ReadyEvents.scss';
6 |
7 | const ReadyEvents = (props) => {
8 | const [isLoading, setIsLoading] = useState(false);
9 | const [events, setEvents] = useState([]);
10 |
11 | useEffect(() => {
12 | async function fetchEvent() {
13 | try {
14 | setIsLoading(true);
15 | const res = await fetch("/api/events?checkInReady=true", {
16 | headers: {
17 | "x-customrequired-header": headerToSend
18 | }
19 | });
20 | const resJson = await res.json();
21 |
22 | setEvents(resJson);
23 | setIsLoading(false);
24 | } catch(error) {
25 | console.log(error);
26 | setIsLoading(false);
27 | }
28 | }
29 |
30 | fetchEvent();
31 | }, []);
32 |
33 | return (
34 |
35 |
Events to check-in for below:
36 | {isLoading ?
Loading...
: (
37 |
38 | {events.length > 0 ? (events.map((event, index) => (
39 |
40 |
41 |
{event.name}
42 |
{event.date}
43 |
{event.location.city}
44 |
{event.location.state}
45 |
46 |
47 | {props.newUser &&
48 |
New User Check-In
49 | }
50 |
51 | {props.returningUser &&
52 |
Returning User Check-In
53 | }
54 |
55 | ))) : (
56 |
Check back later...
57 | )}
58 |
59 | )}
60 |
61 | )
62 | };
63 |
64 | export default ReadyEvents;
65 |
--------------------------------------------------------------------------------
/client/src/components/__snapshots__/Leaderboard.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`renders without crashing 1`] = `
4 |
7 |
10 |
13 | Volunteer Leaderboard
14 |
15 |
16 |
21 |
22 | `;
23 |
--------------------------------------------------------------------------------
/client/src/components/__snapshots__/Leaderboard.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`renders without crashing 1`] = `
4 |
24 | `;
25 |
--------------------------------------------------------------------------------
/client/src/components/admin/dashboard/index.scss:
--------------------------------------------------------------------------------
1 | .admin-dashboard-wrap {
2 | padding: 10px;
3 | font-family: 'Open Sans', sans-serif;
4 | }
5 |
6 | .admin-header {
7 | color: #000000;
8 | margin: 20px auto;
9 | h1{
10 | font-size: 24px;
11 | line-height: 1;
12 | text-align: center;
13 | font-weight: 600;
14 | letter-spacing: normal;
15 | }
16 | }
17 |
18 | .event-header {
19 | font-size: 17px;
20 | font-weight: bold;
21 | margin: 15px 0 5px;
22 | }
23 |
24 | .admin-upcoming-event{
25 | margin: 0 0 35px;
26 |
27 | .dashboard-chart-container img{
28 | max-width: 90px;
29 | }
30 |
31 | .warning-event {
32 | padding: 0;
33 | margin: 0;
34 | font-size: 14px;
35 | .event-name {
36 | font-size: 15px;
37 | font-weight: bold;
38 | }
39 | .event-info{
40 | font-size: 14px;
41 | }
42 | }
43 |
44 | .checkin-toggle {
45 | color: #3D5A6C;
46 | background-color: #FFFFFF;
47 | border: 1px solid #3D5A6C;
48 | border-radius: 20px;
49 | line-height: 1;
50 | padding: 10px 20px;
51 | margin: 0 auto;
52 | transition: .3s ease;
53 | max-width: 170px;
54 | min-width: 170px;
55 | height: auto;
56 | &:hover{
57 | background-color: #3D5A6C;
58 | color: #FFFFFF;
59 | }
60 | }
61 | }
62 |
63 | .stat-select {
64 | select {
65 | max-width: 160px;
66 | min-width: 160px;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/client/src/components/admin/donutChart.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from "react";
2 | import * as d3 from "d3";
3 |
4 | const DonutChart = props => {
5 | const ref = useRef(null);
6 |
7 | const createPie = d3
8 | .pie()
9 | .value(d => d.value)
10 | .sort(null);
11 |
12 | const createArc = d3
13 | .arc()
14 | .innerRadius(props.innerRadius)
15 | .outerRadius(props.outerRadius);
16 |
17 | const format = d3.format(".2f");
18 |
19 | useEffect(() => {
20 | const data = createPie(props.data);
21 | const group = d3.select(ref.current);
22 | const groupWithData = group.selectAll("g.arc").data(data);
23 |
24 | groupWithData.exit().remove();
25 |
26 | const groupWithUpdate = groupWithData
27 | .enter()
28 | .append("g")
29 | .attr("class", "arc");
30 |
31 | const path = groupWithUpdate
32 | .append("path")
33 | .merge(groupWithData.select("path.arc"));
34 |
35 | path
36 | .attr("class", "arc")
37 | .attr("d", createArc)
38 | .attr("fill", (d, i) => {
39 | const { data } = d;
40 | return data.color;
41 | });
42 | }, [props.data]);
43 |
44 | return (
45 |
46 |
52 |
53 | );
54 | };
55 |
56 | export default DonutChart;
57 |
--------------------------------------------------------------------------------
/client/src/components/admin/donutChartLoading.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Box, Container, CircularProgress, Typography } from '@mui/material';
3 | import '../../sass/Dashboard.scss';
4 |
5 | const DonutChartLoading = (props) => (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 |
19 | export default DonutChartLoading;
20 |
--------------------------------------------------------------------------------
/client/src/components/admin/eventOverview.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const eventOverview = props => {
4 | let eventsByType = [];
5 | for (let key in props.chartTypes) {
6 | eventsByType.push(key);
7 | }
8 |
9 | return (
10 |
41 | );
42 | };
43 | export default eventOverview;
44 |
--------------------------------------------------------------------------------
/client/src/components/admin/reports/index.scss:
--------------------------------------------------------------------------------
1 | .admin-table-report{
2 | margin: 10px 0;
3 | font-size: 15px;
4 | text-align: center;
5 | }
6 |
7 | .stats-section{
8 | margin-top: 10px;
9 | }
10 |
11 | .table-header{
12 | font-size: 15px;
13 | font-weight: bold;
14 | background-color: #3d5a6c47;
15 | margin: 20px 0 5px;
16 | padding: 5px;
17 | border-radius: 7px 7px 0 0;
18 | &.m-t-small{
19 | margin-top: 10px;
20 | }
21 | }
22 |
23 | .admin-table{
24 | width: 100%;
25 | margin-top: 10px;
26 | border-collapse: collapse;
27 | th, td{
28 | text-align: center;
29 | height: 30px;
30 | }
31 | tr:last-child{
32 | td{
33 | color: #3D5A6C;
34 | font-weight: bold;
35 | border-top: 1px solid lightgray;
36 | }
37 | }
38 | }
39 |
40 | .filter-button{
41 | color: #3D5A6C;
42 | border: 1px solid lightgray;
43 | border-radius: 20px;
44 | line-height: 1;
45 | padding: 10px 20px;
46 | margin: 0 auto;
47 | transition: .3s ease;
48 | max-width: 120px;
49 | min-width: 120px;
50 | height: auto;
51 | font-family: 'Open Sans', sans-serif;
52 | font-size: 15px;
53 | &.calc-button{
54 | max-width: 150px;
55 | min-width: 150px;
56 | }
57 | &:hover{
58 | background-color: #3d5a6c47;
59 | }
60 | }
61 |
62 | .dashboard-chart-container img{
63 | max-width: 90px;
64 | }
65 |
66 | .time-description{
67 | font-size: 13px;
68 | text-align: right;
69 | margin-top: 10px;
70 | }
71 |
--------------------------------------------------------------------------------
/client/src/components/auth/HandleAuth.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { Redirect } from 'react-router-dom';
3 | import { isValidToken } from '../../services/user.service';
4 | import { authLevelRedirect } from '../../utils/authUtils';
5 |
6 | import '../../sass/MagicLink.scss';
7 | import useAuth from '../../hooks/useAuth';
8 |
9 | const HandleAuth = (props) => {
10 | const { auth, refreshAuth } = useAuth();
11 | const [isMagicLinkValid, setMagicLink] = useState(null);
12 | const [isLoaded, setIsLoaded] = useState(false);
13 |
14 | // Step 1: Validate token from query string
15 | useEffect(() => {
16 | const search = props.location.search;
17 | const params = new URLSearchParams(search);
18 | const api_token = params.get('token');
19 |
20 | if (!api_token) return;
21 | isValidToken(api_token).then((isValid) => {
22 | setMagicLink(isValid);
23 | });
24 | }, [props.location.search]);
25 |
26 | // Step 2: Refresh user auth (requires valid Magic Link)
27 | useEffect(() => {
28 | if (!isMagicLinkValid) return;
29 | if (!auth?.isError) return;
30 |
31 | refreshAuth();
32 | }, [isMagicLinkValid, refreshAuth, auth]);
33 |
34 | // Step 3: Set IsLoaded value to render Component
35 | useEffect(() => {
36 | if (!isMagicLinkValid) {
37 | setIsLoaded(true);
38 | return;
39 | }
40 |
41 | if (!auth || auth.isError) return;
42 |
43 | setIsLoaded(true);
44 | }, [isMagicLinkValid, setIsLoaded, auth]);
45 |
46 | if (!isLoaded) return Loading...
;
47 |
48 | const Delayed = ({ children, waitBeforeShow = 500 }) => {
49 | const [isShown, setIsShown] = useState(false);
50 | useEffect(() => {
51 | const timer = setTimeout(() => {
52 | setIsShown(true);
53 | }, waitBeforeShow);
54 |
55 | return () => clearTimeout(timer);
56 | }, [waitBeforeShow]);
57 |
58 | return isShown ? children : null;
59 | };
60 |
61 | let loginRedirect = '';
62 |
63 | if (auth?.user) {
64 | loginRedirect = authLevelRedirect(auth.user);
65 | }
66 |
67 | return (
68 |
69 |
70 | Sorry, the link is not valid anymore.
71 |
72 | {auth?.user &&
/* Redirect to /welcome */}
73 |
74 | );
75 | };
76 |
77 | export default HandleAuth;
78 |
--------------------------------------------------------------------------------
/client/src/components/dashboard/AddTeamMember.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import "../../sass/AddTeamMember.scss";
3 |
4 | const AddTeamMember = (props) => {
5 | const [email, setEmail] = useState("");
6 |
7 | const handleInputChange = (e) => setEmail(e.currentTarget.value);
8 |
9 | return (
10 |
11 |
12 |
42 | {props.isSuccess ?
User Added
: null}
43 |
{props.isError ? props.errorMessage : null}
44 |
45 |
46 | );
47 | };
48 |
49 | export default AddTeamMember;
50 |
--------------------------------------------------------------------------------
/client/src/components/dashboard/AttendeeTableRow.jsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from "react";
2 | import styles from "../../sass/ProjectLeaderDashboard.module.scss";
3 | import DashboardButton from "./DashboardButton";
4 |
5 | const AttendeeTableRow = ({ name, role, isProjectTeamMember, postUser }) => {
6 | let here = null;
7 |
8 | if (isProjectTeamMember) {
9 | here = Yes
10 | } else {
11 | here = Add To Roster
12 | }
13 |
14 | return (
15 |
16 |
19 |
20 | {role}
21 |
22 | {here}
23 |
24 | );
25 | };
26 |
27 | export default AttendeeTableRow;
28 |
--------------------------------------------------------------------------------
/client/src/components/dashboard/DashboardButton.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styles from "../../sass/ProjectLeaderDashboard.module.scss";
3 |
4 | const DashboardButton = (props) => {
5 | return (
6 |
9 | );
10 | };
11 |
12 | export default DashboardButton;
13 |
--------------------------------------------------------------------------------
/client/src/components/dashboard/ProjectInfo.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const ProjectInfo = ({ project }) => {
4 | return (
5 |
6 |
7 | {project.projectId.name}
8 |
9 |
10 | Project Leader Dashboard
11 |
12 |
13 | );
14 | };
15 |
16 | export default ProjectInfo;
--------------------------------------------------------------------------------
/client/src/components/dashboard/RosterTableRow.jsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import styles from '../../sass/ProjectLeaderDashboard.module.scss';
3 |
4 | const RosterTableRow = ({
5 | name,
6 | role,
7 | isNewMember,
8 | gDriveClicked,
9 | gitHubClicked,
10 | services,
11 | }) => {
12 | // see icons attr. note @ bottom
13 | const checkmark = (
14 |
19 | );
20 | const gitHubIcon = (
21 |
26 | );
27 | const googleDriveIcon = (
28 |
{
33 | gDriveClicked();
34 | }}
35 | />
36 | );
37 |
38 | const slackIcon = (
39 |
44 | );
45 |
46 | let here = (
47 |
48 |
{slackIcon}
49 | {services.googleDrive ? (
50 |
{checkmark}
51 | ) : (
52 |
{googleDriveIcon}
53 | )}
54 | {services.gitHub ? (
55 |
{checkmark}
56 | ) : (
57 |
{gitHubIcon}
58 | )}
59 |
60 | );
61 |
62 | return (
63 |
64 |
67 |
68 | {role}
69 |
70 | {here}
71 |
72 | );
73 | };
74 |
75 | export default RosterTableRow;
76 |
77 | // will eventually need to attribute icons:
78 | // checkmark
79 | // Icons made by Kiranshastry from www.flaticon.com
82 | // Google Drive
83 | // Icons made by Pixel perfect from www.flaticon.com
84 |
--------------------------------------------------------------------------------
/client/src/components/manageProjects/addProject.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ProjectForm from '../ProjectForm';
3 | import { simpleInputs } from '../data';
4 |
5 | function addProject({auth}) {
6 | return (
7 |
16 | );
17 | }
18 |
19 | export default addProject;
20 |
--------------------------------------------------------------------------------
/client/src/components/manageProjects/editableField.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef, useEffect } from 'react';
2 | import { Box, Button, TextareaAutosize, TextField, Typography } from '@mui/material';
3 | import '../../sass/ManageProjects.scss';
4 |
5 | const EditableField = ({
6 | fieldData,
7 | fieldName,
8 | updateProject,
9 | fieldType = 'text',
10 | fieldTitle,
11 | accessLevel,
12 | canEdit = ['admin', 'superadmin'],
13 | }) => {
14 | const [fieldValue, setFieldValue] = useState(fieldData);
15 | const [editable, setEditable] = useState(false);
16 | const [notRestricted] = useState(canEdit.includes(accessLevel));
17 | const ref = useRef();
18 |
19 | // Update the displayed results to match the change just made to the db
20 | useEffect(() => {
21 | if (editable) {
22 | ref.current.focus();
23 | }
24 | }, [editable]);
25 |
26 | const inputProps = {
27 | ref,
28 | className: 'editable-field',
29 | onBlur: () => {
30 | setEditable(false);
31 | updateProject(fieldName, fieldValue);
32 | },
33 | onChange: ({ target }) => {
34 | setFieldValue(target.value);
35 | const onEnterKey = ({ keyCode }) => {
36 | if (keyCode === 13) {
37 | target.removeEventListener('keydown', onEnterKey);
38 | target.blur();
39 | }
40 | };
41 | target.addEventListener('keydown', onEnterKey);
42 | },
43 | value: fieldValue,
44 | };
45 |
46 | return (
47 | // this button will be disabled if user !admin
48 |
49 |
50 |
53 | {fieldTitle}
54 |
55 | {notRestricted &&
56 |
66 | }
67 |
68 |
69 | {editable ? (
70 | <>
71 | {fieldType === 'textarea' ? (
72 | /* eslint-disable react/jsx-props-no-spreading */
73 |
74 | ) : (
75 |
76 | /* eslint-enable react/jsx-props-no-spreading */
77 | )}
78 | >
79 | ) : (
80 | {fieldData}
81 | )}
82 |
83 | );
84 | };
85 |
86 | export default EditableField;
87 |
--------------------------------------------------------------------------------
/client/src/components/manageProjects/editableMeeting.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import EventForm from './eventForm';
3 | import { Box, Button } from '@mui/material';
4 | import '../../sass/ManageProjects.scss';
5 |
6 | const EditableMeeting = ({
7 | eventId,
8 | eventName,
9 | eventDescription = '',
10 | eventType,
11 | eventDayNumber,
12 | eventStartTime,
13 | eventEndTime,
14 | eventDuration,
15 | handleEventUpdate,
16 | handleEventDelete,
17 | formErrors,
18 | videoConferenceLink = '',
19 | }) => {
20 | // *** Initialization Station ***
21 | const initialUpdateFormValues = {
22 | name: `${eventName}`,
23 | description: `${eventDescription}`,
24 | eventType: `${eventType}`,
25 | day: `${eventDayNumber}`,
26 | startTime: `${eventStartTime}`,
27 | endTime: `${eventEndTime}`,
28 | duration: `${eventDuration}`,
29 | videoConferenceLink: `${videoConferenceLink}`,
30 | };
31 |
32 | // One state to rule them all
33 | const [formValues, setFormValues] = useState(initialUpdateFormValues);
34 |
35 | // *** Helper functions ***
36 |
37 | // Handle form input changes
38 | const handleInputChange = (event) => {
39 | setFormValues({ ...formValues, [event.target.name]: event.target.value });
40 | };
41 |
42 | // Handle Clicks
43 | const handleResetEvent = () => () => {
44 | setFormValues(initialUpdateFormValues);
45 | };
46 |
47 | return (
48 |
54 |
55 |
67 |
74 |
81 |
82 |
83 | );
84 | };
85 |
86 | export default EditableMeeting;
87 |
--------------------------------------------------------------------------------
/client/src/components/manageProjects/selectProject.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import '../../sass/ManageProjects.scss';
4 |
5 | import { Button, Typography } from '@mui/material';
6 |
7 | const SelectProject = ({ projects, accessLevel, user }) => {
8 | // If access level is 'admin', display all active projects.
9 | // If access level is 'user' display user managed projects.
10 | const managedProjects = projects
11 | ?.filter((proj) => {
12 | if (accessLevel === 'admin' || accessLevel === 'superadmin') {
13 | return proj.projectStatus === 'Active';
14 | }
15 |
16 | // accessLevel is user
17 | // eslint-disable-next-line no-underscore-dangle
18 | return user?.managedProjects.includes(proj._id);
19 | })
20 | .sort((a, b) => a.name?.localeCompare(b.name))
21 | .map((p) => (
22 | // eslint-disable-next-line no-underscore-dangle
23 |
24 |
25 | {p.name ? p.name : '[unnamed project]'}
26 |
27 |
28 | ));
29 |
30 | return (
31 |
32 |
Project Management
33 |
34 | {accessLevel === 'admin' || accessLevel === 'superadmin' && (
35 |
36 | {' '}
37 |
40 |
41 | )}
42 |
43 |
44 |
45 | );
46 | };
47 |
48 | export default SelectProject;
49 |
--------------------------------------------------------------------------------
/client/src/components/manageProjects/utilities/addDurationToTime.js:
--------------------------------------------------------------------------------
1 | export const addDurationToTime = (startTimeDate, duration) => {
2 | // Create the endTime by adding seconds based on the selected duration to the timestamp and converting it back to date
3 | if(startTimeDate && duration){
4 | return new Date(startTimeDate.getTime() + (Number(duration)*3600000))
5 | } else {
6 | throw new Error('Error: Cannot calculate endTime.')
7 | }
8 | };
--------------------------------------------------------------------------------
/client/src/components/manageProjects/utilities/findNextDayOccuranceOfDay.js:
--------------------------------------------------------------------------------
1 | export const findNextOccuranceOfDay = (dayOfTheWeek) => {
2 | /***
3 | This takes the number of the day of the week - with Sunday
4 | being day 0 - and returns a Data object with the next
5 | occurance of that day
6 | ***/
7 |
8 | let day = parseInt(dayOfTheWeek);
9 | const date = new Date();
10 | date.setDate(date.getDate() + ((7 - date.getDay()) % 7 + day) % 7);
11 |
12 | return date;
13 | }
14 |
--------------------------------------------------------------------------------
/client/src/components/manageProjects/utilities/readableEvent.js:
--------------------------------------------------------------------------------
1 | const readableEvent = (e) => {
2 | // This translates the data from the database into human
3 | // readable format that can be displayed
4 |
5 | // Get date for each of the parts of the event time/day
6 | const d = new Date(e.date);
7 | const start = new Date(e.startTime);
8 | const end = new Date(e.endTime);
9 |
10 | // Get day of the week. (Get the day number for sorting)
11 | const options = { weekday: 'long' };
12 | const dayOfTheWeek = Intl.DateTimeFormat('en-US', options).format(d);
13 | const dayOfTheWeekNumber = d.getDay();
14 |
15 | // Convert end time from 24 to 12 and make pretty
16 | const sHours = start.getHours();
17 | const startHours = sHours % 12 || 12;
18 | const startMinutes =
19 | (start.getMinutes() < 10 ? '0' : '') + start.getMinutes();
20 | const startAorP = sHours >= 12 ? 'pm' : 'am';
21 | const startTime = `${startHours}:${startMinutes}${startAorP}`;
22 |
23 | // Convert end time from 24 to 12 and make pretty
24 | const eHours = end.getHours();
25 | const endHours = eHours % 12 || 12;
26 | const endMinutes = (end.getMinutes() < 10 ? '0' : '') + end.getMinutes();
27 | const endAorP = eHours >= 12 ? 'pm' : 'am';
28 | const endTime = `${endHours}:${endMinutes}${endAorP}`;
29 |
30 | // Create readable object for this event
31 | const newEvent = {
32 | name: e.name,
33 | description: e.description,
34 | eventType: e.eventType,
35 | dayOfTheWeekNumber,
36 | dayOfTheWeek,
37 | startTime,
38 | endTime,
39 | duration: e.hours,
40 | // eslint-disable-next-line no-underscore-dangle
41 | event_id: e._id,
42 | videoConferenceLink: e.videoConferenceLink,
43 | };
44 | return newEvent;
45 | };
46 |
47 | export default readableEvent;
48 |
--------------------------------------------------------------------------------
/client/src/components/manageProjects/utilities/tests/addDurationToTime.test.js:
--------------------------------------------------------------------------------
1 | // const {addDurationToTime} = require('../addDurationToTime')
2 | import {addDurationToTime} from '../addDurationToTime'
3 | import {test, expect} from 'vitest'
4 |
5 | test('adds inputted "0.5" duration to inputted time', () => {
6 | const inputDuration = "0.5"
7 | const inputTime = new Date()
8 | const outputTime = new Date(inputTime.getTime() + (.5*3600000))
9 |
10 | expect(addDurationToTime(inputTime, inputDuration)).toEqual(outputTime)
11 | })
12 |
13 | test('adds inputted ".5" duration to inputted time', () => {
14 | const inputDuration = ".5"
15 | const inputTime = new Date()
16 | const outputTime = new Date(inputTime.getTime() + (.5*3600000))
17 |
18 | expect(addDurationToTime(inputTime, inputDuration)).toEqual(outputTime)
19 | })
20 |
21 | test('adds inputted "3" hour duration to inputted time', () => {
22 | const inputDuration = "3"
23 | const inputTime = new Date()
24 | const outputTime = new Date(inputTime.getTime() + (3*3600000))
25 |
26 | expect(addDurationToTime(inputTime, inputDuration)).toEqual(outputTime)
27 | })
28 |
29 | test('throws an error when there is no valid input', () => {
30 |
31 | expect(addDurationToTime).toThrow(new Error('Error: Cannot calculate endTime.'))
32 | })
--------------------------------------------------------------------------------
/client/src/components/manageProjects/utilities/timeConvertFromForm.js:
--------------------------------------------------------------------------------
1 | export const timeConvertFromForm = (dateObject, startTime) => {
2 | // This takes the time from the form and a
3 | // Data object and adds the time to the object
4 |
5 | // reconstitute time from form to timestamp
6 | const timeParts = startTime.split(':');
7 | const sap = timeParts[1].slice(-2);
8 | let startHour = parseInt(timeParts[0]);
9 | const startMinutes = parseInt(timeParts[1].slice(0,-2));
10 | const startSeconds = 0;
11 |
12 | // set 12am to 0 and make afternoon into military time
13 | if (sap === 'pm' && startHour !== 12) {
14 | startHour = startHour + 12;
15 | } else if (sap === 'am' && startHour === 12) {
16 | startHour = 0;
17 | }
18 |
19 | // Update the date string with the start hours of the meeting
20 | dateObject.setHours(startHour);
21 | dateObject.setMinutes(startMinutes);
22 | dateObject.setSeconds(startSeconds);
23 |
24 | const newDate = new Date(dateObject.getTime());
25 | return newDate;
26 | };
27 |
--------------------------------------------------------------------------------
/client/src/components/manageProjects/utilities/validateEditableField.js:
--------------------------------------------------------------------------------
1 | export const validateEditableField = (fieldName, fieldValue) => {
2 | switch (fieldName) {
3 | case 'hflaWebsiteUrl':
4 | return doesLinkContainFlex(fieldValue, 'hackforla.org');
5 | case 'slackUrl':
6 | return doesLinkContainFlex(fieldValue, 'slack.com');
7 | case 'googleDriveUrl':
8 | return doesLinkContainFlex(fieldValue, 'drive.google.com');
9 | case 'githubUrl':
10 | return doesLinkContainFlex(fieldValue, 'github.com');
11 | case 'description':
12 | return typeof fieldValue === 'string' && fieldValue.length <= 250;
13 | default:
14 | break;
15 | }
16 | return true;
17 | };
18 |
19 | export const generateErrorEditableField = (fieldName) => {
20 | switch (fieldName) {
21 | case 'hflaWebsiteUrl':
22 | case 'slackUrl':
23 | case 'googleDriveUrl':
24 | case 'githubUrl':
25 | return `Invalid field value for ${fieldName}`;
26 | case 'description':
27 | return 'Description is too long, max 250 characters allowed';
28 | default:
29 | break;
30 | }
31 | };
32 |
33 | const doesLinkContainFlex = (link, key) => {
34 | if (link.startsWith(`https://${key}`)) return true;
35 | if (link.startsWith(`https://www.${key}`)) return true;
36 | if (link.startsWith(key)) return true;
37 | return false;
38 | };
39 |
--------------------------------------------------------------------------------
/client/src/components/manageProjects/utilities/validateEventForm.js:
--------------------------------------------------------------------------------
1 | import { isWordInArrayInString } from './../../../utils/stringUtils.js';
2 | import { eventNameBlacklistArr } from '../../../utils/blacklist.js';
3 |
4 | const validateEventForm = (vals, projectToEdit) => {
5 | let newErrors = {};
6 | Object.keys(vals).forEach((key) => {
7 | let blacklistedStrings = isWordInArrayInString(
8 | eventNameBlacklistArr,
9 | vals[key].toLowerCase()
10 | );
11 | switch (key) {
12 | case 'name':
13 | // Required
14 | if (!vals[key]) {
15 | newErrors = { ...newErrors, name: 'Event name is required' };
16 | } else if (blacklistedStrings) {
17 | newErrors = {
18 | ...newErrors,
19 | name: `Event name cannot contain: ${blacklistedStrings.join(', ')}`,
20 | };
21 | } else if (
22 | isWordInArrayInString(
23 | [projectToEdit.name.toLowerCase()],
24 | vals[key].toLowerCase()
25 | )
26 | ) {
27 | if (projectToEdit.name.toLowerCase() === 'onboarding') {
28 | // Do nothing, word `onboarding` has been white-listed
29 | } else {
30 | newErrors = {
31 | ...newErrors,
32 | name: `Event name cannot contain the Project Name: '${projectToEdit.name}'`,
33 | };
34 | }
35 | }
36 | break;
37 |
38 | case 'videoConferenceLink':
39 | // Required
40 | if (!vals[key]) {
41 | newErrors = {
42 | ...newErrors,
43 | videoConferenceLink: 'Event link is required',
44 | };
45 | }
46 | if (!validateLink(vals[key])) {
47 | newErrors = {
48 | ...newErrors,
49 | videoConferenceLink: 'Invalid link',
50 | };
51 | }
52 | break;
53 |
54 | default:
55 | break;
56 | }
57 | });
58 | return Object.keys(newErrors).length ? newErrors : null;
59 | };
60 |
61 | export default validateEventForm;
62 |
63 | function validateLink(url) {
64 | const ZoomMeetRegex =
65 | /^(?:https:\/\/)?(?:www\.)?(?:[a-z0-9-]+\.)?zoom\.us\/j\/[0-9]+(\?pwd=[a-zA-Z0-9]+)?$/;
66 | const GoogleMeetRegex =
67 | /^(?:https:\/\/)?(?:[a-z0-9-]+\.)?meet\.google\.com\/[a-zA-Z0-9-]+$/;
68 | return ZoomMeetRegex.test(url) || GoogleMeetRegex.test(url);
69 | }
70 |
--------------------------------------------------------------------------------
/client/src/components/parts/boxes/TitledBox.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Box, Typography, Divider } from '@mui/material';
3 |
4 | export default function TitledBox({ title, children, badge, childrenBoxSx }) {
5 | return (
6 |
7 |
15 |
16 |
17 | {title}
18 |
19 |
20 | {badge ? badge : ' '}
21 |
22 |
23 | {children}
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/client/src/components/presentational/CheckInButtons.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import Button from '@mui/material/Button';
4 |
5 | const CheckInButtons = (props) => {
6 | return (
7 | <>
8 |
17 |
26 |
33 | >
34 | );
35 | };
36 |
37 | export default CheckInButtons;
38 |
--------------------------------------------------------------------------------
/client/src/components/presentational/CreateNewProfileButton.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | import '../../sass/Home.scss';
5 |
6 | const CreateNewProfileButton = (props) => {
7 | return (
8 |
11 | CREATE NEW PROFILE
12 |
13 | )
14 | };
15 |
16 | export default CreateNewProfileButton;
17 |
--------------------------------------------------------------------------------
/client/src/components/presentational/DashboardReport.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const DashboardReport = (props) => {
4 | console.log(props.total);
5 |
6 | return (
7 |
13 | );
14 | };
15 |
16 | export default DashboardReport;
17 |
--------------------------------------------------------------------------------
/client/src/components/presentational/profile/ProfileOption.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const ProfileOption = ({ index, option, removeOption })=> {
4 | return (
5 |
6 | {option}
7 |
8 |
9 | )
10 | };
11 |
12 | export default ProfileOption;
--------------------------------------------------------------------------------
/client/src/components/presentational/profile/UserEvents.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const UserEvents = ({ context }) => {
4 | const { events } = context;
5 | console.log(events);
6 |
7 | return (
8 |
9 |
10 |
11 |
12 | Event |
13 | Date/Time |
14 | Link |
15 |
16 |
17 | {events ? events.map((event,index) =>
18 | (
19 | {event.name} |
20 | {event.time} |
21 | {event.url} |
22 |
))
23 | : ("")}
24 |
25 |
26 |
27 | );
28 | }
29 |
30 | export default UserEvents;
--------------------------------------------------------------------------------
/client/src/components/presentational/profile/UserTeams.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const UserTeams = ({ context }) => {
4 | const { teams } = context;
5 |
6 | return (
7 |
8 |
9 |
10 |
11 | Team |
12 | Status |
13 |
14 | {teams ? teams.map((team,index) =>
15 | (
16 | {team.name} |
17 | {team.status} |
18 |
))
19 | : ("")}
20 |
21 |
22 |
23 | );
24 | }
25 |
26 | export default UserTeams;
--------------------------------------------------------------------------------
/client/src/components/presentational/projectDashboardContainer.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import AttendeeTable from "../dashboard/AttendeeTable";
4 | import RosterTable from "../dashboard/RosterTable";
5 |
6 | const projectDashboardContainer = (props) => {
7 | return (
8 |
9 | {props.attendeeOrRoster ? (
10 |
17 | ) : (
18 |
23 | )}
24 |
25 | );
26 | };
27 | export default projectDashboardContainer;
28 |
--------------------------------------------------------------------------------
/client/src/components/presentational/upcomingEvent.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ReactComponent as ClockIcon } from "../../svg/Icon_Clock.svg";
3 | import { ReactComponent as LocationIcon } from "../../svg/Icon_Location.svg";
4 | import { Link } from "react-router-dom";
5 | import { Box, Typography } from "@mui/material";
6 |
7 | import moment from "moment";
8 |
9 | const upcomingEvent = (props) => {
10 | return props.nextEvent[0] ? (
11 |
12 |
13 | {props.nextEvent[0].name}
14 |
15 |
16 |
17 | {moment(props.nextEvent[0].date).format(
18 | "ddd, MMM D @ h:mm a"
19 | )}
20 |
21 |
22 | {props.nextEvent[0].location.city !== "" &&
23 |
24 |
25 |
26 | {props.nextEvent[0].location.city},{" "}
27 | {props.nextEvent[0].location.state}
28 |
29 |
30 | }
31 |
32 |
33 | {props.nextEvent[0] && props.isCheckInReady === false ? (
34 |
38 | props.setCheckInReady(e, props.nextEvent[0]._id)
39 | }
40 | >
41 | OPEN CHECK-IN
42 |
43 | ) : (
44 |
48 | props.setCheckInReady(e, props.nextEvent[0]._id)
49 | }
50 | >
51 | CLOSE CHECK-IN
52 |
53 | )}
54 |
55 |
56 | ) : (
57 | No events coming up!
58 | );
59 | };
60 | export default upcomingEvent;
61 |
--------------------------------------------------------------------------------
/client/src/context/authContext.jsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useState, useEffect } from 'react';
2 | import { REACT_APP_CUSTOM_REQUEST_HEADER as headerToSend } from '../utils/globalSettings';
3 | import * as authApi from '../api/auth';
4 | import { useHistory } from 'react-router-dom';
5 |
6 | export const AuthContext = createContext();
7 |
8 | export const AuthProvider = ({ children }) => {
9 | const [auth, setAuth] = useState();
10 | const history = useHistory();
11 |
12 | useEffect(() => {
13 | refreshAuth();
14 | }, []);
15 |
16 | const refreshAuth = async () => {
17 | const userAuth = await fetchAuth();
18 | setAuth(userAuth);
19 | };
20 |
21 | const logout = async () => {
22 | const res = await authApi.fetchLogout();
23 |
24 | if (!res.ok) {
25 | throw new Error(res.statusText);
26 | }
27 |
28 | history.push('/');
29 | setAuth(null);
30 | };
31 |
32 | return (
33 |
34 | {children}
35 |
36 | );
37 | };
38 |
39 | const fetchAuth = async () => {
40 | const request = {
41 | method: 'POST',
42 | headers: {
43 | 'Content-Type': 'application/json',
44 | 'x-customrequired-header': headerToSend,
45 | },
46 | };
47 |
48 | try {
49 | const response = await fetch('/api/auth/me', request);
50 | if (response.status !== 200)
51 | return { user: null, isAdmin: false, isError: true };
52 |
53 | const user = await response.json();
54 | return { user, isAdmin: (user.accessLevel === 'admin' || user.accessLevel === 'superadmin'), isError: false };
55 | } catch (error) {
56 | // this should never be hit...
57 | console.error('fetchAuth - error', error);
58 | }
59 | };
60 |
--------------------------------------------------------------------------------
/client/src/context/snackbarContext.jsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useState } from 'react';
2 | import Snackbar from '@mui/material/Snackbar';
3 | import Alert from '@mui/material/Alert';
4 | import Slide from '@mui/material/Slide';
5 |
6 | const SnackbarContext = createContext();
7 |
8 | export const useSnackbar = () => useContext(SnackbarContext);
9 |
10 | export const SnackbarProvider = ({ children }) => {
11 | const [snackbarState, setSnackbarState] = useState({
12 | open: false,
13 | message: '',
14 | severity: '',
15 | });
16 |
17 | const showSnackbar = (message, severity) => {
18 | setSnackbarState({ open: true, message, severity });
19 | };
20 |
21 | const hideSnackbar = () => {
22 | setSnackbarState({ ...snackbarState, open: false });
23 | };
24 |
25 | return (
26 |
27 | {children}
28 |
38 |
43 | {snackbarState.message}
44 |
45 |
46 |
47 | );
48 | };
--------------------------------------------------------------------------------
/client/src/context/userContext.jsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useState } from 'react';
2 |
3 | export const UserContext = createContext();
4 |
5 | export const UserProvider = ({ children }) => {
6 | const [user, setUser ] = useState({
7 | name: "John Smith",
8 | email: "john.smith@test.com",
9 | github: "www.github.com/j_smith",
10 | slack: "www.slack.com/j_smith",
11 | desiredRoles: ["UX","Front End"],
12 | hackNights: ["DTLA", "Westside"],
13 | availability: ["Mon PM", "Thurs PM"]
14 | });
15 |
16 | // eslint-disable-next-line no-unused-vars
17 | const [events, setEvents] = useState([
18 | {
19 | name: "VRMS Team Meeting",
20 | time: "04/13, 7PM",
21 | url: ""
22 | }
23 | ]);
24 |
25 | // eslint-disable-next-line no-unused-vars
26 | const [teams, setTeams ] = useState([
27 | {
28 | name: 'VRMS',
29 | status: 'Active'
30 | },
31 | {
32 | name: 'Where2Vote2018',
33 | status: 'Inactive'
34 | }
35 | ])
36 |
37 | const removeOption = (category, optionToRemove) => {
38 | const updatedUser = { ...user }
39 | updatedUser[category] = user[category].filter(option => option !== optionToRemove);
40 | setUser(updatedUser);
41 | }
42 |
43 | return (
44 |
45 | { children }
46 |
47 | );
48 | };
49 |
50 |
--------------------------------------------------------------------------------
/client/src/fonts/aliseo-noncommercial-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackforla/VRMS/85370ec62ebbb740c47dfb05b1f8b3cbf8830beb/client/src/fonts/aliseo-noncommercial-webfont.woff
--------------------------------------------------------------------------------
/client/src/fonts/aliseo-noncommercial-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackforla/VRMS/85370ec62ebbb740c47dfb05b1f8b3cbf8830beb/client/src/fonts/aliseo-noncommercial-webfont.woff2
--------------------------------------------------------------------------------
/client/src/hooks/useAuth.js:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 |
3 | import { AuthContext } from '../context/authContext';
4 |
5 | const useAuth = () => {
6 | return useContext(AuthContext);
7 | };
8 |
9 | export default useAuth;
--------------------------------------------------------------------------------
/client/src/hooks/withAuth.jsx:
--------------------------------------------------------------------------------
1 | import { Redirect } from 'react-router-dom';
2 | import useAuth from './useAuth';
3 |
4 | const withAuth = (Component) => (props) => {
5 | const { auth } = useAuth();
6 |
7 | if (!auth) {
8 | return
9 | }
10 |
11 | return ;
12 | }
13 |
14 | export default withAuth;
--------------------------------------------------------------------------------
/client/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.scss';
4 | import App from './App';
5 | import { BrowserRouter } from 'react-router-dom';
6 | import { SnackbarProvider } from './context/snackbarContext';
7 |
8 | ReactDOM.render(
9 |
10 |
11 |
12 |
13 | ,
14 | document.getElementById('root')
15 | );
16 |
17 | // If you want your app to work offline and load faster, you can change
18 | // unregister() to register() below. Note this comes with some pitfalls.
19 | // Learn more about service workers: https://bit.ly/CRA-PWA
20 | // serviceWorker.unregister();
21 |
--------------------------------------------------------------------------------
/client/src/pages/EmailSent.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 |
3 | import '../sass/MagicLink.scss';
4 |
5 | const EmailSent = (props) => {
6 |
7 | useEffect(() => {
8 | let timer = setTimeout(() => {
9 | props.history.push('/');
10 | }, 10000);
11 |
12 | return () => clearTimeout(timer);
13 | }, [props.history]);
14 |
15 | return (
16 |
17 |
18 |
19 |
Success!
20 |
Please check your email for a link to login and see your dashboard.
21 |
22 |
23 |
24 | )
25 | };
26 |
27 | export default EmailSent;
--------------------------------------------------------------------------------
/client/src/pages/Events.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import moment from 'moment';
4 | import { REACT_APP_CUSTOM_REQUEST_HEADER as headerToSend } from '../utils/globalSettings';
5 | import {
6 | Box,
7 | List,
8 | TextField,
9 | ListItem,
10 | ListItemText,
11 | Typography,
12 | } from '@mui/material';
13 |
14 | import '../sass/Events.scss';
15 |
16 | const Events = (props) => {
17 | const [events, setEvents] = useState(null);
18 | const [eventSearchParam, setEventSearchParam] = useState('');
19 |
20 | useEffect(() => {
21 | async function fetchData() {
22 | try {
23 | const res = await fetch('/api/events', {
24 | headers: {
25 | 'x-customrequired-header': headerToSend,
26 | },
27 | });
28 | const resJson = await res.json();
29 | setEvents(resJson);
30 | } catch (error) {
31 | alert(error);
32 | setEvents([]);
33 | }
34 | }
35 | fetchData();
36 | }, []);
37 |
38 | const filteredEvents = events?.filter(
39 | (event) =>
40 | typeof event.name === 'string' &&
41 | event.name.toLowerCase().match(eventSearchParam.toLowerCase())
42 | );
43 |
44 | return (
45 |
46 | setEventSearchParam(e.target.value)}
52 | placeholder="Search events..."
53 | />
54 | {events === null ? (
55 | Loading data...
56 | ) : filteredEvents.length === 0 ? (
57 | No events found.
58 | ) : (
59 |
60 | {filteredEvents.map((event, index) => (
61 |
62 |
63 |
64 | {event.name} (
65 | {moment(event.date).format('ddd, MMM D @ h:mm a')})
66 |
67 |
68 |
69 | ))}
70 |
71 | )}
72 |
73 | )
74 | };
75 |
76 | export default Events;
77 |
--------------------------------------------------------------------------------
/client/src/pages/HealthCheck.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const HealthCheck = () => {
4 | return (
5 |
6 |
"I'm Alive!"
7 |
8 | );
9 | };
10 |
11 | export default HealthCheck;
12 |
--------------------------------------------------------------------------------
/client/src/pages/NewUser.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 |
3 | import ReadyEvents from '../components/ReadyEvents';
4 |
5 | const NewUser = (props) => {
6 | const [newUser] = useState(true);
7 |
8 | useEffect(() => {
9 |
10 | }, []);
11 |
12 | return (
13 |
14 |
15 |
16 |
Welcome!
17 | Thanks for coming.
18 |
19 |
20 |
21 |
22 | )
23 | };
24 |
25 | export default NewUser;
--------------------------------------------------------------------------------
/client/src/pages/ReturningUser.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import ReadyEvents from '../components/ReadyEvents';
3 | import { Box, Typography } from '@mui/material';
4 |
5 | const ReturningUser = (props) => {
6 | const [returningUser] = useState(true);
7 |
8 | return (
9 |
17 |
18 |
29 | Welcome Back!
30 |
31 |
42 | We're happy to see you
43 |
44 |
45 |
46 |
47 | );
48 | };
49 |
50 | export default ReturningUser;
51 |
--------------------------------------------------------------------------------
/client/src/pages/Success.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "../sass/MagicLink.scss";
3 |
4 | const Success = (props) => {
5 | console.log("PROPS", props);
6 |
7 | return (
8 |
9 |
10 |
11 |
Success!
12 | Soon, you'll be able to:
13 |
14 |
15 |
👉 View your detailed, personalized journey...
16 |
👉 Get matched with projects that need you...
17 |
👉 Manage your own project!
18 |
19 |
20 |
21 |
22 | Thanks for being a part of the alpha test! Your feedback is valued
23 | and appreciated.
24 |
25 |
Have fun tonight!
26 |
27 |
28 |
29 | );
30 | };
31 |
32 | export default Success;
33 |
--------------------------------------------------------------------------------
/client/src/pages/UserAdmin.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useCallback } from 'react';
2 | import '../sass/UserAdmin.scss';
3 | import EditUsers from '../components/user-admin/EditUsers';
4 | import UserApiService from '../api/UserApiService';
5 | import ProjectApiService from '../api/ProjectApiService';
6 | import UserManagement from '../components/user-admin/UserManagement';
7 |
8 | const UserAdmin = () => {
9 | // Initialize state hooks
10 | const [users, setUsers] = useState([]); // All users pulled from database
11 | const [projects, setProjects] = useState([]); // All projects pulled from db
12 | const [userToEdit, setUserToEdit] = useState({}); // The selected user that is being edited
13 |
14 | const [userApiService] = useState(new UserApiService());
15 | const [projectApiService] = useState(new ProjectApiService());
16 |
17 | const fetchUsers = useCallback(async () => {
18 | const userRes = await userApiService.fetchUsers();
19 | setUsers(userRes);
20 | }, [userApiService]);
21 |
22 | const updateUserDb = useCallback(
23 | async (user, managedProjects) => {
24 | await userApiService.updateUserDbProjects(user, managedProjects);
25 | fetchUsers();
26 | },
27 | [userApiService, fetchUsers]
28 | );
29 |
30 | const updateUserActiveStatus = useCallback(
31 | async (user, isActive) => {
32 | await userApiService.updateUserDbIsActive(user, isActive);
33 | fetchUsers();
34 | },
35 | [userApiService, fetchUsers]
36 | );
37 |
38 | // Update user's access level (admin/user)
39 | const updateUserAccessLevel = useCallback(
40 | async (user, newAccessLevel) => {
41 | await userApiService.updateUserAccessLevel(user, newAccessLevel);
42 | fetchUsers();
43 | },
44 | [userApiService, fetchUsers]
45 | );
46 |
47 | const fetchProjects = useCallback(async () => {
48 | const projectRes = await projectApiService.fetchProjects();
49 | setProjects(projectRes);
50 | }, [projectApiService]);
51 |
52 | useEffect(() => {
53 | fetchUsers();
54 | fetchProjects();
55 | }, [fetchUsers, fetchProjects]);
56 |
57 | const backToSearch = () => {
58 | setUserToEdit({});
59 | };
60 |
61 | if (Object.keys(userToEdit).length === 0) {
62 | return ;
63 | } else {
64 | return (
65 |
73 | );
74 | }
75 | };
76 |
77 | export default UserAdmin;
78 |
--------------------------------------------------------------------------------
/client/src/pages/UserDashboard.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const UserDashboard = (props) => (
4 |
5 |
6 |
Your Volunteer Journey
7 |
Profile
8 |
Events
9 |
10 |
11 | );
12 |
13 | export default UserDashboard;
14 |
--------------------------------------------------------------------------------
/client/src/pages/UserProfile.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '../sass/UserProfile.scss';
3 | import UserTable from '../components/presentational/profile/UserTable';
4 | import UserEvents from '../components/presentational/profile/UserEvents';
5 | import UserTeams from '../components/presentational/profile/UserTeams';
6 | import { UserProvider, UserContext } from '../context/userContext';
7 | import { Box, Typography, Grid } from '@mui/material';
8 |
9 | const UserProfile = (props) => (
10 |
11 |
12 |
13 |
23 | My Profile
24 |
25 |
26 |
27 | {({ user, removeOption }) => (
28 |
29 | )}
30 |
31 |
32 |
42 | My Upcoming Events
43 |
44 |
45 |
46 | {({ events }) => }
47 |
48 |
49 |
59 | My Teams
60 |
61 |
62 |
63 | {({ teams }) => }
64 |
65 |
66 |
67 | );
68 |
69 | export default UserProfile;
70 |
--------------------------------------------------------------------------------
/client/src/pages/UserWelcome.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Typography, Box, Link } from '@mui/material';
3 | import useAuth from '../hooks/useAuth';
4 |
5 | export default function UserWelcome() {
6 | const { auth } = useAuth();
7 |
8 | const user = auth?.user;
9 |
10 | const firstName = user?.name.firstName;
11 |
12 | console.log('AUTH', auth);
13 | return (
14 |
15 | Welcome {firstName}!
16 |
17 |
18 | For assistance using VRMS, check out the{' '}
19 |
20 |
26 | User Guide
27 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/client/src/pages/Users.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { Button, Box, Container } from '@mui/material';
4 |
5 | import '../sass/Users.scss';
6 |
7 | const Users = () => {
8 | return (
9 |
10 |
11 |
19 |
20 |
21 |
29 |
30 |
31 | );
32 | };
33 |
34 | export default Users;
35 |
--------------------------------------------------------------------------------
/client/src/sass/AddNew.scss:
--------------------------------------------------------------------------------
1 | .addnew {
2 | display: -webkit-box;
3 | display: -ms-flexbox;
4 | display: flex;
5 | flex-direction: column;
6 | margin: 6px;
7 | }
8 |
9 | form {
10 | display: -webkit-box;
11 | display: -ms-flexbox;
12 | display: flex;
13 | flex-flow: row wrap;
14 | justify-content: space-between;
15 | }
16 |
17 | .event-div-container {
18 | margin-top: 3px;
19 | width: 175px;
20 | }
21 |
22 | .div-full-width {
23 | width: 100%;
24 | }
25 |
26 | .inline {
27 | display: inline !important;
28 | margin: 0;
29 | }
30 |
31 | .center {
32 | margin-left: calc((100% - 92.69px)/2);
33 | }
--------------------------------------------------------------------------------
/client/src/sass/AddTeamMember.scss:
--------------------------------------------------------------------------------
1 | .form-add-member {
2 | flex-wrap: nowrap;
3 | width: 320px;
4 | margin: auto;
5 | }
6 |
7 | .form-add-member .form-row {
8 | width: 240px;
9 | }
10 |
11 | .form-add-member input {
12 | width: 240px;
13 | font-size: 16px;
14 | font-weight: 100;
15 | }
16 |
17 | .form-add-member .form-input-button {
18 | width: 60px;
19 | margin-bottom: 12px;
20 | }
21 |
22 | .addmember-warning {
23 | text-align: center;
24 | }
25 |
26 | .addmember-success {
27 | text-align: center;
28 | }
29 |
--------------------------------------------------------------------------------
/client/src/sass/AdminLogin.scss:
--------------------------------------------------------------------------------
1 | .adminlogin-container {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: center;
5 | align-items: center;
6 | z-index: 1;
7 | padding-top: 6vh;
8 | }
9 |
10 | .adminlogin-headers {
11 | margin: 36px 0px;
12 | }
13 |
14 | .adminlogin-warning {
15 | width: 300px;
16 | color: red;
17 | height: 20px;
18 | }
19 |
--------------------------------------------------------------------------------
/client/src/sass/DashboardUsers.scss:
--------------------------------------------------------------------------------
1 | .user-roles {
2 | p {
3 | margin: 0px;
4 | }
5 | }
--------------------------------------------------------------------------------
/client/src/sass/ErrorContainer.scss:
--------------------------------------------------------------------------------
1 | .ErrorContainer {
2 | margin: 30px auto 0;
3 | color: #fa114f;
4 | font-size: 16px;
5 | }
--------------------------------------------------------------------------------
/client/src/sass/Event.scss:
--------------------------------------------------------------------------------
1 | .hidden {
2 | display: none;
3 | }
4 |
5 | .event-container {
6 | display: flex;
7 | flex-direction: column;
8 | justify-content: center;
9 | align-items: center;
10 | z-index: 1;
11 | margin-bottom: 200px;
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/client/src/sass/Events.scss:
--------------------------------------------------------------------------------
1 | .event-name {
2 | font-size: 15px;
3 | margin: 2px 0px;
4 | border: 2px black solid;
5 | border-radius: 5px;
6 | padding: 0.3em;
7 | &:hover {
8 | background-color: lightgrey;
9 | cursor: pointer;
10 | }
11 | }
12 |
13 | .event-info-wrapper {
14 | display: -webkit-inline-box;
15 | display: -ms-inline-flexbox;
16 | display: inline-flex;
17 | -webkit-box-align: center;
18 | -ms-flex-align: center;
19 | align-items: center;
20 | margin: 6px 0px;
21 | }
22 |
23 | .events-list {
24 | height: 100%;
25 | position: relative;
26 | overflow: auto;
27 | .add-event-btn {
28 | position: sticky;
29 | bottom: 0;
30 | display: flex;
31 | justify-content: flex-end;
32 | .add-event-link {
33 | background: white;
34 | border-radius: 10px;
35 | display: flex;
36 | align-items: center;
37 |
38 | /*animation taken from stackoverflow */
39 | overflow: hidden;
40 | width: 40px;
41 | -webkit-transition: width 0.3s;
42 | transition: width 0.3s;
43 | transition-timing-function: ease-in-out;
44 | white-space: nowrap;
45 | svg {
46 | width: 2em;
47 | height: 2em;
48 | }
49 | .add-event-link-text {
50 | display: none;
51 | color: rgb(250, 17, 79);
52 | margin-left: 10px;
53 | }
54 | }
55 | .add-event-link:hover {
56 | width: 126px;
57 | -webkit-transition: width 0.3s;
58 | transition: width 0.3s;
59 | transition-timing-function: ease-in-out;
60 | }
61 | .add-event-link:hover .add-event-link-text {
62 | display: inline-block;
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/client/src/sass/Footer.scss:
--------------------------------------------------------------------------------
1 | .footer {
2 | display: flex;
3 | padding: 5px 0;
4 | }
5 |
6 | .footer-text {
7 | font-size: 9px;
8 | }
9 |
10 | .footer-greeting {
11 | position: absolute;
12 | display: flex;
13 | justify-content: center;
14 | align-items: center;
15 | left: 50%;
16 | -webkit-transform: translateX(-50%);
17 | transform: translateX(-50%);
18 | min-width: max-content;
19 |
20 | p {
21 | white-space: nowrap;
22 | }
23 | }
24 |
25 | .logout-button {
26 | font-size: 9px;
27 | margin-left: 4px;
28 | }
29 |
--------------------------------------------------------------------------------
/client/src/sass/Form.scss:
--------------------------------------------------------------------------------
1 | .Label {
2 | text-transform: lowercase !important;
3 | width: fit-content;
4 | display: block;
5 | margin: 25px 0 0 2.5px;
6 | font-size: 14px;
7 | font-weight: 500;
8 | }
9 |
10 | // for labels with nested radio buttons
11 | .isRadioParent {
12 | margin-top: 15px;
13 | }
14 |
15 | .SupersetInput {
16 | height: 40px;
17 | width: 100% !important;
18 | max-width: 100% !important;
19 | margin: 10px 0 0 !important;
20 | padding: 5px;
21 | display: block;
22 | border: #fa114f solid 2.5px !important;
23 | border-radius: 5px !important;
24 | font-family: "aliseoregular", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
25 | font-size: 16px !important;
26 | font-weight: 500 !important;
27 | color: black !important;
28 | }
29 |
30 | .SupersetInput:disabled {
31 | color: gray !important;
32 | border-color: gray !important;
33 | }
34 |
35 | .SupersetInput.small {
36 | width: 175px !important;
37 | max-width: 175px !important;
38 | }
39 |
40 | .SupersetInput::placeholder,.placeholder {
41 | color: lightgray !important;
42 | }
43 |
44 | .SecondaryButton {
45 | margin-top: 30px;
46 | margin-bottom: 30px;
47 | padding: 10px 25px !important;
48 | width: fit-content !important;
49 | font-size: 16px !important;
50 | letter-spacing: 2px;
51 | font-family: "aliseoregular", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
52 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
53 | sans-serif !important;
54 | border-radius: 5px;
55 | background-color: #102d49 !important;
56 | color: white !important;
57 | }
58 |
59 | .AuxiliaryButton {
60 | font-size: 20px;
61 | font-weight: 700;
62 | width: fit-content;
63 | padding-left: 10px;
64 | }
65 |
66 | .Textarea {
67 | height: 80px;
68 | }
--------------------------------------------------------------------------------
/client/src/sass/Headers.scss:
--------------------------------------------------------------------------------
1 | .HeaderBarTextOnly {
2 | display: -webkit-inline-box;
3 | display: -ms-inline-flexbox;
4 | display: inline-flex;
5 | -webkit-box-flex: 1;
6 | -ms-flex: 1;
7 | flex: 1;
8 | background-color: #FAFAFA;
9 | }
10 |
11 | .HeaderBarTextOnly > p {
12 | display: -webkit-box;
13 | display: -ms-flexbox;
14 | display: flex;
15 | -webkit-box-orient: vertical;
16 | -webkit-box-direction: normal;
17 | -ms-flex-direction: column;
18 | flex-direction: column;
19 | -webkit-box-pack: center;
20 | -ms-flex-pack: center;
21 | justify-content: center;
22 | font-size: 16px;
23 | }
--------------------------------------------------------------------------------
/client/src/sass/MagicLink.scss:
--------------------------------------------------------------------------------
1 | .new {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: center;
5 | align-items: center;
6 | height: 60vh;
7 | }
8 |
9 | .new-headers {
10 | max-width: 400px;
11 | margin-bottom: 4vh;
12 | z-index: 1;
13 | display: flex;
14 | flex-direction: column;
15 | justify-content: center;
16 | align-items: center;
17 | }
18 |
19 | .future-list {
20 | max-width: 400px;
21 | z-index: 1;
22 | font-size: 1.4em;
23 | }
24 |
25 | .last-row {
26 | margin-top: 24px;
27 | }
28 |
29 | .success-info {
30 | font-size: 1.2em;
31 | max-width: 400px;
32 | z-index: 1;
33 |
34 | &:nth-child(2) {
35 | font-size: 1.6em;
36 | }
37 | }
38 |
39 | .rotated-success {
40 | position: absolute;
41 | background-color: white;
42 | width: 800px;
43 | margin-left: -12%;
44 | height: 700px;
45 | top: -10%;
46 | transform: rotate(-14deg);
47 | z-index: 0;
48 | overflow: hidden;
49 | }
50 |
51 | @media (max-width: 500px) {
52 | .rotated-success {
53 | position: absolute;
54 | background-color: white;
55 | width: 800px;
56 | margin-left: -12%;
57 | margin-top: -2%;
58 | height: 700px;
59 | transform: rotate(-8deg);
60 | z-index: 0;
61 | overflow: hidden;
62 | }
63 |
64 | .new-headers {
65 | margin-bottom: 2vh;
66 | }
67 | }
--------------------------------------------------------------------------------
/client/src/sass/Navbar.scss:
--------------------------------------------------------------------------------
1 | .nav-wrapper {
2 | display: flex;
3 | flex-direction: row;
4 | width: 100%;
5 | // height: 110px;
6 | z-index: 1;
7 | }
8 |
9 | .navbar {
10 | position: relative;
11 | flex: 1;
12 | display: flex;
13 | padding: 20px;
14 | z-index: 1;
15 | justify-content: space-between;
16 | }
17 |
18 | .nav-link-container {
19 | margin: 0px 18px 0px 0px;
20 | }
21 |
22 | .nav-link-text {
23 | margin: 0;
24 | padding: 0;
25 | font-weight: bold;
26 |
27 | cursor: pointer;
28 | font-weight: 400;
29 | text-decoration: none;
30 | padding: 0px;
31 | /* letter-spacing: .05em; */
32 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
33 | monospace;
34 |
35 | }
36 |
37 | .nav-link-active {
38 | // color: red;
39 | font-weight: 600;
40 | margin: 0px 18px 0px 0px;
41 | padding-bottom: 1.1em;
42 | border-bottom: 2px #fa114f solid;
43 | }
44 |
45 | .navbar-buttons-container {
46 | height: 1em;
47 | font-size: 16px;
48 | position: initial;
49 | display: flex;
50 | top: 36px;
51 | left: 12px;
52 | }
53 |
54 | .navbar-logo {
55 | height: 75px;
56 | justify-self: center;
57 | align-self: flex-start;
58 | will-change: transition;
59 | transition: all .3s ease-in-out;
60 |
61 | img {
62 | height: 100%;
63 | width: auto;
64 | object-fit: cover;
65 | position: abolute;
66 | }
67 | }
68 |
69 | .justify-right {
70 | position: absolute;
71 | top: 36px;
72 | right: 0px;
73 | }
74 |
75 | .grow {
76 | transform: scale(2);
77 | padding-top: 12px;
78 | }
79 |
80 | @media (max-width: 500px) {
81 | .navbar-logo {
82 | height: 60px;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/client/src/sass/ProjectLeaderDashboard.module.scss:
--------------------------------------------------------------------------------
1 | .attendeeTable {
2 | display: grid;
3 | grid-template-columns: 1fr 1fr 1fr;
4 | grid-column-gap: 0.5rem;
5 | grid-row-gap: 1rem;
6 | padding: 0 1.375rem;
7 | margin-bottom: 1rem;
8 | }
9 |
10 | .attendeeTableBoxLeft {
11 | display: flex;
12 | justify-content: flex-start;
13 | align-items: center;
14 | }
15 |
16 | .attendeeTableBoxCenter {
17 | display: flex;
18 | justify-content: center;
19 | align-items: center;
20 | text-align: center;
21 | flex-direction: column;
22 | }
23 |
24 | .attendeeTableTitle {
25 | font-family: "Source Code Pro", sans-serif;
26 | font-weight: 700;
27 | font-size: 1.1rem;
28 | letter-spacing: 1px;
29 | color: black;
30 | text-transform: uppercase;
31 | }
32 |
33 | .attendeeTableText {
34 | font-family: "Source Code Pro", sans-serif;
35 | font-weight: 600;
36 | font-size: 0.875rem;
37 | letter-spacing: 1px;
38 | color: black;
39 | }
40 |
41 | .dashboardButton {
42 | font-family: "Source Code Pro", sans-serif;
43 | font-weight: 500;
44 | font-size: 0.8125rem;
45 | text-transform: uppercase;
46 | background-color: white;
47 | color: #1a051d;
48 | max-width: 138px;
49 | border: 2px solid #fa114f;
50 | border-radius: 6px;
51 | padding: 4px 4px;
52 | box-sizing: border-box;
53 | transition: 0.3s background-color linear, 0.3s color linear;
54 | cursor: pointer;
55 | }
56 |
57 | .dashboardButton:hover,
58 | .dashboardButton:active {
59 | border: 2px solid #fa114f;
60 | border-radius: 6px;
61 | padding: 4px 4px;
62 | box-sizing: border-box;
63 | background-color: #fa114f;
64 | color: white;
65 | }
66 |
67 | .rosterIconImg {
68 | width: 100%;
69 | height: 100%;
70 | }
71 |
72 | .rosterIconContainer {
73 | display: flex;
74 | flex-direction: row;
75 |
76 | }
77 |
78 | .rosterIcon {
79 | max-width: 20px;
80 | max-height: 20px;
81 | margin: 3px;
82 | }
83 |
84 | .dashboardHeadingProjectLeader {
85 | font-family: "Source Code Pro", sans-serif;
86 | font-weight: 600;
87 | font-size: 0.875rem;
88 | letter-spacing: 1px;
89 | color: black;
90 | }
91 |
92 | .dashboardHeaderFlex {
93 | display: flex;
94 | justify-content: space-between;
95 | align-items: center;
96 | padding: 0 1rem;
97 | }
98 |
99 | .yesColor {
100 | color: green;
101 | }
102 |
103 | .noColor {
104 | color: #FCA657;
105 | }
--------------------------------------------------------------------------------
/client/src/sass/ReadyEvents.scss:
--------------------------------------------------------------------------------
1 | .event {
2 | border: 1px solid black;
3 | border-radius: 4px;
4 | margin: 12px;
5 | padding: 12px;
6 | }
7 |
8 | .checkin-newuser-button {
9 | border: 1px solid black;
10 | margin: 6px;
11 | padding: 6px;
12 | }
--------------------------------------------------------------------------------
/client/src/sass/UserProfile.scss:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@300;400&display=swap');
2 |
3 |
4 | .profile {
5 | &__header {
6 | background-color : #bad3ff;
7 | }
8 | &__title {
9 | font-family: 'Source Code Pro', monospace;
10 | font-weight: bold;
11 | font-size: 1.5rem;
12 | text-transform: none;
13 | padding: 5px 5%;
14 | }
15 | &__subtitle {
16 | font-family: 'Source Code Pro', monospace;
17 | font-weight: bold;
18 | font-size: 1.15rem;
19 | text-transform: none;
20 | padding: 5px 5%;
21 | }
22 | }
23 |
24 | .user-info {
25 | padding: 10px 5%;
26 | }
27 | .user-data {
28 | font-family: 'Source Code Pro', monospace;
29 | color: #3F3356;
30 | font-weight: 400;
31 | width: 100%;
32 | margin: 15px 0;
33 | border-spacing: 8px;
34 | &__header {
35 | font-weight: bold;
36 | text-align: left;
37 | width: 40%;
38 | }
39 | &__body {
40 | text-align: left;
41 | }
42 | &__info {
43 | &--active {
44 | color: lime;
45 | }
46 | &--inactive {
47 | color: goldenrod;
48 | }
49 | &--flex {
50 | display: flex;
51 | align-items: center;
52 | }
53 | }
54 | &__selection {
55 | display: flex;
56 | align-items: center;
57 | min-width: 50px;
58 | justify-content: space-between;
59 | color: #6979F8;
60 | background-color: #E5E7FA;
61 | margin: 0 5px 0 0;
62 | font-size: .65rem;
63 | padding: 1px 3px;
64 | &:nth-child(even) {
65 |
66 | min-width: 70px;
67 |
68 | }
69 | }
70 |
71 | &__delete {
72 | color: #1A051D;
73 | font-weight: bold;
74 | font-size: .65rem;
75 | width: initial;
76 | }
77 | }
78 |
79 | .user-events {
80 | padding: 10px 5%;
81 | }
--------------------------------------------------------------------------------
/client/src/sass/Users.scss:
--------------------------------------------------------------------------------
1 | .container--users {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: center;
5 | align-items: center;
6 | margin-top: 50px;
7 | }
8 |
9 | .center {
10 | display: flex;
11 | justify-content: center;
12 | }
13 |
14 | .button {
15 | width: 250px;
16 | border-radius: 8px;
17 | }
18 |
19 | .margin-bottom {
20 | margin-bottom: 15px;
21 | }
22 |
--------------------------------------------------------------------------------
/client/src/services/projectTeamMember-api-service.js:
--------------------------------------------------------------------------------
1 | import { REACT_APP_CUSTOM_REQUEST_HEADER as headerToSend} from "../utils/globalSettings";
2 |
3 | const ProjectTeamMemberApi = {
4 | /**
5 | * @returns created projectTeamMember object
6 | * @param {object} member object with required paramters userId, projectId. For
7 | * other optional parameters, see projectTeamMember model
8 | */
9 | postMember(member) {
10 | console.log({member});
11 | return fetch('/api/projectteammembers', {
12 | method: 'POST',
13 | headers: {
14 | "Content-Type": "application/json",
15 | "x-customrequired-header": headerToSend
16 | },
17 | body: JSON.stringify(member)
18 | })
19 | .then((res) => (!res.ok)
20 | ? res.json().then((e) => Promise.reject(e))
21 | : res.json()
22 | )
23 | }
24 | }
25 |
26 | export default ProjectTeamMemberApi;
--------------------------------------------------------------------------------
/client/src/services/user.service.js:
--------------------------------------------------------------------------------
1 | import {
2 | HEADERS,
3 | CHECK_USER,
4 | SIGN_IN,
5 | AUTH_VERIFY_SIGN_IN,
6 | } from '../utils/endpoints';
7 |
8 | /**
9 | * Method sent request to the backend to check if user exist in the DB
10 | * @returns user data otherwise null
11 | * @param email user email
12 | * @param auth_origin auth origin 'LOG_IN' or 'CREATE_ACCOUNT'
13 | */
14 | export async function checkUser(email, auth_origin) {
15 | try {
16 | const response = await fetch(CHECK_USER, {
17 | method: 'POST',
18 | headers: HEADERS,
19 | body: JSON.stringify({ email: email, auth_origin: auth_origin }),
20 | });
21 | return await response.json();
22 | } catch (error) {
23 | console.log('User is not registered in the app');
24 | console.log(error);
25 | return null;
26 | }
27 | }
28 |
29 | /**
30 | * Method sent request to the backend to check if user can login in app
31 | * @returns true if user can login otherwise null
32 | * @param email user email
33 | * @param auth_origin auth origin 'LOG_IN' or "CREATE_ACCOUNT'
34 | */
35 | export async function checkAuth(email, auth_origin) {
36 | try {
37 | const response = await fetch(SIGN_IN, {
38 | method: 'POST',
39 | headers: HEADERS,
40 | body: JSON.stringify({ email: email, auth_origin: auth_origin }),
41 | });
42 | return response.status === 200;
43 | } catch (error) {
44 | console.log('User is not authorized in app');
45 | console.log(error);
46 | return null;
47 | }
48 | }
49 |
50 | /**
51 | * Method sent request to the backend to check if token is valid
52 | * @returns true if is valid otherwise false
53 | * @param api_token token
54 | */
55 | export async function isValidToken(api_token) {
56 | try {
57 | const response = await fetch(AUTH_VERIFY_SIGN_IN, {
58 | method: 'POST',
59 | headers: {
60 | ...HEADERS,
61 | 'x-access-token': api_token,
62 | },
63 | });
64 | return response.status === 200;
65 | } catch (error) {
66 | console.log('Token is not valid');
67 | console.log(error);
68 | return false;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/client/src/setupProxy.js:
--------------------------------------------------------------------------------
1 | // setupProxy.js - Dynamically setup a proxy from the frontend to the backend. This file
2 | // does not need to be imported. https://create-react-app.dev/docs/adding-custom-environment-variables/
3 |
4 | const { createProxyMiddleware } = require("http-proxy-middleware");
5 | module.exports = function (app) {
6 | app.use(
7 | "/api",
8 | createProxyMiddleware({
9 | target: process.env.REACT_APP_PROXY,
10 | changeOrigin: true,
11 | })
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/client/src/svg/22.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackforla/VRMS/85370ec62ebbb740c47dfb05b1f8b3cbf8830beb/client/src/svg/22.gif
--------------------------------------------------------------------------------
/client/src/svg/Icon_Clock.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/client/src/svg/Icon_Edit.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/client/src/svg/Icon_Location.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/client/src/svg/Icon_Plus.svg:
--------------------------------------------------------------------------------
1 |
58 |
--------------------------------------------------------------------------------
/client/src/svg/PlusIcon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/client/src/svg/hflalogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackforla/VRMS/85370ec62ebbb740c47dfb05b1f8b3cbf8830beb/client/src/svg/hflalogo.png
--------------------------------------------------------------------------------
/client/src/svg/hflalogo_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackforla/VRMS/85370ec62ebbb740c47dfb05b1f8b3cbf8830beb/client/src/svg/hflalogo_white.png
--------------------------------------------------------------------------------
/client/src/theme/palette.js:
--------------------------------------------------------------------------------
1 | export const applyAlpha = (alpha, color) => color + alpha;
2 |
3 | const grayscale = [
4 | '#FFFFFF', //0
5 | '#F1F1F1', //1
6 | '#AAAAAA', //2
7 | '#333333', //3
8 | '#757575', //4
9 | '#000000', //5
10 | ];
11 |
12 | export const uiKitColors = {
13 | primary: '#00008B',
14 | secondary: '#FA114F',
15 | success: '#008000',
16 | error: '#FF0000',
17 | white: grayscale[0],
18 | black: grayscale[5],
19 | grayscale
20 | }
21 |
22 | const palette = {
23 | type: 'light',
24 | white: uiKitColors.white,
25 | black: uiKitColors.black,
26 | primary: {
27 | main: uiKitColors.primary,
28 | },
29 | secondary: {
30 | main: uiKitColors.secondary,
31 | },
32 | success: {
33 | main: uiKitColors.success,
34 | },
35 | error: {
36 | main: uiKitColors.error,
37 | },
38 | background: {
39 | default: uiKitColors.white,
40 | paper: uiKitColors.grayscale[1],
41 | },
42 | };
43 |
44 | export default palette;
45 |
--------------------------------------------------------------------------------
/client/src/utils/authUtils.js:
--------------------------------------------------------------------------------
1 | export function authLevelRedirect(user) {
2 | let loginRedirect;
3 | let userAccessLevel = user.accessLevel;
4 |
5 | switch (userAccessLevel) {
6 | case 'superadmin':
7 | loginRedirect = '/welcome'
8 | case 'admin':
9 | loginRedirect = '/welcome';
10 | break;
11 | case 'user':
12 | loginRedirect = '/welcome';
13 | break;
14 | default:
15 | // Do nothing (harder than you think).
16 | }
17 |
18 | return loginRedirect;
19 | }
20 |
--------------------------------------------------------------------------------
/client/src/utils/blacklist.js:
--------------------------------------------------------------------------------
1 | export const eventNameBlacklistArr = [
2 | 'general',
3 | 'meet',
4 | 'meeting',
5 | 'mtg',
6 | 'onboarding',
7 | 'onboard',
8 | 'team',
9 | 'semi',
10 | 'bi',
11 | 'weekly',
12 | 'monthly',
13 | 'annual',
14 | 'annually',
15 | 'bi-weekly',
16 | 'twice-weekly',
17 | 'semi-annual',
18 | 'semi-annually',
19 | ]
--------------------------------------------------------------------------------
/client/src/utils/createClockHours.js:
--------------------------------------------------------------------------------
1 | export function createClockHours () {
2 |
3 | let hours = [12];
4 | let clockHours = [];
5 |
6 | for (let i = 1; i < 12; i++) {
7 | hours.push(i);
8 | }
9 |
10 | let aorp = 'am';
11 | for (let i = 0; i < 2; i++) {
12 | for (const d of hours) {
13 | clockHours.push(d + ':00' + aorp);
14 | clockHours.push(d + ':30' + aorp);
15 | }
16 | aorp = "pm";
17 | }
18 | return clockHours;
19 | }
--------------------------------------------------------------------------------
/client/src/utils/endpoints.js:
--------------------------------------------------------------------------------
1 | import { REACT_APP_CUSTOM_REQUEST_HEADER } from "../utils/globalSettings";
2 |
3 | export const HEADERS = {
4 | 'Content-Type': 'application/json',
5 | 'x-customrequired-header': REACT_APP_CUSTOM_REQUEST_HEADER,
6 | };
7 |
8 | export const CHECK_USER = '/api/checkuser';
9 | export const SIGN_IN = '/api/auth/signin';
10 | export const AUTH_VERIFY_SIGN_IN = '/api/auth/verify-signin';
11 |
--------------------------------------------------------------------------------
/client/src/utils/globalSettings.js:
--------------------------------------------------------------------------------
1 | export const REACT_APP_CUSTOM_REQUEST_HEADER="nAb3kY-S%qE#4!d";
--------------------------------------------------------------------------------
/client/src/utils/stringUtils.js:
--------------------------------------------------------------------------------
1 | export const isWordInArrayInString = (arr, str) => {
2 | const words = str.split(' ');
3 | let foundWords = []
4 | for (let word of words) {
5 | if (arr.includes(word)) {
6 | foundWords.push(word)
7 | }
8 | }
9 | if(foundWords.length > 0) {
10 | return foundWords
11 | }
12 | return false;
13 | };
14 |
--------------------------------------------------------------------------------
/client/vite.config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig, loadEnv } from 'vite'
2 | import react from '@vitejs/plugin-react-swc'
3 | import svgr from "vite-plugin-svgr";
4 |
5 | export default defineConfig(({ mode }) => {
6 | // Load env file based on`mode in the current working directory.
7 | const env = loadEnv(mode, process.cwd(), '');
8 | return {
9 | plugins: [
10 | svgr(),
11 | react(),
12 | ],
13 | server: {
14 | port: env.CLIENT_PORT,
15 | host: true,
16 | proxy: {
17 | '/api': {
18 | target: env.REACT_APP_PROXY,
19 | changeOrigin: true,
20 | secure: false,
21 | },
22 | },
23 | },
24 | build: {
25 | outDir: 'build',
26 | },
27 | test: {
28 | environment: 'jsdom',
29 | },
30 | };
31 | });
32 |
--------------------------------------------------------------------------------
/cypress.json:
--------------------------------------------------------------------------------
1 | {"baseUrl":"http://localhost:3003" }
2 |
--------------------------------------------------------------------------------
/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
--------------------------------------------------------------------------------
/cypress/integration/admin_login.js:
--------------------------------------------------------------------------------
1 | describe("Admin Login", () => {
2 | it("User cannot leave field empty", () => {
3 | cy.visit("/login");
4 | cy.get("[data-test=input-email]").click().type(" ");
5 | cy.get("[data-test=login-btn]").click();
6 | cy.contains("Please don't leave the field blank");
7 | });
8 | it("User must complete email", () => {
9 | cy.visit("/login");
10 | cy.get("[data-test=input-email]").click().type("admin@test");
11 | cy.get("[data-test=login-btn]").click();
12 | cy.contains("Please format the email address correctly");
13 | });
14 | it("Admin can login", () => {
15 | cy.visit("/login");
16 | cy.get("[data-test=input-email]").click().type("testAdminAccount@VRMS.io");
17 | cy.get("[data-test=login-btn]").click();
18 | // TODO: Add test database for full end to end testing
19 | // cy.contains("Success");
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/cypress/integration/home_page.js:
--------------------------------------------------------------------------------
1 | describe("Homepage", () => {
2 | it("User has login button", () => {
3 | cy.visit("/");
4 | cy.contains("LOGIN");
5 | });
6 | });
7 |
--------------------------------------------------------------------------------
/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | ///
2 | // ***********************************************************
3 | // This example plugins/index.js can be used to load plugins
4 | //
5 | // You can change the location of this file or turn off loading
6 | // the plugins file with the 'pluginsFile' configuration option.
7 | //
8 | // You can read more here:
9 | // https://on.cypress.io/plugins-guide
10 | // ***********************************************************
11 |
12 | // This function is called when a project is opened or re-opened (e.g. due to
13 | // the project's config changing)
14 |
15 | /**
16 | * @type {Cypress.PluginConfig}
17 | */
18 | module.exports = (on, config) => {
19 | // `on` is used to hook into various events Cypress emits
20 | // `config` is the resolved Cypress config
21 | }
22 |
--------------------------------------------------------------------------------
/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add("login", (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This will overwrite an existing command --
25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
26 |
--------------------------------------------------------------------------------
/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.8"
2 |
3 | services:
4 | backend:
5 | build:
6 | context: ./backend
7 | dockerfile: Dockerfile.api
8 | target: api-development
9 | command: yarn run dev
10 | volumes:
11 | - ./backend:/srv/backend
12 | - backend_node_modules:/srv/backend/node_modules
13 | expose:
14 | - "4000"
15 | - "27017"
16 | ports:
17 | - "4000:4000"
18 | - "27017:27017"
19 | restart: on-failure
20 | networks:
21 | - gateway
22 |
23 | client:
24 | build:
25 | context: ./client
26 | dockerfile: Dockerfile.client
27 | target: client-development
28 | command: yarn run start
29 | volumes:
30 | - ./client:/srv/client
31 | - client_node_modules:/srv/client/node_modules
32 | expose:
33 | - "3000"
34 | ports:
35 | - "3000:3000"
36 | environment:
37 | BACKEND_HOST: backend
38 | depends_on:
39 | - backend
40 | stdin_open: true
41 | restart: on-failure
42 | networks:
43 | - gateway
44 |
45 | nginx:
46 | restart: always
47 | build:
48 | dockerfile: Dockerfile.nginx
49 | context: ./nginx
50 | ports:
51 | - "80:80"
52 | networks:
53 | - gateway
54 | depends_on:
55 | - client
56 | - backend
57 |
58 | mailhog:
59 | image: mailhog/mailhog
60 | ports:
61 | - 1025:1025 # smtp server
62 | - 4100:8025 # web ui
63 | depends_on:
64 | - client
65 | - backend
66 | networks:
67 | - gateway
68 |
69 | networks:
70 | gateway: {}
71 |
72 | volumes:
73 | client_node_modules:
74 | backend_node_modules:
75 |
--------------------------------------------------------------------------------
/github-actions/pr-instructions/create-instruction.js:
--------------------------------------------------------------------------------
1 | // Global variables
2 | var github;
3 | var context;
4 |
5 | /**
6 | * Uses information from the pull request to create commandline instructions.
7 | * @param {Object} g - github object
8 | * @param {Object} c - context object
9 | * @returns {string} string containing commandline instructions
10 | */
11 | function main({ g, c }) {
12 | github = g;
13 | context = c;
14 | return createInstruction();
15 | }
16 |
17 | function createInstruction() {
18 | const nameOfCollaborator = context.payload.pull_request.head.repo.owner.login;
19 | const nameOfFromBranch = context.payload.pull_request.head.ref;
20 | const nameOfIntoBranch = context.payload.pull_request.base.ref;
21 | const cloneURL = context.payload.pull_request.head.repo.clone_url;
22 |
23 | const instructionString =
24 | `git checkout -b ${nameOfCollaborator}-${nameOfFromBranch} ${nameOfIntoBranch}
25 | git pull ${cloneURL} ${nameOfFromBranch}`
26 |
27 | return instructionString
28 | }
29 |
30 | module.exports = main
--------------------------------------------------------------------------------
/github-actions/pr-instructions/post-comment.js:
--------------------------------------------------------------------------------
1 | // Import modules
2 | var fs = require("fs");
3 |
4 | // Global variables
5 | var github;
6 | var context;
7 |
8 | /**
9 | * Formats the commandline instructions into a template, then posts it to the pull request.
10 | * @param {Object} g - github object
11 | * @param {Object} c - context object
12 | * @param {Number} issueNum - the number of the issue where the post will be made
13 | * @param {String} instruction - commandline instructions
14 | */
15 | async function main({ g, c }, { issueNum, instruction }) {
16 | github = g;
17 | context = c;
18 |
19 | const instructions = formatComment(instruction);
20 | postComment(issueNum, instructions);
21 | }
22 |
23 | function formatComment(instruction) {
24 | const path = "./github-actions/pr-instructions/pr-instructions-template.md";
25 | const text = fs.readFileSync(path).toString("utf-8");
26 | const completedInstuctions = text.replace(
27 | "${commandlineInstructions}",
28 | instruction
29 | );
30 | return completedInstuctions;
31 | }
32 |
33 | async function postComment(issueNum, instructions) {
34 | console.log("Posting comment to PR...");
35 | console.log("context.repo.owner", context.repo.owner);
36 | console.log("context.repo.repo", context.repo.repo);
37 | try {
38 | await github.issues.createComment({
39 | owner: context.repo.owner,
40 | repo: context.repo.repo,
41 | issue_number: issueNum,
42 | body: instructions,
43 | });
44 | } catch (err) {
45 | throw new Error(err);
46 | }
47 | }
48 |
49 | module.exports = main;
50 |
--------------------------------------------------------------------------------
/github-actions/pr-instructions/pr-instructions-template.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | Want to review this pull request? Take a look at [this documentation](https://github.com/hackforla/website/wiki/How-to-Review-Pull-Requests) for a step by step guide!
4 |
5 | From your project repository, check out a new branch and test the changes.
6 |
7 | ```
8 | ${commandlineInstructions}
9 | ```
10 |
--------------------------------------------------------------------------------
/nginx/.dockerignore:
--------------------------------------------------------------------------------
1 | .git
2 | .dockerignore
3 | docker-compose*.yml
4 | npm-debug.log*
5 | Dockerfile.*
6 | node_modules
7 |
--------------------------------------------------------------------------------
/nginx/Dockerfile.nginx:
--------------------------------------------------------------------------------
1 | FROM nginx
2 | COPY ./default.conf /etc/nginx/conf.d/default.conf
3 |
--------------------------------------------------------------------------------
/nginx/default.conf:
--------------------------------------------------------------------------------
1 | upstream client {
2 | server client:3000;
3 | }
4 |
5 | upstream backend {
6 | server backend:4000;
7 | }
8 |
9 | server {
10 | listen 80;
11 |
12 | location /api {
13 | proxy_pass http://backend;
14 | }
15 |
16 | location / {
17 | proxy_pass http://client;
18 | proxy_http_version 1.1;
19 | proxy_set_header Upgrade $http_upgrade;
20 | proxy_set_header Connection "upgrade";
21 | proxy_redirect off;
22 | proxy_set_header Host $host;
23 | proxy_set_header X-Real-IP $remote_addr;
24 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
25 | proxy_set_header X-Forwarded-Host $server_name;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vrms-server",
3 | "version": "0.3.0",
4 | "description": "VRMS Server",
5 | "scripts": {
6 | "start": "concurrently \"cd backend && npm run start\" \"cd client && npm run start\"",
7 | "mvp": "concurrently \"cd backend && npm run start\" \"cd client-mvp-04 && npm run start\"",
8 | "dev:api": "cd backend && npm run dev",
9 | "dev:client": "cd client && npm run start",
10 | "dev": "dotenv -e backend/.env -e client/.env concurrently \"npm run dev:api\" \"npm run dev:client\"",
11 | "cy:open": "dotenv -e client/.env -- cross-var cross-env CYPRESS_baseUrl=%CLIENT_URL% cypress open",
12 | "cy:run": "dotenv -e client/.env -- cross-var cross-env CYPRESS_baseUrl=%CLIENT_URL% cypress run --headed",
13 | "test:cy": "dotenv -e ./backend/.env -e ./client/.env cross-var start-test dev:api %BACKEND_PORT% dev:client %CLIENT_PORT% cy:run",
14 | "test:backend": "cd backend && npm run test",
15 | "test:client": "cd client && npm run test",
16 | "test:all": "cross-env NODE_ENV=test npm run test:client && npm run test:backend"
17 | },
18 | "dependencies": {
19 | "@mui/icons-material": "^5.14.19",
20 | "concurrently": "^5.3.0",
21 | "cross-env": "^7.0.2",
22 | "cross-var": "^1.1.0",
23 | "dotenv-cli": "^3.2.0",
24 | "styled-components": "^6.1.13",
25 | "vite": "^5.4.19"
26 | },
27 | "devDependencies": {
28 | "cypress": "^5.0.0",
29 | "start-server-and-test": "^1.11.3"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/project-edit-info.md:
--------------------------------------------------------------------------------
1 | ## Team Lead Contact Sheet
2 |
3 | ### Please find your team lead's email from the table below to request the desired changes.
4 |
5 | | Team Lead | Email |
6 | | :-------: | :---: |
7 | | | |
8 | | | |
9 | | | |
10 | | | |
11 | | | |
12 |
--------------------------------------------------------------------------------
/utils/local_db_backup.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | <<'###BLOCK-COMMENT'
4 | Backs up and restores MongoDB instance.
5 |
6 | Arguments:
7 | -u URI of the source MongoDB instance
8 | -d Database name of the source MongoDB instance
9 | -h Host of MongoDB instance to restore backup
10 | -p Port of MongoDB instance to restore backup
11 | -n Name of new MongoDB database
12 |
13 | Example:
14 | sh local_db_backup.sh -u YOUR_MONGO_DB_URL_SANS_ARGS -d SOURCE_DB_NAME -h localhost -p 27017 -n DESTINATION_DB_NAME
15 |
16 | ###BLOCK-COMMENT
17 |
18 | while getopts u:d:h:p:n: flag
19 |
20 | do
21 | case "${flag}" in
22 | u) mongo_src_uri=${OPTARG};;
23 | d) mongo_src_db_name=${OPTARG};;
24 | h) mongo_dest_host=${OPTARG};;
25 | p) mongo_dest_port=${OPTARG};;
26 | n) mongo_dest_name=${OPTARG};;
27 | esac
28 | done
29 |
30 | if pgrep mongo > /dev/null
31 | then
32 | echo "MongoDB is already Running"
33 | else
34 | echo "Starting mongod daemon"
35 | mongod
36 | fi
37 |
38 | echo "Backing up mongodb from uri=$mongo_src_uri"
39 | mkdir backup
40 | mongodump --uri=$mongo_src_uri --out=backup
41 |
42 | echo "Restoring mongodb to host=$mongo_dest_host and port=$mongo_dest_port with name=$mongo_dest_name"
43 | # need to remove hard coded value for incoming db_name "vrms_test"
44 | mongorestore --host $mongo_dest_host --port $mongo_dest_port --db $mongo_dest_name ./backup/$mongo_src_db_name
45 |
46 | echo "Cleaning up"
47 | rm -rf backup
--------------------------------------------------------------------------------
/vrms.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: VRMS
3 | description: VRMS is a tool used for the engagement, support, and retention of a network of volunteers.
4 | image: /assets/images/projects/311.jpg
5 | alt: "VRMS homepage showing the check-in buttons"
6 | links:
7 | - name: Github
8 | url: 'https://github.com/hackforla/VRMS'
9 | - name: Site
10 | url: 'vrms.io'
11 |
12 | looking: Currently seeking front end and back end developers at any level (from Junior to Lead)
13 | location: Santa Monica, Downtown LA
14 | partner: TBD
15 | status: Active
16 | ---
--------------------------------------------------------------------------------