├── backend
└── typescript
│ ├── Procfile
│ ├── .dockerignore
│ ├── .prettierrc.js
│ ├── nodemon.json
│ ├── seed.js
│ ├── migrate.js
│ ├── utilities
│ ├── errorMessageUtil.ts
│ ├── responseUtil.ts
│ ├── servicesUtils.ts
│ ├── dbUtils.ts
│ └── logger.ts
│ ├── Dockerfile
│ ├── services
│ ├── interfaces
│ │ ├── emailService.ts
│ │ ├── cronService.ts
│ │ ├── contentService.ts
│ │ ├── donorService.ts
│ │ ├── volunteerService.ts
│ │ ├── userService.ts
│ │ └── checkInService.ts
│ └── implementations
│ │ ├── emailService.ts
│ │ └── __tests__
│ │ ├── userService.test.ts
│ │ └── contentService.test.ts
│ ├── models
│ ├── index.ts
│ ├── volunteer.model.ts
│ ├── donor.model.ts
│ ├── content.model.ts
│ ├── user.model.ts
│ ├── checkIn.model.ts
│ └── scheduling.model.ts
│ ├── testUtils
│ ├── testDb.ts
│ └── checkInService.ts
│ ├── nodemailer.config.ts
│ ├── rest
│ ├── healthRouter.ts
│ ├── cronRoutes.ts
│ └── contentRoutes.ts
│ ├── migrations
│ ├── 2021.05.30T04.47.28.add-column-example.ts
│ └── 2021.05.30T04.43.57.create-table-example.ts
│ ├── middlewares
│ ├── validators
│ │ ├── volunteerValidator.ts
│ │ ├── donorValidator.ts
│ │ ├── contentValidators.ts
│ │ ├── userValidators.ts
│ │ ├── util.ts
│ │ ├── checkInValidators.ts
│ │ └── authValidators.ts
│ └── auth.ts
│ ├── .eslintrc.js
│ ├── umzug.ts
│ ├── server.ts
│ └── package.json
├── frontend
├── .dockerignore
├── src
│ ├── react-app-env.d.ts
│ ├── constants
│ │ ├── DashboardConstants.ts
│ │ ├── AuthConstants.ts
│ │ ├── Routes.ts
│ │ └── DaysInWeek.ts
│ ├── assets
│ │ ├── drawer-logo.png
│ │ ├── header-logo.png
│ │ ├── home_page_fridge.png
│ │ ├── ThankYouPageFridge.png
│ │ ├── donation-sizes
│ │ │ ├── lg.png
│ │ │ ├── md.png
│ │ │ ├── sm.png
│ │ │ └── xs.png
│ │ ├── scheduling_getstarted.png
│ │ ├── Verification-Email-Image.png
│ │ ├── donation-process
│ │ │ ├── DonationStep1.png
│ │ │ ├── DonationStep2.png
│ │ │ └── DonationStep3.png
│ │ ├── menuIcon.svg
│ │ ├── pencilIcon.svg
│ │ └── personIcon.svg
│ ├── components
│ │ ├── auth
│ │ │ ├── Signup
│ │ │ │ ├── components
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── MandatoryInputDescription.tsx
│ │ │ │ │ └── PasswordRequirement.tsx
│ │ │ │ ├── types.ts
│ │ │ │ ├── ReturnToLoginModal.tsx
│ │ │ │ ├── ConfirmVerificationPage.tsx
│ │ │ │ ├── VerificationEmail.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── ResetPassword
│ │ │ │ ├── types.ts
│ │ │ │ ├── index.tsx
│ │ │ │ ├── PasswordChanged.tsx
│ │ │ │ ├── VerificationEmail.tsx
│ │ │ │ └── ChangePassword.tsx
│ │ │ ├── utilities
│ │ │ │ └── index.ts
│ │ │ ├── PrivateRoute.tsx
│ │ │ └── Action.tsx
│ │ ├── pages
│ │ │ ├── NotFound.tsx
│ │ │ ├── FridgeManagement
│ │ │ │ └── FridgeCheckIns
│ │ │ │ │ └── ErrorMessages.tsx
│ │ │ ├── AdminDashboard
│ │ │ │ ├── ViewDonationsPage.tsx
│ │ │ │ ├── ViewCheckInsPage.tsx
│ │ │ │ ├── EditCheckInDescriptionPage.tsx
│ │ │ │ └── EditFoodRescueDescriptionPage.tsx
│ │ │ ├── Scheduling
│ │ │ │ ├── CancelEditsButton.tsx
│ │ │ │ ├── BackButton.tsx
│ │ │ │ ├── ErrorMessages.tsx
│ │ │ │ ├── NextButton.tsx
│ │ │ │ ├── SaveChangesButton.tsx
│ │ │ │ ├── selectDateTime.css
│ │ │ │ ├── GetStarted.tsx
│ │ │ │ └── ThankYou.tsx
│ │ │ ├── VolunteerDashboard
│ │ │ │ ├── types.ts
│ │ │ │ ├── index.tsx
│ │ │ │ ├── PendingPage.tsx
│ │ │ │ └── ShiftDetails.tsx
│ │ │ ├── UserManagement
│ │ │ │ └── types.ts
│ │ │ ├── Dashboard
│ │ │ │ ├── EditDashboardSchedule.tsx
│ │ │ │ └── components
│ │ │ │ │ └── ModifyRecurringDonationModal.tsx
│ │ │ ├── VolunteerScheduling
│ │ │ │ ├── CheckInCalendar.tsx
│ │ │ │ ├── CheckIns.tsx
│ │ │ │ ├── FoodRescues.tsx
│ │ │ │ ├── VolunteerShiftTabs.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ └── ThankYouVolunteer.tsx
│ │ │ └── Home
│ │ │ │ └── VolunteerRoles.tsx
│ │ └── common
│ │ │ ├── HeaderLabel.tsx
│ │ │ ├── Banner.tsx
│ │ │ ├── Card
│ │ │ └── index.tsx
│ │ │ ├── GeneralErrorModal.tsx
│ │ │ ├── SchedulingProgressBar.tsx
│ │ │ ├── GeneralDeleteShiftModal.tsx
│ │ │ ├── UserManagement
│ │ │ └── EditAccountModal.tsx
│ │ │ ├── FridgeCheckInDescription.tsx
│ │ │ ├── FridgeFoodRescueDescription.tsx
│ │ │ ├── Footer.tsx
│ │ │ └── Calendar
│ │ │ └── WeeklyEventItems.tsx
│ ├── setupTests.ts
│ ├── types
│ │ ├── ContentTypes.ts
│ │ ├── DonorTypes.ts
│ │ ├── CheckInTypes.ts
│ │ ├── SchedulingTypes.ts
│ │ ├── AuthTypes.ts
│ │ └── VolunteerTypes.ts
│ ├── contexts
│ │ ├── VolunteerContextDispatcher.ts
│ │ ├── VolunteerContext.ts
│ │ └── AuthContext.ts
│ ├── hooks
│ │ └── useViewport.ts
│ ├── theme
│ │ ├── components
│ │ │ ├── Input.ts
│ │ │ ├── Container.ts
│ │ │ └── Button.ts
│ │ ├── index.ts
│ │ └── colors.ts
│ ├── utils
│ │ ├── DashboardUtils.ts
│ │ ├── LocalStorageUtils.ts
│ │ └── __tests__
│ │ │ └── LocalStorageUtils.test.ts
│ ├── index.css
│ ├── reportWebVitals.ts
│ ├── index.tsx
│ ├── reducers
│ │ └── VolunteerContextReducer.ts
│ └── APIClients
│ │ ├── ContentAPIClient.ts
│ │ ├── BaseAPIClient.ts
│ │ ├── UserAPIClient.ts
│ │ ├── DonorAPIClient.ts
│ │ └── CheckInAPIClient.ts
├── .prettierrc.js
├── public
│ ├── robots.txt
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── index.html
├── Dockerfile
├── .firebaserc
├── tsconfig.json
├── firebase.json
├── .gitignore
├── .eslintrc.js
├── README.md
└── package.json
├── .gitignore
├── secret.config
├── .github
├── workflows
│ ├── lint-pr.yml
│ ├── firebase-hosting-merge-prod.yml
│ ├── firebase-hosting-merge-staging.yml
│ ├── firebase-hosting-pull-request.yml
│ └── ci.yml
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
└── PULL_REQUEST_TEMPLATE.md
├── setup.sh
├── docker-compose.ci.yml
├── hooks
└── post-merge
├── LICENSE
├── docker-compose.yml
└── update_secret_files.py
/backend/typescript/Procfile:
--------------------------------------------------------------------------------
1 | web: yarn start
--------------------------------------------------------------------------------
/frontend/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/backend/typescript/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/frontend/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/frontend/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | trailingComma: "all",
3 | };
4 |
--------------------------------------------------------------------------------
/backend/typescript/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | trailingComma: "all",
3 | };
4 |
--------------------------------------------------------------------------------
/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/backend/typescript/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "ext": "ts,js,json",
3 | "exec": "ts-node -r dotenv/config server"
4 | }
5 |
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uwblueprint/community-fridge-kw/HEAD/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uwblueprint/community-fridge-kw/HEAD/frontend/public/logo192.png
--------------------------------------------------------------------------------
/frontend/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uwblueprint/community-fridge-kw/HEAD/frontend/public/logo512.png
--------------------------------------------------------------------------------
/frontend/src/constants/DashboardConstants.ts:
--------------------------------------------------------------------------------
1 | const UPCOMING_WEEK_LIMIT = "0";
2 | export default UPCOMING_WEEK_LIMIT;
3 |
--------------------------------------------------------------------------------
/backend/typescript/seed.js:
--------------------------------------------------------------------------------
1 | require("ts-node/register");
2 | // eslint-disable-next-line
3 | require("./umzug").seeder.runAsCLI();
--------------------------------------------------------------------------------
/frontend/src/assets/drawer-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uwblueprint/community-fridge-kw/HEAD/frontend/src/assets/drawer-logo.png
--------------------------------------------------------------------------------
/frontend/src/assets/header-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uwblueprint/community-fridge-kw/HEAD/frontend/src/assets/header-logo.png
--------------------------------------------------------------------------------
/frontend/src/assets/home_page_fridge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uwblueprint/community-fridge-kw/HEAD/frontend/src/assets/home_page_fridge.png
--------------------------------------------------------------------------------
/frontend/src/assets/ThankYouPageFridge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uwblueprint/community-fridge-kw/HEAD/frontend/src/assets/ThankYouPageFridge.png
--------------------------------------------------------------------------------
/frontend/src/assets/donation-sizes/lg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uwblueprint/community-fridge-kw/HEAD/frontend/src/assets/donation-sizes/lg.png
--------------------------------------------------------------------------------
/frontend/src/assets/donation-sizes/md.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uwblueprint/community-fridge-kw/HEAD/frontend/src/assets/donation-sizes/md.png
--------------------------------------------------------------------------------
/frontend/src/assets/donation-sizes/sm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uwblueprint/community-fridge-kw/HEAD/frontend/src/assets/donation-sizes/sm.png
--------------------------------------------------------------------------------
/frontend/src/assets/donation-sizes/xs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uwblueprint/community-fridge-kw/HEAD/frontend/src/assets/donation-sizes/xs.png
--------------------------------------------------------------------------------
/frontend/src/assets/scheduling_getstarted.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uwblueprint/community-fridge-kw/HEAD/frontend/src/assets/scheduling_getstarted.png
--------------------------------------------------------------------------------
/frontend/src/assets/Verification-Email-Image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uwblueprint/community-fridge-kw/HEAD/frontend/src/assets/Verification-Email-Image.png
--------------------------------------------------------------------------------
/backend/typescript/migrate.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line
2 | require("ts-node/register");
3 | // eslint-disable-next-line
4 | require("./umzug").migrator.runAsCLI();
5 |
--------------------------------------------------------------------------------
/frontend/src/assets/donation-process/DonationStep1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uwblueprint/community-fridge-kw/HEAD/frontend/src/assets/donation-process/DonationStep1.png
--------------------------------------------------------------------------------
/frontend/src/assets/donation-process/DonationStep2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uwblueprint/community-fridge-kw/HEAD/frontend/src/assets/donation-process/DonationStep2.png
--------------------------------------------------------------------------------
/frontend/src/assets/donation-process/DonationStep3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uwblueprint/community-fridge-kw/HEAD/frontend/src/assets/donation-process/DonationStep3.png
--------------------------------------------------------------------------------
/frontend/src/components/auth/Signup/components/index.tsx:
--------------------------------------------------------------------------------
1 | export * as MandatoryInputDescription from "./MandatoryInputDescription";
2 | export * as PasswordRequirement from "./PasswordRequirement";
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **/node_modules
2 | **/.env
3 | **/build
4 | **/venv
5 | **/__pycache__
6 | **/*.log
7 | **/firebaseServiceAccount.json
8 | **/.DS_Store
9 | .vscode
10 | **/*.cache
11 | **/*.egg-info
12 |
--------------------------------------------------------------------------------
/frontend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:14.15.5-slim
2 |
3 | WORKDIR /app
4 |
5 | COPY package.json yarn.lock ./
6 | RUN yarn install
7 |
8 | COPY . ./
9 |
10 | EXPOSE 3000
11 | ENTRYPOINT ["yarn", "start"]
12 |
--------------------------------------------------------------------------------
/secret.config:
--------------------------------------------------------------------------------
1 | ROOT_ENV_FILE=.env
2 | FRONTEND_ENV_FILE=frontend/.env
3 | GOOGLE_APPLICATION_CREDENTIALS=backend/typescript/firebaseServiceAccount.json
4 | NODE_MAILER_CONFIG=backend/typescript/nodemailer.config.ts
5 |
--------------------------------------------------------------------------------
/frontend/src/components/auth/ResetPassword/types.ts:
--------------------------------------------------------------------------------
1 | export interface RequestPasswordChangeFormProps {
2 | email: string;
3 | }
4 |
5 | export interface ResetPasswordChangeFormProps {
6 | password: string;
7 | confirmPassword: string;
8 | }
9 |
--------------------------------------------------------------------------------
/frontend/src/components/pages/NotFound.tsx:
--------------------------------------------------------------------------------
1 | import { Center } from "@chakra-ui/react";
2 | import React from "react";
3 |
4 | const NotFound = (): React.ReactElement => {
5 | return
404 Not Found 🙁;
6 | };
7 |
8 | export default NotFound;
9 |
--------------------------------------------------------------------------------
/backend/typescript/utilities/errorMessageUtil.ts:
--------------------------------------------------------------------------------
1 | const getErrorMessage = (error: unknown): string => {
2 | return error instanceof Error
3 | ? error.message
4 | : "Error: Caught error of invalid type ";
5 | };
6 |
7 | export default getErrorMessage;
8 |
--------------------------------------------------------------------------------
/frontend/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import "@testing-library/jest-dom";
6 |
--------------------------------------------------------------------------------
/frontend/src/types/ContentTypes.ts:
--------------------------------------------------------------------------------
1 | export type Content = {
2 | id: string;
3 | foodRescueDescription: string;
4 | foodRescueUrl: string;
5 | checkinDescription: string;
6 | checkinUrl: string;
7 | };
8 |
9 | export type UpdateContentDataType = Omit;
10 |
--------------------------------------------------------------------------------
/frontend/src/contexts/VolunteerContextDispatcher.ts:
--------------------------------------------------------------------------------
1 | import { createContext, Dispatch } from "react";
2 |
3 | import { VolunteerContextAction } from "../types/VolunteerTypes";
4 |
5 | const VolunteerDispatcherContext = createContext<
6 | Dispatch
7 | >(() => {});
8 |
9 | export default VolunteerDispatcherContext;
10 |
--------------------------------------------------------------------------------
/backend/typescript/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:14.15.5-slim
2 |
3 | WORKDIR /app
4 |
5 | COPY package.json yarn.lock ./
6 |
7 | # libcurl3 is required for mongodb-memory-server, which is used for testing
8 | RUN apt-get update && apt-get install -y libcurl3
9 |
10 | RUN yarn install
11 |
12 | COPY . ./
13 |
14 | EXPOSE 5000
15 | ENTRYPOINT ["yarn", "dev"]
16 |
--------------------------------------------------------------------------------
/frontend/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "community-fridge-kw"
4 | },
5 | "targets": {
6 | "community-fridge-kw": {
7 | "hosting": {
8 | "prod": [
9 | "communityfridgekw"
10 | ],
11 | "staging": [
12 | "communityfridgekw-staging"
13 | ]
14 | }
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/.github/workflows/lint-pr.yml:
--------------------------------------------------------------------------------
1 | name: "PR Title Validation"
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 | - staging
8 |
9 | workflow_dispatch:
10 |
11 | jobs:
12 | main:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: amannn/action-semantic-pull-request@v3.4.2
16 | env:
17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
18 |
--------------------------------------------------------------------------------
/backend/typescript/services/interfaces/emailService.ts:
--------------------------------------------------------------------------------
1 | interface IEmailService {
2 | /**
3 | * Send email
4 | * @param to recipient's email
5 | * @param subject email subject
6 | * @param htmlBody email body as html
7 | * @throws Error if email was not sent successfully
8 | */
9 | sendEmail(to: string, subject: string, htmlBody: string): Promise;
10 | }
11 |
12 | export default IEmailService;
13 |
--------------------------------------------------------------------------------
/frontend/src/assets/menuIcon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/frontend/src/components/pages/FridgeManagement/FridgeCheckIns/ErrorMessages.tsx:
--------------------------------------------------------------------------------
1 | const ErrorMessages = {
2 | bothTimeFieldsRequired: "Both start time and end time are required.",
3 | bothDateFieldsRequired: "Both start and end dates are required.",
4 | endTimeBeforeStartTime: "End time cannot be before start time.",
5 | endTimeEqualsStartTime: "End time cannot be the same as start time.",
6 | };
7 |
8 | export default ErrorMessages;
9 |
--------------------------------------------------------------------------------
/backend/typescript/models/index.ts:
--------------------------------------------------------------------------------
1 | import * as path from "path";
2 | import { Sequelize, SequelizeOptions } from "sequelize-typescript";
3 | import { dbURL, SQLOptions } from "../utilities/dbUtils";
4 |
5 | const sequelizeOptions: SequelizeOptions = SQLOptions(
6 | [path.join(__dirname, "/*.model.{ts,js}")],
7 | false,
8 | );
9 |
10 | const sequelize = new Sequelize(dbURL, sequelizeOptions);
11 |
12 | export default sequelize;
13 |
--------------------------------------------------------------------------------
/frontend/src/components/auth/Signup/types.ts:
--------------------------------------------------------------------------------
1 | export interface SignUpFormProps {
2 | firstName: string;
3 | lastName: string;
4 | email: string;
5 | phoneNumber: string;
6 | password: string;
7 | confirmPassword: string;
8 | businessName: string;
9 | role: string;
10 | acceptedTerms: boolean;
11 | cityQuestionResponse: string;
12 | intentionQuestionResponse: string;
13 | skillsQuestionResponse: string;
14 | }
15 |
--------------------------------------------------------------------------------
/frontend/src/components/pages/AdminDashboard/ViewDonationsPage.tsx:
--------------------------------------------------------------------------------
1 | import { Container } from "@chakra-ui/react";
2 | import React from "react";
3 |
4 | import ViewDonations from "./ViewDonationsAndCheckIns";
5 |
6 | const ViewDonationsPage = () => {
7 | return (
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | export default ViewDonationsPage;
15 |
--------------------------------------------------------------------------------
/backend/typescript/testUtils/testDb.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from "path";
2 | import { Sequelize, SequelizeOptions } from "sequelize-typescript";
3 | import { dbURLTest, SQLOptions } from "../utilities/dbUtils";
4 |
5 | const sequelizeOptions: SequelizeOptions = SQLOptions(
6 | [resolve(__dirname, "../models/*.model.{ts,js}")],
7 | true,
8 | );
9 |
10 | const testSql = new Sequelize(dbURLTest, sequelizeOptions);
11 |
12 | export default testSql;
13 |
--------------------------------------------------------------------------------
/backend/typescript/nodemailer.config.ts:
--------------------------------------------------------------------------------
1 | import { NodemailerConfig } from "./types";
2 |
3 | const config: NodemailerConfig = {
4 | service: "gmail",
5 | auth: {
6 | type: "OAuth2",
7 | user: process.env.NODEMAILER_USER,
8 | clientId: process.env.NODEMAILER_CLIENT_ID,
9 | clientSecret: process.env.NODEMAILER_CLIENT_SECRET,
10 | refreshToken: process.env.NODEMAILER_REFRESH_TOKEN,
11 | },
12 | };
13 |
14 | export default config;
15 |
--------------------------------------------------------------------------------
/frontend/src/components/pages/AdminDashboard/ViewCheckInsPage.tsx:
--------------------------------------------------------------------------------
1 | import { Container } from "@chakra-ui/react";
2 | import React from "react";
3 |
4 | import ViewCheckIns from "./ViewDonationsAndCheckIns";
5 |
6 | const ViewCheckInsPage = () => {
7 | return (
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | export default ViewCheckInsPage;
15 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useViewport.ts:
--------------------------------------------------------------------------------
1 | import { useMediaQuery } from "@chakra-ui/react";
2 |
3 | type ViewportType = {
4 | isMobile: boolean;
5 | isDesktop: boolean;
6 | };
7 |
8 | function useViewport(): ViewportType {
9 | const [isMobile, isDesktop] = useMediaQuery([
10 | "(max-width: 768px)",
11 | "(min-width: 768px)",
12 | ]);
13 | return {
14 | isMobile,
15 | isDesktop,
16 | };
17 | }
18 |
19 | export default useViewport;
20 |
--------------------------------------------------------------------------------
/frontend/src/theme/components/Input.ts:
--------------------------------------------------------------------------------
1 | const Input = {
2 | variants: {
3 | customFilled: {
4 | field: {
5 | color: "hubbard.100",
6 | background: "squash.100",
7 | fontWeight: "500",
8 | size: "md",
9 | border: "1px",
10 | borderColor: "dorian.100",
11 | borderRadius: "6px",
12 | },
13 | },
14 | },
15 | sizes: {},
16 | defaultProps: {},
17 | };
18 |
19 | export default Input;
20 |
--------------------------------------------------------------------------------
/frontend/src/types/DonorTypes.ts:
--------------------------------------------------------------------------------
1 | export type DonorResponse = {
2 | id: string;
3 | businessName: string;
4 | email: string;
5 | facebookLink?: string;
6 | firstName: string;
7 | instagramLink?: string;
8 | lastName: string;
9 | phoneNumber: string;
10 | role: string;
11 | userId: string;
12 | };
13 |
14 | export type UpdateDonorDataType = {
15 | businessName?: string;
16 | facebookLink?: string;
17 | instagramLink?: string;
18 | };
19 |
--------------------------------------------------------------------------------
/frontend/src/components/common/HeaderLabel.tsx:
--------------------------------------------------------------------------------
1 | import { Text } from "@chakra-ui/react";
2 | import React from "react";
3 |
4 | interface HeaderLabelProps {
5 | text: string;
6 | }
7 |
8 | const HeaderLabel = ({ text }: HeaderLabelProps) => (
9 |
14 | {text}
15 |
16 | );
17 |
18 | export default HeaderLabel;
19 |
--------------------------------------------------------------------------------
/frontend/src/contexts/VolunteerContext.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 |
3 | import { Status } from "../types/AuthTypes";
4 | import { VolunteerContextType } from "../types/VolunteerTypes";
5 |
6 | export const DEFAULT_VOLUNTEER_CONTEXT = {
7 | volunteerId: null,
8 | volunteerStatus: Status.PENDING,
9 | };
10 |
11 | const VolunteerContext = createContext(
12 | DEFAULT_VOLUNTEER_CONTEXT,
13 | );
14 |
15 | export default VolunteerContext;
16 |
--------------------------------------------------------------------------------
/frontend/src/types/CheckInTypes.ts:
--------------------------------------------------------------------------------
1 | export type CheckIn = {
2 | id: string;
3 | volunteerId?: string | null;
4 | startDate: string;
5 | endDate: string;
6 | notes?: string;
7 | isAdmin?: boolean;
8 | };
9 |
10 | export type UpdatedCheckInFields = Partial>;
11 |
12 | export type CreateCheckInFields = Omit;
13 |
14 | export type UpdateCheckInFields = Omit<
15 | CheckIn,
16 | "id" | "volunteerId" | "isAdmin"
17 | >;
18 |
--------------------------------------------------------------------------------
/frontend/src/components/pages/AdminDashboard/EditCheckInDescriptionPage.tsx:
--------------------------------------------------------------------------------
1 | import { Container } from "@chakra-ui/react";
2 | import React from "react";
3 |
4 | import EditCheckInDescription from "./EditCheckInOrFoodRescueDescription";
5 |
6 | const EditCheckInDescriptionPage = () => {
7 | return (
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | export default EditCheckInDescriptionPage;
15 |
--------------------------------------------------------------------------------
/frontend/src/components/pages/AdminDashboard/EditFoodRescueDescriptionPage.tsx:
--------------------------------------------------------------------------------
1 | import { Container } from "@chakra-ui/react";
2 | import React from "react";
3 |
4 | import EditFoodRescueDescription from "./EditCheckInOrFoodRescueDescription";
5 |
6 | const EditFoodRescueDescriptionPage = () => {
7 | return (
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | export default EditFoodRescueDescriptionPage;
15 |
--------------------------------------------------------------------------------
/frontend/src/components/pages/Scheduling/CancelEditsButton.tsx:
--------------------------------------------------------------------------------
1 | import { CloseButton, Flex } from "@chakra-ui/react";
2 | import React from "react";
3 |
4 | import { ButtonProps } from "./types";
5 |
6 | export default function CancelButton({ discardChanges }: ButtonProps) {
7 | return (
8 |
9 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/frontend/src/utils/DashboardUtils.ts:
--------------------------------------------------------------------------------
1 | import { format, isToday, isTomorrow } from "date-fns";
2 |
3 | /* eslint-disable-next-line import/prefer-default-export */
4 | export const getAssistanceType = (isPickup: boolean) =>
5 | isPickup ? "Pickup" : "Dropoff";
6 |
7 | export const dateHeadingText = (startDate: Date) => {
8 | if (isToday(startDate)) {
9 | return "Today";
10 | }
11 | if (isTomorrow(startDate)) {
12 | return "Tomorrow";
13 | }
14 | return format(startDate, "E MMM d, yyyy");
15 | };
16 |
--------------------------------------------------------------------------------
/frontend/src/components/auth/utilities/index.ts:
--------------------------------------------------------------------------------
1 | export const checkLength = (val: string) => val.length >= 12;
2 |
3 | export const checkForNumbers = (val: string) => !!val.match(/\d+/g);
4 |
5 | export const checkForUpperCase = (val: string) => !!val.match(/[A-Z]/g);
6 |
7 | export const checkForLowerCase = (val: string) => !!val.match(/[a-z]/g);
8 |
9 | /* eslint-disable no-useless-escape */
10 | export const checkForSpecialCharacters = (val: string) =>
11 | !!val.match(/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/g);
12 |
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
12 | monospace;
13 | }
14 |
15 | /* .chakra-modal__content {
16 | width: 270px !important;
17 | } */
18 |
--------------------------------------------------------------------------------
/frontend/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from "web-vitals";
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler): void => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/frontend/src/components/pages/Scheduling/BackButton.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowBackIcon } from "@chakra-ui/icons";
2 | import { Box, Button } from "@chakra-ui/react";
3 | import React from "react";
4 |
5 | import { ButtonProps } from "./types";
6 |
7 | export default function BackButton({ previous }: ButtonProps) {
8 | return (
9 |
10 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/backend/typescript/rest/healthRouter.ts:
--------------------------------------------------------------------------------
1 | import { Router } from "express";
2 | import getErrorMessage from "../utilities/errorMessageUtil";
3 |
4 | const healthRouter: Router = Router();
5 |
6 | /*
7 | Health endpoint for the pinger to hit to keep the server running
8 | */
9 | healthRouter.get("/", async (req, res) => {
10 | try {
11 | res.status(200).json({ res: "Health OK" });
12 | } catch (error) {
13 | res.status(500).json({ error: getErrorMessage(error) });
14 | }
15 | });
16 |
17 | export default healthRouter;
18 |
--------------------------------------------------------------------------------
/frontend/src/components/pages/VolunteerDashboard/types.ts:
--------------------------------------------------------------------------------
1 | import colors from "../../../theme/colors";
2 | import { ShiftType } from "../../../types/VolunteerTypes";
3 |
4 | export const getShiftColor = (shift: string, isPickup: boolean): string => {
5 | switch (shift) {
6 | case ShiftType.SCHEDULING:
7 | return isPickup ? colors.turnip["50"] : colors.onion["50"];
8 | case ShiftType.CHECKIN:
9 | return colors.h20["50"];
10 | default:
11 | return "";
12 | }
13 | };
14 |
15 | export default getShiftColor;
16 |
--------------------------------------------------------------------------------
/frontend/src/components/pages/Scheduling/ErrorMessages.tsx:
--------------------------------------------------------------------------------
1 | const ErrorMessages = {
2 | requiredField: "This is a required field.",
3 | recurringDonationEndDateWithinSixMonths:
4 | "End date must be within 6 months of start date.",
5 | recurringEndDateAfterStartDate:
6 | "The recurring donation end date must be after the selected start date.",
7 | invalidRecurringDonationEndDateFormat: "Required format: MM/DD/YYYY",
8 | invalidStartTime: "Scheduled donation time should be after the current time",
9 | };
10 |
11 | export default ErrorMessages;
12 |
--------------------------------------------------------------------------------
/backend/typescript/migrations/2021.05.30T04.47.28.add-column-example.ts:
--------------------------------------------------------------------------------
1 | import { DataType } from "sequelize-typescript";
2 |
3 | import { Migration } from "../umzug";
4 |
5 | export const up: Migration = async ({ context: sequelize }) => {
6 | await sequelize.getQueryInterface().addColumn("test", "new", {
7 | type: DataType.BOOLEAN,
8 | defaultValue: false,
9 | allowNull: false,
10 | });
11 | };
12 |
13 | export const down: Migration = async ({ context: sequelize }) => {
14 | await sequelize.getQueryInterface().removeColumn("test", "new");
15 | };
16 |
--------------------------------------------------------------------------------
/frontend/src/components/pages/Scheduling/NextButton.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Flex } from "@chakra-ui/react";
2 | import React from "react";
3 |
4 | import { ButtonProps } from "./types";
5 |
6 | export default function NextButton({ canSubmit, handleNext }: ButtonProps) {
7 | return (
8 |
9 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | vault_path_replacement_str="s|^vault_path=.*|vault_path=\"$1\"|g"
4 | default_branch_replacement_str="s|^default_branch=.*|default_branch=\"$2\"|g"
5 |
6 | # MacOS
7 | if [[ $OSTYPE =~ darwin.* ]]; then
8 | sed -i "" -e $vault_path_replacement_str ./hooks/post-merge
9 | sed -i "" -e $default_branch_replacement_str ./hooks/post-merge
10 | else
11 | sed -i $vault_path_replacement_str ./hooks/post-merge
12 | sed -i $default_branch_replacement_str ./hooks/post-merge
13 | fi
14 | cp ./hooks/post-merge ./.git/hooks/post-merge
15 |
--------------------------------------------------------------------------------
/frontend/src/components/auth/Signup/components/MandatoryInputDescription.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Text } from "@chakra-ui/react";
2 | import React from "react";
3 |
4 | const MandatoryInputDescription = ({
5 | label,
6 | }: {
7 | label: string;
8 | }): JSX.Element => (
9 |
10 |
11 | {label}
12 |
13 | {" "}
14 | *
15 |
16 |
17 |
18 | );
19 |
20 | export default MandatoryInputDescription;
21 |
--------------------------------------------------------------------------------
/backend/typescript/middlewares/validators/volunteerValidator.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from "express";
2 | import { Status } from "../../types";
3 | import { getApiValidationError } from "./util";
4 |
5 | const volunteerDtoValidator = async (
6 | req: Request,
7 | res: Response,
8 | next: NextFunction,
9 | ) => {
10 | if (req.body.status && !Object.values(Status).includes(req.body.status)) {
11 | return res.status(400).send(getApiValidationError("status", "Status"));
12 | }
13 |
14 | return next();
15 | };
16 |
17 | export default volunteerDtoValidator;
18 |
--------------------------------------------------------------------------------
/frontend/src/contexts/AuthContext.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 |
3 | import { AuthenticatedUser } from "../types/AuthTypes";
4 |
5 | type AuthContextType = {
6 | authenticatedUser: AuthenticatedUser;
7 | setAuthenticatedUser: (_authenticatedUser: AuthenticatedUser) => void;
8 | };
9 |
10 | const AuthContext = createContext({
11 | authenticatedUser: null,
12 | /* eslint-disable-next-line @typescript-eslint/no-unused-vars */
13 | setAuthenticatedUser: (_authenticatedUser: AuthenticatedUser): void => {},
14 | });
15 |
16 | export default AuthContext;
17 |
--------------------------------------------------------------------------------
/frontend/src/index.tsx:
--------------------------------------------------------------------------------
1 | import "./index.css";
2 |
3 | import React from "react";
4 | import ReactDOM from "react-dom";
5 |
6 | import App from "./App";
7 | import reportWebVitals from "./reportWebVitals";
8 |
9 | ReactDOM.render(
10 |
11 |
12 | ,
13 | document.getElementById("root"),
14 | );
15 |
16 | // If you want to start measuring performance in your app, pass a function
17 | // to log results (for example: reportWebVitals(console.log))
18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
19 | reportWebVitals();
20 |
--------------------------------------------------------------------------------
/backend/typescript/utilities/responseUtil.ts:
--------------------------------------------------------------------------------
1 | import { Response } from "express";
2 | import { Readable } from "stream";
3 |
4 | /* eslint-disable-next-line import/prefer-default-export */
5 | export const sendResponseByMimeType = async (
6 | res: Response,
7 | responseCode: number,
8 | contentType: string | undefined,
9 | rawData: Readonly | ReadonlyArray | Readable,
10 | ): Promise => {
11 | if (contentType === "application/json" || contentType === undefined) {
12 | return res.status(responseCode).json(rawData);
13 | }
14 | return res.status(415).json(rawData);
15 | };
16 |
--------------------------------------------------------------------------------
/frontend/src/assets/pencilIcon.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/frontend/src/types/SchedulingTypes.ts:
--------------------------------------------------------------------------------
1 | export type Schedule = {
2 | id: string;
3 | donorId: string;
4 | categories: string[];
5 | size: string;
6 | isPickup: boolean | null;
7 | pickupLocation: string | null;
8 | dayPart: string;
9 | startTime: string;
10 | endTime: string;
11 | status: string;
12 | volunteerNeeded: boolean;
13 | volunteerTime: string | null;
14 | frequency: string;
15 | recurringDonationId: string;
16 | recurringDonationEndDate: string;
17 | notes: string;
18 | volunteerId?: string | null;
19 | };
20 |
21 | export type UpdatedSchedulingFields = Partial>;
22 |
--------------------------------------------------------------------------------
/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Community Fridge KW",
3 | "name": "Community Fridge KW Scheduling Platform",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/src/constants/AuthConstants.ts:
--------------------------------------------------------------------------------
1 | import { getLocalStorageObjProperty } from "../utils/LocalStorageUtils";
2 |
3 | export const AUTHENTICATED_USER_KEY = `${window.location.hostname}:AUTHENTICATED_USER`;
4 | export const AUTHENTICATED_VOLUNTEER_CONTEXT_KEY = `${window.location.hostname}:AUTHENTICATED_VOLUNTEER_CONTEXT`;
5 |
6 | export const BEARER_TOKEN = `Bearer ${getLocalStorageObjProperty(
7 | AUTHENTICATED_USER_KEY,
8 | "accessToken",
9 | )}`;
10 |
11 | export enum SignupErrorMessage {
12 | HEADER = "Sign up failed",
13 | BODY = "Sorry, something went wrong. Please try again later and check all fields have correct formatting.",
14 | }
15 |
--------------------------------------------------------------------------------
/frontend/src/reducers/VolunteerContextReducer.ts:
--------------------------------------------------------------------------------
1 | import {
2 | VolunteerContextAction,
3 | VolunteerContextType,
4 | } from "../types/VolunteerTypes";
5 |
6 | export default function volunteerContextReducer(
7 | state: VolunteerContextType,
8 | action: VolunteerContextAction,
9 | ): VolunteerContextType {
10 | switch (action.type) {
11 | case "SET_VOLUNTEER_ID":
12 | return {
13 | ...state,
14 | volunteerId: action.value,
15 | };
16 | case "SET_VOLUNTEER_STATUS":
17 | return {
18 | ...state,
19 | volunteerStatus: action.value,
20 | };
21 | default:
22 | return state;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/backend/typescript/migrations/2021.05.30T04.43.57.create-table-example.ts:
--------------------------------------------------------------------------------
1 | import { DataType } from "sequelize-typescript";
2 |
3 | import { Migration } from "../umzug";
4 |
5 | export const up: Migration = async ({ context: sequelize }) => {
6 | await sequelize.getQueryInterface().createTable("test", {
7 | id: {
8 | type: DataType.INTEGER,
9 | allowNull: false,
10 | primaryKey: true,
11 | },
12 | name: {
13 | type: DataType.STRING,
14 | allowNull: false,
15 | },
16 | });
17 | };
18 |
19 | export const down: Migration = async ({ context: sequelize }) => {
20 | await sequelize.getQueryInterface().dropTable("test");
21 | };
22 |
--------------------------------------------------------------------------------
/frontend/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "hosting": [
3 | {
4 | "target": "prod",
5 | "public": "build",
6 | "ignore": [
7 | "firebase.json",
8 | "**/.*",
9 | "**/node_modules/**"
10 | ],
11 | "rewrites": [
12 | {
13 | "source": "**",
14 | "destination": "/index.html"
15 | }
16 | ]
17 | },
18 | {
19 | "target": "staging",
20 | "public": "build",
21 | "ignore": [
22 | "firebase.json",
23 | "**/.*",
24 | "**/node_modules/**"
25 | ],
26 | "rewrites": [
27 | {
28 | "source": "**",
29 | "destination": "/index.html"
30 | }
31 | ]
32 | }
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/frontend/src/theme/components/Container.ts:
--------------------------------------------------------------------------------
1 | const Container = {
2 | variants: {
3 | headerContainer: {
4 | maxWidth: { base: "default", md: "70%" },
5 | px: { base: "42px", md: "0px" },
6 | py: "1.5em",
7 | },
8 | baseContainer: {
9 | maxWidth: { base: "default", md: "70%" },
10 | px: { base: "42px", md: "0px" },
11 | pt: "55px",
12 | },
13 | calendarContainer: {
14 | px: "0px",
15 | py: "0px",
16 | maxWidth: "100%",
17 | },
18 | responsiveContainer: {
19 | maxWidth: { base: "default", md: "70%" },
20 | px: { base: "42px", md: "0px" },
21 | },
22 | },
23 | };
24 |
25 | export default Container;
26 |
--------------------------------------------------------------------------------
/frontend/src/components/pages/VolunteerDashboard/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 |
3 | import VolunteerContext from "../../../contexts/VolunteerContext";
4 | import { Status } from "../../../types/AuthTypes";
5 | import PendingPage from "./PendingPage";
6 | import ScheduledVolunteerShiftsPage from "./ShiftsPage";
7 |
8 | const VolunteerDashboard = () => {
9 | const { volunteerStatus } = useContext(VolunteerContext);
10 |
11 | return (
12 | <>
13 | {volunteerStatus === Status.APPROVED ? (
14 |
15 | ) : (
16 |
17 | )}
18 | >
19 | );
20 | };
21 |
22 | export default VolunteerDashboard;
23 |
--------------------------------------------------------------------------------
/backend/typescript/models/volunteer.model.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Column,
3 | DataType,
4 | Model,
5 | Table,
6 | ForeignKey,
7 | BelongsTo,
8 | AllowNull,
9 | } from "sequelize-typescript";
10 | import { Status } from "../types";
11 | import User from "./user.model";
12 |
13 | @Table({ tableName: "volunteers" })
14 | export default class Volunteer extends Model {
15 | @ForeignKey(() => User)
16 | @AllowNull(false)
17 | @Column({ type: DataType.INTEGER })
18 | user_id!: number;
19 |
20 | @BelongsTo(() => User)
21 | user!: User;
22 |
23 | @AllowNull(false)
24 | @Column({
25 | type: DataType.ENUM("Rejected", "Approved", "Pending"),
26 | defaultValue: "Pending",
27 | })
28 | status!: Status;
29 | }
30 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 |
12 |
13 | **Describe the solution you'd like**
14 |
15 |
16 | **Describe alternatives you've considered**
17 |
18 |
19 | **Additional context**
20 |
21 |
--------------------------------------------------------------------------------
/frontend/src/components/auth/Signup/components/PasswordRequirement.tsx:
--------------------------------------------------------------------------------
1 | import { CheckIcon, SmallCloseIcon } from "@chakra-ui/icons";
2 | import { Center, Text } from "@chakra-ui/react";
3 | import React from "react";
4 |
5 | const CheckMarkOrClose = (isTrue: boolean): JSX.Element => {
6 | return isTrue ? (
7 |
8 | ) : (
9 |
10 | );
11 | };
12 |
13 | const PasswordRequirement = ({
14 | state,
15 | label,
16 | }: {
17 | state: boolean;
18 | label: string;
19 | }): JSX.Element => (
20 |
21 | {CheckMarkOrClose(state)}
22 | {label}
23 |
24 | );
25 |
26 | export default PasswordRequirement;
27 |
--------------------------------------------------------------------------------
/backend/typescript/models/donor.model.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Column,
3 | DataType,
4 | Model,
5 | Table,
6 | ForeignKey,
7 | BelongsTo,
8 | AllowNull,
9 | } from "sequelize-typescript";
10 | import User from "./user.model";
11 |
12 | @Table({ tableName: "donors" })
13 | export default class Donor extends Model {
14 | @AllowNull(false)
15 | @ForeignKey(() => User)
16 | @Column({ type: DataType.INTEGER })
17 | user_id!: number;
18 |
19 | @BelongsTo(() => User)
20 | user!: User;
21 |
22 | @AllowNull(false)
23 | @Column({ type: DataType.STRING })
24 | business_name!: string;
25 |
26 | @Column({ type: DataType.STRING })
27 | facebook_link?: string;
28 |
29 | @Column({ type: DataType.STRING })
30 | instagram_link?: string;
31 | }
32 |
--------------------------------------------------------------------------------
/backend/typescript/services/interfaces/cronService.ts:
--------------------------------------------------------------------------------
1 | import Schedule from "../../models/scheduling.model";
2 |
3 | interface ICronService {
4 | /**
5 | * Generate an email regarding a 24 hour email reminder for a donation
6 | * @param schedule object that contains information on scheduled donation
7 | * @throws Error if unable to send email
8 | */
9 | sendScheduledDonationEmail(schedule: Schedule): Promise;
10 |
11 | /**
12 | * Sends donor reminder email
13 | * @throws Error if email was not sent successfully
14 | */
15 | checkScheduleReminders(): Promise;
16 |
17 | /**
18 | * Sends volunteer reminder email
19 | * @throws Error if email was not sent successfully
20 | */
21 | checkCheckInReminders(): Promise;
22 | }
23 |
24 | export default ICronService;
25 |
--------------------------------------------------------------------------------
/frontend/src/components/common/Banner.tsx:
--------------------------------------------------------------------------------
1 | import { Alert, AlertIcon, Link } from "@chakra-ui/react";
2 | import React from "react";
3 |
4 | const FeedbackBanner = () => (
5 |
13 |
14 | As a beta user, we'd love to know your feedback!
15 |
20 | Click here to complete a short survey.
21 | {" "}
22 |
23 | );
24 |
25 | export default FeedbackBanner;
26 |
--------------------------------------------------------------------------------
/backend/typescript/models/content.model.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Column,
3 | DataType,
4 | Model,
5 | Table,
6 | AllowNull,
7 | } from "sequelize-typescript";
8 |
9 | @Table({ tableName: "content" })
10 | export default class Content extends Model {
11 | @AllowNull(false)
12 | @Column({ type: DataType.STRING })
13 | food_rescue_description!: string;
14 |
15 | @AllowNull(false)
16 | @Column({
17 | type: DataType.STRING,
18 | validate: {
19 | isUrl: true,
20 | },
21 | })
22 | food_rescue_url!: string;
23 |
24 | @AllowNull(false)
25 | @Column({ type: DataType.STRING })
26 | checkin_description!: string;
27 |
28 | @AllowNull(false)
29 | @Column({
30 | type: DataType.STRING,
31 | validate: {
32 | isUrl: true,
33 | },
34 | })
35 | checkin_url!: string;
36 | }
37 |
--------------------------------------------------------------------------------
/frontend/src/theme/index.ts:
--------------------------------------------------------------------------------
1 | import { extendTheme } from "@chakra-ui/react";
2 | import { StepsStyleConfig as Steps } from "chakra-ui-steps";
3 |
4 | import colors from "./colors";
5 | import Button from "./components/Button";
6 | import Container from "./components/Container";
7 | import Input from "./components/Input";
8 | import textStyles from "./textStyles";
9 |
10 | const customTheme = extendTheme({
11 | fonts: {
12 | heading: "Inter, sans-serif",
13 | body: "Inter, sans-serif",
14 | },
15 | textStyles,
16 | colors,
17 | breakpoints: {
18 | sm: "320px",
19 | md: "768px",
20 | lg: "960px",
21 | },
22 | components: {
23 | Button,
24 | Steps,
25 | Container,
26 | Input,
27 | },
28 | config: {
29 | initialColorMode: "light",
30 | },
31 | });
32 |
33 | export default customTheme;
34 |
--------------------------------------------------------------------------------
/docker-compose.ci.yml:
--------------------------------------------------------------------------------
1 | # Configurations for containers in Github Actions ci
2 |
3 | version: "3.7"
4 |
5 | services:
6 | frontend:
7 | build:
8 | context: ./frontend
9 | dockerfile: Dockerfile
10 | ports:
11 | - 3000:3000
12 | environment:
13 | - CHOKIDAR_USEPOLLING=true
14 | ts-backend:
15 | build:
16 | context: ./backend/typescript
17 | dockerfile: Dockerfile
18 | ports:
19 | - 5000:5000
20 | dns:
21 | - 8.8.8.8
22 | depends_on:
23 | - db-test
24 | environment:
25 | - POSTGRES_DB
26 | - POSTGRES_USER
27 | - POSTGRES_PASSWORD
28 | - DB_TEST_HOST
29 | db-test:
30 | image: postgres:12-alpine
31 | ports:
32 | - 5430:5432
33 | environment:
34 | - POSTGRES_DB
35 | - POSTGRES_USER
36 | - POSTGRES_PASSWORD
37 |
--------------------------------------------------------------------------------
/frontend/src/components/pages/UserManagement/types.ts:
--------------------------------------------------------------------------------
1 | import { Role, Status } from "../../../types/AuthTypes";
2 |
3 | export type UserMgmtTableRecord = {
4 | userId: string;
5 | id: string;
6 | firstName: string;
7 | pointOfContact: string;
8 | company: string;
9 | email: string;
10 | phoneNumber: string;
11 | accountType: Role | string;
12 | approvalStatus: Status | string;
13 | };
14 |
15 | export enum AccountFilterType {
16 | ALL = "all",
17 | VOLUNTEER = "volunteers",
18 | DONOR = "donors",
19 | }
20 |
21 | export const accountTypefilterOptions = [
22 | {
23 | value: AccountFilterType.ALL,
24 | label: "All accounts",
25 | },
26 | {
27 | value: AccountFilterType.VOLUNTEER,
28 | label: "Volunteers",
29 | },
30 | {
31 | value: AccountFilterType.DONOR,
32 | label: "Donors",
33 | },
34 | ];
35 |
--------------------------------------------------------------------------------
/frontend/src/components/common/Card/index.tsx:
--------------------------------------------------------------------------------
1 | import { Text, VStack } from "@chakra-ui/react";
2 | import React from "react";
3 |
4 | const CardSubInformation = ({
5 | description,
6 | value,
7 | isFrequencyBlock,
8 | frequencyColorScheme,
9 | }: {
10 | description: string;
11 | value: string;
12 | isFrequencyBlock?: boolean;
13 | frequencyColorScheme?: string;
14 | }) => (
15 |
16 |
17 | {description}
18 |
19 |
25 | {value}
26 |
27 |
28 | );
29 |
30 | export default CardSubInformation;
31 |
--------------------------------------------------------------------------------
/backend/typescript/models/user.model.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Column,
3 | DataType,
4 | Model,
5 | Table,
6 | AllowNull,
7 | } from "sequelize-typescript";
8 | import { DataTypes } from "sequelize";
9 | import { Role } from "../types";
10 |
11 | @Table({ tableName: "users" })
12 | export default class User extends Model {
13 | @Column({ type: DataType.STRING })
14 | first_name!: string;
15 |
16 | @AllowNull(false)
17 | @Column({ type: DataType.STRING })
18 | last_name!: string;
19 |
20 | @AllowNull(false)
21 | @Column({ type: DataType.STRING })
22 | auth_id!: string;
23 |
24 | @AllowNull(false)
25 | @Column({ type: DataType.STRING })
26 | email!: string;
27 |
28 | @AllowNull(false)
29 | @Column({ type: DataType.ENUM("User", "Admin", "Volunteer", "Donor") })
30 | role!: Role;
31 |
32 | @Column({ type: DataTypes.STRING })
33 | phone_number!: string;
34 | }
35 |
--------------------------------------------------------------------------------
/frontend/src/components/common/GeneralErrorModal.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Modal,
3 | ModalBody,
4 | ModalCloseButton,
5 | ModalContent,
6 | ModalHeader,
7 | ModalOverlay,
8 | } from "@chakra-ui/react";
9 | import React from "react";
10 |
11 | interface GeneralErrorModalProps {
12 | isOpen: boolean;
13 | onClose: () => void;
14 | headerText: string;
15 | bodyText: string;
16 | }
17 | const GeneralErrorModal = ({
18 | isOpen,
19 | onClose,
20 | headerText,
21 | bodyText,
22 | }: GeneralErrorModalProps) => {
23 | return (
24 |
25 |
26 |
27 | {headerText}
28 |
29 | {bodyText}
30 |
31 |
32 | );
33 | };
34 |
35 | export default GeneralErrorModal;
36 |
--------------------------------------------------------------------------------
/hooks/post-merge:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # update secret files if git pull on master resulted in new changes being merged locally
3 |
4 | branch=`git symbolic-ref HEAD`
5 | root_dir=`git rev-parse --show-toplevel`
6 | # must replace with actual vault_path and default_branch, can run setup.sh
7 | vault_path="kv/community-fridge-kw"
8 | default_branch="main"
9 |
10 | if [ $branch = "refs/heads/${default_branch}" ]; then
11 | if [ -f "${root_dir}/update_secret_files.py" ]; then
12 | vault kv get -format=json $vault_path | python "${root_dir}/update_secret_files.py"
13 | if [ $? -eq 0 ]; then
14 | echo "Successfully pulled secrets from Vault"
15 | else
16 | echo "An error occurred while pulling secrets from Vault"
17 | fi
18 | else
19 | echo "To automatically update secrets after git pull on default branch, place update_secret_files.py in repo root directory"
20 | fi
21 | fi
22 |
--------------------------------------------------------------------------------
/frontend/src/components/pages/Scheduling/SaveChangesButton.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Flex } from "@chakra-ui/react";
2 | import React from "react";
3 |
4 | import useViewport from "../../../hooks/useViewport";
5 | import { ButtonProps } from "./types";
6 |
7 | export default function SaveButton({ onSaveClick }: ButtonProps) {
8 | const { isDesktop } = useViewport();
9 |
10 | return (
11 | <>
12 | {isDesktop ? (
13 |
14 |
21 |
22 | ) : (
23 |
24 |
27 |
28 | )}
29 | >
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/frontend/src/types/AuthTypes.ts:
--------------------------------------------------------------------------------
1 | export enum Role {
2 | USER = "User",
3 | ADMIN = "Admin",
4 | VOLUNTEER = "Volunteer",
5 | DONOR = "Donor",
6 | }
7 |
8 | export enum Status {
9 | APPROVED = "Approved",
10 | PENDING = "Pending",
11 | REJECTED = "Rejected",
12 | }
13 |
14 | export type AuthenticatedUser = {
15 | id: string;
16 | firstName: string;
17 | lastName: string;
18 | email: string;
19 | role: Role;
20 | accessToken: string;
21 | phoneNumber: string;
22 | isEmailVerified: boolean;
23 | } | null;
24 |
25 | export type AuthenticatedDonor = {
26 | id: string;
27 | firstName: string;
28 | lastName: string;
29 | email: string;
30 | role: Role.DONOR;
31 | phoneNumber: string;
32 | accessToken: string;
33 | businessName: string;
34 | facebookLink?: string;
35 | instagramLink?: string;
36 | } | null;
37 |
38 | export type DecodedJWT =
39 | | string
40 | | null
41 | | { [key: string]: unknown; exp: number };
42 |
--------------------------------------------------------------------------------
/frontend/src/APIClients/ContentAPIClient.ts:
--------------------------------------------------------------------------------
1 | import { BEARER_TOKEN } from "../constants/AuthConstants";
2 | import { Content, UpdateContentDataType } from "../types/ContentTypes";
3 | import baseAPIClient from "./BaseAPIClient";
4 |
5 | const getContent = async (): Promise => {
6 | try {
7 | const { data } = await baseAPIClient.get("/content", {
8 | headers: { Authorization: BEARER_TOKEN },
9 | });
10 | return data;
11 | } catch (error) {
12 | return error as Content;
13 | }
14 | };
15 |
16 | const updateContent = async (
17 | id: string,
18 | contentData: UpdateContentDataType,
19 | ): Promise => {
20 | try {
21 | const { data } = await baseAPIClient.put(`/content/${id}`, contentData, {
22 | headers: { Authorization: BEARER_TOKEN },
23 | });
24 | return data;
25 | } catch (error) {
26 | return error as Content;
27 | }
28 | };
29 |
30 | export default {
31 | getContent,
32 | updateContent,
33 | };
34 |
--------------------------------------------------------------------------------
/backend/typescript/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: "@typescript-eslint/parser",
4 | parserOptions: {
5 | ecmaVersion: 2020,
6 | project: "./tsconfig.json",
7 | sourceType: "module",
8 | createDefaultProgram: true,
9 | tsconfigRootDir: __dirname,
10 | },
11 | extends: [
12 | "airbnb-typescript/base",
13 | "prettier",
14 | "plugin:prettier/recommended",
15 | "plugin:@typescript-eslint/eslint-recommended",
16 | "plugin:@typescript-eslint/recommended",
17 | ],
18 | plugins: ["unused-imports"],
19 | rules: {
20 | "prettier/prettier": ["error", { endOfLine: "auto" }],
21 | "no-unused-vars": "off",
22 | "unused-imports/no-unused-imports": "warn",
23 | "unused-imports/no-unused-vars": [
24 | "warn",
25 | {
26 | vars: "all",
27 | varsIgnorePattern: "^_",
28 | args: "after-used",
29 | argsIgnorePattern: "^_",
30 | },
31 | ],
32 | },
33 | };
34 |
--------------------------------------------------------------------------------
/backend/typescript/middlewares/validators/donorValidator.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from "express";
2 | import { getApiValidationError, validatePrimitive } from "./util";
3 |
4 | const donorDtoValidator = async (
5 | req: Request,
6 | res: Response,
7 | next: NextFunction,
8 | ) => {
9 | if (
10 | req.body.facebookLink &&
11 | !validatePrimitive(req.body.facebookLink, "string")
12 | ) {
13 | return res
14 | .status(400)
15 | .send(getApiValidationError("facebookLink", "string"));
16 | }
17 | if (
18 | req.body.instagramLink &&
19 | !validatePrimitive(req.body.instagramLink, "string")
20 | ) {
21 | return res
22 | .status(400)
23 | .send(getApiValidationError("instagramLink", "string"));
24 | }
25 | if (!validatePrimitive(req.body.businessName, "string")) {
26 | return res
27 | .status(400)
28 | .send(getApiValidationError("businessName", "string"));
29 | }
30 |
31 | return next();
32 | };
33 |
34 | export default donorDtoValidator;
35 |
--------------------------------------------------------------------------------
/backend/typescript/models/checkIn.model.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Column,
3 | DataType,
4 | Model,
5 | Table,
6 | AllowNull,
7 | ForeignKey,
8 | AutoIncrement,
9 | PrimaryKey,
10 | BelongsTo,
11 | } from "sequelize-typescript";
12 | import Volunteer from "./volunteer.model";
13 |
14 | @Table({ tableName: "checkin" })
15 | export default class CheckIn extends Model {
16 | @PrimaryKey
17 | @AutoIncrement
18 | @Column({ type: DataType.INTEGER })
19 | id!: number;
20 |
21 | @BelongsTo(() => Volunteer)
22 | volunteer!: Volunteer;
23 |
24 | @ForeignKey(() => Volunteer)
25 | @Column({ type: DataType.INTEGER })
26 | volunteer_id!: number;
27 |
28 | @AllowNull(false)
29 | @Column({ type: DataType.DATE })
30 | start_date!: Date;
31 |
32 | @AllowNull(false)
33 | @Column({ type: DataType.DATE })
34 | end_date!: Date;
35 |
36 | @Column({ type: DataType.TEXT })
37 | notes!: string;
38 |
39 | @Column({
40 | type: DataType.BOOLEAN,
41 | defaultValue: false,
42 | })
43 | is_admin!: boolean;
44 | }
45 |
--------------------------------------------------------------------------------
/backend/typescript/utilities/servicesUtils.ts:
--------------------------------------------------------------------------------
1 | import { snakeCase } from "lodash";
2 | import dayjs from "dayjs";
3 | import { DTOTypes } from "../types";
4 | import logger from "./logger";
5 |
6 | const Logger = logger(__filename);
7 |
8 | // eslint-disable-next-line import/prefer-default-export
9 | export const toSnakeCase = (dto: DTOTypes): DTOTypes => {
10 | const dtoSnakeCase: DTOTypes = {};
11 | Object.entries(dto).forEach(([key, value]) => {
12 | dtoSnakeCase[snakeCase(key)] = value;
13 | });
14 | return dtoSnakeCase;
15 | };
16 |
17 | export const getDateWithVolunteerTime = (
18 | date: Date,
19 | volunteerTime: string | null | undefined,
20 | ): dayjs.Dayjs => {
21 | if (volunteerTime) {
22 | try {
23 | const [hours, minutes] = volunteerTime.split(":");
24 | return dayjs(date).hour(Number(hours)).minute(Number(minutes));
25 | } catch (error) {
26 | Logger.error(
27 | `volunteerTime ${volunteerTime} is not in the correct format`,
28 | );
29 | }
30 | }
31 | return dayjs(date);
32 | };
33 |
--------------------------------------------------------------------------------
/.github/workflows/firebase-hosting-merge-prod.yml:
--------------------------------------------------------------------------------
1 | # This file was auto-generated by the Firebase CLI
2 | # https://github.com/firebase/firebase-tools
3 |
4 | name: Deploy to prod Firebase Hosting on merge to main
5 |
6 | on:
7 | push:
8 | branches:
9 | - main
10 | paths:
11 | - "frontend/**"
12 |
13 | defaults:
14 | run:
15 | working-directory: frontend
16 |
17 | jobs:
18 | build_and_deploy:
19 | runs-on: ubuntu-latest
20 | steps:
21 | - uses: actions/checkout@v2
22 | - run: echo "REACT_APP_BACKEND_URL=${{ secrets.REACT_APP_BACKEND_URL_PROD }}" > .env
23 | - run: rm -rf node_modules && yarn install --frozen-lockfile && yarn build
24 | - uses: FirebaseExtended/action-hosting-deploy@v0
25 | with:
26 | repoToken: "${{ secrets.GITHUB_TOKEN }}"
27 | firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_COMMUNITY_FRIDGE_KW }}"
28 | channelId: live
29 | projectId: community-fridge-kw
30 | target: prod
31 | entryPoint: ./frontend
32 |
--------------------------------------------------------------------------------
/.github/workflows/firebase-hosting-merge-staging.yml:
--------------------------------------------------------------------------------
1 | # This file was auto-generated by the Firebase CLI
2 | # https://github.com/firebase/firebase-tools
3 |
4 | name: Deploy to staging Firebase Hosting env on merge to staging
5 |
6 | on:
7 | push:
8 | branches:
9 | - staging
10 | paths:
11 | - "frontend/**"
12 |
13 | defaults:
14 | run:
15 | working-directory: frontend
16 |
17 | jobs:
18 | build_and_deploy:
19 | runs-on: ubuntu-latest
20 | steps:
21 | - uses: actions/checkout@v2
22 | - run: echo "REACT_APP_BACKEND_URL=${{ secrets.REACT_APP_BACKEND_URL_STAGING }}" > .env
23 | - run: rm -rf node_modules && yarn install --frozen-lockfile && yarn build
24 | - uses: FirebaseExtended/action-hosting-deploy@v0
25 | with:
26 | repoToken: "${{ secrets.GITHUB_TOKEN }}"
27 | firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_COMMUNITY_FRIDGE_KW }}"
28 | channelId: live
29 | projectId: community-fridge-kw
30 | target: staging
31 | entryPoint: ./frontend
32 |
--------------------------------------------------------------------------------
/backend/typescript/services/interfaces/contentService.ts:
--------------------------------------------------------------------------------
1 | import { ContentDTO, CreateContentDTO, UpdateContentDTO } from "../../types";
2 |
3 | interface IContentService {
4 | /**
5 | * Create content description and url's for food rescue and checkins
6 | * @param content a ContentDTO with the content information
7 | * @returns a ContentDTO with the content information
8 | * @throws Error if creation fails
9 | */
10 | createContent(content: CreateContentDTO): Promise;
11 |
12 | /**
13 | * Get content description and url's for food rescue and checkins
14 | * @returns a ContentDTO with the content information
15 | * @throws Error if getting content fails
16 | */
17 | getContent(): Promise;
18 |
19 | /**
20 | * Get content description and url's for food rescue and checkins
21 | * @param id which is always 1 to retrieve the first entry
22 | * @throws Error if updating content fails
23 | */
24 | updateContent(id: string, content: UpdateContentDTO): Promise;
25 | }
26 |
27 | export default IContentService;
28 |
--------------------------------------------------------------------------------
/.github/workflows/firebase-hosting-pull-request.yml:
--------------------------------------------------------------------------------
1 | # This file was auto-generated by the Firebase CLI
2 | # https://github.com/firebase/firebase-tools
3 |
4 | name: Deploy to Firebase Hosting on PR in frontend
5 |
6 | on:
7 | pull_request:
8 | paths:
9 | - "frontend/**"
10 |
11 | defaults:
12 | run:
13 | working-directory: frontend
14 |
15 | jobs:
16 | build_and_preview:
17 | if: "${{ github.event.pull_request.head.repo.full_name == github.repository }}"
18 | runs-on: ubuntu-latest
19 | steps:
20 | - uses: actions/checkout@v2
21 | - run: echo "REACT_APP_BACKEND_URL=${{ secrets.REACT_APP_BACKEND_URL_PREVIEW }}" > .env
22 | - run: rm -rf node_modules && yarn install --frozen-lockfile && yarn build
23 | - uses: FirebaseExtended/action-hosting-deploy@v0
24 | with:
25 | repoToken: "${{ secrets.GITHUB_TOKEN }}"
26 | firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_COMMUNITY_FRIDGE_KW }}"
27 | projectId: community-fridge-kw
28 | target: staging
29 | entryPoint: ./frontend
30 |
--------------------------------------------------------------------------------
/backend/typescript/utilities/dbUtils.ts:
--------------------------------------------------------------------------------
1 | import { SequelizeOptions } from "sequelize-typescript";
2 |
3 | export const dbURL =
4 | process.env.DATABASE_URL ||
5 | `postgres://${process.env.POSTGRES_USER}:${process.env.POSTGRES_PASSWORD}@${process.env.DB_HOST}:5432/${process.env.POSTGRES_DB}`;
6 |
7 | export const dbURLTest = `postgres://${process.env.POSTGRES_USER}:${process.env.POSTGRES_PASSWORD}@${process.env.DB_TEST_HOST}:5432/${process.env.POSTGRES_DB}`;
8 |
9 | export const SQLOptions = (
10 | modelPath: string[],
11 | testdb: boolean,
12 | ): SequelizeOptions => {
13 | return process.env.NODE_ENV === "production" ||
14 | process.env.NODE_ENV === "staging"
15 | ? {
16 | dialect: "postgres",
17 | protocol: "postgres",
18 | dialectOptions: {
19 | ssl: {
20 | require: true,
21 | rejectUnauthorized: false,
22 | },
23 | },
24 | models: modelPath,
25 | ...(testdb && { logging: false }),
26 | }
27 | : {
28 | models: modelPath,
29 | ...(testdb && { logging: false }),
30 | };
31 | };
32 |
--------------------------------------------------------------------------------
/frontend/src/APIClients/BaseAPIClient.ts:
--------------------------------------------------------------------------------
1 | import axios, { AxiosRequestConfig } from "axios";
2 | import jwt from "jsonwebtoken";
3 |
4 | import { DecodedJWT } from "../types/AuthTypes";
5 |
6 | const baseAPIClient = axios.create({
7 | baseURL: process.env.REACT_APP_BACKEND_URL,
8 | });
9 |
10 | baseAPIClient.interceptors.request.use(async (config: AxiosRequestConfig) => {
11 | const newConfig = { ...config };
12 |
13 | // if access token in header has expired, auto log-out the user
14 | const authHeaderParts = config.headers.Authorization?.split(" ");
15 | if (
16 | authHeaderParts &&
17 | authHeaderParts.length >= 2 &&
18 | authHeaderParts[0].toLowerCase() === "bearer"
19 | ) {
20 | const decodedToken = jwt.decode(authHeaderParts[1]) as DecodedJWT;
21 |
22 | if (
23 | decodedToken &&
24 | (typeof decodedToken === "string" ||
25 | decodedToken.exp <= Math.round(new Date().getTime() / 1000))
26 | ) {
27 | localStorage.clear();
28 | window.location.reload();
29 | }
30 | }
31 |
32 | return newConfig;
33 | });
34 |
35 | export default baseAPIClient;
36 |
--------------------------------------------------------------------------------
/backend/typescript/umzug.ts:
--------------------------------------------------------------------------------
1 | import * as path from "path";
2 |
3 | import { Umzug, SequelizeStorage } from "umzug";
4 | import { Sequelize, SequelizeOptions } from "sequelize-typescript";
5 | import { dbURL, SQLOptions } from "./utilities/dbUtils";
6 |
7 | const sequelizeOptions: SequelizeOptions = SQLOptions(
8 | [path.join(__dirname, "/*.pgmodel.{ts,js}")],
9 | false,
10 | );
11 |
12 | const sequelize = new Sequelize(dbURL, sequelizeOptions);
13 |
14 | export const migrator = new Umzug({
15 | migrations: {
16 | glob: ["migrations/*.ts", { cwd: __dirname }],
17 | },
18 | context: sequelize,
19 | storage: new SequelizeStorage({
20 | sequelize,
21 | }),
22 | logger: console,
23 | });
24 |
25 | export type Migration = typeof migrator._types.migration;
26 |
27 | export const seeder = new Umzug({
28 | migrations: {
29 | glob: ["seeders/*.ts", { cwd: __dirname }],
30 | },
31 | context: sequelize,
32 | storage: new SequelizeStorage({
33 | sequelize,
34 | modelName: "seeder_meta",
35 | }),
36 | logger: console,
37 | });
38 |
39 | export type Seeder = typeof seeder._types.migration;
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 UW Blueprint
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/frontend/src/components/pages/Dashboard/EditDashboardSchedule.tsx:
--------------------------------------------------------------------------------
1 | import { Center, Spinner } from "@chakra-ui/react";
2 | import React, { useEffect, useState } from "react";
3 | import { useParams } from "react-router-dom";
4 |
5 | import SchedulingAPIClient from "../../../APIClients/SchedulingAPIClient";
6 | import { Schedule } from "../../../types/SchedulingTypes";
7 | import Scheduling from "../Scheduling";
8 |
9 | const EditDashboard = (): JSX.Element => {
10 | const { id } = useParams<{ id: string }>();
11 | const [currentSchedule, setCurrentSchedule] = useState();
12 | const getScheduleData = async () => {
13 | const scheduleResponse = await SchedulingAPIClient.getScheduleById(id);
14 | setCurrentSchedule(scheduleResponse);
15 | };
16 |
17 | useEffect(() => {
18 | getScheduleData();
19 | }, [id]);
20 |
21 | if (!currentSchedule) {
22 | return (
23 |
24 |
25 |
26 | );
27 | }
28 |
29 | return (
30 | currentSchedule && (
31 |
32 | )
33 | );
34 | };
35 |
36 | export default EditDashboard;
37 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Severity**
11 |
12 |
13 |
14 | - [ ] Low
15 | - [ ] Moderate
16 | - [ ] Critical
17 |
18 | **Describe the bug**
19 |
20 |
21 | **Additional context**
22 |
23 |
24 | **To reproduce**
25 |
31 |
32 | **Device information**
33 |
34 | Type:
35 | OS:
36 | Browser:
37 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Brief description. What is this change?
2 |
3 | ### [Ticket Name](https://www.notion.so/uwblueprintexecs/Team-Hub-Phase-1-Enhancements-Phase-2-cf7744a5b04748059761c1649a61a463)
4 |
5 |
6 |
7 | ## Implementation description. How did you make this change?
8 | *
9 |
10 |
11 |
12 | ## Steps to test
13 | 1.
14 |
15 |
16 |
17 |
18 | ## Checklist
19 | - [ ] My PR name is descriptive and in imperative tense
20 | - [ ] My commit messages follow conventional commits and are descriptive. My commits are atomic and trivial commits are squashed or fixup'd into non-trivial commits
21 | - [ ] I have run the appropriate linter(s)
22 | - [ ] I have requested a review from the PL, as well as other devs who have background knowledge on this PR or who will be building on top of this PR
23 | - [ ] The appropriate tests if necessary have been written
24 |
--------------------------------------------------------------------------------
/frontend/src/components/common/SchedulingProgressBar.tsx:
--------------------------------------------------------------------------------
1 | import { Flex } from "@chakra-ui/react";
2 | import { Step, Steps } from "chakra-ui-steps";
3 | import React from "react";
4 |
5 | import useViewport from "../../hooks/useViewport";
6 | import { SchedulingProgessBarProps } from "../pages/Scheduling/types";
7 |
8 | const labels = [
9 | "Date and time",
10 | "Donation information",
11 | "Volunteer information",
12 | "Confirm",
13 | ];
14 |
15 | const SchedulingProgressBar = ({
16 | activeStep,
17 | totalSteps,
18 | }: SchedulingProgessBarProps): JSX.Element => {
19 | const { isDesktop } = useViewport();
20 |
21 | return (
22 |
27 |
33 | {[...Array(totalSteps)].map((e, i) => {
34 | return isDesktop ? (
35 |
36 | ) : (
37 |
38 | );
39 | })}
40 |
41 |
42 | );
43 | };
44 |
45 | export default SchedulingProgressBar;
46 |
--------------------------------------------------------------------------------
/frontend/src/utils/LocalStorageUtils.ts:
--------------------------------------------------------------------------------
1 | // Get a string value from localStorage as an object
2 | export const getLocalStorageObj = (localStorageKey: string): O | null => {
3 | const stringifiedObj = localStorage.getItem(localStorageKey);
4 | let object = null;
5 |
6 | if (stringifiedObj) {
7 | try {
8 | object = JSON.parse(stringifiedObj);
9 | } catch (error) {
10 | object = null;
11 | }
12 | }
13 |
14 | return object;
15 | };
16 |
17 | // Get a property of an object value from localStorage
18 | export const getLocalStorageObjProperty = , P>(
19 | localStorageKey: string,
20 | property: string,
21 | ): P | null => {
22 | const object = getLocalStorageObj(localStorageKey);
23 | if (!object) return null;
24 |
25 | return object[property];
26 | };
27 |
28 | // Set a property of an object value in localStorage
29 | export const setLocalStorageObjProperty = >(
30 | localStorageKey: string,
31 | property: string,
32 | value: string,
33 | ): void => {
34 | const object: Record | null = getLocalStorageObj(
35 | localStorageKey,
36 | );
37 |
38 | if (!object) return;
39 |
40 | object[property] = value;
41 | localStorage.setItem(localStorageKey, JSON.stringify(object));
42 | };
43 |
--------------------------------------------------------------------------------
/backend/typescript/middlewares/validators/contentValidators.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from "express";
2 | import { getApiValidationError, validatePrimitive } from "./util";
3 |
4 | const contentDtoValidator = async (
5 | req: Request,
6 | res: Response,
7 | next: NextFunction,
8 | ) => {
9 | if (
10 | req.body.foodRescueDescription &&
11 | !validatePrimitive(req.body.foodRescueDescription, "string")
12 | ) {
13 | return res
14 | .status(400)
15 | .send(getApiValidationError("foodRescueDescription", "string"));
16 | }
17 | if (
18 | req.body.foodRescueUrl &&
19 | !validatePrimitive(req.body.foodRescueUrl, "string")
20 | ) {
21 | return res
22 | .status(400)
23 | .send(getApiValidationError("foodRescueUrl", "string"));
24 | }
25 | if (
26 | req.body.checkinDescription &&
27 | !validatePrimitive(req.body.checkinDescription, "string")
28 | ) {
29 | return res
30 | .status(400)
31 | .send(getApiValidationError("checkinDescription", "string"));
32 | }
33 | if (
34 | req.body.checkinUrl &&
35 | !validatePrimitive(req.body.checkinUrl, "string")
36 | ) {
37 | return res.status(400).send(getApiValidationError("checkinUrl", "string"));
38 | }
39 |
40 | return next();
41 | };
42 |
43 | export default contentDtoValidator;
44 |
--------------------------------------------------------------------------------
/frontend/src/components/auth/ResetPassword/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { NavigationProps, Step, useForm, useStep } from "react-hooks-helper";
3 |
4 | import ChangePassword from "./ChangePassword";
5 | import VerificationPage from "./VerificationEmail";
6 |
7 | export const steps = [
8 | { id: "forgot password" },
9 | { id: "verification email" },
10 | { id: "new password" },
11 | { id: "password changed success screen" },
12 | ];
13 |
14 | export interface UseStepType {
15 | step: number | Step | any;
16 | navigation: NavigationProps | any;
17 | }
18 |
19 | const ForgetPassword = ({ initialStep }: { initialStep: number }) => {
20 | const [formValues, setForm] = useForm({
21 | email: "",
22 | newPassword: "",
23 | confirmPassword: "",
24 | });
25 |
26 | const { step, navigation }: UseStepType = useStep({ steps, initialStep });
27 | const { id } = step;
28 |
29 | switch (id) {
30 | case "forgot password":
31 | return (
32 |
37 | );
38 | case "verification email":
39 | return ;
40 | default:
41 | return null;
42 | }
43 | };
44 |
45 | export default ForgetPassword;
46 |
--------------------------------------------------------------------------------
/backend/typescript/utilities/logger.ts:
--------------------------------------------------------------------------------
1 | import * as winston from "winston";
2 |
3 | const WinstonLogger: winston.Logger = winston.createLogger({
4 | level: "info",
5 | format: winston.format.combine(
6 | winston.format.timestamp(),
7 | winston.format.json(),
8 | ),
9 | transports: [
10 | new winston.transports.File({ filename: "error.log", level: "error" }),
11 | new winston.transports.File({ filename: "combined.log" }),
12 | ],
13 | });
14 |
15 | if (process.env.NODE_ENV !== "production") {
16 | WinstonLogger.add(new winston.transports.Console());
17 | }
18 |
19 | const logger = (fileName: string) => {
20 | return {
21 | error: (message: string) => {
22 | WinstonLogger.error(`[${fileName}] ${message}`);
23 | },
24 | warn: (message: string) => {
25 | WinstonLogger.warn(`[${fileName}] ${message}`);
26 | },
27 | info: (message: string) => {
28 | WinstonLogger.info(`[${fileName}] ${message}`);
29 | },
30 | http: (message: string) => {
31 | WinstonLogger.http(`[${fileName}] ${message}`);
32 | },
33 | verbose: (message: string) => {
34 | WinstonLogger.verbose(`[${fileName}] ${message}`);
35 | },
36 | debug: (message: string) => {
37 | WinstonLogger.debug(`[${fileName}] ${message}`);
38 | },
39 | };
40 | };
41 |
42 | export default logger;
43 |
--------------------------------------------------------------------------------
/frontend/src/constants/Routes.ts:
--------------------------------------------------------------------------------
1 | export const LANDING_PAGE = "/";
2 |
3 | export const LOGIN_PAGE = "/login";
4 |
5 | export const SIGNUP_PAGE = "/signup";
6 |
7 | export const HOME_PAGE = "/home";
8 |
9 | export const SCHEDULING_PAGE = "/schedule";
10 |
11 | export const DASHBOARD_PAGE = "/dashboard";
12 |
13 | export const DASHBOARD_SCHEDULE_EDIT_PAGE = "/dashboard/:id";
14 |
15 | export const ADMIN_VIEW_DONATIONS = "/admin/view-donations";
16 |
17 | export const ACCOUNT_PAGE = "/account";
18 |
19 | export const ACTION = "/action";
20 |
21 | export const USER_MANAGEMENT_PAGE = "/user-management";
22 |
23 | export const VOLUNTEER_SHIFTS_PAGE = "/volunteer-shifts";
24 |
25 | export const VOLUNTEER_SHIFT_DETAILS_PAGE = "/volunteer-shifts/:id/:type";
26 |
27 | export const FORGET_PASSWORD = "/forget-password";
28 |
29 | export const ADMIN_CHECK_INS = "/admin/check-ins";
30 |
31 | export const VOLUNTEER_DASHBOARD_PAGE = "/volunteer-dashboard";
32 |
33 | export const ADMIN_DELETE_CHECK_INS = "/admin/delete-check-ins";
34 |
35 | export const CREATE_CHECKIN = "/admin/create-checkin";
36 |
37 | export const ADMIN_CHECKIN_EDIT = "/admin/edit-checkin/:id";
38 |
39 | export const ADMIN_CHECK_IN_EDIT_DESCRIPTION_PAGE =
40 | "/admin/edit-check-in-description";
41 |
42 | export const ADMIN_FOOD_RESCUE_EDIT_DESCRIPTION_PAGE =
43 | "/admin/edit-food-rescue-description";
44 |
--------------------------------------------------------------------------------
/frontend/src/types/VolunteerTypes.ts:
--------------------------------------------------------------------------------
1 | import { Role, Status } from "./AuthTypes";
2 | import { CheckIn } from "./CheckInTypes";
3 | import { Schedule } from "./SchedulingTypes";
4 |
5 | export type VolunteerResponse = {
6 | id: string;
7 | firstName: string;
8 | lastName: string;
9 | email: string;
10 | role: Role.VOLUNTEER;
11 | userId: string;
12 | phoneNumber: string;
13 | status: Status;
14 | };
15 |
16 | export type AuthenticatedVolunteer = VolunteerResponse | null;
17 |
18 | export type UpdateVolunteerDataType = {
19 | status: Status;
20 | };
21 |
22 | export type VolunteerContextType = {
23 | volunteerId: string | null;
24 | volunteerStatus: Status;
25 | };
26 |
27 | export type AuthenticatedVolunteerContext = VolunteerContextType | null;
28 |
29 | export type VolunteerContextAction =
30 | | {
31 | type: "SET_VOLUNTEER_ID";
32 | value: string;
33 | }
34 | | {
35 | type: "SET_VOLUNTEER_STATUS";
36 | value: Status;
37 | };
38 |
39 | export enum ShiftType {
40 | CHECKIN = "checkIn",
41 | SCHEDULING = "scheduling",
42 | }
43 |
44 | export type ScheduleWithShiftType = Schedule & {
45 | type: ShiftType.SCHEDULING;
46 | };
47 | export type CheckInWithShiftType = CheckIn & { type: ShiftType.CHECKIN };
48 |
49 | export type VolunteerDTO = {
50 | id: string;
51 | userId: string;
52 | status: Status;
53 | };
54 |
--------------------------------------------------------------------------------
/backend/typescript/services/implementations/emailService.ts:
--------------------------------------------------------------------------------
1 | import nodemailer, { Transporter } from "nodemailer";
2 | import IEmailService from "../interfaces/emailService";
3 | import { NodemailerConfig } from "../../types";
4 | import logger from "../../utilities/logger";
5 | import getErrorMessage from "../../utilities/errorMessageUtil";
6 |
7 | const Logger = logger(__filename);
8 |
9 | class EmailService implements IEmailService {
10 | transporter: Transporter;
11 |
12 | sender?: string;
13 |
14 | constructor(nodemailerConfig: NodemailerConfig, displayName?: string) {
15 | this.transporter = nodemailer.createTransport(nodemailerConfig);
16 | if (displayName) {
17 | this.sender = `${displayName} <${nodemailerConfig.auth.user}>`;
18 | } else {
19 | this.sender = nodemailerConfig.auth.user;
20 | }
21 | }
22 |
23 | async sendEmail(
24 | to: string,
25 | subject: string,
26 | htmlBody: string,
27 | ): Promise {
28 | const mailOptions = {
29 | from: this.sender,
30 | to,
31 | subject,
32 | html: htmlBody,
33 | };
34 |
35 | try {
36 | return await this.transporter.sendMail(mailOptions);
37 | } catch (error) {
38 | Logger.error(`Failed to send email. Reason = ${getErrorMessage(error)}`);
39 | throw error;
40 | }
41 | }
42 | }
43 |
44 | export default EmailService;
45 |
--------------------------------------------------------------------------------
/frontend/src/components/auth/Signup/ReturnToLoginModal.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Modal,
4 | ModalBody,
5 | ModalCloseButton,
6 | ModalContent,
7 | ModalFooter,
8 | ModalHeader,
9 | ModalOverlay,
10 | } from "@chakra-ui/react";
11 | import React from "react";
12 | import { useHistory } from "react-router-dom";
13 |
14 | import * as Routes from "../../../constants/Routes";
15 |
16 | const ReturnToLoginModal = ({
17 | errorHeader,
18 | errorMessage,
19 | isOpen,
20 | onClose,
21 | }: {
22 | errorHeader: string;
23 | errorMessage: string;
24 | isOpen: boolean;
25 | onClose: () => void;
26 | }) => {
27 | const history = useHistory();
28 |
29 | return (
30 |
31 |
32 |
33 | {errorHeader}
34 |
35 | {errorMessage}
36 |
37 |
38 |
47 |
48 |
49 |
50 | );
51 | };
52 |
53 | export default ReturnToLoginModal;
54 |
--------------------------------------------------------------------------------
/frontend/src/components/common/GeneralDeleteShiftModal.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Modal,
4 | ModalBody,
5 | ModalCloseButton,
6 | ModalContent,
7 | ModalFooter,
8 | ModalHeader,
9 | ModalOverlay,
10 | Text,
11 | } from "@chakra-ui/react";
12 | import React from "react";
13 |
14 | interface GeneralDeleteShiftModalProps {
15 | title: string;
16 | bodyText: string;
17 | buttonLabel: string;
18 | isOpen: boolean;
19 | onClose: () => void;
20 | onDelete: () => void;
21 | }
22 | const GeneralDeleteShiftModal = ({
23 | title,
24 | bodyText,
25 | buttonLabel,
26 | isOpen,
27 | onClose,
28 | onDelete,
29 | }: GeneralDeleteShiftModalProps) => {
30 | return (
31 |
32 |
33 |
34 |
35 | {title}
36 |
37 |
38 |
39 | {bodyText}
40 |
41 |
42 |
45 |
46 |
47 |
48 | );
49 | };
50 |
51 | export default GeneralDeleteShiftModal;
52 |
--------------------------------------------------------------------------------
/frontend/src/constants/DaysInWeek.ts:
--------------------------------------------------------------------------------
1 | import { Locale } from "date-fns";
2 | import { enUS } from "date-fns/locale";
3 |
4 | export const convertTime = (dateToConvert: string): string => {
5 | return new Date(dateToConvert).toLocaleTimeString(navigator.language, {
6 | hour: "2-digit",
7 | minute: "2-digit",
8 | hour12: true,
9 | });
10 | };
11 |
12 | export const colorMap = {
13 | "One time": "spinach",
14 | Daily: "h20",
15 | Weekly: "onion",
16 | Monthly: "turnip",
17 | };
18 |
19 | export const getFrequencyColor = (frequency: string): string => {
20 | switch (frequency) {
21 | case "One time":
22 | return "#317C71";
23 | case "Daily":
24 | return "#496DB6";
25 | case "Weekly":
26 | return "#8557BC";
27 | case "Monthly":
28 | return "#BC577B";
29 | default:
30 | return "";
31 | }
32 | };
33 | type DaysInWeekProps = {
34 | locale?: Locale;
35 | };
36 |
37 | const daysInWeek = ({ locale = enUS }: DaysInWeekProps) => [
38 | { day: 0, label: locale.localize?.day(0) },
39 | { day: 1, label: locale.localize?.day(1) },
40 | { day: 2, label: locale.localize?.day(2) },
41 | { day: 3, label: locale.localize?.day(3) },
42 | { day: 4, label: locale.localize?.day(4) },
43 | { day: 5, label: locale.localize?.day(5) },
44 | { day: 6, label: locale.localize?.day(6) },
45 | ];
46 |
47 | export default daysInWeek;
48 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.7"
2 |
3 | services:
4 | frontend:
5 | build:
6 | context: ./frontend
7 | dockerfile: Dockerfile
8 | volumes:
9 | - ./frontend:/app
10 | - /app/node_modules
11 | ports:
12 | - 3000:3000
13 | environment:
14 | - CHOKIDAR_USEPOLLING=true
15 | env_file:
16 | - ./frontend/.env
17 | ts-backend:
18 | build:
19 | context: ./backend/typescript
20 | dockerfile: Dockerfile
21 | volumes:
22 | - ./backend/typescript:/app
23 | - /app/node_modules
24 | ports:
25 | - 5000:5000
26 | dns:
27 | - 8.8.8.8
28 | depends_on:
29 | - db
30 | - db-test
31 | env_file:
32 | - ./.env
33 | db:
34 | image: postgres:12-alpine
35 | ports:
36 | - 5432:5432
37 | volumes:
38 | - postgres_data:/var/lib/postgresql/data/
39 | env_file:
40 | - ./.env
41 | db-test:
42 | image: postgres:12-alpine
43 | ports:
44 | - 5430:5432
45 | volumes:
46 | - postgres_data_test:/var/lib/postgresql/data/
47 | env_file:
48 | - ./.env
49 | pgadmin:
50 | container_name: pgadmin4_container
51 | image: dpage/pgadmin4
52 | logging:
53 | driver: none
54 | restart: always
55 | ports:
56 | - 5050:80
57 | env_file:
58 | - ./.env
59 | depends_on:
60 | - db
61 |
62 | volumes:
63 | postgres_data:
64 | postgres_data_test:
65 |
--------------------------------------------------------------------------------
/frontend/src/theme/colors.ts:
--------------------------------------------------------------------------------
1 | const colors = {
2 | raddish: {
3 | 50: "#ffe5f9",
4 | 100: "#C24A84",
5 | 200: "#f38dcf",
6 | 300: "#ec5fbb",
7 | 400: "#e633a7",
8 | 500: "#cc198d",
9 | 600: "#a0116e",
10 | 700: "#720a50",
11 | 800: "#470330",
12 | 900: "#1d0013",
13 | },
14 | black: {
15 | // colorScheme prop requires a full range of colours in Chakra
16 | 50: "#EDF2F7",
17 | 100: "#171717",
18 | 200: "#E2E8F0",
19 | 300: "#CBD5E0",
20 | 400: "#A0AEC0",
21 | 500: "#171717",
22 | 600: "#171717",
23 | 700: "#2D3748",
24 | 800: "#1A202C",
25 | 900: "#171923",
26 | },
27 | squash: {
28 | 100: "#FAFCFE",
29 | },
30 | hubbard: {
31 | 100: "#6C6C84",
32 | },
33 | mold: {
34 | 100: "#D8DDE0",
35 | },
36 | dorian: {
37 | 100: "#ECF1F4",
38 | },
39 | tomato: {
40 | 100: "#D10000",
41 | },
42 | champagne: {
43 | 100: "#8D2569",
44 | },
45 | strawberry: {
46 | 100: "#F0C5E1",
47 | },
48 | cottonCandy: {
49 | 100: "#FDF5FA",
50 | },
51 | spinach: {
52 | 50: "#EFFCF9",
53 | 100: "#317C71",
54 | },
55 | h20: {
56 | 50: "#EFF6FC",
57 | 100: "#496DB6",
58 | },
59 | onion: {
60 | 50: "#F5EFFC",
61 | 100: "#8557BC",
62 | },
63 | turnip: {
64 | 50: "#FCEFF2",
65 | 100: "#BC577B",
66 | },
67 | gray: {
68 | 100: "#C4C4C4",
69 | },
70 | };
71 | export default colors;
72 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | firebase-debug.log*
8 | firebase-debug.*.log*
9 |
10 | # Firebase cache
11 | .firebase/
12 |
13 | # Firebase config
14 |
15 | # Uncomment this if you'd like others to create their own Firebase project.
16 | # For a team working on the same Firebase project(s), it is recommended to leave
17 | # it commented so all members can deploy to the same project(s) in .firebaserc.
18 | # .firebaserc
19 |
20 | # Runtime data
21 | pids
22 | *.pid
23 | *.seed
24 | *.pid.lock
25 |
26 | # Directory for instrumented libs generated by jscoverage/JSCover
27 | lib-cov
28 |
29 | # Coverage directory used by tools like istanbul
30 | coverage
31 |
32 | # nyc test coverage
33 | .nyc_output
34 |
35 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
36 | .grunt
37 |
38 | # Bower dependency directory (https://bower.io/)
39 | bower_components
40 |
41 | # node-waf configuration
42 | .lock-wscript
43 |
44 | # Compiled binary addons (http://nodejs.org/api/addons.html)
45 | build/Release
46 |
47 | # Dependency directories
48 | node_modules/
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Optional REPL history
57 | .node_repl_history
58 |
59 | # Output of 'npm pack'
60 | *.tgz
61 |
62 | # Yarn Integrity file
63 | .yarn-integrity
64 |
65 | # dotenv environment variables file
66 | .env
67 |
--------------------------------------------------------------------------------
/frontend/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: "@typescript-eslint/parser",
4 | parserOptions: {
5 | ecmaVersion: 2020,
6 | sourceType: "module",
7 | ecmaFeatures: {
8 | jsx: true,
9 | },
10 | project: "./tsconfig.json",
11 | createDefaultProgram: true,
12 | tsconfigRootDir: __dirname,
13 | },
14 | settings: {
15 | react: {
16 | version: "detect",
17 | },
18 | },
19 | extends: [
20 | "airbnb-typescript",
21 | "prettier",
22 | "plugin:prettier/recommended",
23 | "plugin:react/recommended",
24 | "plugin:react-hooks/recommended",
25 | "plugin:@typescript-eslint/eslint-recommended",
26 | "plugin:@typescript-eslint/recommended",
27 | ],
28 | plugins: ["simple-import-sort", "unused-imports"],
29 | rules: {
30 | "prettier/prettier": ["warn", { endOfLine: "auto" }],
31 | "react/require-default-props": "off",
32 | "react/no-array-index-key": "off",
33 | "jsx-a11y/click-events-have-key-events": "off",
34 | "jsx-a11y/no-static-element-interactions": "off",
35 | "sort-imports": "off",
36 | "simple-import-sort/imports": "warn",
37 | "simple-import-sort/exports": "warn",
38 | "no-unused-vars": "off",
39 | "unused-imports/no-unused-imports": "warn",
40 | "unused-imports/no-unused-vars": [
41 | "warn",
42 | {
43 | vars: "all",
44 | varsIgnorePattern: "^_",
45 | args: "after-used",
46 | argsIgnorePattern: "^_",
47 | },
48 | ],
49 | },
50 | };
51 |
--------------------------------------------------------------------------------
/frontend/src/components/auth/ResetPassword/PasswordChanged.tsx:
--------------------------------------------------------------------------------
1 | import { CloseIcon } from "@chakra-ui/icons";
2 | import { Container, IconButton, Image, Text } from "@chakra-ui/react";
3 | import React from "react";
4 | import { useHistory } from "react-router-dom";
5 |
6 | import confirmVerificationImage from "../../../assets/authentication_complete.svg";
7 | import * as Routes from "../../../constants/Routes";
8 |
9 | const PasswordChanged = () => {
10 | const history = useHistory();
11 | return (
12 |
13 | history.push(Routes.LANDING_PAGE)}
18 | display={{ md: "none" }}
19 | >
20 |
21 |
22 |
23 |
29 |
30 | Your password has been changed!
31 |
32 |
38 | Your password has been changed!
39 |
40 |
41 |
42 | );
43 | };
44 |
45 | export default PasswordChanged;
46 |
--------------------------------------------------------------------------------
/frontend/src/components/auth/Signup/ConfirmVerificationPage.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Container, Image, Text } from "@chakra-ui/react";
2 | import React from "react";
3 | import { useHistory } from "react-router-dom";
4 |
5 | import confirmVerificationImage from "../../../assets/authentication_complete.svg";
6 | import * as Routes from "../../../constants/Routes";
7 |
8 | const ConfirmVerificationPage = () => {
9 | const history = useHistory();
10 |
11 | return (
12 |
13 |
14 |
20 |
25 | Thank you for verifying your email address!
26 |
27 |
33 | Almost done! Click “Finish” to contine.
34 |
35 |
43 |
44 |
45 | );
46 | };
47 |
48 | export default ConfirmVerificationPage;
49 |
--------------------------------------------------------------------------------
/frontend/src/components/pages/VolunteerDashboard/PendingPage.tsx:
--------------------------------------------------------------------------------
1 | import { Container, Image, Text } from "@chakra-ui/react";
2 | import React from "react";
3 | import { useHistory } from "react-router-dom";
4 |
5 | import pendingImage from "../../../assets/pending.svg";
6 | import * as Routes from "../../../constants/Routes";
7 |
8 | const PendingPage = () => {
9 | const history = useHistory();
10 | const navigateToDashboard = () => {
11 | history.push(Routes.LANDING_PAGE);
12 | };
13 |
14 | return (
15 |
16 |
17 |
22 | Pending account approval
23 |
24 |
30 |
37 | We’re evaluating your profile!
38 |
39 |
45 | You will receive an email when your account has been approved!
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | export default PendingPage;
53 |
--------------------------------------------------------------------------------
/frontend/src/APIClients/UserAPIClient.ts:
--------------------------------------------------------------------------------
1 | import { BEARER_TOKEN } from "../constants/AuthConstants";
2 | import { Role } from "../types/AuthTypes";
3 | import baseAPIClient from "./BaseAPIClient";
4 |
5 | type UserRequest = {
6 | id: string;
7 | firstName: string;
8 | lastName: string;
9 | role: Role;
10 | phoneNumber: string;
11 | email: string;
12 | };
13 |
14 | type UserResponse = {
15 | id: string;
16 | firstName: string;
17 | lastName: string;
18 | role: Role;
19 | phoneNumber: string;
20 | email: string;
21 | };
22 |
23 | const getUserById = async (id: string): Promise => {
24 | try {
25 | const { data } = await baseAPIClient.get(`/users/${id}`, {
26 | headers: { Authorization: BEARER_TOKEN },
27 | });
28 | return data;
29 | } catch (error) {
30 | return error as UserResponse;
31 | }
32 | };
33 |
34 | const updateUserById = async (
35 | id: string,
36 | {
37 | userData,
38 | }: {
39 | userData: UserRequest;
40 | },
41 | ): Promise => {
42 | try {
43 | const { data } = await baseAPIClient.put(`/users/${id}`, userData, {
44 | headers: { Authorization: BEARER_TOKEN },
45 | });
46 | return data;
47 | } catch (error) {
48 | return error as UserResponse;
49 | }
50 | };
51 |
52 | const deleteUserById = async (id: string): Promise => {
53 | try {
54 | await baseAPIClient.delete(`/users?userId=${id}`, {
55 | headers: { Authorization: BEARER_TOKEN },
56 | });
57 | return true;
58 | } catch (error) {
59 | return false;
60 | }
61 | };
62 |
63 | export default { getUserById, updateUserById, deleteUserById };
64 |
--------------------------------------------------------------------------------
/frontend/src/components/pages/VolunteerDashboard/ShiftDetails.tsx:
--------------------------------------------------------------------------------
1 | import { Center, Spinner } from "@chakra-ui/react";
2 | import React, { useEffect, useState } from "react";
3 | import { useParams } from "react-router-dom";
4 |
5 | import CheckInAPIClient from "../../../APIClients/CheckInAPIClient";
6 | import SchedulingAPIClient from "../../../APIClients/SchedulingAPIClient";
7 | import {
8 | CheckInWithShiftType,
9 | ScheduleWithShiftType,
10 | ShiftType,
11 | } from "../../../types/VolunteerTypes";
12 | import ConfirmShiftDetails from "../VolunteerScheduling/ConfirmShiftDetails";
13 |
14 | const VolunteerShiftDetailsPage = (): JSX.Element => {
15 | const { id, type } = useParams<{ id: string; type: ShiftType }>();
16 | const [currentShift, setCurrentShift] = useState<
17 | ScheduleWithShiftType | CheckInWithShiftType
18 | >();
19 |
20 | useEffect(() => {
21 | const getShiftData = async () => {
22 | if (type === ShiftType.SCHEDULING) {
23 | const schedule = await SchedulingAPIClient.getScheduleById(id);
24 | setCurrentShift({ ...schedule, type: ShiftType.SCHEDULING });
25 | } else {
26 | const checkIn = await CheckInAPIClient.getCheckInsById(id);
27 | setCurrentShift({ ...checkIn, type: ShiftType.CHECKIN });
28 | }
29 | };
30 | getShiftData();
31 | }, [id]);
32 |
33 | if (!currentShift) {
34 | return (
35 |
36 |
37 |
38 | );
39 | }
40 |
41 | return (
42 | currentShift && (
43 |
44 | )
45 | );
46 | };
47 |
48 | export default VolunteerShiftDetailsPage;
49 |
--------------------------------------------------------------------------------
/frontend/src/components/auth/PrivateRoute.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import { Redirect, Route } from "react-router-dom";
3 |
4 | import * as Routes from "../../constants/Routes";
5 | import AuthContext from "../../contexts/AuthContext";
6 | import { Role } from "../../types/AuthTypes";
7 |
8 | type PrivateRouteProps = {
9 | component: React.FC;
10 | path: string;
11 | exact: boolean;
12 | adminOnly?: boolean;
13 | volunteerOnly?: boolean;
14 | donorOnly?: boolean;
15 | };
16 |
17 | const PrivateRoute: React.FC = ({
18 | component,
19 | exact,
20 | path,
21 | adminOnly,
22 | donorOnly,
23 | volunteerOnly,
24 | }: PrivateRouteProps) => {
25 | const { authenticatedUser } = useContext(AuthContext);
26 |
27 | if (adminOnly) {
28 | return authenticatedUser?.role === Role.ADMIN ? (
29 |
30 | ) : (
31 |
32 | );
33 | }
34 |
35 | if (donorOnly) {
36 | return authenticatedUser?.role === Role.DONOR ? (
37 |
38 | ) : (
39 |
40 | );
41 | }
42 |
43 | if (volunteerOnly) {
44 | return authenticatedUser?.role === Role.VOLUNTEER ? (
45 |
46 | ) : (
47 |
48 | );
49 | }
50 |
51 | return authenticatedUser ? (
52 |
53 | ) : (
54 |
55 | );
56 | };
57 |
58 | export default PrivateRoute;
59 |
--------------------------------------------------------------------------------
/backend/typescript/rest/cronRoutes.ts:
--------------------------------------------------------------------------------
1 | import { Router } from "express";
2 | import getErrorMessage from "../utilities/errorMessageUtil";
3 | import ICronService from "../services/interfaces/cronService";
4 | import CronService from "../services/implementations/cronService";
5 | import DonorService from "../services/implementations/donorService";
6 | import IEmailService from "../services/interfaces/emailService";
7 | import IDonorService from "../services/interfaces/donorService";
8 | import EmailService from "../services/implementations/emailService";
9 | import nodemailerConfig from "../nodemailer.config";
10 | import VolunteerService from "../services/implementations/volunteerService";
11 | import IVolunteerService from "../services/interfaces/volunteerService";
12 |
13 | const emailService: IEmailService = new EmailService(nodemailerConfig);
14 | const donorService: IDonorService = new DonorService();
15 | const volunteerService: IVolunteerService = new VolunteerService();
16 | const cronRouter: Router = Router();
17 | const cronService: ICronService = new CronService(
18 | emailService,
19 | donorService,
20 | volunteerService,
21 | );
22 |
23 | cronRouter.post("/schedules", async (req, res) => {
24 | try {
25 | await cronService.checkScheduleReminders();
26 | res.status(200).send();
27 | } catch (error) {
28 | res.status(500).json({ error: getErrorMessage(error) });
29 | }
30 | });
31 |
32 | cronRouter.post("/checkins", async (req, res) => {
33 | try {
34 | await cronService.checkCheckInReminders();
35 | res.status(200).send();
36 | } catch (error) {
37 | res.status(500).json({ error: getErrorMessage(error) });
38 | }
39 | });
40 |
41 | export default cronRouter;
42 |
--------------------------------------------------------------------------------
/backend/typescript/rest/contentRoutes.ts:
--------------------------------------------------------------------------------
1 | import { Router } from "express";
2 | import ContentService from "../services/implementations/contentService";
3 | import IContentService from "../services/interfaces/contentService";
4 | import getErrorMessage from "../utilities/errorMessageUtil";
5 | import { sendResponseByMimeType } from "../utilities/responseUtil";
6 | import contentDtoValidator from "../middlewares/validators/contentValidators";
7 |
8 | const contentRouter: Router = Router();
9 | const contentService: IContentService = new ContentService();
10 |
11 | contentRouter.post("/", contentDtoValidator, async (req, res) => {
12 | try {
13 | const newContent = await contentService.createContent({
14 | ...req.body,
15 | });
16 | res.status(201).json(newContent);
17 | } catch (error) {
18 | res.status(500).json({ error: getErrorMessage(error) });
19 | }
20 | });
21 |
22 | contentRouter.get("/", async (req, res) => {
23 | const contentType = req.headers["content-type"];
24 |
25 | try {
26 | const contents = await contentService.getContent();
27 | await sendResponseByMimeType(res, 200, contentType, contents);
28 | } catch (error) {
29 | await sendResponseByMimeType(res, 500, contentType, [
30 | {
31 | error: getErrorMessage(error),
32 | },
33 | ]);
34 | }
35 | });
36 |
37 | contentRouter.put("/:id", contentDtoValidator, async (req, res) => {
38 | try {
39 | const updatedContent = await contentService.updateContent(req.params.id, {
40 | ...req.body,
41 | });
42 | res.status(200).json(updatedContent);
43 | } catch (error) {
44 | res.status(500).json({ error: getErrorMessage(error) });
45 | }
46 | });
47 |
48 | export default contentRouter;
49 |
--------------------------------------------------------------------------------
/frontend/src/components/common/UserManagement/EditAccountModal.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Modal,
4 | ModalBody,
5 | ModalCloseButton,
6 | ModalContent,
7 | ModalFooter,
8 | ModalHeader,
9 | ModalOverlay,
10 | Text,
11 | } from "@chakra-ui/react";
12 | import React from "react";
13 |
14 | import useViewport from "../../../hooks/useViewport";
15 |
16 | interface EditAccountModalProps {
17 | isOpen: boolean;
18 | onClose: () => void;
19 | discardChanges: () => void;
20 | }
21 | const EditAccountModal = ({
22 | isOpen,
23 | onClose,
24 | discardChanges,
25 | }: EditAccountModalProps) => {
26 | const { isDesktop } = useViewport();
27 |
28 | return (
29 | <>
30 |
36 |
37 |
38 |
39 |
40 |
41 | Are you sure you want to leave the page?
42 |
43 |
44 |
45 | Any changes made to account information will not be saved.
46 |
47 |
48 |
56 |
57 |
58 |
59 | >
60 | );
61 | };
62 |
63 | export default EditAccountModal;
64 |
--------------------------------------------------------------------------------
/frontend/src/components/pages/Scheduling/selectDateTime.css:
--------------------------------------------------------------------------------
1 | .rmdp-mobile.rmdp-wrapper {
2 | border: none;
3 | margin-top: 25px;
4 | margin-bottom: 25px;
5 | }
6 |
7 | .rmdp-mobile.desktop .rmdp-day-picker > div {
8 | width: 50vw;
9 | }
10 |
11 | .rmdp-calendar {
12 | background-color: #FAFCFE;
13 | border-radius: 10px;
14 | padding: 20px !important;
15 | }
16 |
17 | .rmdp-header {
18 | font-weight: 600;
19 | padding-left: 4px;
20 | }
21 |
22 | .rmdp-week-day {
23 | color: #000 !important;
24 | }
25 |
26 | .rmdp-day.rmdp-today span {
27 | color: #000 !important;
28 | background-color: #ffe5f9 !important;
29 | }
30 |
31 | .rmdp-day.rmdp-selected span, .rmdp-day.rmdp-selected.rmdp-today span, .rmdp-day.rmdp-range.start span, .rmdp-day.rmdp-range.end span {
32 | background-color: #C24A84 !important;
33 | color: #fff !important;
34 | }
35 |
36 | .rmdp-container {
37 | width: 100%;
38 | }
39 | .rmdp-input {
40 | width: 100%;
41 | height: 40px !important;
42 | }
43 |
44 | .rmdp-arrow {
45 | border: solid #C24A84 !important;
46 | border-width: 0 2px 2px 0 !important;
47 | margin-top: 6px;
48 | }
49 |
50 | .rmdp-arrow:hover {
51 | border: solid #fff !important;
52 | border-width: 0 2px 2px 0 !important;
53 | }
54 |
55 | .rmdp-arrow-container:hover, .rmdp-day span:hover {
56 | background-color: #f38dcf !important;
57 | }
58 |
59 | .rmdp-day.rmdp-disabled span:hover, .rmdp-day.rmdp-day-hidden span:hover {
60 | background-color: transparent !important;
61 | }
62 |
63 | .rmdp-arrow-container.disabled.rmdp-right, .rmdp-arrow-container.disabled.rmdp-left {
64 | display: none !important;
65 | }
66 |
67 | .rmdp-day.rmdp-range {
68 | box-shadow: none !important;
69 | color: #000 !important;
70 | background-color: #ffe5f9 !important;
71 | }
--------------------------------------------------------------------------------
/frontend/src/components/pages/VolunteerScheduling/CheckInCalendar.tsx:
--------------------------------------------------------------------------------
1 | import { Container, Flex } from "@chakra-ui/react";
2 | import React, { useState } from "react";
3 | import { NavigationProps } from "react-hooks-helper";
4 | import { DateObject } from "react-multi-date-picker";
5 |
6 | import {
7 | CheckInWithShiftType,
8 | ScheduleWithShiftType,
9 | } from "../../../types/VolunteerTypes";
10 | import Calendar from "../../common/Calendar/Calendar";
11 |
12 | const CheckInCalendar = ({
13 | isAdminView = false,
14 | checkIns,
15 | setSelectedVolunteerShift,
16 | navigation,
17 | }: {
18 | isAdminView?: boolean;
19 | checkIns: CheckInWithShiftType[];
20 | navigation: NavigationProps;
21 | setSelectedVolunteerShift: (
22 | shift: ScheduleWithShiftType | CheckInWithShiftType,
23 | ) => void;
24 | }): React.ReactElement => {
25 | const [selectedDay, setSelectedDay] = useState(
26 | new Date(),
27 | );
28 |
29 | return (
30 |
31 |
37 | setSelectedDay(day)}
41 | items={checkIns}
42 | isAdminView={isAdminView}
43 | isCheckInView
44 | isCheckInShiftView
45 | navigation={navigation}
46 | setSelectedVolunteerShift={setSelectedVolunteerShift}
47 | />
48 |
49 |
50 | );
51 | };
52 |
53 | export default CheckInCalendar;
54 |
--------------------------------------------------------------------------------
/frontend/src/utils/__tests__/LocalStorageUtils.test.ts:
--------------------------------------------------------------------------------
1 | import * as LocalStorageUtils from "../LocalStorageUtils";
2 |
3 | class LocalStorageMock {
4 | store: Record = {};
5 |
6 | readonly length: number = 0;
7 |
8 | clear() {
9 | this.store = {};
10 | }
11 |
12 | getItem(key: string) {
13 | return this.store[key] || null;
14 | }
15 |
16 | setItem(key: string, value: string) {
17 | this.store[key] = value;
18 | }
19 |
20 | removeItem(key: string) {
21 | delete this.store[key];
22 | }
23 | }
24 |
25 | const mockStorage = new LocalStorageMock();
26 |
27 | describe("LocalStorageUtils", () => {
28 | beforeAll(() => {
29 | Object.defineProperty(global, "localStorage", {
30 | value: mockStorage,
31 | });
32 | });
33 |
34 | afterEach(() => {
35 | localStorage.clear();
36 | });
37 |
38 | it("getLocalStorageObj should retrieve obj by key", () => {
39 | localStorage.setItem("hello", JSON.stringify({ value: "world" }));
40 | expect(LocalStorageUtils.getLocalStorageObj("hello")).toEqual({
41 | value: "world",
42 | });
43 | });
44 |
45 | it("getLocalStorageObjProperty should retrieve obj property by key and property", () => {
46 | localStorage.setItem("hello", JSON.stringify({ value: "world" }));
47 | expect(
48 | LocalStorageUtils.getLocalStorageObjProperty("hello", "value"),
49 | ).toEqual("world");
50 | });
51 |
52 | it("setLocalStorageObjproperty should set obj property by key, property and value", () => {
53 | localStorage.setItem("club", JSON.stringify({}));
54 | LocalStorageUtils.setLocalStorageObjProperty("club", "name", "Blueprint");
55 | expect(LocalStorageUtils.getLocalStorageObj("club")).toEqual({
56 | name: "Blueprint",
57 | });
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/backend/typescript/services/interfaces/donorService.ts:
--------------------------------------------------------------------------------
1 | import {
2 | UpdateDonorDTO,
3 | UserDonorDTO,
4 | DonorDTO,
5 | CreateDonorDTO,
6 | } from "../../types";
7 |
8 | interface IDonorService {
9 | /**
10 | * Get donor associated with id
11 | * @param id donors's id
12 | * @returns a UserDonorDTO with donor's information
13 | * @throws Error if donor retrieval fails
14 | */
15 | getDonorById(id: string): Promise;
16 |
17 | /**
18 | * Get donor associated with a user id
19 | * @param userId id associated with user
20 | * @returns a UserDonorDTO with donor's information
21 | * @throws Error if donor retrieval fails
22 | */
23 | getDonorByUserId(userId: string): Promise;
24 |
25 | /**
26 | * Get all donor information (possibly paginated in the future)
27 | * @returns array of UserDonorDTO
28 | * @throws Error if donors retrieval fails
29 | */
30 | getDonors(): Promise>;
31 |
32 | /**
33 | * Create new Donor in database
34 | * @param donor is a new donor object
35 | * @returns a DonorDTO with Donor's information
36 | * @throws Error if donor creation fails
37 | */
38 | createDonor(donor: CreateDonorDTO): Promise;
39 |
40 | /**
41 | * Update a donor.
42 | * Note: the password cannot be updated using this method, use IAuthService.resetPassword instead
43 | * @param id donor's id
44 | * @param donor the donor to be updated
45 | * @throws Error if donor update fails
46 | */
47 | updateDonorById(id: string, donor: UpdateDonorDTO): Promise;
48 |
49 | /**
50 | * Delete a donor by id
51 | * @param id donor's id
52 | * @throws Error if donor deletion fails
53 | */
54 | deleteDonorById(id: string): Promise;
55 | }
56 |
57 | export default IDonorService;
58 |
--------------------------------------------------------------------------------
/frontend/src/assets/personIcon.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/frontend/src/components/pages/VolunteerScheduling/CheckIns.tsx:
--------------------------------------------------------------------------------
1 | import { Divider, Spinner } from "@chakra-ui/react";
2 | import React, { useEffect, useState } from "react";
3 | import { NavigationProps } from "react-hooks-helper";
4 |
5 | import CheckInAPIClient from "../../../APIClients/CheckInAPIClient";
6 | import {
7 | CheckInWithShiftType,
8 | ScheduleWithShiftType,
9 | ShiftType,
10 | } from "../../../types/VolunteerTypes";
11 | import FridgeCheckInDescription from "../../common/FridgeCheckInDescription";
12 | import CheckInCalendar from "./CheckInCalendar";
13 |
14 | export interface ShiftProps {
15 | navigation: NavigationProps;
16 | setSelectedVolunteerShift: (
17 | shift: ScheduleWithShiftType | CheckInWithShiftType,
18 | ) => void;
19 | }
20 |
21 | const CheckIns = ({
22 | navigation,
23 | setSelectedVolunteerShift,
24 | }: ShiftProps): JSX.Element => {
25 | const [checkIns, setCheckIns] = useState([]);
26 |
27 | useEffect(() => {
28 | const getCheckIns = async () => {
29 | const checkInResponse: CheckInWithShiftType[] = await (
30 | await CheckInAPIClient.getAllCheckIns()
31 | ).map((checkin) => ({ ...checkin, type: ShiftType.CHECKIN }));
32 | const needVolunteerCheckIns = checkInResponse.filter(
33 | (checkin) => !checkin.volunteerId && !checkin.isAdmin,
34 | );
35 | setCheckIns(needVolunteerCheckIns);
36 | };
37 | getCheckIns();
38 | }, []);
39 |
40 | if (!checkIns || checkIns === null) {
41 | return ;
42 | }
43 |
44 | return (
45 | <>
46 |
47 |
48 |
53 | >
54 | );
55 | };
56 |
57 | export default CheckIns;
58 |
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | Community Fridge KW
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/frontend/src/components/auth/ResetPassword/VerificationEmail.tsx:
--------------------------------------------------------------------------------
1 | import { CloseIcon } from "@chakra-ui/icons";
2 | import { Container, IconButton, Image, Text } from "@chakra-ui/react";
3 | import React from "react";
4 | import { useHistory } from "react-router-dom";
5 |
6 | import VerificationPageImage from "../../../assets/Verification-Email-Image.png";
7 | import * as Routes from "../../../constants/Routes";
8 | import { RequestPasswordChangeFormProps } from "./types";
9 |
10 | interface VerificationPageProps {
11 | formValues: RequestPasswordChangeFormProps;
12 | }
13 |
14 | const VerificationPage = ({ formValues }: VerificationPageProps) => {
15 | const history = useHistory();
16 | const { email } = formValues;
17 | return (
18 |
19 | history.push(Routes.LANDING_PAGE)}
24 | display={{ md: "none" }}
25 | >
26 |
27 |
28 |
29 |
36 |
37 | Please check your email for next steps!
38 |
39 |
45 | We sent a password change request through email to {email}. Please
46 | check your email for further information.
47 |
48 |
49 |
50 | );
51 | };
52 |
53 | export default VerificationPage;
54 |
--------------------------------------------------------------------------------
/frontend/src/components/pages/Scheduling/GetStarted.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Container, Img, Text } from "@chakra-ui/react";
2 | import React from "react";
3 |
4 | import cfImage from "../../../assets/scheduling_getstarted.png";
5 | import { SchedulingStepProps } from "./types";
6 |
7 | const GetStarted = ({ navigation }: SchedulingStepProps) => {
8 | const { next } = navigation;
9 |
10 | return (
11 |
12 |
13 | Schedule a donation drop-off
14 |
15 |
16 | Submit information for your upcoming planned donation(s) to the
17 | community fridge. Pick the date and time window in which you hope to
18 | make your donation.
19 |
20 |
21 | Our goal is to maintain a consistent stock of donated food in the fridge
22 | & pantry, at any given time, on any given day. The scheduling tool
23 | enables us to space out donations - effectively maximizing your impact
24 | on the community.
25 |
26 |
34 |

41 |
50 |
51 |
52 | );
53 | };
54 |
55 | export default GetStarted;
56 |
--------------------------------------------------------------------------------
/frontend/src/components/auth/Signup/VerificationEmail.tsx:
--------------------------------------------------------------------------------
1 | import { CloseIcon } from "@chakra-ui/icons";
2 | import { Container, IconButton, Image, Text } from "@chakra-ui/react";
3 | import React from "react";
4 | import { useHistory } from "react-router-dom";
5 |
6 | import verificationEmailImage from "../../../assets/authentication_incomplete.svg";
7 | import * as Routes from "../../../constants/Routes";
8 | import { SignUpFormProps } from "./types";
9 |
10 | interface VerificationPageProps {
11 | formValues: SignUpFormProps;
12 | }
13 |
14 | const VerificationPage = ({ formValues }: VerificationPageProps) => {
15 | const history = useHistory();
16 | const { email } = formValues;
17 | return (
18 |
19 | history.push(Routes.LANDING_PAGE)}
24 | display={{ md: "none" }}
25 | >
26 |
27 |
28 |
29 |
35 |
40 | Please verify your email address!
41 |
42 |
48 | We sent a verification email to {email}.
49 |
50 |
51 | If you don’t see an email within a few minutes, please check your junk
52 | folders and add us to your Trusted Senders!
53 |
54 |
55 |
56 | );
57 | };
58 |
59 | export default VerificationPage;
60 |
--------------------------------------------------------------------------------
/frontend/src/APIClients/DonorAPIClient.ts:
--------------------------------------------------------------------------------
1 | import { BEARER_TOKEN } from "../constants/AuthConstants";
2 | import { DonorResponse, UpdateDonorDataType } from "../types/DonorTypes";
3 | import baseAPIClient from "./BaseAPIClient";
4 |
5 | const getAllDonors = async (): Promise => {
6 | try {
7 | const { data } = await baseAPIClient.get("/donors", {
8 | headers: { Authorization: BEARER_TOKEN },
9 | });
10 | return data;
11 | } catch (error) {
12 | return error as DonorResponse[];
13 | }
14 | };
15 |
16 | const getDonorById = async (id: string): Promise => {
17 | try {
18 | const { data } = await baseAPIClient.get(`/donors/${id}`, {
19 | headers: { Authorization: BEARER_TOKEN },
20 | });
21 | return data;
22 | } catch (error) {
23 | return error as DonorResponse;
24 | }
25 | };
26 |
27 | const getDonorByUserId = async (userId: string): Promise => {
28 | try {
29 | const { data } = await baseAPIClient.get(`/donors/?userId=${userId}`, {
30 | headers: { Authorization: BEARER_TOKEN },
31 | });
32 | return data;
33 | } catch (error) {
34 | return error as DonorResponse;
35 | }
36 | };
37 |
38 | const updateDonorById = async (
39 | id: string,
40 | donorData: UpdateDonorDataType,
41 | ): Promise => {
42 | try {
43 | const { data } = await baseAPIClient.put(`/donors/${id}`, donorData, {
44 | headers: { Authorization: BEARER_TOKEN },
45 | });
46 | return data;
47 | } catch (error) {
48 | return error as DonorResponse;
49 | }
50 | };
51 |
52 | const deleteDonorById = async (id: string): Promise => {
53 | try {
54 | const { data } = await baseAPIClient.delete(`/donors/${id}`, {
55 | headers: { Authorization: BEARER_TOKEN },
56 | });
57 | return data;
58 | } catch (error) {
59 | return error as DonorResponse;
60 | }
61 | };
62 |
63 | export default {
64 | getAllDonors,
65 | getDonorById,
66 | getDonorByUserId,
67 | updateDonorById,
68 | deleteDonorById,
69 | };
70 |
--------------------------------------------------------------------------------
/frontend/src/components/pages/VolunteerScheduling/FoodRescues.tsx:
--------------------------------------------------------------------------------
1 | import { Divider, Spinner, StackDivider, VStack } from "@chakra-ui/react";
2 | import isAfter from "date-fns/isAfter";
3 | import React, { useEffect, useState } from "react";
4 |
5 | import SchedulingAPIClient from "../../../APIClients/SchedulingAPIClient";
6 | import {
7 | ScheduleWithShiftType,
8 | ShiftType,
9 | } from "../../../types/VolunteerTypes";
10 | import FridgeFoodRescueDescription from "../../common/FridgeFoodRescueDescription";
11 | import ShiftCard from "../VolunteerDashboard/ShiftCard";
12 | import { ShiftProps } from "./CheckIns";
13 |
14 | const FoodRescues = ({
15 | navigation,
16 | setSelectedVolunteerShift,
17 | }: ShiftProps): JSX.Element => {
18 | const [foodRescues, setFoodRescues] = useState([]);
19 |
20 | useEffect(() => {
21 | const getFoodRescues = async () => {
22 | const scheduleResponse: ScheduleWithShiftType[] = await (
23 | await SchedulingAPIClient.getAllSchedulesThatNeedVolunteers(false)
24 | )
25 | .filter((scheduling) =>
26 | isAfter(new Date(scheduling.startTime), new Date()),
27 | )
28 | .map((scheduling) => ({ ...scheduling, type: ShiftType.SCHEDULING }));
29 | setFoodRescues(scheduleResponse);
30 | };
31 |
32 | getFoodRescues();
33 | }, []);
34 |
35 | if (!foodRescues || foodRescues === null) {
36 | return ;
37 | }
38 |
39 | return (
40 | <>
41 |
42 |
43 | }
45 | marginTop={["10px", "40px"]}
46 | >
47 | {foodRescues.map((scheduleObject: ScheduleWithShiftType, id) => (
48 |
55 | ))}
56 |
57 | >
58 | );
59 | };
60 |
61 | export default FoodRescues;
62 |
--------------------------------------------------------------------------------
/update_secret_files.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import json
3 |
4 | # Open secret.config file
5 | configFileNotFound = False
6 | try:
7 | configFile = open('secret.config')
8 | except Exception as e:
9 | print("File secret.config could not be opened in current directory.")
10 | print(e)
11 | configFileNotFound = True
12 | # Script will exit after checking if the Vault request is valid
13 |
14 | # Decode json result
15 | try:
16 | rawInput = ''.join(sys.stdin.readlines())
17 | decodedJson = json.loads(rawInput)
18 | except Exception as e:
19 | print("Unable to retrieve secrets from Vault and obtain valid json result.")
20 | print("Please ensure you are authenticated and have supplied the correct path argument.")
21 | exit()
22 |
23 | # Extract the data field containting the secrets
24 | if "data" in decodedJson and "data" in decodedJson["data"]:
25 | data = decodedJson["data"]["data"]
26 | else:
27 | print("Unable to access the field data:{data:{}} from result which should contain the secrets.")
28 | print("Please ensure you are authenticated and have supplied the correct path argument.")
29 | exit()
30 |
31 | # Even if the config file is not found, it is useful to still indicate if the Vault request has any problems before exiting
32 | if configFileNotFound:
33 | exit()
34 |
35 | # Read all the secret file locations from secret.config
36 | locations = {}
37 | for line in configFile:
38 | key, val = line.rstrip().partition('=')[::2]
39 | if key in locations:
40 | print("Key <{keyName}> appeared more than once on configuration file. Ignoring second instance of the key.".format(keyName=key))
41 | else:
42 | locations[key] = val
43 | configFile.close()
44 |
45 | # Write values to the secret file corresponding to their keys
46 | for key in data:
47 | if key in locations:
48 | try:
49 | f = open(locations[key], 'w')
50 | f.write(data[key])
51 | f.close()
52 | except Exception as e:
53 | print("Could not write the values for key <{keyName}> to location <{locName}>".format(keyName=key, locName=locations[key]))
54 | print(e)
55 | else:
56 | print("File location for key <{keyName}> was not found.".format(keyName=key))
--------------------------------------------------------------------------------
/backend/typescript/middlewares/validators/userValidators.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from "express";
2 | import { getApiValidationError, validatePrimitive } from "./util";
3 |
4 | export const createUserDtoValidator = async (
5 | req: Request,
6 | res: Response,
7 | next: NextFunction,
8 | ) => {
9 | if (!validatePrimitive(req.body.firstName, "string")) {
10 | return res.status(400).send(getApiValidationError("firstName", "string"));
11 | }
12 | if (!validatePrimitive(req.body.lastName, "string")) {
13 | return res.status(400).send(getApiValidationError("lastName", "string"));
14 | }
15 | if (!validatePrimitive(req.body.email, "string")) {
16 | return res.status(400).send(getApiValidationError("email", "string"));
17 | }
18 | if (!validatePrimitive(req.body.role, "string")) {
19 | return res.status(400).send(getApiValidationError("role", "string"));
20 | }
21 | if (!validatePrimitive(req.body.phoneNumber, "string")) {
22 | return res.status(400).send(getApiValidationError("phoneNumber", "string"));
23 | }
24 | if (!validatePrimitive(req.body.password, "string")) {
25 | return res.status(400).send(getApiValidationError("password", "string"));
26 | }
27 |
28 | return next();
29 | };
30 |
31 | export const updateUserDtoValidator = async (
32 | req: Request,
33 | res: Response,
34 | next: NextFunction,
35 | ) => {
36 | if (!validatePrimitive(req.body.firstName, "string")) {
37 | return res.status(400).send(getApiValidationError("firstName", "string"));
38 | }
39 | if (!validatePrimitive(req.body.lastName, "string")) {
40 | return res.status(400).send(getApiValidationError("lastName", "string"));
41 | }
42 | if (!validatePrimitive(req.body.email, "string")) {
43 | return res.status(400).send(getApiValidationError("email", "string"));
44 | }
45 | if (!validatePrimitive(req.body.role, "string")) {
46 | return res.status(400).send(getApiValidationError("role", "string"));
47 | }
48 | if (!validatePrimitive(req.body.phoneNumber, "string")) {
49 | return res.status(400).send(getApiValidationError("phoneNumber", "string"));
50 | }
51 | return next();
52 | };
53 |
--------------------------------------------------------------------------------
/frontend/src/components/pages/VolunteerScheduling/VolunteerShiftTabs.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Container,
3 | Tab,
4 | TabList,
5 | TabPanel,
6 | TabPanels,
7 | Tabs,
8 | Text,
9 | VStack,
10 | } from "@chakra-ui/react";
11 | import React from "react";
12 | import { NavigationProps } from "react-hooks-helper";
13 | import { NavLink } from "react-router-dom";
14 |
15 | import {
16 | CheckInWithShiftType,
17 | ScheduleWithShiftType,
18 | } from "../../../types/VolunteerTypes";
19 | import CheckIns from "./CheckIns";
20 | import FoodRescues from "./FoodRescues";
21 |
22 | const VolunteerShiftsTabs = ({
23 | navigation,
24 | setSelectedVolunteerShift,
25 | }: {
26 | navigation: NavigationProps;
27 | setSelectedVolunteerShift: (
28 | shift: ScheduleWithShiftType | CheckInWithShiftType,
29 | ) => void;
30 | }): JSX.Element => {
31 | return (
32 |
33 |
34 |
35 | Volunteer Shifts
36 |
37 |
42 |
43 |
44 | Fridge check-in{" "}
45 |
46 |
47 | Food rescue{" "}
48 |
49 |
50 |
51 |
52 |
56 |
57 |
58 |
62 |
63 |
64 |
65 |
66 |
67 | );
68 | };
69 |
70 | export default VolunteerShiftsTabs;
71 |
--------------------------------------------------------------------------------
/backend/typescript/testUtils/checkInService.ts:
--------------------------------------------------------------------------------
1 | export const testCheckIns = [
2 | {
3 | startDate: new Date("2021-09-01T09:00:00.000Z"),
4 | endDate: new Date("2021-09-07T10:00:00.000Z"),
5 | notes: "volunteer null test",
6 | },
7 | {
8 | volunteerId: "1",
9 | startDate: new Date("2022-02-28T09:00:00.000Z"),
10 | endDate: new Date("2022-02-28T10:00:00.000Z"),
11 | notes: "test with volunteer 1",
12 | },
13 | {
14 | volunteerId: "1",
15 | startDate: new Date("2022-02-29T09:00:00.000Z"),
16 | endDate: new Date("2022-02-29T10:00:00.000Z"),
17 | notes: "test with volunteer 1 again",
18 | },
19 | {
20 | volunteerId: "2",
21 | startDate: new Date("2022-03-14T09:00:00.000Z"),
22 | endDate: new Date("2022-03-28T10:00:00.000Z"),
23 | notes: "test with volunteer 2",
24 | isAdmin: true,
25 | },
26 | ];
27 |
28 | export const testUpdatedCheckIns = [
29 | {
30 | volunteerId: "1",
31 | startDate: new Date("2021-09-01T09:00:00.000Z"),
32 | endDate: new Date("2021-09-07T10:00:00.000Z"),
33 | notes: "updated notes",
34 | },
35 | ];
36 |
37 | export const testContent = [
38 | {
39 | food_rescue_description: "test food rescue description",
40 | food_rescue_url: "https://www.google.com",
41 | checkin_description: "test checkin description",
42 | checkin_url: "https://www.google.com",
43 | },
44 | ];
45 |
46 | export const testUsersDb = [
47 | {
48 | first_name: "Test",
49 | last_name: "User",
50 | auth_id: "test id",
51 | role: "Donor",
52 | email: "test@email.com",
53 | },
54 | {
55 | first_name: "Test",
56 | last_name: "User 2",
57 | auth_id: "test id 2",
58 | role: "Donor",
59 | email: "test2@email.com",
60 | },
61 | {
62 | first_name: "Test",
63 | last_name: "User 3",
64 | auth_id: "test id 3",
65 | role: "Volunteer",
66 | email: "test3@email.com",
67 | },
68 | {
69 | first_name: "Test",
70 | last_name: "User 4",
71 | auth_id: "test id 4",
72 | role: "Volunteer",
73 | email: "test4@email.com",
74 | },
75 | ];
76 |
77 | export const testVolunteersDb = [
78 | {
79 | user_id: 3,
80 | },
81 | {
82 | user_id: 4,
83 | },
84 | ];
85 |
--------------------------------------------------------------------------------
/backend/typescript/services/implementations/__tests__/userService.test.ts:
--------------------------------------------------------------------------------
1 | import { snakeCase } from "lodash";
2 | import User from "../../../models/user.model";
3 | import UserService from "../userService";
4 |
5 | import { UserDTO } from "../../../types";
6 |
7 | import testSql from "../../../testUtils/testDb";
8 |
9 | const testUsers = [
10 | {
11 | firstName: "Peter",
12 | email: "peter@test.com",
13 | lastName: "Pan",
14 | authId: "123",
15 | role: "Admin",
16 | phoneNumber: "111-111-1111",
17 | },
18 | {
19 | firstName: "Wendy",
20 | email: "wendy@test.com",
21 | lastName: "Darling",
22 | authId: "321",
23 | role: "User",
24 | phoneNumber: "111-111-1111",
25 | },
26 | ];
27 |
28 | const expectedUsers = [
29 | {
30 | email: "peter@test.com",
31 | firstName: "Peter",
32 | id: 1,
33 | lastName: "Pan",
34 | role: "Admin",
35 | phoneNumber: "111-111-1111",
36 | },
37 | {
38 | email: "wendy@test.com",
39 | firstName: "Wendy",
40 | id: 2,
41 | lastName: "Darling",
42 | role: "Volunteer",
43 | phoneNumber: "111-111-1111",
44 | },
45 | ];
46 |
47 | jest.mock("firebase-admin", () => {
48 | const auth = jest.fn().mockReturnValue({
49 | getUser: jest.fn().mockReturnValue({ email: "test@test.com" }),
50 | });
51 | return { auth };
52 | });
53 |
54 | describe.skip("pg userService", () => {
55 | let userService: UserService;
56 |
57 | beforeEach(async () => {
58 | await testSql.sync({ force: true });
59 | userService = new UserService();
60 | });
61 |
62 | afterAll(async () => {
63 | await testSql.sync({ force: true });
64 | await testSql.close();
65 | });
66 |
67 | it("getUsers", async () => {
68 | const users = testUsers.map((user) => {
69 | const userSnakeCase: Record = {};
70 | Object.entries(user).forEach(([key, value]) => {
71 | userSnakeCase[snakeCase(key)] = value;
72 | });
73 | return userSnakeCase;
74 | });
75 |
76 | await User.bulkCreate(users);
77 |
78 | const res = await userService.getUsers();
79 |
80 | res.forEach((user: UserDTO, i) => {
81 | expect(user).toContainEqual(expectedUsers[i]);
82 | });
83 | });
84 | });
85 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `yarn start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
13 |
14 | The page will reload if you make edits.\
15 | You will also see any lint errors in the console.
16 |
17 | ### `yarn test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `yarn build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `yarn eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
35 |
36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
39 |
40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
--------------------------------------------------------------------------------
/frontend/src/components/pages/Scheduling/ThankYou.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Container, HStack, Img, Text } from "@chakra-ui/react";
2 | import { format } from "date-fns";
3 | import React, { useContext } from "react";
4 | import { useHistory } from "react-router-dom";
5 |
6 | import ThankYouPageFridge from "../../../assets/ThankYouPageFridge.png";
7 | import * as Routes from "../../../constants/Routes";
8 | import AuthContext from "../../../contexts/AuthContext";
9 | import { SchedulingStepProps } from "./types";
10 |
11 | const ThankYou = ({ formValues }: SchedulingStepProps) => {
12 | const { startTime } = formValues;
13 | const { authenticatedUser } = useContext(AuthContext);
14 | const history = useHistory();
15 | return (
16 |
21 |
25 | Thank you for scheduling a donation!
26 |
27 |
28 | We appreciate your support and dedication! An email confirmation has
29 | been sent to {authenticatedUser!.email}.
30 |
31 |
37 | See you at the fridge on{" "}
38 | {format(new Date(startTime), "eeee MMMM d, yyyy")} at{" "}
39 | {format(new Date(startTime), "h:mm aa")}!
40 |
41 |
52 |
53 |
62 |
63 |
64 | );
65 | };
66 |
67 | export default ThankYou;
68 |
--------------------------------------------------------------------------------
/frontend/src/components/common/FridgeCheckInDescription.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, Link, Text } from "@chakra-ui/react";
2 | import React, { useEffect, useState } from "react";
3 | import { useHistory } from "react-router-dom";
4 |
5 | import ContentAPIClient from "../../APIClients/ContentAPIClient";
6 | import * as Routes from "../../constants/Routes";
7 | import AuthContext from "../../contexts/AuthContext";
8 | import { Role } from "../../types/AuthTypes";
9 | import { Content } from "../../types/ContentTypes";
10 |
11 | const FridgeCheckInDescription = () => {
12 | const [content, setContent] = useState();
13 | const { authenticatedUser } = React.useContext(AuthContext);
14 |
15 | const history = useHistory();
16 | const navigateToEditPage = () => {
17 | history.push(Routes.ADMIN_CHECK_IN_EDIT_DESCRIPTION_PAGE);
18 | };
19 |
20 | useEffect(() => {
21 | const getContent = async () => {
22 | const contentResponse = await ContentAPIClient.getContent();
23 | setContent(contentResponse);
24 | };
25 |
26 | getContent();
27 | }, []);
28 |
29 | return (
30 |
31 |
32 | Fridge check-in description
33 | {authenticatedUser?.role === Role.ADMIN && (
34 |
44 | )}
45 |
46 | {content?.checkinDescription && (
47 |
48 | {content?.checkinDescription}
49 |
50 | )}
51 | {content?.checkinUrl && (
52 | <>
53 |
61 | Link to instructions
62 |
63 | >
64 | )}
65 |
66 | );
67 | };
68 | export default FridgeCheckInDescription;
69 |
--------------------------------------------------------------------------------
/frontend/src/components/common/FridgeFoodRescueDescription.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, Link, Text } from "@chakra-ui/react";
2 | import React, { useEffect, useState } from "react";
3 | import { useHistory } from "react-router-dom";
4 |
5 | import ContentAPIClient from "../../APIClients/ContentAPIClient";
6 | import * as Routes from "../../constants/Routes";
7 | import AuthContext from "../../contexts/AuthContext";
8 | import { Role } from "../../types/AuthTypes";
9 | import { Content } from "../../types/ContentTypes";
10 |
11 | const FridgeFoodRescueDescription = () => {
12 | const [content, setContent] = useState();
13 | const { authenticatedUser } = React.useContext(AuthContext);
14 |
15 | const history = useHistory();
16 | const navigateToEditPage = () => {
17 | history.push(Routes.ADMIN_FOOD_RESCUE_EDIT_DESCRIPTION_PAGE);
18 | };
19 |
20 | useEffect(() => {
21 | const getContent = async () => {
22 | const contentResponse = await ContentAPIClient.getContent();
23 | setContent(contentResponse);
24 | };
25 |
26 | getContent();
27 | }, []);
28 |
29 | return (
30 |
31 |
32 | Food rescue description
33 | {authenticatedUser?.role === Role.ADMIN && (
34 |
44 | )}
45 |
46 | {content?.foodRescueDescription && (
47 |
48 | {content?.foodRescueDescription}
49 |
50 | )}
51 | {content?.foodRescueUrl && (
52 | <>
53 |
61 | Link to instructions
62 |
63 | >
64 | )}
65 |
66 | );
67 | };
68 | export default FridgeFoodRescueDescription;
69 |
--------------------------------------------------------------------------------
/backend/typescript/services/implementations/__tests__/contentService.test.ts:
--------------------------------------------------------------------------------
1 | import Content from "../../../models/content.model";
2 | import ContentService from "../contentService";
3 |
4 | import testSql from "../../../testUtils/testDb";
5 |
6 | const mockContentData = {
7 | foodRescueDescription: "test food rescue description",
8 | foodRescueUrl: "uwblueprint.org",
9 | checkinDescription: "test checkin description",
10 | checkinUrl: "testurl.com",
11 | };
12 |
13 | const mockUpdateContentData = {
14 | foodRescueDescription: "update test food rescue description",
15 | foodRescueUrl: "update.uwblueprint.org",
16 | checkinDescription: "update test checkin description",
17 | checkinUrl: "update.testurl.com",
18 | };
19 |
20 | describe("Testing ContentService functions", () => {
21 | let contentService: ContentService;
22 |
23 | beforeEach(async () => {
24 | await testSql.sync({ force: true });
25 | contentService = new ContentService();
26 | });
27 |
28 | afterAll(async () => {
29 | await testSql.sync({ force: true });
30 | await testSql.close();
31 | });
32 |
33 | it("testing createContent", async () => {
34 | const result = await contentService.createContent(mockContentData);
35 | expect(result).toMatchObject({
36 | id: "1",
37 | ...mockContentData,
38 | });
39 | });
40 |
41 | it("testing updateContent", async () => {
42 | await Content.create({
43 | food_rescue_description: mockContentData.foodRescueDescription,
44 | food_rescue_url: mockContentData.foodRescueUrl,
45 | checkin_description: mockContentData.checkinDescription,
46 | checkin_url: mockContentData.checkinUrl,
47 | });
48 | const result = await contentService.updateContent(
49 | "1",
50 | mockUpdateContentData,
51 | );
52 | expect(result).toMatchObject({
53 | id: "1",
54 | ...mockUpdateContentData,
55 | });
56 | });
57 |
58 | it("testing getContent", async () => {
59 | await Content.create({
60 | food_rescue_description: mockContentData.foodRescueDescription,
61 | food_rescue_url: mockContentData.foodRescueUrl,
62 | checkin_description: mockContentData.checkinDescription,
63 | checkin_url: mockContentData.checkinUrl,
64 | });
65 | const result = await contentService.getContent();
66 | expect(result).toMatchObject({
67 | id: "1",
68 | ...mockContentData,
69 | });
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/frontend/src/components/pages/VolunteerScheduling/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useContext, useEffect, useState } from "react";
2 | import { NavigationProps, Step, useStep } from "react-hooks-helper";
3 |
4 | import VolunteerContext from "../../../contexts/VolunteerContext";
5 | import { Status } from "../../../types/AuthTypes";
6 | import {
7 | CheckInWithShiftType,
8 | ScheduleWithShiftType,
9 | } from "../../../types/VolunteerTypes";
10 | import PendingPage from "../VolunteerDashboard/PendingPage";
11 | import ConfirmShiftDetails from "./ConfirmShiftDetails";
12 | import ThankYouVolunteer from "./ThankYouVolunteer";
13 | import VolunteerShiftsTabs from "./VolunteerShiftTabs";
14 |
15 | const steps = [
16 | {
17 | id: "shifts tab",
18 | },
19 | {
20 | id: "confirm shift sign up",
21 | },
22 | {
23 | id: "thank you page",
24 | },
25 | ];
26 |
27 | interface UseStepType {
28 | step: number | Step | any;
29 | navigation: NavigationProps | any;
30 | }
31 |
32 | const VolunteerScheduling = () => {
33 | const { volunteerStatus } = useContext(VolunteerContext);
34 | const [selectedShift, setSelectedShift] = useState<
35 | CheckInWithShiftType | ScheduleWithShiftType
36 | >({} as CheckInWithShiftType | ScheduleWithShiftType);
37 |
38 | const setSelectedVolunteerShift = useCallback(
39 | (shift: CheckInWithShiftType | ScheduleWithShiftType) =>
40 | setSelectedShift(shift),
41 | [setSelectedShift],
42 | );
43 |
44 | const { step, navigation }: UseStepType = useStep({
45 | steps,
46 | initialStep: 0,
47 | });
48 | const { id } = step;
49 |
50 | useEffect(() => {
51 | window.scrollTo(0, 0);
52 | }, [id]);
53 |
54 | switch (id) {
55 | case "shifts tab":
56 | return (
57 | <>
58 | {volunteerStatus === Status.APPROVED ? (
59 |
63 | ) : (
64 |
65 | )}
66 | >
67 | );
68 | case "confirm shift sign up":
69 | return (
70 |
71 | );
72 | case "thank you page":
73 | return ;
74 | default:
75 | return <>>;
76 | }
77 | };
78 |
79 | export default VolunteerScheduling;
80 |
--------------------------------------------------------------------------------
/backend/typescript/server.ts:
--------------------------------------------------------------------------------
1 | import cookieParser from "cookie-parser";
2 | import cors from "cors";
3 | import express from "express";
4 | import * as firebaseAdmin from "firebase-admin";
5 | import swaggerUi from "swagger-ui-express";
6 | import YAML from "yamljs";
7 |
8 | import sequelize from "./models";
9 | import authRouter from "./rest/authRoutes";
10 | import donorRouter from "./rest/donorRoutes";
11 | import userRouter from "./rest/userRoutes";
12 | import volunteerRouter from "./rest/volunteerRoutes";
13 | import schedulingRouter from "./rest/schedulingRoutes";
14 | import checkInRouter from "./rest/checkInRoutes";
15 | import contentRouter from "./rest/contentRoutes";
16 | import healthRouter from "./rest/healthRouter";
17 | import cronRouter from "./rest/cronRoutes";
18 |
19 | const CORS_ALLOW_LIST: (string | RegExp)[] = ["http://localhost:3000"];
20 | if (process.env.NODE_ENV === "production") {
21 | CORS_ALLOW_LIST.push(
22 | "https://communityfridgekw.web.app",
23 | "https://schedule.communityfridgekw.ca",
24 | );
25 | } else if (process.env.NODE_ENV === "staging") {
26 | const clientHost = new RegExp(
27 | "https://communityfridgekw-staging(--([A-Za-z0-9-])+-[A-Za-z0-9]+)?.web.app",
28 | );
29 | CORS_ALLOW_LIST.push(clientHost);
30 | }
31 |
32 | const CORS_OPTIONS: cors.CorsOptions = {
33 | origin: CORS_ALLOW_LIST,
34 | credentials: true,
35 | };
36 |
37 | const swaggerDocument = YAML.load("swagger.yml");
38 |
39 | const app = express();
40 | app.use(cookieParser());
41 | app.use(cors(CORS_OPTIONS));
42 | app.use(express.json());
43 | app.use(express.urlencoded({ extended: true }));
44 |
45 | app.use("/auth", authRouter);
46 | app.use("/donors", donorRouter);
47 | app.use("/users", userRouter);
48 | app.use("/volunteers", volunteerRouter);
49 | app.use("/scheduling", schedulingRouter);
50 | app.use("/checkin", checkInRouter);
51 | app.use("/content", contentRouter);
52 | app.use("/health", healthRouter);
53 | app.use("/email-reminders", cronRouter);
54 | app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument));
55 |
56 | const eraseDatabaseOnSync = false;
57 | sequelize.sync({ force: eraseDatabaseOnSync });
58 |
59 | firebaseAdmin.initializeApp({
60 | credential: firebaseAdmin.credential.applicationDefault(),
61 | });
62 |
63 | const PORT = process.env.PORT || 5000;
64 | app.listen({ port: PORT }, () => {
65 | /* eslint-disable-next-line no-console */
66 | console.info(`Server is listening on port ${PORT}!`);
67 | });
68 |
--------------------------------------------------------------------------------
/frontend/src/components/auth/Action.tsx:
--------------------------------------------------------------------------------
1 | import { Center, Spinner, Text } from "@chakra-ui/react";
2 | import React, { useEffect, useState } from "react";
3 |
4 | import AuthAPIClient from "../../APIClients/AuthAPIClient";
5 | import NewPassword from "./ResetPassword/NewPassword";
6 | import ConfirmVerificationPage from "./Signup/ConfirmVerificationPage";
7 |
8 | enum ActionModes {
9 | EMAIL_VERIFICATION = "verifyEmail",
10 | PASSWORD_RESET = "resetPassword",
11 | }
12 |
13 | const Action = () => {
14 | const urlParams = new URLSearchParams(window.location.search);
15 | const mode = urlParams.get("mode");
16 | const oobCode = urlParams.get("oobCode");
17 |
18 | const [emailVerified, setEmailVerified] = useState(false);
19 | const [passwordResetVerified, setPasswordResetVerified] = React.useState(
20 | false,
21 | );
22 | const [loading, setLoading] = useState(true);
23 | const [error, setError] = useState(false);
24 |
25 | useEffect(() => {
26 | const confirmEmailVerification = async () => {
27 | const confirmEmailVerificationResponse = await AuthAPIClient.confirmEmailVerification(
28 | oobCode ?? "",
29 | );
30 | if (confirmEmailVerificationResponse) {
31 | setEmailVerified(true);
32 | } else {
33 | setError(true);
34 | }
35 | setLoading(false);
36 | };
37 |
38 | const confirmPasswordReset = async () => {
39 | const confirmPasswordResetResponse = await AuthAPIClient.verifyPasswordResetCode(
40 | oobCode ?? "",
41 | );
42 | if (confirmPasswordResetResponse) {
43 | setPasswordResetVerified(true);
44 | } else {
45 | setError(true);
46 | }
47 | setLoading(false);
48 | };
49 | if (mode === ActionModes.EMAIL_VERIFICATION) {
50 | confirmEmailVerification();
51 | }
52 | if (mode === ActionModes.PASSWORD_RESET) {
53 | confirmPasswordReset();
54 | }
55 | }, [mode, oobCode]);
56 |
57 | return (
58 | <>
59 | {loading && (
60 |
61 |
62 |
63 | )}
64 | {error && (
65 |
66 | Error Occured. Please try again!
67 |
68 | )}
69 | {emailVerified && }
70 | {passwordResetVerified && (
71 |
72 | {" "}
73 | {" "}
74 |
75 | )}
76 | >
77 | );
78 | };
79 | export default Action;
80 |
--------------------------------------------------------------------------------
/frontend/src/components/pages/Home/VolunteerRoles.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowForwardIcon } from "@chakra-ui/icons";
2 | import { Box, Flex, Spacer, Stack, Text } from "@chakra-ui/react";
3 | import React from "react";
4 |
5 | interface VolunteerRoleStepProps {
6 | title: string;
7 | description: string;
8 | }
9 | const VolunteerRoleStep = ({
10 | title,
11 | description,
12 | }: VolunteerRoleStepProps): JSX.Element => {
13 | return (
14 |
15 |
21 |
26 |
30 | {title}
31 |
32 |
37 | {description}
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | const VolunteerRoles = () => (
45 |
52 |
57 | For volunteers
58 |
59 |
64 | Volunteer Roles
65 |
66 |
67 |
71 |
72 |
76 |
77 |
78 | );
79 |
80 | export default VolunteerRoles;
81 |
--------------------------------------------------------------------------------
/frontend/src/components/common/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { Container, Divider, Flex, Link, Text, VStack } from "@chakra-ui/react";
2 | import React from "react";
3 |
4 | import { Email, Facebook, Instagram } from "./icons";
5 |
6 | const Footer = (): JSX.Element => (
7 | <>
8 |
9 |
16 |
21 | Community Fridge KW
22 | Take what you need, leave what you can.
23 |
24 |
25 |
30 |
31 | Location
32 |
33 |
34 | Kitchener Market:
35 | 300 King Street East,
36 | Kitchener, ON N2H 2V5
37 | (647) 607-1312
38 |
39 |
40 |
41 |
42 |
43 | Contact Us
44 |
45 |
46 |
51 |
52 | Facebook
53 |
54 |
55 |
56 |
61 |
62 | Instagram
63 |
64 |
65 |
66 |
71 |
72 | Email
73 |
74 |
75 |
76 |
77 | >
78 | );
79 |
80 | export default Footer;
81 |
--------------------------------------------------------------------------------
/backend/typescript/models/scheduling.model.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Column,
3 | DataType,
4 | Model,
5 | Table,
6 | AllowNull,
7 | ForeignKey,
8 | BelongsTo,
9 | AutoIncrement,
10 | PrimaryKey,
11 | } from "sequelize-typescript";
12 | import { DayPart, Frequency, Status } from "../types";
13 | import Donor from "./donor.model";
14 | import Volunteer from "./volunteer.model";
15 |
16 | @Table({ tableName: "scheduling" })
17 | export default class Scheduling extends Model {
18 | @PrimaryKey
19 | @AutoIncrement
20 | @Column({ type: DataType.INTEGER })
21 | id!: number;
22 |
23 | @AllowNull(false)
24 | @Column({ type: DataType.ARRAY(DataType.TEXT) })
25 | categories!: string[];
26 |
27 | @Column({ type: DataType.TEXT })
28 | size!: string;
29 |
30 | @Column({ type: DataType.BOOLEAN })
31 | is_pickup!: boolean;
32 |
33 | @Column({ type: DataType.TEXT })
34 | pickup_location!: string;
35 |
36 | @AllowNull(false)
37 | @Column({
38 | type: DataType.ENUM(
39 | "Early Morning (12am - 6am)",
40 | "Morning (6am - 11am)",
41 | "Afternoon (11am - 4pm)",
42 | "Evening (4pm - 9pm)",
43 | "Night (9pm - 12am)",
44 | ),
45 | })
46 | day_part!: DayPart;
47 |
48 | @AllowNull(false)
49 | @Column({ type: DataType.DATE })
50 | start_time!: Date;
51 |
52 | @AllowNull(false)
53 | @Column({ type: DataType.DATE })
54 | end_time!: Date;
55 |
56 | @Column({
57 | type: DataType.ENUM("Rejected", "Approved", "Pending"),
58 | defaultValue: "Approved",
59 | })
60 | status!: Status;
61 |
62 | @AllowNull(false)
63 | @Column({ type: DataType.BOOLEAN })
64 | volunteer_needed!: boolean;
65 |
66 | @Column({ type: DataType.TEXT })
67 | volunteer_time!: string;
68 |
69 | @AllowNull(false)
70 | @Column({
71 | type: DataType.ENUM("One time", "Daily", "Weekly", "Monthly"),
72 | })
73 | frequency!: Frequency;
74 |
75 | @Column({ type: DataType.INTEGER })
76 | recurring_donation_id!: number;
77 |
78 | @Column({ type: DataType.DATE })
79 | recurring_donation_end_date!: Date;
80 |
81 | @Column({ type: DataType.TEXT })
82 | notes!: string;
83 |
84 | @ForeignKey(() => Donor)
85 | @AllowNull(false)
86 | @Column({ type: DataType.INTEGER })
87 | donor_id!: number;
88 |
89 | @ForeignKey(() => Volunteer)
90 | @AllowNull(true)
91 | @Column({ type: DataType.INTEGER })
92 | volunteer_id!: number;
93 |
94 | @BelongsTo(() => Volunteer)
95 | volunteer!: Volunteer;
96 |
97 | @BelongsTo(() => Donor)
98 | donor!: Donor;
99 | }
100 |
--------------------------------------------------------------------------------
/frontend/src/components/auth/Signup/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { NavigationProps, Step, useForm, useStep } from "react-hooks-helper";
3 |
4 | import AccountDetails from "./AccountDetails";
5 | import AccountType from "./AccountType";
6 | import CreateAccount from "./CreateAccount";
7 | import TermsConditions from "./TermsConditions";
8 | import VerificationPage from "./VerificationEmail";
9 | import VolunteerQuestions from "./VolunteerQuestions";
10 |
11 | const steps = [
12 | { id: "account type" },
13 | { id: "create account" },
14 | { id: "account details" },
15 | { id: "volunteer questions" },
16 | { id: "terms conditions" },
17 | { id: "email verification" },
18 | ];
19 |
20 | interface UseStepType {
21 | step: number | Step | any;
22 | index: number;
23 | navigation: NavigationProps | any;
24 | }
25 |
26 | const Signup = () => {
27 | const [formValues, setForm] = useForm({
28 | firstName: "",
29 | lastName: "",
30 | email: "",
31 | phoneNumber: "",
32 | password: "",
33 | confirmPassword: "",
34 | businessName: "",
35 | role: "",
36 | acceptedTerms: false,
37 | cityQuestionResponse: "",
38 | intentionQuestionResponse: "",
39 | skillsQuestionResponse: "",
40 | });
41 |
42 | const { step, navigation }: UseStepType = useStep({ steps, initialStep: 0 });
43 | const { id } = step;
44 |
45 | switch (id) {
46 | case "account type":
47 | return (
48 |
53 | );
54 | case "create account":
55 | return (
56 |
61 | );
62 | case "account details":
63 | return (
64 |
69 | );
70 | case "volunteer questions":
71 | return (
72 |
77 | );
78 | case "terms conditions":
79 | return (
80 |
85 | );
86 | case "email verification":
87 | return ;
88 | default:
89 | return null;
90 | }
91 | };
92 |
93 | export default Signup;
94 |
--------------------------------------------------------------------------------
/backend/typescript/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "typescript-backend",
3 | "version": "1.0.0",
4 | "description": "Backend services for Community Fridge KW",
5 | "main": "server.ts",
6 | "scripts": {
7 | "start": "node build/server.js",
8 | "build": "tsc",
9 | "postinstall": "if [ $PROJECT_PATH ]; then yarn run build; fi",
10 | "test": "jest --runInBand --forceExit --detectOpenHandles",
11 | "dev": "nodemon -L",
12 | "lint": "eslint . --ext .ts,.js",
13 | "fix": "eslint . --ext .ts,.js --fix"
14 | },
15 | "keywords": [],
16 | "author": "UW Blueprint and Contributors",
17 | "license": "MIT",
18 | "dependencies": {
19 | "@types/multer": "^1.4.6",
20 | "@types/swagger-ui-express": "^4.1.2",
21 | "@types/uuid": "^8.3.1",
22 | "@types/yamljs": "^0.2.31",
23 | "cookie-parser": "^1.4.5",
24 | "cors": "^2.8.5",
25 | "dayjs": "^1.10.7",
26 | "dotenv": "^8.2.0",
27 | "eslint-plugin-unused-imports": "^1.1.5",
28 | "express": "^4.17.1",
29 | "firebase-admin": "^9.5.0",
30 | "lodash": "^4.17.21",
31 | "mongoose": "^5.12.12",
32 | "multer": "^1.4.2",
33 | "node-cron": "^2.0.3",
34 | "node-fetch": "^2.6.7",
35 | "nodemailer": "^6.5.0",
36 | "ordinal": "^1.0.3",
37 | "pg": "^8.5.1",
38 | "reflect-metadata": "^0.1.13",
39 | "sequelize": "^6.5.0",
40 | "sequelize-typescript": "^2.1.0",
41 | "swagger-ui-express": "^4.1.6",
42 | "ts-node": "^10.0.0",
43 | "umzug": "^3.0.0-beta.16",
44 | "uuid": "^8.3.2",
45 | "winston": "^3.3.3",
46 | "yamljs": "^0.3.0"
47 | },
48 | "devDependencies": {
49 | "@types/cookie-parser": "^1.4.2",
50 | "@types/cors": "^2.8.10",
51 | "@types/dotenv": "^8.2.0",
52 | "@types/express": "^4.17.11",
53 | "@types/jest": "^26.0.23",
54 | "@types/lodash": "^4.14.168",
55 | "@types/mongoose": "^5.10.3",
56 | "@types/node": "^14.14.31",
57 | "@types/node-fetch": "^2.5.8",
58 | "@types/nodemailer": "^6.4.1",
59 | "@types/pg": "^7.14.10",
60 | "@types/umzug": "^2.3.0",
61 | "@types/validator": "^13.1.3",
62 | "@typescript-eslint/eslint-plugin": "^4.4.1",
63 | "@typescript-eslint/parser": "^4.15.2",
64 | "eslint": "^7.20.0",
65 | "eslint-config-airbnb-typescript": "^12.3.1",
66 | "eslint-config-prettier": "^8.0.0",
67 | "eslint-plugin-import": "^2.22.1",
68 | "eslint-plugin-prettier": "^3.3.1",
69 | "jest": "^27.0.4",
70 | "mongodb-memory-server": "^6.9.6",
71 | "nodemon": "^2.0.7",
72 | "prettier": "^2.2.1",
73 | "ts-jest": "^27.0.3",
74 | "typescript": "^4.1.5"
75 | },
76 | "resolutions": {
77 | "fs-capacitor": "^6.2.0",
78 | "graphql-upload": "^11.0.0"
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: CI
4 |
5 | # Controls when the workflow will run
6 | on:
7 | # Triggers the workflow on push or pull request events but only for the main and staging branch
8 | push:
9 | branches:
10 | - main
11 | - staging
12 | pull_request:
13 | branches:
14 | - main
15 | - staging
16 |
17 | # Allows you to run this workflow manually from the Actions tab
18 | workflow_dispatch:
19 |
20 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
21 | jobs:
22 | # This workflow contains a single job called "ci"
23 | ci:
24 | # The type of runner that the job will run on
25 | runs-on: ubuntu-latest
26 | env:
27 | POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
28 | POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
29 | POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
30 | DB_TEST_HOST: ${{ secrets.DB_TEST_HOST }}
31 |
32 | # Steps represent a sequence of tasks that will be executed as part of the job
33 | steps:
34 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
35 | - uses: actions/checkout@v2
36 |
37 | - name: Get yarn cache directory path
38 | id: yarn-cache-dir-path
39 | run: echo "::set-output name=dir::$(yarn cache dir)"
40 |
41 | - name: Cache node_modules
42 | uses: actions/cache@v2
43 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
44 | with:
45 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
46 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
47 | restore-keys: |
48 | ${{ runner.os }}-yarn-
49 |
50 | # Install dependencies
51 | - name: Install dependencies
52 | run: yarn --cwd ./frontend/ && yarn --cwd ./backend/typescript/
53 |
54 | - name: Build the docker-compose stack
55 | run: docker-compose -f docker-compose.ci.yml up -d
56 |
57 | - name: Check running containers
58 | run: docker ps -a
59 |
60 | # Linting check
61 | - name: Run lint for frontend
62 | working-directory: ./frontend
63 | run: yarn lint
64 |
65 | - name: Run lint for backend
66 | working-directory: ./backend/typescript
67 | run: yarn lint
68 |
69 | # Run tests
70 | - name: Run tests for frontend
71 | working-directory: ./frontend
72 | run: yarn test
73 |
74 | - name: Run tests for backend
75 | run: docker exec community-fridge-kw_ts-backend_1 yarn test
76 |
77 |
78 |
--------------------------------------------------------------------------------
/backend/typescript/services/interfaces/volunteerService.ts:
--------------------------------------------------------------------------------
1 | import {
2 | VolunteerDTO,
3 | UserVolunteerDTO,
4 | UpdateVolunteerDTO,
5 | CheckInDTO,
6 | SchedulingDTO,
7 | } from "../../types";
8 |
9 | interface IVolunteerService {
10 | /**
11 | * Create new Volunteer in database
12 | * @param volunteer new volunteer object
13 | * @returns a VolunteerDTO with volunteer's information
14 | * @throws Error if volunteer creation fails
15 | */
16 | createVolunteer(volunteer: Omit): Promise;
17 |
18 | /**
19 | * Get volunteer associated with id
20 | * @param id volunteer's id
21 | * @returns a VolunteerDTO with volunteer's information
22 | * @throws Error if volunteer retrieval fails
23 | */
24 | getVolunteerById(id: string): Promise;
25 |
26 | /**
27 | * Get volunteer associated with user id
28 | * @param userId id associated with user
29 | * @returns a VolunteerDTO with volunteer's information
30 | * @throws Error if volunteer retrieval fails
31 | */
32 | getVolunteerByUserId(userId: string): Promise;
33 |
34 | /**
35 | * Get all volunteer information (possibly paginated in the future)
36 | * @returns array of VolunteerDTOs
37 | * @throws Error if volunteer retrieval fails
38 | */
39 | getVolunteers(): Promise>;
40 |
41 | /**
42 | * Get all checkins and schedulings sorted by most recent to least recent
43 | * @param volunteerId id associated with volunteer
44 | * @returns all checkins and schedulings sorted by most recent to least recent
45 | * @throws Error if scheduling or checkin retrieval fails
46 | */
47 | getCheckInsAndSchedules(
48 | volunteerId: string,
49 | ): Promise>;
50 |
51 | /**
52 | * Update a volunteer by volunteerId.
53 | * @param id volunteer's id
54 | * @param volunteer the volunteer to be updated
55 | * @throws Error if volunteer update fails
56 | */
57 | updateVolunteerById(
58 | id: string,
59 | volunteer: UpdateVolunteerDTO,
60 | ): Promise;
61 |
62 | /**
63 | * Update a volunteer by userId.
64 | * @param userId id associated with user
65 | * @param volunteer the volunteer to be updated
66 | * @throws Error if volunteer update fails
67 | */
68 | updateVolunteerByUserId(
69 | userId: string,
70 | volunteer: UpdateVolunteerDTO,
71 | ): Promise;
72 |
73 | /**
74 | * Delete a volunteer by id
75 | * @param volunteerId volunteer's volunteerId
76 | * @throws Error if volunteer deletion fails
77 | */
78 | deleteVolunteerById(id: string): Promise;
79 | }
80 |
81 | export default IVolunteerService;
82 |
--------------------------------------------------------------------------------
/frontend/src/components/pages/Dashboard/components/ModifyRecurringDonationModal.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Modal,
4 | ModalBody,
5 | ModalCloseButton,
6 | ModalContent,
7 | ModalFooter,
8 | ModalHeader,
9 | ModalOverlay,
10 | Radio,
11 | RadioGroup,
12 | Stack,
13 | Text,
14 | } from "@chakra-ui/react";
15 | import React, { useState } from "react";
16 |
17 | import useViewport from "../../../../hooks/useViewport";
18 |
19 | interface ModifyRecurringModalProps {
20 | isOpen: boolean;
21 | onClose: () => void;
22 | onModification: (isOneTimeEvent?: boolean) => void;
23 | modificationType: string;
24 | isRecurringDisabled?: boolean;
25 | }
26 | const ModifyRecurringModal = ({
27 | isOpen,
28 | onClose,
29 | onModification,
30 | modificationType,
31 | isRecurringDisabled = false,
32 | }: ModifyRecurringModalProps) => {
33 | const { isDesktop } = useViewport();
34 | const [modifyScheduleValue, setmodifyScheduleValue] = useState("one");
35 |
36 | return (
37 | <>
38 |
44 |
45 |
46 |
47 |
48 | {modificationType[0].toUpperCase() + modificationType.slice(1)}{" "}
49 | Donation
50 |
51 |
52 |
53 |
54 |
58 |
59 |
60 | This donation
61 |
62 |
68 | This and all following donations
69 |
70 |
71 |
72 |
73 |
74 |
82 |
83 |
84 |
85 | >
86 | );
87 | };
88 |
89 | export default ModifyRecurringModal;
90 |
--------------------------------------------------------------------------------
/frontend/src/components/common/Calendar/WeeklyEventItems.tsx:
--------------------------------------------------------------------------------
1 | import { Badge, Container, Text, useDisclosure } from "@chakra-ui/react";
2 | import React, { useEffect, useState } from "react";
3 |
4 | import DonorAPIClient from "../../../APIClients/DonorAPIClient";
5 | import { colorMap, convertTime } from "../../../constants/DaysInWeek";
6 | import useViewport from "../../../hooks/useViewport";
7 | import { DonorResponse } from "../../../types/DonorTypes";
8 | import { Schedule } from "../../../types/SchedulingTypes";
9 | import WeeklyEventItemPopUp from "./WeeklyEventItemPopUp";
10 |
11 | type DefaultWeeklyEventItemProps = {
12 | schedule: Schedule;
13 | date: string;
14 | };
15 |
16 | const DefaultWeeklyEventItem = ({
17 | schedule,
18 | date,
19 | }: DefaultWeeklyEventItemProps) => {
20 | const { isMobile } = useViewport();
21 | const { isOpen, onOpen, onClose } = useDisclosure();
22 | const [donor, setDonor] = useState();
23 |
24 | useEffect(() => {
25 | const getDonor = async () => {
26 | const donorResponse = await DonorAPIClient.getDonorById(
27 | schedule!.donorId,
28 | );
29 | setDonor(donorResponse);
30 | };
31 |
32 | getDonor();
33 | }, []);
34 |
35 | return (
36 | <>
37 | {donor && (
38 | <>
39 |
50 |
51 | {donor?.firstName} {donor?.lastName}
52 |
53 |
54 | {convertTime(schedule!.startTime)} -{" "}
55 | {convertTime(schedule!.endTime)}
56 |
57 |
64 | {schedule!.frequency}
65 |
66 |
67 |
68 |
74 | >
75 | )}
76 | >
77 | );
78 | };
79 |
80 | export default DefaultWeeklyEventItem;
81 |
--------------------------------------------------------------------------------
/backend/typescript/middlewares/validators/util.ts:
--------------------------------------------------------------------------------
1 | import { Categories } from "../../types";
2 |
3 | type Type =
4 | | "string"
5 | | "integer"
6 | | "boolean"
7 | | "Status"
8 | | "Date string"
9 | | "24 Hour Time String";
10 |
11 | const allowableContentTypes = new Set([
12 | "text/plain",
13 | "application/pdf",
14 | "image/png",
15 | "image/jpeg",
16 | "image/gif",
17 | ]);
18 |
19 | export const validateDate = (value: string): boolean => {
20 | return !!Date.parse(value);
21 | };
22 |
23 | export const validate24HourTime = (value: string): boolean => {
24 | const regEx = new RegExp(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/);
25 | return regEx.test(value);
26 | };
27 |
28 | export const validateRecurringDonationEndDate = (
29 | startDateString: string,
30 | recurringEndDateString: string,
31 | ): boolean => {
32 | const startDate = new Date(startDateString);
33 | const maxEndDate = new Date(startDateString);
34 | maxEndDate.setMonth(startDate.getMonth() + 6);
35 | const endDate = new Date(recurringEndDateString);
36 | return startDate <= endDate && endDate <= maxEndDate;
37 | };
38 |
39 | export const validatePrimitive = (value: any, type: Type): boolean => {
40 | if (value === undefined || value === null) return false;
41 |
42 | switch (type) {
43 | case "string": {
44 | return typeof value === "string";
45 | }
46 | case "boolean": {
47 | return typeof value === "boolean";
48 | }
49 | case "integer": {
50 | return typeof value === "number" && Number.isInteger(value);
51 | }
52 | default: {
53 | return false;
54 | }
55 | }
56 | };
57 |
58 | export const validateArray = (value: any, type: Type): boolean => {
59 | return (
60 | value !== undefined &&
61 | value !== null &&
62 | typeof value === "object" &&
63 | Array.isArray(value) &&
64 | value.every((item) => validatePrimitive(item, type))
65 | );
66 | };
67 |
68 | export const validateCategories = (value: string[]): boolean => {
69 | return (
70 | validateArray(value, "string") &&
71 | value.every((item) => Categories.has(item))
72 | );
73 | };
74 |
75 | export const validateFileType = (mimetype: string): boolean => {
76 | return allowableContentTypes.has(mimetype);
77 | };
78 |
79 | export const getApiValidationError = (
80 | fieldName: string,
81 | type: Type,
82 | isArray = false,
83 | isDateError = false,
84 | ): string => {
85 | if (isDateError) {
86 | return "startTime must be before endTime";
87 | }
88 | return `The ${fieldName} is not a ${type}${isArray ? " Array" : ""}`;
89 | };
90 |
91 | export const getFileTypeValidationError = (mimetype: string): string => {
92 | const allowableContentTypesString = [...allowableContentTypes].join(", ");
93 | return `The file type ${mimetype} is not one of ${allowableContentTypesString}`;
94 | };
95 |
--------------------------------------------------------------------------------
/backend/typescript/middlewares/validators/checkInValidators.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from "express";
2 | import { getApiValidationError, validatePrimitive, validateDate } from "./util";
3 |
4 | export const createCheckInDtoValidator = async (
5 | req: Request,
6 | res: Response,
7 | next: NextFunction,
8 | ) => {
9 | if (!validateDate(req.body.startDate)) {
10 | return res
11 | .status(400)
12 | .send(getApiValidationError("startDate", "Date string"));
13 | }
14 | if (!validateDate(req.body.endDate)) {
15 | return res
16 | .status(400)
17 | .send(getApiValidationError("endDate", "Date string"));
18 | }
19 | if (req.body.notes && !validatePrimitive(req.body.notes, "string")) {
20 | return res.status(400).send(getApiValidationError("notes", "string"));
21 | }
22 | if (req.body.isAdmin && !validatePrimitive(req.body.isAdmin, "boolean")) {
23 | return res.status(400).send(getApiValidationError("isAdmin", "boolean"));
24 | }
25 | if (
26 | req.body.volunteerId &&
27 | !validatePrimitive(req.body.volunteerId, "string")
28 | ) {
29 | return res.status(400).send(getApiValidationError("volunteerId", "string"));
30 | }
31 | if (
32 | new Date(req.body.startDate).getTime() >=
33 | new Date(req.body.endDate).getTime()
34 | ) {
35 | return res
36 | .status(400)
37 | .send(getApiValidationError("dates", "Date string", false, true));
38 | }
39 |
40 | return next();
41 | };
42 |
43 | // checks that each field of type CheckInDTO if it exists
44 | export const CheckInGeneralDtoValidator = async (
45 | req: Request,
46 | res: Response,
47 | next: NextFunction,
48 | ) => {
49 | if (req.body.startDate && !validateDate(req.body.startDate)) {
50 | return res
51 | .status(400)
52 | .send(getApiValidationError("startDate", "Date string"));
53 | }
54 | if (req.body.endDate && !validateDate(req.body.endDate)) {
55 | return res
56 | .status(400)
57 | .send(getApiValidationError("endDate", "Date string"));
58 | }
59 | if (
60 | new Date(req.body.startDate).getTime() >=
61 | new Date(req.body.endDate).getTime()
62 | ) {
63 | return res
64 | .status(400)
65 | .send(getApiValidationError("dates", "Date string", false, true));
66 | }
67 | if (req.body.notes && !validatePrimitive(req.body.notes, "string")) {
68 | return res.status(400).send(getApiValidationError("notes", "string"));
69 | }
70 | if (req.body.isAdmin && !validatePrimitive(req.body.isAdmin, "boolean")) {
71 | return res.status(400).send(getApiValidationError("isAdmin", "boolean"));
72 | }
73 | if (
74 | req.body.volunteerId &&
75 | !validatePrimitive(req.body.volunteerId, "string")
76 | ) {
77 | return res.status(400).send(getApiValidationError("volunteerId", "string"));
78 | }
79 |
80 | return next();
81 | };
82 |
--------------------------------------------------------------------------------
/backend/typescript/middlewares/validators/authValidators.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from "express";
2 | import { getApiValidationError, validatePrimitive } from "./util";
3 | import { Role, Status } from "../../types";
4 | /* eslint-disable-next-line import/prefer-default-export */
5 | export const loginRequestValidator = async (
6 | req: Request,
7 | res: Response,
8 | next: NextFunction,
9 | ) => {
10 | if (req.body.idToken) {
11 | if (!validatePrimitive(req.body.idToken, "string")) {
12 | return res.status(400).json(getApiValidationError("idToken", "string"));
13 | }
14 | } else {
15 | if (!validatePrimitive(req.body.email, "string")) {
16 | return res.status(400).send(getApiValidationError("email", "string"));
17 | }
18 | if (!validatePrimitive(req.body.password, "string")) {
19 | return res.status(400).send(getApiValidationError("password", "string"));
20 | }
21 | }
22 | return next();
23 | };
24 |
25 | export const registerRequestValidator = async (
26 | req: Request,
27 | res: Response,
28 | next: NextFunction,
29 | ) => {
30 | if (!validatePrimitive(req.body.firstName, "string")) {
31 | return res.status(400).send(getApiValidationError("firstName", "string"));
32 | }
33 | if (!validatePrimitive(req.body.lastName, "string")) {
34 | return res.status(400).send(getApiValidationError("lastName", "string"));
35 | }
36 | if (!validatePrimitive(req.body.email, "string")) {
37 | return res.status(400).send(getApiValidationError("email", "string"));
38 | }
39 | if (!validatePrimitive(req.body.password, "string")) {
40 | return res.status(400).send(getApiValidationError("password", "string"));
41 | }
42 | if (!validatePrimitive(req.body.phoneNumber, "string")) {
43 | return res.status(400).send(getApiValidationError("phoneNumber", "string"));
44 | }
45 |
46 | if (req.body.role === Role.VOLUNTEER) {
47 | if (req.body.status && !Object.values(Status).includes(req.body.status)) {
48 | return res.status(400).send(getApiValidationError("status", "Status"));
49 | }
50 | return next();
51 | }
52 | if (req.body.role === Role.DONOR) {
53 | if (
54 | req.body.facebookLink &&
55 | !validatePrimitive(req.body.facebookLink, "string")
56 | ) {
57 | return res
58 | .status(400)
59 | .send(getApiValidationError("facebookLink", "string"));
60 | }
61 | if (
62 | req.body.instagramLink &&
63 | !validatePrimitive(req.body.instagramLink, "string")
64 | ) {
65 | return res
66 | .status(400)
67 | .send(getApiValidationError("instagramLink", "string"));
68 | }
69 | if (!validatePrimitive(req.body.businessName, "string")) {
70 | return res
71 | .status(400)
72 | .send(getApiValidationError("businessName", "string"));
73 | }
74 | }
75 | return next();
76 | };
77 |
--------------------------------------------------------------------------------
/frontend/src/theme/components/Button.ts:
--------------------------------------------------------------------------------
1 | const Button = {
2 | variants: {
3 | navigation: {
4 | background: "raddish.100",
5 | color: "squash.100",
6 | fontSize: "16px",
7 | py: "20px",
8 | px: "35px",
9 | _disabled: {
10 | background: "hubbard.100",
11 | },
12 | _hover: {
13 | _disabled: {
14 | background: "hubbard.100",
15 | },
16 | },
17 | },
18 | cancelNavigation: {
19 | border: "1px",
20 | borderColor: "hubbard.100",
21 | color: "hubbard.100",
22 | size: "lg",
23 | width: "100%",
24 | },
25 | editInfo: {
26 | border: "none",
27 | color: "hubbard.100",
28 | fontSize: "14px",
29 | height: "20px",
30 | width: "28px",
31 | background: "none",
32 | marginBottom: "10px",
33 | textDecoration: "underline",
34 | },
35 | cancelEditInfo: {
36 | border: "none",
37 | color: "black.100",
38 | height: "12.73px",
39 | width: "12.73px",
40 | background: "none",
41 | marginBottom: "10px",
42 | },
43 | deleteDonation: {
44 | background: "tomato.100",
45 | color: "squash.100",
46 | },
47 | outlined: {
48 | border: "1px",
49 | borderColor: "raddish.100",
50 | color: "raddish.100",
51 | fontSize: "16px",
52 | py: "12px",
53 | px: "53px",
54 | },
55 | edit: {
56 | color: "hubbard.100",
57 | fontSize: "16px",
58 | fontWeight: 300,
59 | textDecoration: "underline",
60 | },
61 | approve: {
62 | background: "raddish.100",
63 | color: "squash.100",
64 | fontSize: "14px",
65 | py: "12px",
66 | px: "16px",
67 | _disabled: {
68 | background: "hubbard.100",
69 | },
70 | _hover: {
71 | _disabled: {
72 | background: "hubbard.100",
73 | },
74 | },
75 | },
76 | export: {
77 | background: "none",
78 | border: "1px",
79 | borderColor: "dorian.100",
80 | color: "hubbard.100",
81 | fontSize: "14px",
82 | py: "20px",
83 | px: "20px",
84 | lineHeight: "20px",
85 | fontWeight: "400",
86 | },
87 | exportMobile: {
88 | background: "none",
89 | border: "1px",
90 | borderColor: "dorian.100",
91 | color: "hubbard.100",
92 | fontSize: "14px",
93 | py: "20px",
94 | px: "20px",
95 | lineHeight: "20px",
96 | fontWeight: "400",
97 | },
98 | create: {
99 | background: "raddish.100",
100 | color: "white",
101 | border: "1px",
102 | borderColor: "champagne.100",
103 | fontSize: "14px",
104 | py: "20px",
105 | px: "53px",
106 | lineHeight: "20px",
107 | },
108 | },
109 | };
110 |
111 | export default Button;
112 |
--------------------------------------------------------------------------------
/frontend/src/components/auth/ResetPassword/ChangePassword.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Button,
4 | Container,
5 | FormControl,
6 | FormErrorMessage,
7 | Input,
8 | Text,
9 | } from "@chakra-ui/react";
10 | import React from "react";
11 | import { NavigationProps, SetForm } from "react-hooks-helper";
12 | import { useHistory } from "react-router-dom";
13 |
14 | import authAPIClient from "../../../APIClients/AuthAPIClient";
15 | import useViewport from "../../../hooks/useViewport";
16 | import MandatoryInputDescription from "../Signup/components/MandatoryInputDescription";
17 | import { RequestPasswordChangeFormProps } from "./types";
18 |
19 | const ChangePassword = ({
20 | formData,
21 | setForm,
22 | navigation,
23 | }: {
24 | formData: RequestPasswordChangeFormProps;
25 | setForm: SetForm;
26 | navigation: NavigationProps;
27 | }) => {
28 | const { next } = navigation;
29 | const history = useHistory();
30 |
31 | const { isDesktop } = useViewport();
32 | const { email } = formData;
33 |
34 | const [interaction, setInteraction] = React.useState({
35 | email: false,
36 | });
37 |
38 | const onSendRequestClick = async () => {
39 | if (!email) {
40 | setInteraction({ ...interaction, email: true });
41 | return false;
42 | }
43 | await authAPIClient.resetPassword(email);
44 | return next();
45 | };
46 |
47 | return (
48 |
49 |
50 | Change Password
51 |
52 |
57 | Enter the email you registered with to send a password change request.
58 |
59 |
60 |
64 |
65 |
66 | {
70 | setInteraction({ ...interaction, email: true });
71 | setForm(e);
72 | }}
73 | name="email"
74 | placeholder="Enter email address"
75 | />
76 |
77 | Please enter a valid email address.
78 |
79 |
80 |
81 |
90 |
91 |
92 |
93 | );
94 | };
95 |
96 | export default ChangePassword;
97 |
--------------------------------------------------------------------------------
/backend/typescript/services/interfaces/userService.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CreateUserDTO,
3 | Role,
4 | SignUpMethod,
5 | UpdateUserDTO,
6 | UserDTO,
7 | } from "../../types";
8 |
9 | interface IUserService {
10 | /**
11 | * Get user associated with id
12 | * @param id user's id
13 | * @returns a UserDTO with user's information
14 | * @throws Error if user retrieval fails
15 | */
16 | getUserById(userId: string): Promise;
17 |
18 | /**
19 | * Get user associated with email
20 | * @param email user's email
21 | * @returns a UserDTO with user's information
22 | * @throws Error if user retrieval fails
23 | */
24 | getUserByEmail(email: string): Promise;
25 |
26 | /**
27 | * Get role of user associated with authId
28 | * @param authId user's authId
29 | * @returns role of the user
30 | * @throws Error if user role retrieval fails
31 | */
32 | getUserRoleByAuthId(authId: string): Promise;
33 |
34 | /**
35 | * Get id of user associated with authId
36 | * @param authId user's authId
37 | * @returns id of the user
38 | * @throws Error if user id retrieval fails
39 | */
40 | getUserIdByAuthId(authId: string): Promise;
41 |
42 | /**
43 | * Get authId of user associated with id
44 | * @param userId user's id
45 | * @returns user's authId
46 | * @throws Error if user authId retrieval fails
47 | */
48 | getAuthIdById(userId: string): Promise;
49 |
50 | /**
51 | * Get all user information (possibly paginated in the future)
52 | * @returns array of UserDTOs
53 | * @throws Error if user retrieval fails
54 | */
55 | getUsers(): Promise>;
56 |
57 | /**
58 | * Create a user, email verification configurable
59 | * @param user the user to be created
60 | * @param authId the user's firebase auth id, optional
61 | * @param signUpMethod the method user used to signup
62 | * @returns a UserDTO with the created user's information
63 | * @throws Error if user creation fails
64 | */
65 | createUser(
66 | user: CreateUserDTO,
67 | authId?: string,
68 | signUpMethod?: SignUpMethod,
69 | ): Promise;
70 |
71 | /**
72 | * Update a user.
73 | * Note: the password cannot be updated using this method, use IAuthService.resetPassword instead
74 | * @param userId user's id
75 | * @param user the user to be updated
76 | * @returns a UserDTO with the updated user's information
77 | * @throws Error if user update fails
78 | */
79 | updateUserById(userId: string, user: UpdateUserDTO): Promise;
80 |
81 | /**
82 | * Delete a user by id
83 | * @param userId user's userId
84 | * @throws Error if user deletion fails
85 | */
86 | deleteUserById(userId: string): Promise;
87 |
88 | /**
89 | * Delete a user by email
90 | * @param email user's email
91 | * @throws Error if user deletion fails
92 | */
93 | deleteUserByEmail(email: string): Promise;
94 | }
95 |
96 | export default IUserService;
97 |
--------------------------------------------------------------------------------
/backend/typescript/middlewares/auth.ts:
--------------------------------------------------------------------------------
1 | import { NextFunction, Request, Response } from "express";
2 |
3 | import AuthService from "../services/implementations/authService";
4 | import UserService from "../services/implementations/userService";
5 | import IAuthService from "../services/interfaces/authService";
6 | import { Role } from "../types";
7 |
8 | const authService: IAuthService = new AuthService(new UserService());
9 |
10 | export const getAccessToken = (req: Request) => {
11 | const authHeaderParts = req.headers.authorization?.split(" ");
12 | if (
13 | authHeaderParts &&
14 | authHeaderParts.length >= 2 &&
15 | authHeaderParts[0].toLowerCase() === "bearer"
16 | ) {
17 | return authHeaderParts[1];
18 | }
19 | return null;
20 | };
21 |
22 | /* Determine if request is authorized based on accessToken validity and role of client */
23 | export const isAuthorizedByRole = (roles: Set) => {
24 | return async (req: Request, res: Response, next: NextFunction) => {
25 | const accessToken = getAccessToken(req);
26 | const authorized =
27 | accessToken && (await authService.isAuthorizedByRole(accessToken, roles));
28 | if (!authorized) {
29 | return res
30 | .status(401)
31 | .json({ error: "You are not authorized to make this request." });
32 | }
33 | return next();
34 | };
35 | };
36 |
37 | /* Determine if request for a user-specific resource is authorized based on accessToken
38 | * validity and if the userId that the token was issued to matches the requested userId
39 | * Note: userIdField is the name of the request parameter containing the requested userId */
40 | export const isAuthorizedByUserId = (userIdField: string) => {
41 | return async (req: Request, res: Response, next: NextFunction) => {
42 | const accessToken = getAccessToken(req);
43 | const authorized =
44 | accessToken &&
45 | (await authService.isAuthorizedByUserId(
46 | accessToken,
47 | req.params[userIdField],
48 | ));
49 | if (!authorized) {
50 | return res
51 | .status(401)
52 | .json({ error: "You are not authorized to make this request." });
53 | }
54 | return next();
55 | };
56 | };
57 |
58 | /* Determine if request for a user-specific resource is authorized based on accessToken
59 | * validity and if the email that the token was issued to matches the requested email
60 | * Note: emailField is the name of the request parameter containing the requested email */
61 | export const isAuthorizedByEmail = (emailField: string) => {
62 | return async (req: Request, res: Response, next: NextFunction) => {
63 | const accessToken = getAccessToken(req);
64 | const authorized =
65 | accessToken &&
66 | (await authService.isAuthorizedByEmail(
67 | accessToken,
68 | req.params[emailField],
69 | ));
70 | if (!authorized) {
71 | return res
72 | .status(401)
73 | .json({ error: "You are not authorized to make this request." });
74 | }
75 | return next();
76 | };
77 | };
78 |
--------------------------------------------------------------------------------
/frontend/src/components/pages/VolunteerScheduling/ThankYouVolunteer.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Container, HStack, Img, Text } from "@chakra-ui/react";
2 | import { format, parse } from "date-fns";
3 | import React, { useContext } from "react";
4 | import { useHistory } from "react-router-dom";
5 |
6 | import ThankYouPageFridge from "../../../assets/ThankYouPageFridge.png";
7 | import * as Routes from "../../../constants/Routes";
8 | import AuthContext from "../../../contexts/AuthContext";
9 | import {
10 | CheckInWithShiftType,
11 | ScheduleWithShiftType,
12 | ShiftType,
13 | } from "../../../types/VolunteerTypes";
14 |
15 | const ThankYouVolunteer = ({
16 | shift,
17 | }: {
18 | shift: ScheduleWithShiftType | CheckInWithShiftType;
19 | }) => {
20 | const { authenticatedUser } = useContext(AuthContext);
21 | const history = useHistory();
22 | return (
23 |
24 |
28 | Thank you for volunteering with CFKW!
29 |
30 | {shift.type === ShiftType.SCHEDULING && (
31 |
32 | {`We can't wait to see you at the fridge on ${
33 | shift.startTime
34 | ? format(new Date(shift.startTime), "eeee MMMM d, yyyy")
35 | : ""
36 | } at
37 | ${
38 | shift.volunteerTime
39 | ? format(parse(shift.volunteerTime, "HH:mm", new Date()), "h:mm a")
40 | : ""
41 | } `}
42 |
43 | )}
44 | {shift.type === ShiftType.CHECKIN && (
45 |
46 | {`We can't wait to see you at the fridge on ${
47 | shift.startDate
48 | ? format(new Date(shift.startDate), "eeee MMMM d, yyyy")
49 | : ""
50 | } at
51 | ${
52 | shift.startDate ? format(new Date(shift.startDate), "h:mm aa") : ""
53 | } `}
54 |
55 | )}
56 |
57 |
58 | We appreciate your support and dedication! An email confirmation has
59 | been sent to {authenticatedUser!.email}.
60 |
61 |
62 |
73 |
74 |
83 |
84 |
85 | );
86 | };
87 |
88 | export default ThankYouVolunteer;
89 |
--------------------------------------------------------------------------------
/backend/typescript/services/interfaces/checkInService.ts:
--------------------------------------------------------------------------------
1 | import { CheckInDTO, CreateCheckInDTO, UpdateCheckInDTO } from "../../types";
2 |
3 | interface ICheckInService {
4 | /**
5 | * Create check in
6 | * @param checkIn CreateCheckInDTO object containing check in info
7 | * @returns a CheckInDTO[] with all the created check ins
8 | * @throws Error if check in creation fails
9 | */
10 | createCheckIn(checkIn: CreateCheckInDTO): Promise;
11 |
12 | /**
13 | * Update checkIn.
14 | * @param id id of checkIn object
15 | * @param checkIn UpdateCheckInDTO object containing checkIn info to be updated
16 | * @returns a CheckInDTO with the updated checkIn information
17 | * @throws Error if checkIn update fails
18 | */
19 | updateCheckInById(id: string, checkIn: UpdateCheckInDTO): Promise;
20 |
21 | /**
22 | * Generate a confirmation email with check-in shift information for volunteer who signed up for the shift
23 | * @param volunteerId of volunteer who signed up for shift
24 | * @param checkIn object that contains check-in information
25 | * @param isAdmin boolean for if the email is to be sent to an admin or volunteer
26 | * @throws Error if unable to send email
27 | */
28 | sendVolunteerCheckInSignUpConfirmationEmail(
29 | volunteerId: string,
30 | checkIn: CheckInDTO,
31 | isAdmin: boolean,
32 | ): Promise;
33 |
34 | /**
35 | *
36 | * Generate a confirmation email when a volunteer cancels a check-in shift
37 | * @param volunteerId of volunteer who cancelled the shift
38 | * @param checkIn object that contains the check-in information
39 | * @param isAdmin boolean for if the email is to be sent to an admin or volunteer
40 | * @throws Error if unable to send email
41 | */
42 | sendVolunteerCancelCheckInEmail(
43 | volunteerId: string,
44 | checkIn: CheckInDTO,
45 | isAdmin: boolean,
46 | ): Promise;
47 |
48 | /**
49 | * Gets all checkins from table
50 | * @throws Error if retrieving checkins fail
51 | */
52 | getAllCheckIns(): Promise;
53 |
54 | /**
55 | * Gets checkin by primary key id
56 | * @param id primary key id
57 | * @throws Error if retrieving checkin by specific key fails
58 | */
59 | getCheckInsById(id: string): Promise;
60 |
61 | /**
62 | * Gets checkin by volunteerId
63 | * @param volunteerId volunteer id
64 | * @throws Error if retrieving checkin by volunteer id fails
65 | */
66 | getCheckInsByVolunteerId(volunteerId: string): Promise;
67 |
68 | /**
69 | * Deletes checkin by primary key id
70 | * @param id checkin's id
71 | * @throws Error if checkin deletion fails
72 | */
73 | deleteCheckInById(id: string): Promise;
74 |
75 | /**
76 | * Deletes checkin in the specified start and end date range inclusive
77 | * @param startDate start date of the range
78 | * @param endDate end date of the range
79 | */
80 | deleteCheckInsByDateRange(startDate: string, endDate: string): Promise;
81 | }
82 |
83 | export default ICheckInService;
84 |
--------------------------------------------------------------------------------
/frontend/src/APIClients/CheckInAPIClient.ts:
--------------------------------------------------------------------------------
1 | import { BEARER_TOKEN } from "../constants/AuthConstants";
2 | import {
3 | CheckIn,
4 | CreateCheckInFields,
5 | UpdatedCheckInFields,
6 | } from "../types/CheckInTypes";
7 | import baseAPIClient from "./BaseAPIClient";
8 |
9 | const getAllCheckIns = async (): Promise => {
10 | try {
11 | const { data } = await baseAPIClient.get("/checkin", {
12 | headers: { Authorization: BEARER_TOKEN },
13 | });
14 | return data;
15 | } catch (error) {
16 | return error as CheckIn[];
17 | }
18 | };
19 |
20 | const getCheckInsById = async (checkInId: string): Promise => {
21 | try {
22 | const { data } = await baseAPIClient.get(`/checkin/${checkInId}`, {
23 | headers: { Authorization: BEARER_TOKEN },
24 | });
25 | return data;
26 | } catch (error) {
27 | return error as CheckIn;
28 | }
29 | };
30 |
31 | const getCheckInsByVolunteerId = async (
32 | volunteerId: string,
33 | ): Promise => {
34 | try {
35 | const url = `/checkin/?volunteerId=${volunteerId}`;
36 | const { data } = await baseAPIClient.get(url, {
37 | headers: { Authorization: BEARER_TOKEN },
38 | });
39 | return data;
40 | } catch (error) {
41 | return error as CheckIn[];
42 | }
43 | };
44 |
45 | const deleteCheckInById = async (checkInId: string): Promise => {
46 | try {
47 | await baseAPIClient.delete(`/checkin/${checkInId}`, {
48 | headers: { Authorization: BEARER_TOKEN },
49 | });
50 | return true;
51 | } catch (error) {
52 | return false;
53 | }
54 | };
55 |
56 | const deleteCheckInsByDateRange = async (
57 | startDate: string,
58 | endDate: string,
59 | ): Promise => {
60 | try {
61 | await baseAPIClient.delete(
62 | `/checkin?startDate=${startDate}&endDate=${endDate}`,
63 | {
64 | headers: { Authorization: BEARER_TOKEN },
65 | },
66 | );
67 | return true;
68 | } catch (error) {
69 | return false;
70 | }
71 | };
72 |
73 | const createCheckIn = async (
74 | checkIn: CreateCheckInFields,
75 | ): Promise => {
76 | try {
77 | const { data } = await baseAPIClient.post(
78 | "/checkin",
79 | {
80 | ...checkIn,
81 | },
82 | { headers: { Authorization: BEARER_TOKEN } },
83 | );
84 | return data;
85 | } catch (error) {
86 | return false;
87 | }
88 | };
89 |
90 | const updateCheckInById = async (
91 | checkInId: string,
92 | fields: UpdatedCheckInFields,
93 | ): Promise => {
94 | try {
95 | const { data } = await baseAPIClient.put(
96 | `/checkin/${checkInId}`,
97 | {
98 | ...fields,
99 | },
100 | { headers: { Authorization: BEARER_TOKEN } },
101 | );
102 | return data;
103 | } catch (error) {
104 | return false;
105 | }
106 | };
107 |
108 | export default {
109 | getAllCheckIns,
110 | getCheckInsById,
111 | getCheckInsByVolunteerId,
112 | deleteCheckInById,
113 | deleteCheckInsByDateRange,
114 | createCheckIn,
115 | updateCheckInById,
116 | };
117 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "description": "Frontend UI for Community Fridge KW",
6 | "author": "UW Blueprint and Contributors",
7 | "license": "MIT",
8 | "dependencies": {
9 | "@chakra-ui/icons": "^1.1.1",
10 | "@chakra-ui/react": "^1.6.10",
11 | "@emotion/react": "^11",
12 | "@emotion/styled": "^11",
13 | "@fontsource/inter": "^4.5.0",
14 | "@rjsf/core": "^2.5.1",
15 | "@testing-library/jest-dom": "^5.11.4",
16 | "@testing-library/react": "^11.1.0",
17 | "@testing-library/user-event": "^12.1.10",
18 | "@types/jest": "^26.0.15",
19 | "@types/json2csv": "^5.0.3",
20 | "@types/node": "^12.0.0",
21 | "@types/react": "^17.0.0",
22 | "@types/react-dom": "^17.0.0",
23 | "@types/react-hooks-helper": "^1.6.4",
24 | "@types/react-jsonschema-form": "^1.7.4",
25 | "@types/react-table": "^7.0.29",
26 | "axios": "^0.21.2",
27 | "chakra-ui-steps": "^1.5.0",
28 | "date-fns": "^2.25.0",
29 | "depcheck": "^1.4.2",
30 | "eslint-plugin-simple-import-sort": "^7.0.0",
31 | "eslint-plugin-unused-imports": "^1.1.5",
32 | "framer-motion": "^4.1.17",
33 | "humps": "^2.0.1",
34 | "json-schema": "^0.3.0",
35 | "json2csv": "^5.0.7",
36 | "jsonwebtoken": "^8.5.1",
37 | "patch-package": "^6.4.7",
38 | "postinstall-postinstall": "^2.1.0",
39 | "react": "^17.0.1",
40 | "react-dom": "^17.0.1",
41 | "react-google-login": "^5.2.2",
42 | "react-hooks-helper": "^1.6.0",
43 | "react-json-schema": "^1.2.2",
44 | "react-jsonschema-form": "^1.8.1",
45 | "react-multi-date-picker": "^3.1.7",
46 | "react-router-dom": "^5.2.0",
47 | "react-scripts": "4.0.2",
48 | "react-table": "^7.7.0",
49 | "typescript": "4.1.2",
50 | "web-vitals": "^1.0.1"
51 | },
52 | "scripts": {
53 | "start": "react-scripts start",
54 | "build": "react-scripts build",
55 | "test": "react-scripts test",
56 | "eject": "react-scripts eject",
57 | "lint": "eslint . --ext .ts,.tsx,.js,.jsx",
58 | "fix": "eslint . --ext .ts,.tsx,.js,.jsx --fix"
59 | },
60 | "eslintConfig": {
61 | "extends": [
62 | "react-app",
63 | "react-app/jest"
64 | ]
65 | },
66 | "browserslist": {
67 | "production": [
68 | ">0.2%",
69 | "not dead",
70 | "not op_mini all"
71 | ],
72 | "development": [
73 | "last 1 chrome version",
74 | "last 1 firefox version",
75 | "last 1 safari version"
76 | ]
77 | },
78 | "devDependencies": {
79 | "@types/humps": "^2.0.0",
80 | "@types/jsonwebtoken": "^8.5.1",
81 | "@types/react-router-dom": "^5.1.7",
82 | "@types/react-test-renderer": "^17.0.1",
83 | "@typescript-eslint/eslint-plugin": "^4.15.2",
84 | "@typescript-eslint/parser": "^4.15.2",
85 | "eslint-config-airbnb-typescript": "^12.3.1",
86 | "eslint-config-prettier": "^8.1.0",
87 | "eslint-plugin-import": "^2.22.1",
88 | "eslint-plugin-jsx-a11y": "^6.4.1",
89 | "eslint-plugin-prettier": "^3.3.1",
90 | "eslint-plugin-react": "^7.22.0",
91 | "eslint-plugin-react-hooks": "^4.2.0",
92 | "prettier": "^2.2.1",
93 | "react-test-renderer": "^17.0.2"
94 | }
95 | }
96 |
--------------------------------------------------------------------------------