├── 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 | 2 | 3 | 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 | 2 | 3 | 4 | 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 | Verification email image 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 | Verification email image 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 | pending approval image 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 | 2 | 3 | 4 | 5 | 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 | Verification page image 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 | Community Fridge 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 | Verification email image 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 | Community Fridge Thank You Page 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 | Community Fridge Thank You Page 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 | --------------------------------------------------------------------------------