├── .npmrc ├── packages ├── shared │ ├── tests │ │ ├── setupTests.ts │ │ ├── index.test.ts │ │ ├── schema │ │ │ ├── judge │ │ │ │ ├── post.test.ts │ │ │ │ ├── put.test.ts │ │ │ │ └── common.test.ts │ │ │ ├── criteriaJudgingSubmission │ │ │ │ └── criteriaJudgingSubmission.test.ts │ │ │ ├── expoJudgingSession │ │ │ │ └── post.test.ts │ │ │ ├── project │ │ │ │ ├── contributors │ │ │ │ │ └── put.test.ts │ │ │ │ ├── put.test.ts │ │ │ │ └── post.test.ts │ │ │ ├── criteria │ │ │ │ └── criteria.test.ts │ │ │ └── criteriaJudgingSession │ │ │ │ └── criteriaJudgingSession.test.ts │ │ └── utils │ │ │ └── wait.test.ts │ ├── .npmrc │ ├── src │ │ ├── utils │ │ │ ├── index.ts │ │ │ └── wait.ts │ │ ├── schema │ │ │ ├── user │ │ │ │ ├── index.ts │ │ │ │ └── put.ts │ │ │ ├── criteria │ │ │ │ ├── index.ts │ │ │ │ └── criteria.ts │ │ │ ├── expoJudgingSession │ │ │ │ ├── index.ts │ │ │ │ └── post.ts │ │ │ ├── expoJudgingVote │ │ │ │ ├── index.ts │ │ │ │ └── post.ts │ │ │ ├── project │ │ │ │ ├── contributors │ │ │ │ │ ├── index.ts │ │ │ │ │ └── put.ts │ │ │ │ ├── index.ts │ │ │ │ ├── put.ts │ │ │ │ ├── post.ts │ │ │ │ └── core.ts │ │ │ ├── criteriaJudgingSession │ │ │ │ ├── index.ts │ │ │ │ └── post.ts │ │ │ ├── criteriaScore │ │ │ │ ├── index.ts │ │ │ │ └── criteriaScore.ts │ │ │ ├── criteriaJudgingSubmission │ │ │ │ ├── index.ts │ │ │ │ └── post.ts │ │ │ ├── judge │ │ │ │ ├── index.ts │ │ │ │ ├── common.ts │ │ │ │ ├── put.ts │ │ │ │ └── post.ts │ │ │ └── index.ts │ │ ├── types │ │ │ ├── ApiError.ts │ │ │ ├── utilities │ │ │ │ └── Pretty.ts │ │ │ ├── index.ts │ │ │ ├── DTOs │ │ │ │ ├── index.ts │ │ │ │ ├── ExpoJudgingSessionProjects.ts │ │ │ │ └── CriteriaJudgingSessionResults.ts │ │ │ └── entities │ │ │ │ ├── Criteria.ts │ │ │ │ ├── User.ts │ │ │ │ ├── Admin.ts │ │ │ │ ├── Judge.ts │ │ │ │ ├── Prize.ts │ │ │ │ ├── CriteriaScore.ts │ │ │ │ ├── CriteriaJudgingSubmission.ts │ │ │ │ ├── JudgingSession.ts │ │ │ │ ├── ExpoJudgingVote.ts │ │ │ │ ├── ExpoJudgingSession.ts │ │ │ │ ├── CriteriaJudgingSession.ts │ │ │ │ ├── Project.ts │ │ │ │ ├── index.ts │ │ │ │ ├── Event.ts │ │ │ │ └── Node.ts │ │ ├── config │ │ │ ├── project.ts │ │ │ ├── global.ts │ │ │ ├── index.ts │ │ │ └── auth.ts │ │ └── index.ts │ ├── tsconfig.build.json │ ├── .eslintrc │ ├── jest.config.js │ └── tsconfig.json ├── api │ ├── .npmrc │ ├── src │ │ ├── env │ │ │ ├── auth │ │ │ │ ├── index.ts │ │ │ │ ├── slackAuth.ts │ │ │ │ └── pingfedAuth.ts │ │ │ ├── index.ts │ │ │ └── env.ts │ │ ├── api │ │ │ ├── auth │ │ │ │ ├── utils │ │ │ │ │ ├── authUrlFormatters │ │ │ │ │ │ ├── types.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── formatPingfedAuthUrl.ts │ │ │ │ │ │ └── formatSlackAuthUrl.ts │ │ │ │ │ └── formatRedirectUri.ts │ │ │ │ ├── logout │ │ │ │ │ ├── index.ts │ │ │ │ │ └── get.ts │ │ │ │ ├── callback │ │ │ │ │ ├── pingfed │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── slack │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ └── get.ts │ │ │ ├── admin │ │ │ │ ├── me │ │ │ │ │ ├── index.ts │ │ │ │ │ └── get.ts │ │ │ │ └── index.ts │ │ │ ├── user │ │ │ │ ├── me │ │ │ │ │ ├── index.ts │ │ │ │ │ └── get.ts │ │ │ │ ├── index.ts │ │ │ │ └── put.ts │ │ │ ├── event │ │ │ │ ├── index.ts │ │ │ │ └── get.ts │ │ │ ├── health │ │ │ │ ├── index.ts │ │ │ │ └── get.ts │ │ │ ├── prize │ │ │ │ ├── index.ts │ │ │ │ └── get.ts │ │ │ ├── project │ │ │ │ ├── contributors │ │ │ │ │ └── index.ts │ │ │ │ ├── id │ │ │ │ │ ├── index.ts │ │ │ │ │ └── get.ts │ │ │ │ ├── get.ts │ │ │ │ └── index.ts │ │ │ ├── expoJudgingSession │ │ │ │ ├── id │ │ │ │ │ ├── skip │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── post.ts │ │ │ │ │ ├── projects │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── results │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── get.ts │ │ │ │ │ ├── continueSession │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── post.ts │ │ │ │ │ ├── get.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ └── get.ts │ │ │ ├── criteriaJudgingSession │ │ │ │ ├── id │ │ │ │ │ ├── results │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── projects │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── get.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── get.ts │ │ │ │ ├── index.ts │ │ │ │ └── get.ts │ │ │ ├── expoJudgingVote │ │ │ │ └── index.ts │ │ │ ├── criteriaJudgingSubmission │ │ │ │ └── index.ts │ │ │ ├── judge │ │ │ │ └── index.ts │ │ │ └── settings.ts │ │ ├── utils │ │ │ ├── database.ts │ │ │ └── logger.ts │ │ ├── @types │ │ │ └── index.d.ts │ │ └── middleware │ │ │ ├── sessionMiddleware.ts │ │ │ ├── adminMiddleware.ts │ │ │ ├── expoJudgeAccessMiddleware.ts │ │ │ └── judgeMiddleware.ts │ ├── tests │ │ ├── testUtils │ │ │ ├── expressHelpers │ │ │ │ ├── createMockNext.ts │ │ │ │ ├── createMockResponse.ts │ │ │ │ ├── createMockHandler.ts │ │ │ │ └── createMockRequest.ts │ │ │ ├── mockEnv.ts │ │ │ ├── getMock.ts │ │ │ ├── mockEnv.test.ts │ │ │ └── createMockEntityManager.ts │ │ ├── setupTests.ts │ │ └── api │ │ │ ├── user │ │ │ └── me │ │ │ │ ├── get.test.ts │ │ │ │ └── index.test.ts │ │ │ ├── admin │ │ │ └── me │ │ │ │ ├── get.test.ts │ │ │ │ └── index.test.ts │ │ │ ├── project │ │ │ ├── id │ │ │ │ └── index.test.ts │ │ │ ├── index.test.ts │ │ │ └── contributors │ │ │ │ └── index.test.ts │ │ │ ├── expoJudgingSession │ │ │ └── id │ │ │ │ ├── skip │ │ │ │ └── index.test.ts │ │ │ │ ├── projects │ │ │ │ └── index.test.ts │ │ │ │ ├── results │ │ │ │ └── index.test.ts │ │ │ │ └── continueSession │ │ │ │ └── index.test.ts │ │ │ ├── auth │ │ │ ├── logout │ │ │ │ ├── get.test.ts │ │ │ │ └── index.test.ts │ │ │ ├── index.test.ts │ │ │ └── callback │ │ │ │ └── slack │ │ │ │ └── index.test.ts │ │ │ ├── criteriaJudgingSession │ │ │ └── id │ │ │ │ ├── results │ │ │ │ └── index.test.ts │ │ │ │ └── projects │ │ │ │ └── index.test.ts │ │ │ ├── event │ │ │ └── index.test.ts │ │ │ ├── prizes │ │ │ └── index.test.ts │ │ │ └── health │ │ │ └── index.test.ts │ ├── tsconfig.build.json │ ├── .eslintrc │ ├── tsconfig.json │ ├── jest.config.js │ └── .env.sample ├── web │ ├── .npmrc │ ├── src │ │ ├── components │ │ │ ├── Prizes │ │ │ │ ├── index.ts │ │ │ │ ├── Prizes.tsx │ │ │ │ └── PrizeCard.tsx │ │ │ ├── EventsList │ │ │ │ ├── index.tsx │ │ │ │ ├── utils.tsx │ │ │ │ └── EventsList.tsx │ │ │ ├── Forms │ │ │ │ ├── index.ts │ │ │ │ └── FormHelperHint │ │ │ │ │ ├── index.ts │ │ │ │ │ └── FormHelperHint.tsx │ │ │ ├── Admin │ │ │ │ ├── index.ts │ │ │ │ ├── JudgingSessionList │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── JudgingSessionsOptionsButton │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── JudgingSessionOptionsButton.tsx │ │ │ │ │ └── JudgingSessionListItemSkeleton.tsx │ │ │ │ ├── ExpoJudgingSessionForm │ │ │ │ │ └── index.ts │ │ │ │ ├── CriteriaJudgingSessionForm │ │ │ │ │ ├── CriteriaList │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── CriteriaList.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── FormStyleContext.ts │ │ │ │ └── CriteriaJudgingSessionProjectResults │ │ │ │ │ ├── index.ts │ │ │ │ │ └── CriteriaJudgingSessionProjectResults.tsx │ │ │ ├── HintTooltip │ │ │ │ ├── index.tsx │ │ │ │ └── HintTooltip.tsx │ │ │ ├── ProjectCard │ │ │ │ └── index.tsx │ │ │ ├── layout │ │ │ │ ├── AppLayout │ │ │ │ │ ├── index.ts │ │ │ │ │ └── NavBar │ │ │ │ │ │ ├── NavElements │ │ │ │ │ │ ├── PageLinks │ │ │ │ │ │ │ ├── Prizes.tsx │ │ │ │ │ │ │ ├── Schedule.tsx │ │ │ │ │ │ │ ├── AdminDashboard.tsx │ │ │ │ │ │ │ └── MyProject.tsx │ │ │ │ │ │ └── AuthButtons │ │ │ │ │ │ │ ├── Logout.tsx │ │ │ │ │ │ │ ├── Login.tsx │ │ │ │ │ │ │ └── SignUp.tsx │ │ │ │ │ │ └── NavLogo.tsx │ │ │ │ ├── PageSpinner │ │ │ │ │ ├── index.ts │ │ │ │ │ └── PageSpinner.tsx │ │ │ │ ├── PageContainer │ │ │ │ │ ├── PageImage │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── PageImage.tsx │ │ │ │ │ ├── PageTitle │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── PageTitle.tsx │ │ │ │ │ ├── PageDescription │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── PageDescription.tsx │ │ │ │ │ └── index.ts │ │ │ │ └── RedirectToAuthModal │ │ │ │ │ ├── SlackContent │ │ │ │ │ └── index.ts │ │ │ │ │ ├── PingfedContent │ │ │ │ │ ├── index.ts │ │ │ │ │ └── PingfedContent.tsx │ │ │ │ │ └── index.ts │ │ │ ├── ErrorPageContent │ │ │ │ ├── index.ts │ │ │ │ └── catGifs.ts │ │ │ ├── JoinSlackButton │ │ │ │ ├── index.ts │ │ │ │ └── JoinSlackButton.tsx │ │ │ ├── ProjectsSelect │ │ │ │ ├── index.ts │ │ │ │ ├── ProjectsSelectStyleContext.ts │ │ │ │ └── fetchProjects.ts │ │ │ ├── utils │ │ │ │ └── CustomToast │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── defaultInfoToastProps.ts │ │ │ │ │ ├── defaultErrorToastProps.ts │ │ │ │ │ ├── defaultSuccessToastProps.ts │ │ │ │ │ └── CustomToast.tsx │ │ │ ├── ExpoJudging │ │ │ │ ├── ProjectCard │ │ │ │ │ └── index.ts │ │ │ │ ├── ExpoJudgingIntro │ │ │ │ │ └── index.ts │ │ │ │ ├── hooks │ │ │ │ │ └── useExpoJudging │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── skip.ts │ │ │ │ │ │ ├── continueSession.ts │ │ │ │ │ │ ├── vote.ts │ │ │ │ │ │ └── fetchProjects.ts │ │ │ │ ├── ProjectCardsContainer │ │ │ │ │ └── index.ts │ │ │ │ └── index.ts │ │ │ ├── CriteriaJudging │ │ │ │ ├── hooks │ │ │ │ │ ├── index.ts │ │ │ │ │ └── useCriteriaJudging │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── fetchProjects.ts │ │ │ │ ├── CriteriaJudgingForm │ │ │ │ │ ├── index.ts │ │ │ │ │ └── ProjectDetails.tsx │ │ │ │ ├── ProjectSelectionMenu │ │ │ │ │ └── index.ts │ │ │ │ └── index.ts │ │ │ ├── ProjectRegistrationCTA │ │ │ │ └── index.ts │ │ │ ├── ProjectRegistrationForm │ │ │ │ └── index.ts │ │ │ ├── ProjectRegistrationButton │ │ │ │ ├── index.ts │ │ │ │ └── CopyProjectInviteCode │ │ │ │ │ └── index.tsx │ │ │ ├── useProjectInviteCodeHandler │ │ │ │ └── index.ts │ │ │ └── Chakra.tsx │ │ ├── stores │ │ │ ├── expoJudgingSession │ │ │ │ ├── index.ts │ │ │ │ ├── fetchExpoJudgingSessions.ts │ │ │ │ ├── createExpoJudgingSession.ts │ │ │ │ └── expoJudgingSession.ts │ │ │ ├── criteriaJudgingSession │ │ │ │ ├── index.ts │ │ │ │ └── fetchCriteriaJudgingSessions.ts │ │ │ └── admin.ts │ │ ├── pageUtils │ │ │ ├── judgingSession │ │ │ │ ├── index.ts │ │ │ │ └── createOrUpdateJudge.ts │ │ │ ├── expoJudgingSession │ │ │ │ └── [id] │ │ │ │ │ └── fetchExpoJudgingSessionResults.ts │ │ │ └── admin │ │ │ │ └── criteriaJudgingSession │ │ │ │ └── [id] │ │ │ │ └── fetchCriteriaJudgingSessionResults.ts │ │ ├── theme │ │ │ ├── types │ │ │ │ └── Colors.ts │ │ │ ├── colors.ts │ │ │ └── index.ts │ │ ├── env.ts │ │ ├── pages │ │ │ ├── 404.tsx │ │ │ ├── admin │ │ │ │ ├── createExpoJudgingSession.tsx │ │ │ │ ├── createCriteriaJudgingSession.tsx │ │ │ │ └── dashboard.tsx │ │ │ ├── _app.tsx │ │ │ ├── error.tsx │ │ │ └── expoJudgingSession │ │ │ │ └── [id] │ │ │ │ └── index.tsx │ │ └── index.ts │ ├── public │ │ ├── SEM │ │ │ └── Logo.png │ │ ├── favicon.ico │ │ ├── apple-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon-96x96.png │ │ ├── ms-icon-70x70.png │ │ ├── ms-icon-144x144.png │ │ ├── ms-icon-150x150.png │ │ ├── ms-icon-310x310.png │ │ ├── android-icon-36x36.png │ │ ├── android-icon-48x48.png │ │ ├── android-icon-72x72.png │ │ ├── android-icon-96x96.png │ │ ├── apple-icon-114x114.png │ │ ├── apple-icon-120x120.png │ │ ├── apple-icon-144x144.png │ │ ├── apple-icon-152x152.png │ │ ├── apple-icon-180x180.png │ │ ├── apple-icon-57x57.png │ │ ├── apple-icon-60x60.png │ │ ├── apple-icon-72x72.png │ │ ├── apple-icon-76x76.png │ │ ├── apple-touch-icon.png │ │ ├── android-icon-144x144.png │ │ ├── android-icon-192x192.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-icon-precomposed.png │ │ └── manifest.json │ ├── next.config.js │ ├── next-env.d.ts │ ├── tsconfig.build.json │ ├── .gitignore │ ├── tsconfig.json │ └── .eslintrc └── database │ ├── .npmrc │ ├── tests │ ├── setupTests.ts │ └── entities │ │ └── Node.test.ts │ ├── src │ ├── entitiesUtils │ │ ├── Judge │ │ │ └── index.ts │ │ └── index.ts │ ├── config │ │ ├── index.ts │ │ └── api.orm.config.ts │ ├── index.ts │ ├── entities │ │ ├── ExpoJudgingSession.ts │ │ ├── index.ts │ │ ├── Admin.ts │ │ ├── Node.ts │ │ ├── Event.ts │ │ ├── CriteriaJudgingSession.ts │ │ ├── User.ts │ │ ├── CriteriaScore.ts │ │ └── JudgingSession.ts │ ├── migrations │ │ ├── 0005-add-repo-url-to-project-table.ts │ │ ├── 0014-drop-judge-count-column-from-project-table.ts │ │ ├── 0016-add-title-to-judgingsession-table.ts │ │ ├── 0013-changing-columntype-of-weight-to-decimal.ts │ │ ├── 0011-add-title-and-description-to-cjs.ts │ │ ├── 0001-add-email-to-user-table.ts │ │ ├── 0006-add-invite-code-to-project.ts │ │ ├── 0009-add-winner-to-prize.ts │ │ ├── 0015-remove-app-config.ts │ │ ├── 0003-add-the-admin-table.ts │ │ ├── 0012-adding-criteria-judging-session-judges-table.ts │ │ └── 0008-drops-judge_expojudgingsessioncontexts-table.ts │ ├── env.ts │ └── initDatabase.ts │ ├── seeds │ ├── utils │ │ ├── index.ts │ │ └── createSeededRandomGenerator.ts │ ├── types │ │ └── FakerEntity.ts │ ├── .env.sample │ ├── env.ts │ ├── factories │ │ ├── UserFactory.ts │ │ ├── PrizeFactory.ts │ │ ├── EventFactory.ts │ │ └── ProjectFactory.ts │ ├── seeders │ │ ├── PrizeSeeder.ts │ │ ├── AdminSeeder.ts │ │ ├── UserSeeder.ts │ │ ├── EventSeeder.ts │ │ └── ExpoJudgingSessionSeeder.ts │ └── DatabaseSeeder.ts │ ├── tsconfig.build.json │ ├── .eslintrc │ ├── jest.config.js │ └── tsconfig.json ├── docs ├── Logo.png ├── Dashboard.png └── Logo-Small.png ├── .prettierignore ├── .dockerignore ├── lerna.json ├── .editorconfig ├── .gitignore ├── THIRD-PARTY-NOTICES.txt ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── quality.yaml │ └── tests.yaml ├── scripts └── depsExist.js ├── .prettierrc ├── .env.docker.sample ├── codecov.yml ├── cspell.json ├── Dockerfile ├── LICENSE └── Authors.md /.npmrc: -------------------------------------------------------------------------------- 1 | "@hangar:registry"="none" 2 | -------------------------------------------------------------------------------- /packages/shared/tests/setupTests.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/api/.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org 2 | -------------------------------------------------------------------------------- /packages/shared/.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org 2 | -------------------------------------------------------------------------------- /packages/shared/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './wait'; 2 | -------------------------------------------------------------------------------- /packages/web/.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org 2 | -------------------------------------------------------------------------------- /packages/database/.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org 2 | -------------------------------------------------------------------------------- /packages/database/tests/setupTests.ts: -------------------------------------------------------------------------------- 1 | jest.mock('../src/env'); 2 | -------------------------------------------------------------------------------- /packages/shared/src/schema/user/index.ts: -------------------------------------------------------------------------------- 1 | export * from './put'; 2 | -------------------------------------------------------------------------------- /packages/web/src/components/Prizes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Prizes'; 2 | -------------------------------------------------------------------------------- /packages/shared/src/schema/criteria/index.ts: -------------------------------------------------------------------------------- 1 | export * from './criteria'; 2 | -------------------------------------------------------------------------------- /packages/shared/src/schema/expoJudgingSession/index.ts: -------------------------------------------------------------------------------- 1 | export * from './post'; 2 | -------------------------------------------------------------------------------- /packages/shared/src/schema/expoJudgingVote/index.ts: -------------------------------------------------------------------------------- 1 | export * from './post'; 2 | -------------------------------------------------------------------------------- /packages/shared/src/schema/project/contributors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './put'; 2 | -------------------------------------------------------------------------------- /packages/web/src/components/EventsList/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './EventsList'; 2 | -------------------------------------------------------------------------------- /packages/web/src/components/Forms/index.ts: -------------------------------------------------------------------------------- 1 | export * from './FormHelperHint'; 2 | -------------------------------------------------------------------------------- /docs/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/docs/Logo.png -------------------------------------------------------------------------------- /packages/database/src/entitiesUtils/Judge/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getNextProject'; 2 | -------------------------------------------------------------------------------- /packages/shared/src/schema/criteriaJudgingSession/index.ts: -------------------------------------------------------------------------------- 1 | export * from './post'; 2 | -------------------------------------------------------------------------------- /packages/shared/src/schema/criteriaScore/index.ts: -------------------------------------------------------------------------------- 1 | export * from './criteriaScore'; 2 | -------------------------------------------------------------------------------- /packages/web/src/components/Admin/index.ts: -------------------------------------------------------------------------------- 1 | export * from './JudgingSessionList'; 2 | -------------------------------------------------------------------------------- /packages/web/src/components/HintTooltip/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './HintTooltip'; 2 | -------------------------------------------------------------------------------- /packages/web/src/components/ProjectCard/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './ProjectCard'; 2 | -------------------------------------------------------------------------------- /packages/web/src/components/layout/AppLayout/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AppLayout'; 2 | -------------------------------------------------------------------------------- /packages/database/seeds/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './createSeededRandomGenerator'; 2 | -------------------------------------------------------------------------------- /packages/shared/src/schema/criteriaJudgingSubmission/index.ts: -------------------------------------------------------------------------------- 1 | export * from './post'; 2 | -------------------------------------------------------------------------------- /packages/web/src/components/ErrorPageContent/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ErrorPageContent'; 2 | -------------------------------------------------------------------------------- /packages/web/src/components/JoinSlackButton/index.ts: -------------------------------------------------------------------------------- 1 | export * from './JoinSlackButton'; 2 | -------------------------------------------------------------------------------- /packages/web/src/components/ProjectsSelect/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ProjectsSelect'; 2 | -------------------------------------------------------------------------------- /packages/web/src/components/layout/PageSpinner/index.ts: -------------------------------------------------------------------------------- 1 | export * from './PageSpinner'; 2 | -------------------------------------------------------------------------------- /packages/web/src/components/utils/CustomToast/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useCustomToast'; 2 | -------------------------------------------------------------------------------- /packages/web/src/stores/expoJudgingSession/index.ts: -------------------------------------------------------------------------------- 1 | export * from './expoJudgingSession'; 2 | -------------------------------------------------------------------------------- /docs/Dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/docs/Dashboard.png -------------------------------------------------------------------------------- /docs/Logo-Small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/docs/Logo-Small.png -------------------------------------------------------------------------------- /packages/shared/src/schema/judge/index.ts: -------------------------------------------------------------------------------- 1 | export * from './post'; 2 | export * from './put'; 3 | -------------------------------------------------------------------------------- /packages/shared/src/types/ApiError.ts: -------------------------------------------------------------------------------- 1 | export type ApiError = { 2 | message?: string; 3 | }; 4 | -------------------------------------------------------------------------------- /packages/web/src/components/ExpoJudging/ProjectCard/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ProjectCard'; 2 | -------------------------------------------------------------------------------- /packages/web/src/components/Forms/FormHelperHint/index.ts: -------------------------------------------------------------------------------- 1 | export * from './FormHelperHint'; 2 | -------------------------------------------------------------------------------- /packages/web/src/pageUtils/judgingSession/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useJudgingSessionFetcher'; 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next 2 | .gitkeep 3 | dist 4 | coverage 5 | .github/*_TEMPLATE* 6 | migrations 7 | -------------------------------------------------------------------------------- /packages/api/src/env/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from './pingfedAuth'; 2 | export * from './slackAuth'; 3 | -------------------------------------------------------------------------------- /packages/web/src/components/Admin/JudgingSessionList/index.ts: -------------------------------------------------------------------------------- 1 | export * from './JudgingSessionList'; 2 | -------------------------------------------------------------------------------- /packages/web/src/components/CriteriaJudging/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useCriteriaJudging'; 2 | -------------------------------------------------------------------------------- /packages/web/src/components/layout/PageContainer/PageImage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './PageImage'; 2 | -------------------------------------------------------------------------------- /packages/web/src/components/layout/PageContainer/PageTitle/index.ts: -------------------------------------------------------------------------------- 1 | export * from './PageTitle'; 2 | -------------------------------------------------------------------------------- /packages/web/src/stores/criteriaJudgingSession/index.ts: -------------------------------------------------------------------------------- 1 | export * from './criteriaJudgingSession'; 2 | -------------------------------------------------------------------------------- /packages/database/src/entitiesUtils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './scoreVotes'; 2 | export * from './Judge'; 3 | -------------------------------------------------------------------------------- /packages/web/src/components/ExpoJudging/ExpoJudgingIntro/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ExpoJudgingIntro'; 2 | -------------------------------------------------------------------------------- /packages/web/src/components/ExpoJudging/hooks/useExpoJudging/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useExpoJudging'; 2 | -------------------------------------------------------------------------------- /packages/web/src/components/ProjectRegistrationCTA/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ProjectRegistrationCTA'; 2 | -------------------------------------------------------------------------------- /packages/web/src/components/ProjectRegistrationForm/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ProjectRegistrationForm'; 2 | -------------------------------------------------------------------------------- /packages/database/src/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './orm.config'; 2 | export * from './api.orm.config'; 3 | -------------------------------------------------------------------------------- /packages/web/src/components/Admin/ExpoJudgingSessionForm/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ExpoJudgingSessionForm'; 2 | -------------------------------------------------------------------------------- /packages/web/src/components/ProjectRegistrationButton/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ProjectRegistrationButton'; 2 | -------------------------------------------------------------------------------- /packages/web/src/components/layout/PageContainer/PageDescription/index.ts: -------------------------------------------------------------------------------- 1 | export * from './PageDescription'; 2 | -------------------------------------------------------------------------------- /packages/web/src/components/layout/RedirectToAuthModal/SlackContent/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SlackContent'; 2 | -------------------------------------------------------------------------------- /packages/shared/src/types/utilities/Pretty.ts: -------------------------------------------------------------------------------- 1 | export type Pretty = { 2 | [T in keyof K]: K[T]; 3 | } & {}; 4 | -------------------------------------------------------------------------------- /packages/web/src/components/Admin/CriteriaJudgingSessionForm/CriteriaList/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CriteriaList'; 2 | -------------------------------------------------------------------------------- /packages/web/src/components/CriteriaJudging/CriteriaJudgingForm/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CriteriaJudgingForm'; 2 | -------------------------------------------------------------------------------- /packages/web/src/components/CriteriaJudging/ProjectSelectionMenu/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ProjectSelectionMenu'; 2 | -------------------------------------------------------------------------------- /packages/web/src/components/ExpoJudging/ProjectCardsContainer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ProjectCardsContainer'; 2 | -------------------------------------------------------------------------------- /packages/web/src/components/layout/RedirectToAuthModal/PingfedContent/index.ts: -------------------------------------------------------------------------------- 1 | export * from './PingfedContent'; 2 | -------------------------------------------------------------------------------- /packages/web/src/components/useProjectInviteCodeHandler/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useProjectInviteCodeHandler'; 2 | -------------------------------------------------------------------------------- /packages/web/public/SEM/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/packages/web/public/SEM/Logo.png -------------------------------------------------------------------------------- /packages/web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/packages/web/public/favicon.ico -------------------------------------------------------------------------------- /packages/web/src/components/Admin/CriteriaJudgingSessionForm/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CriteriaJudgingSessionForm'; 2 | -------------------------------------------------------------------------------- /packages/web/src/components/CriteriaJudging/hooks/useCriteriaJudging/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useCriteriaJudging'; 2 | -------------------------------------------------------------------------------- /packages/shared/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ApiError'; 2 | export * from './DTOs'; 3 | export * from './entities'; 4 | -------------------------------------------------------------------------------- /packages/web/public/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/packages/web/public/apple-icon.png -------------------------------------------------------------------------------- /packages/web/src/components/CriteriaJudging/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hooks'; 2 | export * from './ProjectSelectionMenu'; 3 | -------------------------------------------------------------------------------- /packages/web/src/components/layout/PageContainer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './PageContainer'; 2 | export * from './PageTitle'; 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .github 2 | **/.next 3 | **/*/dist 4 | **/*/coverage 5 | **/*/docs 6 | **/*/.env* 7 | .git 8 | **/*/node_modules 9 | -------------------------------------------------------------------------------- /packages/shared/src/config/project.ts: -------------------------------------------------------------------------------- 1 | export const project = { 2 | locationLabel: 'Location', // e.g., "Table Number" 3 | }; 4 | -------------------------------------------------------------------------------- /packages/web/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/packages/web/public/favicon-16x16.png -------------------------------------------------------------------------------- /packages/web/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/packages/web/public/favicon-32x32.png -------------------------------------------------------------------------------- /packages/web/public/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/packages/web/public/favicon-96x96.png -------------------------------------------------------------------------------- /packages/web/public/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/packages/web/public/ms-icon-70x70.png -------------------------------------------------------------------------------- /packages/web/src/components/ProjectRegistrationButton/CopyProjectInviteCode/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './CopyProjectInviteCode'; 2 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*"], 3 | "version": "3.0.0", 4 | "npmClient": "yarn", 5 | "useWorkspaces": true 6 | } 7 | -------------------------------------------------------------------------------- /packages/api/src/api/auth/utils/authUrlFormatters/types.ts: -------------------------------------------------------------------------------- 1 | export type AuthUrlFormatter = (args: { returnTo?: string }) => string; 2 | -------------------------------------------------------------------------------- /packages/shared/src/config/global.ts: -------------------------------------------------------------------------------- 1 | export const global = { 2 | appName: 'Hangar', 3 | authReturnUriParamName: 'returnTo', 4 | }; 5 | -------------------------------------------------------------------------------- /packages/web/public/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/packages/web/public/ms-icon-144x144.png -------------------------------------------------------------------------------- /packages/web/public/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/packages/web/public/ms-icon-150x150.png -------------------------------------------------------------------------------- /packages/web/public/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/packages/web/public/ms-icon-310x310.png -------------------------------------------------------------------------------- /packages/web/src/components/ExpoJudging/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ProjectCardsContainer'; 2 | export * from './hooks/useExpoJudging'; 3 | -------------------------------------------------------------------------------- /packages/shared/src/types/DTOs/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CriteriaJudgingSessionResults'; 2 | export * from './ExpoJudgingSessionProjects'; 3 | -------------------------------------------------------------------------------- /packages/web/public/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/packages/web/public/android-icon-36x36.png -------------------------------------------------------------------------------- /packages/web/public/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/packages/web/public/android-icon-48x48.png -------------------------------------------------------------------------------- /packages/web/public/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/packages/web/public/android-icon-72x72.png -------------------------------------------------------------------------------- /packages/web/public/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/packages/web/public/android-icon-96x96.png -------------------------------------------------------------------------------- /packages/web/public/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/packages/web/public/apple-icon-114x114.png -------------------------------------------------------------------------------- /packages/web/public/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/packages/web/public/apple-icon-120x120.png -------------------------------------------------------------------------------- /packages/web/public/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/packages/web/public/apple-icon-144x144.png -------------------------------------------------------------------------------- /packages/web/public/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/packages/web/public/apple-icon-152x152.png -------------------------------------------------------------------------------- /packages/web/public/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/packages/web/public/apple-icon-180x180.png -------------------------------------------------------------------------------- /packages/web/public/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/packages/web/public/apple-icon-57x57.png -------------------------------------------------------------------------------- /packages/web/public/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/packages/web/public/apple-icon-60x60.png -------------------------------------------------------------------------------- /packages/web/public/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/packages/web/public/apple-icon-72x72.png -------------------------------------------------------------------------------- /packages/web/public/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/packages/web/public/apple-icon-76x76.png -------------------------------------------------------------------------------- /packages/web/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/packages/web/public/apple-touch-icon.png -------------------------------------------------------------------------------- /packages/web/src/components/Admin/CriteriaJudgingSessionProjectResults/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CriteriaJudgingSessionProjectResults'; 2 | -------------------------------------------------------------------------------- /packages/web/src/components/Admin/JudgingSessionList/JudgingSessionsOptionsButton/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './JudgingSessionOptionsButton'; 2 | -------------------------------------------------------------------------------- /packages/web/public/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/packages/web/public/android-icon-144x144.png -------------------------------------------------------------------------------- /packages/web/public/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/packages/web/public/android-icon-192x192.png -------------------------------------------------------------------------------- /packages/web/src/components/layout/RedirectToAuthModal/index.ts: -------------------------------------------------------------------------------- 1 | export * from './RedirectToAuthModal'; 2 | export * from './useRedirectToAuth'; 3 | -------------------------------------------------------------------------------- /packages/web/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/packages/web/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /packages/web/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/packages/web/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /packages/web/public/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmericanAirlines/Hangar/HEAD/packages/web/public/apple-icon-precomposed.png -------------------------------------------------------------------------------- /packages/shared/src/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth'; 2 | export * from './global'; 3 | export * from './homepage'; 4 | export * from './project'; 5 | -------------------------------------------------------------------------------- /packages/shared/src/schema/judge/common.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const commonSchema = z.object({ 4 | inviteCode: z.string(), 5 | }); 6 | -------------------------------------------------------------------------------- /packages/database/src/index.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | export * from './entities'; 3 | export * from './config'; 4 | export * from './initDatabase'; 5 | -------------------------------------------------------------------------------- /packages/shared/src/schema/project/contributors/put.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const put = z.object({ 4 | inviteCode: z.string().uuid(), 5 | }); 6 | -------------------------------------------------------------------------------- /packages/api/src/api/admin/me/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { get } from './get'; 3 | 4 | export const me = Router(); 5 | 6 | me.get('', get); 7 | -------------------------------------------------------------------------------- /packages/api/src/api/user/me/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { get } from './get'; 3 | 4 | export const me = Router(); 5 | 6 | me.get('', get); 7 | -------------------------------------------------------------------------------- /packages/shared/src/schema/expoJudgingSession/post.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod'; 2 | 3 | export const post = z.object({ 4 | projectIds: z.array(z.string()).min(1), 5 | }); 6 | -------------------------------------------------------------------------------- /packages/api/src/api/event/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { get } from './get'; 3 | 4 | export const event = Router(); 5 | 6 | event.get('', get); 7 | -------------------------------------------------------------------------------- /packages/api/src/api/health/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { get } from './get'; 3 | 4 | export const health = Router(); 5 | 6 | health.get('', get); 7 | -------------------------------------------------------------------------------- /packages/api/src/api/prize/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { get } from './get'; 3 | 4 | export const prize = Router(); 5 | 6 | prize.get('', get); 7 | -------------------------------------------------------------------------------- /packages/shared/src/schema/project/index.ts: -------------------------------------------------------------------------------- 1 | export * from './post'; 2 | export * from './put'; 3 | export * from './core'; 4 | export * as contributors from './contributors'; 5 | -------------------------------------------------------------------------------- /packages/api/src/api/admin/me/get.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | 3 | export const get = (req: Request, res: Response) => { 4 | res.send(req.admin); 5 | }; 6 | -------------------------------------------------------------------------------- /packages/api/src/api/auth/logout/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { get } from './get'; 3 | 4 | export const logout = Router(); 5 | 6 | logout.get('', get); 7 | -------------------------------------------------------------------------------- /packages/api/src/api/user/me/get.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | 3 | export const get = (req: Request, res: Response) => { 4 | res.send(req.user); 5 | }; 6 | -------------------------------------------------------------------------------- /packages/shared/src/utils/wait.ts: -------------------------------------------------------------------------------- 1 | export const wait = async (milliseconds: number) => 2 | new Promise((resolve) => { 3 | setTimeout(resolve, milliseconds); 4 | }); 5 | -------------------------------------------------------------------------------- /packages/api/tests/testUtils/expressHelpers/createMockNext.ts: -------------------------------------------------------------------------------- 1 | import { Handler } from 'express'; 2 | 3 | export const createMockNext = () => jest.fn((req, res, next): Handler => next()); 4 | -------------------------------------------------------------------------------- /packages/api/src/api/auth/callback/pingfed/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { get } from './get'; 3 | 4 | export const pingfed = Router(); 5 | 6 | pingfed.get('', get); 7 | -------------------------------------------------------------------------------- /packages/api/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "incremental": true 6 | }, 7 | "include": ["src"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/shared/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "incremental": true 6 | }, 7 | "include": ["src"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/database/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "incremental": true 6 | }, 7 | "include": ["src"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/shared/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | describe('Shared test test', () => { 2 | it('basic test', async () => { 3 | const field = 'hello'; 4 | expect(field).toEqual('hello'); 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /packages/api/src/api/project/contributors/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { put } from './put'; 3 | 4 | export const contributors = Router(); 5 | 6 | contributors.put('', put); 7 | -------------------------------------------------------------------------------- /packages/shared/src/schema/expoJudgingVote/post.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod'; 2 | 3 | export const post = z.object({ 4 | currentProjectChosen: z.boolean(), 5 | expoJudgingSessionId: z.string(), 6 | }); 7 | -------------------------------------------------------------------------------- /packages/api/src/api/auth/logout/get.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | 3 | export const get = (req: Request, res: Response) => { 4 | req.session = null; 5 | res.redirect('/'); 6 | }; 7 | -------------------------------------------------------------------------------- /packages/api/src/env/index.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv-flow'; 2 | 3 | config(); // Must be called before exports 4 | 5 | /** 6 | * All core environment variables 7 | */ 8 | export * from './env'; 9 | -------------------------------------------------------------------------------- /packages/web/src/theme/types/Colors.ts: -------------------------------------------------------------------------------- 1 | import { colors } from '..'; 2 | 3 | export type Colors = typeof colors; 4 | export type ColorValue = Colors[keyof Colors] | Colors['status'][keyof Colors['status']]; 5 | -------------------------------------------------------------------------------- /packages/shared/src/schema/project/put.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { core } from './core'; 3 | 4 | export const put = core.merge( 5 | z.object({ 6 | // Add respective properties. 7 | }), 8 | ); 9 | -------------------------------------------------------------------------------- /packages/web/next.config.js: -------------------------------------------------------------------------------- 1 | /* cSpell:disable */ 2 | /** @type {import('next').NextConfig} */ 3 | module.exports = { 4 | reactStrictMode: true, 5 | eslint: { 6 | ignoreDuringBuilds: true, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /packages/api/src/api/expoJudgingSession/id/skip/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { post } from './post'; 3 | 4 | export const skip = Router({ mergeParams: true }); 5 | 6 | skip.post('', post); 7 | -------------------------------------------------------------------------------- /packages/shared/src/schema/project/post.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { core } from './core'; 3 | 4 | export const post = core.merge( 5 | z.object({ 6 | // Add respective properties. 7 | }), 8 | ); 9 | -------------------------------------------------------------------------------- /packages/api/src/api/criteriaJudgingSession/id/results/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { get } from './get'; 3 | 4 | export const results = Router({ mergeParams: true }); 5 | 6 | results.use('', get); 7 | -------------------------------------------------------------------------------- /packages/api/src/api/expoJudgingSession/id/projects/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { get } from './get'; 3 | 4 | export const projects = Router({ mergeParams: true }); 5 | 6 | projects.get('', get); 7 | -------------------------------------------------------------------------------- /packages/api/src/api/expoJudgingSession/id/results/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { get } from './get'; 3 | 4 | export const results = Router({ mergeParams: true }); 5 | 6 | results.get('', get); 7 | -------------------------------------------------------------------------------- /packages/api/tests/setupTests.ts: -------------------------------------------------------------------------------- 1 | import { mockEnv } from './testUtils/mockEnv'; 2 | 3 | jest.mock('../src/utils/logger'); 4 | jest.mock('../src/env'); 5 | 6 | // Reset the env between tests 7 | beforeEach(() => mockEnv()); 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | **/*/tsconfig.tsbuildinfo 4 | 5 | # dotenv environment variables file (only local) 6 | .env.local 7 | .env.docker 8 | 9 | # Log Files 10 | *.log 11 | 12 | coverage 13 | dist 14 | -------------------------------------------------------------------------------- /packages/api/src/api/criteriaJudgingSession/id/projects/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { get } from './get'; 3 | 4 | export const projects = Router({ mergeParams: true }); 5 | 6 | projects.get('', get); 7 | -------------------------------------------------------------------------------- /packages/shared/src/schema/judge/put.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { commonSchema } from './common'; 3 | 4 | export const put = commonSchema.merge( 5 | z.object({ 6 | // Add respective properties. 7 | }), 8 | ); 9 | -------------------------------------------------------------------------------- /packages/shared/src/schema/judge/post.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { commonSchema } from './common'; 3 | 4 | export const post = commonSchema.merge( 5 | z.object({ 6 | // Add respective properties. 7 | }), 8 | ); 9 | -------------------------------------------------------------------------------- /packages/shared/src/types/entities/Criteria.ts: -------------------------------------------------------------------------------- 1 | import { CriteriaDTO } from '@hangar/database'; 2 | 3 | // Intentionally omit dates; we won't need them in the UI 4 | export type Criteria = Omit; 5 | -------------------------------------------------------------------------------- /packages/api/tests/testUtils/expressHelpers/createMockResponse.ts: -------------------------------------------------------------------------------- 1 | export const createMockResponse = () => ({ 2 | send: jest.fn(), 3 | sendStatus: jest.fn(), 4 | status: jest.fn().mockReturnThis(), 5 | redirect: jest.fn(), 6 | }); 7 | -------------------------------------------------------------------------------- /packages/shared/src/types/entities/User.ts: -------------------------------------------------------------------------------- 1 | import { UserDTO } from '@hangar/database'; 2 | import { SerializedNode, Node } from './Node'; 3 | 4 | export type User = Node; 5 | export type SerializedUser = SerializedNode; 6 | -------------------------------------------------------------------------------- /packages/shared/src/types/entities/Admin.ts: -------------------------------------------------------------------------------- 1 | import { AdminDTO } from '@hangar/database'; 2 | import { Node, SerializedNode } from './Node'; 3 | 4 | export type Admin = Node; 5 | export type SerializedAdmin = SerializedNode; 6 | -------------------------------------------------------------------------------- /packages/shared/src/types/entities/Judge.ts: -------------------------------------------------------------------------------- 1 | import { JudgeDTO } from '@hangar/database'; 2 | import { Node, SerializedNode } from './Node'; 3 | 4 | export type Judge = Node; 5 | export type SerializedJudge = SerializedNode; 6 | -------------------------------------------------------------------------------- /packages/shared/src/types/entities/Prize.ts: -------------------------------------------------------------------------------- 1 | import { PrizeDTO } from '@hangar/database'; 2 | import { Node, SerializedNode } from './Node'; 3 | 4 | export type Prize = Node; 5 | export type SerializedPrize = SerializedNode; 6 | -------------------------------------------------------------------------------- /packages/web/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /packages/api/src/api/expoJudgingSession/id/continueSession/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { post } from './post'; 3 | 4 | export const continueSession = Router({ mergeParams: true }); 5 | 6 | continueSession.post('', post); 7 | -------------------------------------------------------------------------------- /packages/shared/src/types/entities/CriteriaScore.ts: -------------------------------------------------------------------------------- 1 | import { CriteriaScoreDTO } from '@hangar/database'; 2 | 3 | // Intentionally omit dates; we won't need them in the UI 4 | export type CriteriaScore = Omit; 5 | -------------------------------------------------------------------------------- /packages/api/src/api/auth/callback/slack/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { get } from './get'; 3 | import { localRedirect } from './utils/localRedirect'; 4 | 5 | export const slack = Router(); 6 | 7 | slack.get('', localRedirect, get); 8 | -------------------------------------------------------------------------------- /packages/shared/src/types/entities/CriteriaJudgingSubmission.ts: -------------------------------------------------------------------------------- 1 | import { CriteriaJudgingSubmissionDTO } from '@hangar/database'; 2 | 3 | export type CriteriaJudgingSubmission = Omit< 4 | CriteriaJudgingSubmissionDTO, 5 | 'createdAt' | 'updatedAt' 6 | >; 7 | -------------------------------------------------------------------------------- /packages/web/src/components/Admin/JudgingSessionList/JudgingSessionListItemSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from '@chakra-ui/react'; 2 | 3 | export const JudgingSessionListItemSkeleton: React.FC = () => ( 4 | 5 | ); 6 | -------------------------------------------------------------------------------- /THIRD-PARTY-NOTICES.txt: -------------------------------------------------------------------------------- 1 | With the exception of dependencies documented within package.json and code written by those listed in Authors.md, this project is entirely composed of custom code. If 3rd party code is utilized in the future, attribution will be added here. 2 | -------------------------------------------------------------------------------- /packages/api/tests/testUtils/expressHelpers/createMockHandler.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | 3 | export const createMockHandler = (status = 200) => 4 | jest.fn((req: Request, res: Response) => { 5 | res.sendStatus(status); 6 | }); 7 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description of Changes 2 | 3 | 4 | ### Related Issues 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/api/src/api/auth/utils/authUrlFormatters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './formatPingfedAuthUrl'; 2 | export * from './formatSlackAuthUrl'; 3 | 4 | // This directory houses the formatters that create URLs for redirection that triggers the initial step of the auth flow 5 | -------------------------------------------------------------------------------- /packages/database/seeds/types/FakerEntity.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity } from '@mikro-orm/core'; 2 | import { Node } from '../../src/entities/Node'; 3 | 4 | export type FakerEntity = Omit< 5 | Omit>, 6 | K 7 | >; 8 | -------------------------------------------------------------------------------- /packages/web/src/components/utils/CustomToast/defaultInfoToastProps.ts: -------------------------------------------------------------------------------- 1 | import { UseToastOptions } from '@chakra-ui/react'; 2 | 3 | export const defaultInfoToastProps: UseToastOptions = { 4 | status: 'info', 5 | position: 'top', 6 | duration: 5 * 1000, 7 | isClosable: true, 8 | }; 9 | -------------------------------------------------------------------------------- /packages/api/src/api/admin/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { me } from './me'; 3 | import { adminMiddleware } from '../../middleware/adminMiddleware'; 4 | 5 | export const admin = Router(); 6 | 7 | admin.use(adminMiddleware); 8 | 9 | admin.use('/me', me); 10 | -------------------------------------------------------------------------------- /packages/api/src/api/expoJudgingVote/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { post } from './post'; 3 | import { judgeMiddleware } from '../../middleware/judgeMiddleware'; 4 | 5 | export const expoJudgingVote = Router(); 6 | 7 | expoJudgingVote.post('', judgeMiddleware, post); 8 | -------------------------------------------------------------------------------- /packages/shared/src/schema/criteriaJudgingSubmission/post.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { criteriaScore } from '../criteriaScore'; 3 | 4 | export const post = z.object({ 5 | criteriaJudgingSession: z.string(), 6 | project: z.string(), 7 | scores: z.array(criteriaScore), 8 | }); 9 | -------------------------------------------------------------------------------- /packages/web/src/components/utils/CustomToast/defaultErrorToastProps.ts: -------------------------------------------------------------------------------- 1 | import { UseToastOptions } from '@chakra-ui/react'; 2 | 3 | export const defaultErrorToastProps: UseToastOptions = { 4 | status: 'error', 5 | position: 'top', 6 | duration: 30 * 1000, 7 | isClosable: true, 8 | }; 9 | -------------------------------------------------------------------------------- /packages/shared/src/types/entities/JudgingSession.ts: -------------------------------------------------------------------------------- 1 | import { JudgingSessionDTO } from '@hangar/database'; 2 | import { Node, SerializedNode } from './Node'; 3 | 4 | export type JudgingSession = Node; 5 | export type SerializedJudgingSession = SerializedNode; 6 | -------------------------------------------------------------------------------- /packages/web/src/components/utils/CustomToast/defaultSuccessToastProps.ts: -------------------------------------------------------------------------------- 1 | import { UseToastOptions } from '@chakra-ui/react'; 2 | 3 | export const defaultSuccessToastProps: UseToastOptions = { 4 | status: 'success', 5 | position: 'top', 6 | duration: 5 * 1000, 7 | isClosable: true, 8 | }; 9 | -------------------------------------------------------------------------------- /packages/shared/src/types/entities/ExpoJudgingVote.ts: -------------------------------------------------------------------------------- 1 | import { ExpoJudgingVoteDTO } from '@hangar/database'; 2 | import { Node, SerializedNode } from './Node'; 3 | 4 | export type ExpoJudgingVote = Node; 5 | export type SerializedExpoJudgingVote = SerializedNode; 6 | -------------------------------------------------------------------------------- /packages/web/src/components/layout/AppLayout/NavBar/NavElements/PageLinks/Prizes.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from '@chakra-ui/react'; 2 | import NextLink from 'next/link'; 3 | 4 | export const Prizes: React.FC = () => ( 5 | 6 | Prizes 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /packages/web/src/components/layout/AppLayout/NavBar/NavElements/PageLinks/Schedule.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from '@chakra-ui/react'; 2 | import NextLink from 'next/link'; 3 | 4 | export const Schedule: React.FC = () => ( 5 | 6 | Schedule 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /packages/database/src/entities/ExpoJudgingSession.ts: -------------------------------------------------------------------------------- 1 | import { Entity, EntityDTO } from '@mikro-orm/core'; 2 | import { JudgingSession } from './JudgingSession'; 3 | 4 | export type ExpoJudgingSessionDTO = EntityDTO; 5 | 6 | @Entity() 7 | export class ExpoJudgingSession extends JudgingSession {} 8 | -------------------------------------------------------------------------------- /packages/shared/src/types/entities/ExpoJudgingSession.ts: -------------------------------------------------------------------------------- 1 | import { ExpoJudgingSessionDTO } from '@hangar/database'; 2 | import { Node, SerializedNode } from './Node'; 3 | 4 | export type ExpoJudgingSession = Node; 5 | export type SerializedExpoJudgingSession = SerializedNode; 6 | -------------------------------------------------------------------------------- /packages/web/src/components/ExpoJudging/hooks/useExpoJudging/skip.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | type SkipArgs = { 4 | expoJudgingSessionId: string; 5 | }; 6 | 7 | export const skip = async ({ expoJudgingSessionId: id }: SkipArgs) => { 8 | await axios.post(`/api/expoJudgingSession/${id}/skip`); 9 | }; 10 | -------------------------------------------------------------------------------- /packages/api/src/api/auth/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { get } from './get'; 3 | import { callback } from './callback'; 4 | import { logout } from './logout'; 5 | 6 | export const auth = Router(); 7 | 8 | auth.get('/', get); 9 | auth.use('/callback', callback); 10 | auth.use('/logout', logout); 11 | -------------------------------------------------------------------------------- /packages/api/src/env/auth/slackAuth.ts: -------------------------------------------------------------------------------- 1 | import setEnv from '@americanairlines/simple-env'; 2 | 3 | export const slackAuth = setEnv({ 4 | required: { 5 | botToken: 'SLACK_BOT_TOKEN', 6 | signingSecret: 'SLACK_SIGNING_SECRET', 7 | clientId: 'SLACK_CLIENT_ID', 8 | clientSecret: 'SLACK_CLIENT_SECRET', 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /packages/database/seeds/utils/createSeededRandomGenerator.ts: -------------------------------------------------------------------------------- 1 | import seedrandom from 'seedrandom'; 2 | 3 | function createSeededRandomGenerator(seed: string | undefined) { 4 | const rng = seedrandom(seed); 5 | 6 | return function unnamed() { 7 | return rng(); 8 | }; 9 | } 10 | 11 | export { createSeededRandomGenerator }; 12 | -------------------------------------------------------------------------------- /packages/shared/src/types/entities/CriteriaJudgingSession.ts: -------------------------------------------------------------------------------- 1 | import { CriteriaJudgingSessionDTO } from '@hangar/database'; 2 | import { Node, SerializedNode } from './Node'; 3 | 4 | export type CriteriaJudgingSession = Node; 5 | export type SerializedCriteriaJudgingSession = SerializedNode; 6 | -------------------------------------------------------------------------------- /packages/web/src/components/layout/AppLayout/NavBar/NavElements/AuthButtons/Logout.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@chakra-ui/react'; 2 | 3 | export const Logout: React.FC = () => ( 4 | 11 | ); 12 | -------------------------------------------------------------------------------- /packages/web/src/components/layout/AppLayout/NavBar/NavElements/PageLinks/AdminDashboard.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from '@chakra-ui/react'; 2 | import NextLink from 'next/link'; 3 | 4 | export const AdminDashboard: React.FC = () => ( 5 | 6 | Admin Dashboard 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /packages/shared/src/types/entities/Project.ts: -------------------------------------------------------------------------------- 1 | import { ProjectDTO, ProjectDTOWithInviteCode } from '@hangar/database'; 2 | import { Node, SerializedNode } from './Node'; 3 | 4 | export type Project = Node; 5 | export type ProjectWithInviteCode = Node; 6 | export type SerializedProject = SerializedNode; 7 | -------------------------------------------------------------------------------- /packages/api/src/api/criteriaJudgingSubmission/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { post } from './post'; 3 | import { judgeMiddleware } from '../../middleware/judgeMiddleware'; 4 | 5 | export const criteriaJudgingSubmission = Router(); 6 | 7 | criteriaJudgingSubmission.use(judgeMiddleware); 8 | criteriaJudgingSubmission.post('', post); 9 | -------------------------------------------------------------------------------- /packages/api/src/api/project/id/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { get } from './get'; 3 | import { mountUserMiddleware } from '../../../middleware/mountUserMiddleware'; 4 | import { put } from './put'; 5 | 6 | export const id = Router({ mergeParams: true }); 7 | 8 | id.get('', get); 9 | id.put('', mountUserMiddleware, put); 10 | -------------------------------------------------------------------------------- /packages/api/src/api/user/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { put } from './put'; 3 | import { me } from './me'; 4 | import { mountUserMiddleware } from '../../middleware/mountUserMiddleware'; 5 | 6 | export const user = Router(); 7 | 8 | user.use(mountUserMiddleware); 9 | 10 | user.put('', put); 11 | user.use('/me', me); 12 | -------------------------------------------------------------------------------- /packages/api/tests/testUtils/mockEnv.ts: -------------------------------------------------------------------------------- 1 | import { env } from '../../src/env'; 2 | 3 | type Env = Partial>; 4 | 5 | const defaults: Env = { 6 | nodeEnv: 'test', 7 | sessionSecret: 'tacocat', 8 | }; 9 | 10 | export const mockEnv = (envData?: Env) => { 11 | (env as Env) = { ...defaults, ...envData }; 12 | }; 13 | -------------------------------------------------------------------------------- /scripts/depsExist.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); // eslint-disable-line 2 | 3 | const packagesRoot = `${__dirname}/../packages`; 4 | const depsPaths = [`${packagesRoot}/database/dist`, `${packagesRoot}/shared/dist`]; 5 | 6 | if (depsPaths.some((path) => !fs.existsSync(path))) { 7 | // Package has not been built; bail 8 | process.exit(1); 9 | } 10 | -------------------------------------------------------------------------------- /packages/api/src/env/auth/pingfedAuth.ts: -------------------------------------------------------------------------------- 1 | import setEnv from '@americanairlines/simple-env'; 2 | 3 | export const pingfedAuth = setEnv({ 4 | required: { 5 | clientId: 'PINGFED_CLIENT_ID', 6 | clientSecret: 'PINGFED_CLIENT_SECRET', 7 | authBaseUrl: 'PINGFED_AUTH_BASE_URL', 8 | tokenBaseUrl: 'PINGFED_TOKEN_BASE_URL', 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /packages/api/src/env/env.ts: -------------------------------------------------------------------------------- 1 | import setEnv from '@americanairlines/simple-env'; 2 | 3 | export const env = setEnv({ 4 | required: { 5 | nodeEnv: 'NODE_ENV', 6 | port: 'PORT', 7 | baseUrl: 'NEXT_PUBLIC_BASE_URL', 8 | sessionSecret: 'SESSION_SECRET', 9 | }, 10 | optional: { 11 | // Add optional env vars here 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /packages/web/src/components/layout/PageSpinner/PageSpinner.tsx: -------------------------------------------------------------------------------- 1 | import { Center, Spinner } from '@chakra-ui/react'; 2 | import { colors } from '../../../theme/colors'; 3 | 4 | export const PageSpinner: React.FC = () => ( 5 |
6 | 7 |
8 | ); 9 | -------------------------------------------------------------------------------- /packages/shared/tests/schema/judge/post.test.ts: -------------------------------------------------------------------------------- 1 | import { v4 } from 'uuid'; 2 | import { post } from '../../../src/schema/judge'; 3 | 4 | describe('post schema', () => { 5 | it('validates a new judge correctly', () => { 6 | expect( 7 | post.safeParse({ 8 | inviteCode: `${v4()}`, 9 | }).success, 10 | ).toBe(true); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/shared/tests/schema/judge/put.test.ts: -------------------------------------------------------------------------------- 1 | import { v4 } from 'uuid'; 2 | import { put } from '../../../src/schema/judge'; 3 | 4 | describe('put schema', () => { 5 | it('validates an updated judge correctly', () => { 6 | expect( 7 | put.safeParse({ 8 | inviteCode: `${v4()}`, 9 | }).success, 10 | ).toBe(true); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/shared/src/config/auth.ts: -------------------------------------------------------------------------------- 1 | type AuthMethod = 'slack' | 'pingfed'; // To support new methods, add them here 2 | type AuthData = { 3 | method: AuthMethod; 4 | }; 5 | 6 | /** 7 | * Configuration for authentication throughout the app 8 | */ 9 | export const Auth: AuthData = { 10 | method: 'pingfed', // Change this value to drive auth throughout the app 11 | }; 12 | -------------------------------------------------------------------------------- /packages/web/src/components/ExpoJudging/hooks/useExpoJudging/continueSession.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | type ContinueSessionArgs = { 4 | expoJudgingSessionId: string; 5 | }; 6 | 7 | export const continueSession = async ({ expoJudgingSessionId: id }: ContinueSessionArgs) => { 8 | await axios.post(`/api/expoJudgingSession/${id}/continueSession`); 9 | }; 10 | -------------------------------------------------------------------------------- /packages/web/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "incremental": true, 6 | "lib": ["esnext"], 7 | "module": "commonjs", 8 | "outDir": "dist", 9 | "declaration": true, 10 | "jsx": "react-jsxdev" 11 | }, 12 | "include": ["src/index.ts"], 13 | "exclude": ["node_modules"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/shared/src/index.ts: -------------------------------------------------------------------------------- 1 | export * as Config from './config'; 2 | export * as Schema from './schema'; 3 | export * from './types'; 4 | export * from './utils'; 5 | export type { 6 | AdminDTO, 7 | EventDTO, 8 | ExpoJudgingSessionDTO, 9 | ExpoJudgingVoteDTO, 10 | JudgeDTO, 11 | JudgingSessionDTO, 12 | ProjectDTO, 13 | PrizeDTO, 14 | UserDTO, 15 | } from '@hangar/database'; 16 | -------------------------------------------------------------------------------- /packages/web/src/components/layout/AppLayout/NavBar/NavElements/AuthButtons/Login.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@chakra-ui/react'; 2 | import { triggerRedirect } from '../../../../RedirectToAuthModal/useRedirectToAuth'; 3 | 4 | export const Login: React.FC = () => ( 5 | 12 | ); 13 | -------------------------------------------------------------------------------- /packages/shared/src/types/DTOs/ExpoJudgingSessionProjects.ts: -------------------------------------------------------------------------------- 1 | import { Project, SerializedProject } from '../entities'; 2 | 3 | export type SerializedExpoJudgingSessionProjects = { 4 | currentProject?: SerializedProject; 5 | previousProject?: SerializedProject; 6 | }; 7 | 8 | export type ExpoJudgingSessionProjects = { 9 | currentProject?: Project; 10 | previousProject?: Project; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/api/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint", "jest"], 4 | "extends": ["airbnb-base", "airbnb-typescript/base", "prettier", "../../.eslintrc"], 5 | "parserOptions": { 6 | "ecmaVersion": 2018, 7 | "project": "./tsconfig.json" 8 | }, 9 | "settings": { "import/resolver": { "node": { "extensions": [".js", ".ts"] } } } 10 | } 11 | -------------------------------------------------------------------------------- /packages/database/seeds/.env.sample: -------------------------------------------------------------------------------- 1 | # Add seeder override data here 2 | 3 | # Provide an email if a user should be auto-seeded for your login 4 | PRIMARY_USER_EMAIL="first.last@email.com" 5 | PRIMARY_USER_FIRST_NAME="John" 6 | PRIMARY_USER_LAST_NAME="Doe" 7 | 8 | # If an email is provided, should your user also be an admin and/or a judge? 9 | PRIMARY_USER_IS_ADMIN="true" 10 | PRIMARY_USER_IS_JUDGE="true" 11 | -------------------------------------------------------------------------------- /packages/shared/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint", "jest"], 4 | "extends": ["airbnb-base", "airbnb-typescript/base", "prettier", "../../.eslintrc"], 5 | "parserOptions": { 6 | "ecmaVersion": 2018, 7 | "project": "./tsconfig.json" 8 | }, 9 | "settings": { "import/resolver": { "node": { "extensions": [".js", ".ts"] } } } 10 | } 11 | -------------------------------------------------------------------------------- /packages/web/src/components/Admin/CriteriaJudgingSessionForm/FormStyleContext.ts: -------------------------------------------------------------------------------- 1 | import { InputProps, TextareaProps } from '@chakra-ui/react'; 2 | import { createContext } from 'react'; 3 | 4 | export type FormStyleContextValues = { 5 | inputStyleProps: InputProps & TextareaProps; 6 | }; 7 | 8 | export const FormStyleContext = createContext({ 9 | inputStyleProps: {}, 10 | }); 11 | -------------------------------------------------------------------------------- /packages/web/src/components/ExpoJudging/hooks/useExpoJudging/vote.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | type VoteArgs = { 4 | expoJudgingSessionId: string; 5 | currentProjectChosen: boolean; 6 | }; 7 | 8 | export const vote = async ({ expoJudgingSessionId, currentProjectChosen }: VoteArgs) => { 9 | await axios.post(`/api/expoJudgingVote`, { expoJudgingSessionId, currentProjectChosen }); 10 | }; 11 | -------------------------------------------------------------------------------- /packages/api/src/api/judge/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { post } from './post'; 3 | import { mountUserMiddleware } from '../../middleware/mountUserMiddleware'; 4 | import { judgeMiddleware } from '../../middleware/judgeMiddleware'; 5 | import { put } from './put'; 6 | 7 | export const judge = Router(); 8 | 9 | judge.post('', mountUserMiddleware, post); 10 | judge.put('', judgeMiddleware, put); 11 | -------------------------------------------------------------------------------- /packages/shared/src/schema/user/put.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const PutValidation = { 4 | MIN_NAME_LENGTH: 2, 5 | MAX_NAME_LENGTH: 50, 6 | }; 7 | 8 | const nameSchema = z 9 | .string() 10 | .trim() 11 | .min(PutValidation.MIN_NAME_LENGTH) 12 | .max(PutValidation.MAX_NAME_LENGTH); 13 | 14 | export const put = z.object({ 15 | firstName: nameSchema, 16 | lastName: nameSchema, 17 | }); 18 | -------------------------------------------------------------------------------- /packages/api/tests/testUtils/getMock.ts: -------------------------------------------------------------------------------- 1 | export const getMock = >( 2 | originalImplementation: (...args: Parameters) => Return, 3 | ): jest.Mock => { 4 | if (!jest.isMockFunction(originalImplementation)) { 5 | throw new Error(`${originalImplementation.name} is not a mock function`); 6 | } 7 | 8 | return originalImplementation as jest.Mock; 9 | }; 10 | -------------------------------------------------------------------------------- /packages/database/src/migrations/0005-add-repo-url-to-project-table.ts: -------------------------------------------------------------------------------- 1 | import { Migration } from '@mikro-orm/migrations'; 2 | 3 | export class Migration20230912194234 extends Migration { 4 | 5 | async up(): Promise { 6 | this.addSql('alter table "Project" add column "repoUrl" text not null;'); 7 | } 8 | 9 | async down(): Promise { 10 | this.addSql('alter table "Project" drop column "repoUrl";'); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /packages/database/tests/entities/Node.test.ts: -------------------------------------------------------------------------------- 1 | import { Node } from '../../src/entities/Node'; 2 | 3 | class TestNode extends Node { 4 | field?: string; 5 | } 6 | 7 | describe('Node', () => { 8 | it('sets extra fields to class that exist', async () => { 9 | const field = 'hello'; 10 | 11 | expect(new TestNode({ field }).field).toEqual(field); 12 | expect(new TestNode().field).toEqual(undefined); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /packages/web/src/components/HintTooltip/HintTooltip.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Tooltip } from '@chakra-ui/react'; 3 | import { colors } from '../../theme/colors'; 4 | 5 | export const HintTooltip: React.FC<{ children: string }> = ({ children }) => ( 6 | 7 | 8 | ⓘ 9 | 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /packages/web/src/env.ts: -------------------------------------------------------------------------------- 1 | // NOTE: All env vars must be prefixed with `NEXT_PUBLIC_` 2 | // NOTE: All web env vars must be present at BUILD TIME (see dockerfile) 3 | const baseUrl = process.env.NEXT_PUBLIC_BASE_URL; 4 | const slackWorkspaceName = process.env.NEXT_PUBLIC_SLACK_WORKSPACE_NAME; 5 | const slackInviteUrl = process.env.NEXT_PUBLIC_SLACK_INVITE_URL; 6 | 7 | export const env = { 8 | baseUrl, 9 | slackWorkspaceName, 10 | slackInviteUrl, 11 | }; 12 | -------------------------------------------------------------------------------- /packages/database/src/env.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | import setEnv from '@americanairlines/simple-env'; 3 | 4 | export const getEnv = () => 5 | setEnv({ 6 | required: { 7 | databaseUrl: 'DATABASE_URL', 8 | }, 9 | optional: { 10 | dbLoggingEnabled: 'DB_LOGGING_ENABLED', 11 | databaseUser: 'DATABASE_USER', 12 | databasePassword: 'DATABASE_PASS', 13 | disableDatabaseSSL: 'DISABLE_DATABASE_SSL', 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /packages/database/src/migrations/0014-drop-judge-count-column-from-project-table.ts: -------------------------------------------------------------------------------- 1 | import { Migration } from '@mikro-orm/migrations'; 2 | 3 | export class Migration20231128230604 extends Migration { 4 | 5 | async up(): Promise { 6 | this.addSql('alter table "Project" drop column "judgeVisits";'); 7 | } 8 | 9 | async down(): Promise { 10 | this.addSql('alter table "Project" add column "judgeVisits" int not null;'); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /packages/database/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint", "jest"], 4 | "extends": ["airbnb-base", "airbnb-typescript/base", "prettier", "../../.eslintrc"], 5 | "parserOptions": { 6 | "ecmaVersion": 2018, 7 | "project": "./tsconfig.json" 8 | }, 9 | "settings": { "import/resolver": { "node": { "extensions": [".js", ".ts"] } } }, 10 | "rules": { 11 | "import/no-cycle": ["off"] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/database/src/migrations/0016-add-title-to-judgingsession-table.ts: -------------------------------------------------------------------------------- 1 | import { Migration } from '@mikro-orm/migrations'; 2 | 3 | export class Migration20231218190523 extends Migration { 4 | 5 | async up(): Promise { 6 | this.addSql('alter table "ExpoJudgingSession" add column "title" text not null;'); 7 | } 8 | 9 | async down(): Promise { 10 | this.addSql('alter table "ExpoJudgingSession" drop column "title";'); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /packages/web/src/components/layout/PageContainer/PageImage/PageImage.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | 3 | interface PageImageProps { 4 | imagePath: string; 5 | } 6 | 7 | export const pageImageKey = 'image'; 8 | export const ogPageImageKey = 'ogImage'; 9 | 10 | export const PageImage: React.FC = ({ imagePath }) => ( 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /packages/database/src/config/api.orm.config.ts: -------------------------------------------------------------------------------- 1 | import { Options } from '@mikro-orm/core'; 2 | import { PostgreSqlDriver } from '@mikro-orm/postgresql'; 3 | import { getBaseConfig, migrations } from './orm.config'; 4 | 5 | /** 6 | * Configuration for the API that accesses the database 7 | */ 8 | export const getApiConfig = (): Options => ({ 9 | ...getBaseConfig(), 10 | pool: { 11 | min: 2, 12 | max: 10, 13 | }, 14 | migrations, 15 | }); 16 | -------------------------------------------------------------------------------- /packages/api/src/api/criteriaJudgingSession/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { post } from './post'; 3 | import { adminMiddleware } from '../../middleware/adminMiddleware'; 4 | import { get } from './get'; 5 | import { id } from './id'; 6 | 7 | export const criteriaJudgingSession = Router(); 8 | 9 | criteriaJudgingSession.use('/:id', id); 10 | criteriaJudgingSession.post('', adminMiddleware, post); 11 | criteriaJudgingSession.get('', adminMiddleware, get); 12 | -------------------------------------------------------------------------------- /packages/shared/tests/utils/wait.test.ts: -------------------------------------------------------------------------------- 1 | import { wait } from '../../src/utils/wait'; 2 | 3 | jest.useFakeTimers(); 4 | 5 | describe('wait function', () => { 6 | it('waits for specified time', async () => { 7 | const delay = 1000; 8 | const mockFunc = jest.fn(); 9 | 10 | const waitPromise = wait(delay).then(mockFunc); 11 | 12 | jest.advanceTimersByTime(delay); 13 | 14 | await waitPromise; 15 | 16 | expect(mockFunc).toHaveBeenCalled(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/shared/src/types/entities/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Admin'; 2 | export * from './Criteria'; 3 | export * from './CriteriaJudgingSession'; 4 | export * from './CriteriaJudgingSubmission'; 5 | export * from './CriteriaScore'; 6 | export * from './Event'; 7 | export * from './ExpoJudgingSession'; 8 | export * from './ExpoJudgingVote'; 9 | export * from './Judge'; 10 | export * from './JudgingSession'; 11 | export * from './Prize'; 12 | export * from './Project'; 13 | export * from './User'; 14 | -------------------------------------------------------------------------------- /packages/api/tests/api/user/me/get.test.ts: -------------------------------------------------------------------------------- 1 | import { get } from '../../../../src/api/user/me/get'; 2 | 3 | const mockRequest: any = { 4 | user: { id: '1' }, 5 | }; 6 | 7 | const mockResponse: any = { 8 | send: jest.fn(), 9 | }; 10 | 11 | describe('user me handler', () => { 12 | it('returns the user associated with the request', () => { 13 | // test 14 | get(mockRequest, mockResponse); 15 | // assert 16 | expect(mockResponse.send).toBeCalledWith(mockRequest.user); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/shared/src/types/DTOs/CriteriaJudgingSessionResults.ts: -------------------------------------------------------------------------------- 1 | import { Project, SerializedProject } from '../entities'; 2 | 3 | type CriteriaJudgingSessionResultsValues = { results: { score: number } }; 4 | 5 | export type SerializedCriteriaJudgingSessionResults = (SerializedProject & 6 | CriteriaJudgingSessionResultsValues)[]; 7 | 8 | export type CriteriaJudgingSessionResult = Project & CriteriaJudgingSessionResultsValues; 9 | export type CriteriaJudgingSessionResults = CriteriaJudgingSessionResult[]; 10 | -------------------------------------------------------------------------------- /packages/database/src/migrations/0013-changing-columntype-of-weight-to-decimal.ts: -------------------------------------------------------------------------------- 1 | import { Migration } from '@mikro-orm/migrations'; 2 | 3 | export class Migration20231026154301 extends Migration { 4 | 5 | async up(): Promise { 6 | this.addSql('alter table "Criteria" alter column "weight" type decimal using ("weight"::decimal);'); 7 | } 8 | 9 | async down(): Promise { 10 | this.addSql('alter table "Criteria" alter column "weight" type int using ("weight"::int);'); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /packages/shared/src/schema/index.ts: -------------------------------------------------------------------------------- 1 | export * as criteria from './criteria'; 2 | export * as criteriaJudgingSession from './criteriaJudgingSession'; 3 | export * as criteriaJudgingSubmission from './criteriaJudgingSubmission'; 4 | export * as criteriaScore from './criteriaScore'; 5 | export * as project from './project'; 6 | export * as user from './user'; 7 | export * as judge from './judge'; 8 | export * as expoJudgingSession from './expoJudgingSession'; 9 | export * as expoJudgingVote from './expoJudgingVote'; 10 | -------------------------------------------------------------------------------- /packages/api/src/api/project/get.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { Project } from '@hangar/database'; 3 | import { logger } from '../../utils/logger'; 4 | 5 | export const get = async (req: Request, res: Response) => { 6 | const { entityManager } = req; 7 | try { 8 | const projects = await entityManager.find(Project, {}); 9 | res.send(projects); 10 | } catch (error) { 11 | logger.error('Unable to fetch projects from DB: ', error); 12 | res.sendStatus(500); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "semi": true, 4 | "printWidth": 100, 5 | "singleQuote": true, 6 | "tabWidth": 2, 7 | "trailingComma": "all", 8 | "useTabs": false, 9 | "endOfLine": "lf", 10 | "overrides": [ 11 | { 12 | "files": "packages/database/src/migrations/*.ts", 13 | "options": { 14 | "printWidth": 99999 15 | } 16 | }, 17 | { 18 | "files": "**/*.md", 19 | "options": { 20 | "printWidth": 99999 21 | } 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /packages/api/src/api/auth/callback/index.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '@hangar/shared'; 2 | import { Router } from 'express'; 3 | import { slack } from './slack'; 4 | import { pingfed } from './pingfed'; 5 | 6 | export const callback = Router(); 7 | 8 | // Register the appropriate callback route based on the auth method 9 | const { method: authMethod } = Config.Auth; 10 | if (authMethod === 'slack') { 11 | callback.use('/slack', slack); 12 | } else if (authMethod === 'pingfed') { 13 | callback.use('/pingfed', pingfed); 14 | } 15 | -------------------------------------------------------------------------------- /packages/api/src/api/health/get.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | 3 | export const get = async (req: Request, res: Response) => { 4 | const db = await req.entityManager.getConnection().isConnected(); 5 | 6 | const dependencies: { [id: string]: boolean } = { db }; 7 | const ok = Object.values(dependencies).every((val) => !!val); 8 | 9 | res.status(ok ? 200 : 503).send({ 10 | ok, 11 | ...Object.assign({}, ...Object.entries(dependencies).map(([key, val]) => ({ [key]: val }))), 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /packages/api/src/api/expoJudgingSession/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { post } from './post'; 3 | import { get } from './get'; 4 | import { adminMiddleware } from '../../middleware/adminMiddleware'; 5 | import { id } from './id'; 6 | 7 | export const expoJudgingSession = Router(); 8 | 9 | // Self-managed auth 10 | expoJudgingSession.use('/:id', id); 11 | 12 | // admin auth required 13 | expoJudgingSession.use(adminMiddleware); 14 | expoJudgingSession.post('', post); 15 | expoJudgingSession.get('', get); 16 | -------------------------------------------------------------------------------- /packages/database/seeds/env.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | import { config } from 'dotenv-flow'; 3 | import setEnv from '@americanairlines/simple-env'; 4 | 5 | config({ path: __dirname }); 6 | 7 | export const env = setEnv({ 8 | optional: { 9 | primaryUserEmail: 'PRIMARY_USER_EMAIL', 10 | primaryUserFirstName: 'PRIMARY_USER_FIRST_NAME', 11 | primaryUserLastName: 'PRIMARY_USER_LAST_NAME', 12 | primaryUserIsAdmin: 'PRIMARY_USER_IS_ADMIN', 13 | primaryUserIsJudge: 'PRIMARY_USER_IS_JUDGE', 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /packages/shared/src/types/entities/Event.ts: -------------------------------------------------------------------------------- 1 | import { EventDTO } from '@hangar/database'; 2 | import { Dayjs } from 'dayjs'; 3 | import { Node, SerializedNode } from './Node'; 4 | import { Pretty } from '../utilities/Pretty'; 5 | 6 | export type Event = Pretty< 7 | Omit, 'start' | 'end'> & { 8 | start: Dayjs; 9 | end: Dayjs; 10 | } 11 | >; 12 | 13 | export type SerializedEvent = Pretty< 14 | Omit, 'start' | 'end'> & { 15 | start: string; 16 | end: string; 17 | } 18 | >; 19 | -------------------------------------------------------------------------------- /packages/web/src/components/ProjectsSelect/ProjectsSelectStyleContext.ts: -------------------------------------------------------------------------------- 1 | import { CheckboxProps, FlexProps } from '@chakra-ui/react'; 2 | import { createContext } from 'react'; 3 | 4 | type ProjectsSelectStyleContextArgs = { 5 | rowPadding: FlexProps['px']; 6 | checkboxSize: CheckboxProps['size']; 7 | checkboxPadding: FlexProps['px']; 8 | }; 9 | 10 | export const ProjectsSelectStyleContext = createContext({ 11 | rowPadding: 0, 12 | checkboxSize: 'md', 13 | checkboxPadding: 0, 14 | }); 15 | -------------------------------------------------------------------------------- /packages/web/src/components/layout/AppLayout/NavBar/NavElements/PageLinks/MyProject.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from '@chakra-ui/react'; 2 | import NextLink from 'next/link'; 3 | import { useUserStore } from '../../../../../../stores/user'; 4 | 5 | export const MyProject: React.FC = () => { 6 | const { user } = useUserStore(); 7 | 8 | if (!user?.project) return null; 9 | 10 | return ( 11 | 12 | My Project 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/api/src/utils/database.ts: -------------------------------------------------------------------------------- 1 | import { getApiConfig, initDatabase } from '@hangar/database'; 2 | import { env } from '../env'; 3 | 4 | let orm: Awaited> | undefined; 5 | 6 | export const initDb = async () => { 7 | orm = await initDatabase({ 8 | migrate: env.nodeEnv === 'production', 9 | config: getApiConfig(), 10 | }); 11 | 12 | return orm; 13 | }; 14 | 15 | export const getDbConnection = () => { 16 | if (!orm) throw new Error('DB connection not initialized'); 17 | 18 | return orm; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/database/seeds/factories/UserFactory.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | import { faker } from '@faker-js/faker'; 3 | import { Factory } from '@mikro-orm/seeder'; 4 | import { FakerEntity } from '../types/FakerEntity'; 5 | import { User } from '../../src'; 6 | 7 | export class UserFactory extends Factory { 8 | model = User; 9 | 10 | definition = (): FakerEntity => ({ 11 | firstName: faker.name.firstName(), 12 | lastName: faker.name.lastName(), 13 | email: faker.internet.email(), 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /packages/web/src/components/ErrorPageContent/catGifs.ts: -------------------------------------------------------------------------------- 1 | export const catGifs: string[] = [ 2 | 'https://media3.giphy.com/media/mlvseq9yvZhba/giphy.gif', 3 | 'https://media3.giphy.com/media/13CoXDiaCcCoyk/giphy.gif', 4 | 'https://media1.giphy.com/media/VbnUQpnihPSIgIXuZv/giphy.gif', 5 | 'https://media0.giphy.com/media/lJNoBCvQYp7nq/giphy.gif', 6 | 'https://media4.giphy.com/media/xTiQygY6HW1GjoYKFq/giphy.gif', 7 | 'https://media3.giphy.com/media/bPWyTsy2huZji/giphy.gif', 8 | 'https://media2.giphy.com/media/12bjQ7uASAaCKk/giphy.gif', 9 | ]; 10 | -------------------------------------------------------------------------------- /packages/database/seeds/factories/PrizeFactory.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | import { faker } from '@faker-js/faker'; 3 | import { Factory } from '@mikro-orm/seeder'; 4 | import { FakerEntity } from '../types/FakerEntity'; 5 | import { Prize } from '../../src'; 6 | 7 | export class PrizeFactory extends Factory { 8 | model = Prize; 9 | 10 | definition = (): FakerEntity => ({ 11 | name: faker.lorem.words(), 12 | description: faker.lorem.paragraph(), 13 | position: 0, 14 | isBonus: false, 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /packages/database/seeds/factories/EventFactory.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | import { faker } from '@faker-js/faker'; 3 | import { Factory } from '@mikro-orm/seeder'; 4 | import { FakerEntity } from '../types/FakerEntity'; 5 | import { Event } from '../../src'; 6 | 7 | export class EventFactory extends Factory { 8 | model = Event; 9 | 10 | definition = (): FakerEntity => ({ 11 | name: faker.lorem.words(), 12 | description: faker.lorem.paragraph(), 13 | start: new Date(), 14 | end: new Date(), 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /packages/shared/tests/schema/criteriaJudgingSubmission/criteriaJudgingSubmission.test.ts: -------------------------------------------------------------------------------- 1 | import { post } from '../../../src/schema/criteriaJudgingSubmission'; 2 | 3 | describe('criteriaJudgingSubmission POST schema', () => { 4 | it('validates a valid object', () => { 5 | const result = post.safeParse({ 6 | project: '123', 7 | criteriaJudgingSession: '456', 8 | scores: [ 9 | { 10 | score: 2, 11 | criteria: '789', 12 | }, 13 | ], 14 | }); 15 | expect(result.success).toBe(true); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | /dist/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | -------------------------------------------------------------------------------- /packages/web/src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { VStack } from '@chakra-ui/react'; 2 | import { NextPage } from 'next'; 3 | import React from 'react'; 4 | import { ErrorPageContent } from '../components/ErrorPageContent'; 5 | import { PageContainer } from '../components/layout/PageContainer'; 6 | 7 | const FourOhFour: NextPage = () => ( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | 15 | export default FourOhFour; 16 | -------------------------------------------------------------------------------- /packages/shared/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | moduleFileExtensions: ['js', 'json', 'ts'], 5 | testMatch: ['**/*.test.ts'], 6 | setupFilesAfterEnv: ['./tests/setupTests.ts'], 7 | clearMocks: true, 8 | testPathIgnorePatterns: ['/node_modules/'], 9 | coverageDirectory: './coverage', 10 | collectCoverageFrom: ['./src/**/*.ts'], 11 | coverageThreshold: { 12 | global: { 13 | statements: 100, 14 | branches: 100, 15 | functions: 100, 16 | lines: 100, 17 | }, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /packages/web/src/components/utils/CustomToast/CustomToast.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useToast } from '@chakra-ui/react'; 3 | import { useCustomToast } from './useCustomToast'; 4 | 5 | export const CustomToast: React.FC = () => { 6 | const { toastValues } = useCustomToast(); 7 | const showToast = useToast(); 8 | 9 | React.useEffect(() => { 10 | // Whenever the value of `toastValues` changes, reopen the toast 11 | if (toastValues) { 12 | showToast(toastValues); 13 | } 14 | }, [showToast, toastValues]); 15 | 16 | return null; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/api/src/api/project/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { post } from './post'; 3 | import { mountUserMiddleware } from '../../middleware/mountUserMiddleware'; 4 | import { contributors } from './contributors'; 5 | import { id } from './id'; 6 | import { get } from './get'; 7 | 8 | export const project = Router(); 9 | 10 | project.post('', mountUserMiddleware, post); 11 | project.use('/contributors', mountUserMiddleware, contributors); 12 | project.get('', get); 13 | 14 | // this route must be registered last to prevent collisions 15 | project.use('/:id', id); 16 | -------------------------------------------------------------------------------- /packages/database/src/entities/index.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | export * from './Admin'; 3 | export * from './Criteria'; 4 | export * from './CriteriaJudgingSession'; 5 | export * from './CriteriaJudgingSubmission'; 6 | export * from './CriteriaScore'; 7 | export * from './Event'; 8 | export * from './ExpoJudgingSession'; 9 | export * from './ExpoJudgingSessionContext'; 10 | export * from './ExpoJudgingVote'; 11 | export * from './Judge'; 12 | export { JudgingSessionDTO } from './JudgingSession'; 13 | export * from './Prize'; 14 | export * from './Project'; 15 | export * from './User'; 16 | -------------------------------------------------------------------------------- /packages/web/src/components/layout/PageContainer/PageDescription/PageDescription.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | 3 | interface PageDescriptionProps { 4 | description: string; 5 | } 6 | 7 | export const pageDescriptionKey = 'description'; 8 | export const ogPageDescriptionKey = 'ogDescription'; 9 | 10 | export const PageDescription: React.FC = ({ description }) => ( 11 | 12 | 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /packages/web/src/components/layout/PageContainer/PageTitle/PageTitle.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import { Config } from '@hangar/shared'; 3 | 4 | interface PageTitleProps { 5 | title: string; 6 | } 7 | 8 | export const pageTitleKey = 'title'; 9 | export const ogPageTitleKey = 'ogTitle'; 10 | export const defaultPageTitle = Config.global.appName; 11 | 12 | export const PageTitle: React.FC = ({ title }) => ( 13 | 14 | {title} 15 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /packages/api/src/api/event/get.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '@hangar/database'; 2 | import { QueryOrder } from '@mikro-orm/core'; 3 | import { Request, Response } from 'express'; 4 | import { logger } from '../../utils/logger'; 5 | 6 | export const get = async (req: Request, res: Response) => { 7 | const { entityManager } = req; 8 | try { 9 | const events = await entityManager.find(Event, {}, { orderBy: { start: QueryOrder.ASC } }); 10 | res.send(events); 11 | } catch (error) { 12 | logger.error('Unable to fetch events from DB: ', error); 13 | res.sendStatus(500); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /packages/api/src/api/prize/get.ts: -------------------------------------------------------------------------------- 1 | import { Prize } from '@hangar/database'; 2 | import { QueryOrder } from '@mikro-orm/core'; 3 | import { Request, Response } from 'express'; 4 | import { logger } from '../../utils/logger'; 5 | 6 | export const get = async (req: Request, res: Response) => { 7 | const { entityManager } = req; 8 | try { 9 | const prizes = await entityManager.find(Prize, {}, { orderBy: { position: QueryOrder.ASC } }); 10 | res.send(prizes); 11 | } catch (error) { 12 | logger.error('Unable to fetch prizes from DB: ', error); 13 | res.sendStatus(500); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /packages/database/src/migrations/0011-add-title-and-description-to-cjs.ts: -------------------------------------------------------------------------------- 1 | import { Migration } from '@mikro-orm/migrations'; 2 | 3 | export class Migration20231020214310 extends Migration { 4 | 5 | async up(): Promise { 6 | this.addSql('alter table "CriteriaJudgingSession" add column "title" text not null, add column "description" text not null;'); 7 | } 8 | 9 | async down(): Promise { 10 | this.addSql('alter table "CriteriaJudgingSession" drop column "title";'); 11 | this.addSql('alter table "CriteriaJudgingSession" drop column "description";'); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /packages/database/seeds/seeders/PrizeSeeder.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | import { EntityManager } from '@mikro-orm/core'; 3 | import { Seeder } from '@mikro-orm/seeder'; 4 | import { PrizeFactory } from '../factories/PrizeFactory'; 5 | 6 | const prizesToMake = 5; 7 | 8 | export class PrizeSeeder extends Seeder { 9 | run = async (em: EntityManager): Promise => { 10 | const prizeFactory = new PrizeFactory(em); 11 | 12 | for (let i = 0; i < prizesToMake; i += 1) { 13 | em.persist(prizeFactory.makeOne({ position: i, isBonus: i > 2 })); 14 | } 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /packages/database/src/migrations/0001-add-email-to-user-table.ts: -------------------------------------------------------------------------------- 1 | import { Migration } from '@mikro-orm/migrations'; 2 | 3 | export class Migration20230808232125 extends Migration { 4 | 5 | async up(): Promise { 6 | this.addSql('alter table "User" add column "email" text not null;'); 7 | this.addSql('alter table "User" add constraint "User_email_unique" unique ("email");'); 8 | } 9 | 10 | async down(): Promise { 11 | this.addSql('alter table "User" drop constraint "User_email_unique";'); 12 | this.addSql('alter table "User" drop column "email";'); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /packages/shared/tests/schema/expoJudgingSession/post.test.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from '../../../src'; 2 | 3 | describe('expoJudgingSession post schema', () => { 4 | it('validates expoJudgingSession with array of projectIds correctly', () => { 5 | expect(Schema.expoJudgingSession.post.safeParse({ projectIds: ['1', '2'] }).success).toBe(true); 6 | }); 7 | 8 | it('invalidates an empty array and a missing projectIds array', () => { 9 | expect(Schema.expoJudgingSession.post.safeParse({ projectIds: [] }).success).toBe(false); 10 | expect(Schema.expoJudgingSession.post.safeParse({}).success).toBe(false); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/web/src/pages/admin/createExpoJudgingSession.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NextPage } from 'next'; 3 | import { PageContainer } from '../../components/layout/PageContainer'; 4 | import { ExpoJudgingSessionForm } from '../../components/Admin/ExpoJudgingSessionForm'; 5 | 6 | const CreateExpoJudgingSession: NextPage = () => { 7 | React.useEffect(() => {}, []); 8 | 9 | return ( 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default CreateExpoJudgingSession; 17 | -------------------------------------------------------------------------------- /packages/database/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | moduleFileExtensions: ['js', 'json', 'ts'], 5 | testMatch: ['**/*.test.ts'], 6 | setupFilesAfterEnv: ['./tests/setupTests.ts'], 7 | clearMocks: true, 8 | testPathIgnorePatterns: ['/node_modules/'], 9 | coverageDirectory: './coverage', 10 | collectCoverageFrom: ['./src/**/*.ts', '!./src/seeds', '!./src/migrations'], 11 | // coverageThreshold: { 12 | // global: { 13 | // statements: 100, 14 | // branches: 100, 15 | // functions: 100, 16 | // lines: 100, 17 | // }, 18 | // }, 19 | }; 20 | -------------------------------------------------------------------------------- /packages/database/src/entities/Admin.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Ref, EntityDTO, OneToOne } from '@mikro-orm/core'; 2 | import { ConstructorValues } from '../types/ConstructorValues'; 3 | import { Node } from './Node'; 4 | import { User } from './User'; 5 | 6 | export type AdminDTO = EntityDTO; 7 | 8 | export type AdminConstructorValues = ConstructorValues; 9 | 10 | @Entity() 11 | export class Admin extends Node { 12 | @OneToOne({ entity: () => User, nullable: false, ref: true }) 13 | user: Ref; 14 | 15 | constructor({ user }: AdminConstructorValues) { 16 | super(); 17 | 18 | this.user = user; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.env.docker.sample: -------------------------------------------------------------------------------- 1 | # Docker Secrets for local use 2 | # All overrides used for Docker that may differ from /packages/api/.env.local 3 | 4 | NODE_ENV=production 5 | PORT=8080 6 | DATABASE_URL=postgresql://host.docker.internal:5432/hangar 7 | LOG_LEVEL=debug 8 | 9 | # Database (Required if not set in the parent env file) 10 | # Can be found by running "SELECT rolname FROM pg_roles;" 11 | # macOS: User/pass defaults to your macOS Username/nothing 12 | # DATABASE_PASS= 13 | # DATABASE_USER= 14 | 15 | # Additional overrides 16 | # Add relevant values here if you do not want them to be set for your local app but they are required for the Docker instance 17 | -------------------------------------------------------------------------------- /packages/database/src/migrations/0006-add-invite-code-to-project.ts: -------------------------------------------------------------------------------- 1 | import { Migration } from '@mikro-orm/migrations'; 2 | 3 | export class Migration20230926210155 extends Migration { 4 | 5 | async up(): Promise { 6 | this.addSql('alter table "Project" add column "inviteCode" text not null;'); 7 | this.addSql('alter table "Project" add constraint "Project_inviteCode_unique" unique ("inviteCode");'); 8 | } 9 | 10 | async down(): Promise { 11 | this.addSql('alter table "Project" drop constraint "Project_inviteCode_unique";'); 12 | this.addSql('alter table "Project" drop column "inviteCode";'); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /packages/web/src/components/layout/AppLayout/NavBar/NavLogo.tsx: -------------------------------------------------------------------------------- 1 | import { Heading } from '@chakra-ui/react'; 2 | import { Config } from '@hangar/shared'; 3 | import NextLink from 'next/link'; 4 | 5 | const LOGO_HEIGHT = { base: '24px', sm: '28px', md: '40px' }; 6 | 7 | export const NavLogo: React.FC = () => ( 8 | 9 | 19 | {Config.global.appName} 20 | 21 | 22 | ); 23 | -------------------------------------------------------------------------------- /packages/api/src/@types/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { EntityManager, PostgreSqlDriver } from '@mikro-orm/postgresql'; 2 | import { User, Admin, Judge } from '@hangar/database'; 3 | 4 | type Session = { 5 | id?: string; 6 | }; 7 | 8 | declare global { 9 | namespace Express { 10 | export interface Request { 11 | session: Session | null; 12 | entityManager: EntityManager; 13 | /** 14 | * These will only be populated if the corresponding middleware populates these values first 15 | */ 16 | user: User; 17 | admin: Admin; 18 | judge: Judge; 19 | loggerSuffix: string; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/web/src/components/layout/AppLayout/NavBar/NavElements/AuthButtons/SignUp.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@chakra-ui/react'; 2 | import { colors } from '../../../../../../theme'; 3 | import { triggerRedirect } from '../../../../RedirectToAuthModal/useRedirectToAuth'; 4 | 5 | type SignUpProps = { 6 | mentionLogin?: boolean; 7 | }; 8 | 9 | export const SignUp: React.FC = ({ mentionLogin }) => ( 10 | 19 | ); 20 | -------------------------------------------------------------------------------- /packages/web/src/components/ProjectsSelect/fetchProjects.ts: -------------------------------------------------------------------------------- 1 | import { Project } from '@hangar/shared'; 2 | import axios, { isAxiosError } from 'axios'; 3 | import { useCustomToast } from '../utils/CustomToast'; 4 | 5 | export const fetchProjects = async (): Promise => { 6 | try { 7 | const res = await axios.get('/api/project'); 8 | return res.data; 9 | } catch (error) { 10 | const description = isAxiosError(error) 11 | ? error.response?.data?.message 12 | : (error as Error).message; 13 | useCustomToast.getState().openErrorToast({ title: 'Failed to fetch projects', description }); 14 | return []; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /packages/api/tests/testUtils/expressHelpers/createMockRequest.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | import { User } from '@hangar/database'; 3 | import { createMockEntityManager } from '../createMockEntityManager'; 4 | 5 | type MockRequest = jest.Mocked< 6 | Partial> & { 7 | entityManager: ReturnType; 8 | } 9 | >; 10 | 11 | export const createMockRequest = (defaults?: Partial) => ({ 12 | entityManager: createMockEntityManager(), 13 | loggerSuffix: '', 14 | user: undefined as unknown as User, 15 | body: undefined, 16 | query: {}, 17 | params: {}, 18 | ...defaults, 19 | }); 20 | -------------------------------------------------------------------------------- /packages/shared/src/schema/criteriaScore/criteriaScore.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const criteriaScore = z.object({ 4 | criteria: z.string(), 5 | score: z 6 | .number() 7 | .or(z.string()) 8 | .transform((value, ctx) => { 9 | if (typeof value === 'number') return Math.floor(value); 10 | 11 | const int = Number.parseInt(value, 10); 12 | 13 | if (value.trim() === '' || Number.isNaN(int)) { 14 | ctx.addIssue({ 15 | code: z.ZodIssueCode.custom, 16 | message: 'Must be an integer value', 17 | }); 18 | return z.NEVER; 19 | } 20 | 21 | return int; 22 | }), 23 | }); 24 | -------------------------------------------------------------------------------- /packages/database/src/migrations/0009-add-winner-to-prize.ts: -------------------------------------------------------------------------------- 1 | import { Migration } from '@mikro-orm/migrations'; 2 | 3 | export class Migration20231013152153 extends Migration { 4 | 5 | async up(): Promise { 6 | this.addSql('alter table "Prize" add column "winner" bigint null;'); 7 | this.addSql('alter table "Prize" add constraint "Prize_winner_foreign" foreign key ("winner") references "Project" ("id") on update cascade on delete set null;'); 8 | } 9 | 10 | async down(): Promise { 11 | this.addSql('alter table "Prize" drop constraint "Prize_winner_foreign";'); 12 | 13 | this.addSql('alter table "Prize" drop column "winner";'); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /packages/api/src/api/project/id/get.ts: -------------------------------------------------------------------------------- 1 | import { Project } from '@hangar/database'; 2 | import { Request, Response } from 'express'; 3 | import { logger } from '../../../utils/logger'; 4 | 5 | export const get = async (req: Request, res: Response) => { 6 | const { 7 | entityManager, 8 | params: { id: projectId }, 9 | } = req; 10 | try { 11 | const project = await entityManager.findOne(Project, { id: projectId as string }); 12 | if (!project) { 13 | res.sendStatus(404); 14 | return; 15 | } 16 | res.send(project); 17 | } catch (error) { 18 | logger.error('Unable to fetch project details', error); 19 | res.sendStatus(500); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /packages/database/src/migrations/0015-remove-app-config.ts: -------------------------------------------------------------------------------- 1 | import { Migration } from '@mikro-orm/migrations'; 2 | 3 | export class Migration20231215144720 extends Migration { 4 | 5 | async up(): Promise { 6 | this.addSql('drop table if exists "AppConfig" cascade;'); 7 | } 8 | 9 | async down(): Promise { 10 | this.addSql('create table "AppConfig" ("id" bigserial primary key, "createdAt" timestamptz not null default clock_timestamp(), "updatedAt" timestamptz not null default clock_timestamp(), "key" text not null, "value" jsonb null);'); 11 | this.addSql('alter table "AppConfig" add constraint "AppConfig_key_unique" unique ("key");'); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /packages/api/src/api/user/put.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from '@hangar/shared'; 2 | import { Request, Response } from 'express'; 3 | import { logger } from '../../utils/logger'; 4 | import { validatePayload } from '../../utils/validatePayload'; 5 | 6 | export const put = async (req: Request, res: Response) => { 7 | const { entityManager: em, user } = req; 8 | 9 | const { errorHandled, data } = validatePayload({ req, res, schema: Schema.user.put }); 10 | 11 | if (errorHandled) return; 12 | 13 | try { 14 | user.assign(data); 15 | await em.persistAndFlush(user); 16 | res.send(user); 17 | } catch (err) { 18 | logger.error(err); 19 | res.sendStatus(500); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /packages/web/src/components/EventsList/utils.tsx: -------------------------------------------------------------------------------- 1 | import { colors } from '../../theme/colors'; 2 | 3 | export type EventStatus = 'IN PROGRESS' | 'PAST' | 'FUTURE'; 4 | 5 | export const addHexTransparency = (hex: string | undefined, alpha: number) => { 6 | const hexAlpha = Math.round(alpha * 255).toString(16); 7 | return hex + hexAlpha; 8 | }; 9 | 10 | export const eventStyle = (badge: EventStatus) => ({ 11 | color: badge === 'PAST' ? addHexTransparency(colors.grayscale, 0.5) : colors.grayscale, 12 | 13 | background: 14 | badge !== 'PAST' ? colors.brandPrimaryDark : addHexTransparency(colors.brandPrimaryDark, 0.5), 15 | 16 | filter: badge !== 'PAST' ? '' : 'blur(1px)', 17 | }); 18 | -------------------------------------------------------------------------------- /packages/shared/src/types/entities/Node.ts: -------------------------------------------------------------------------------- 1 | import { Dayjs } from 'dayjs'; 2 | import { Pretty } from '../utilities/Pretty'; 3 | 4 | type NodeDTO = { 5 | createdAt: Date; 6 | updatedAt: Date; 7 | }; 8 | 9 | export type Node = Pretty< 10 | Omit & { 11 | createdAt: Dayjs; 12 | updatedAt: Dayjs; 13 | } 14 | >; 15 | 16 | export type SerializedNode> = Pretty< 17 | Omit & { 18 | // NOTE: Dates will always be stringified when sent but the serialized DTO on the backend may still be a date 19 | createdAt: string | Date; 20 | updatedAt: string | Date; 21 | } 22 | >; 23 | -------------------------------------------------------------------------------- /packages/web/src/pages/admin/createCriteriaJudgingSession.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NextPage } from 'next'; 3 | import { PageContainer } from '../../components/layout/PageContainer'; 4 | import { CriteriaJudgingSessionForm } from '../../components/Admin/CriteriaJudgingSessionForm'; 5 | 6 | const CreateCriteriaJudgingSession: NextPage = () => { 7 | React.useEffect(() => {}, []); 8 | 9 | return ( 10 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default CreateCriteriaJudgingSession; 20 | -------------------------------------------------------------------------------- /packages/api/tests/api/admin/me/get.test.ts: -------------------------------------------------------------------------------- 1 | import { get } from '../../../../src/api/admin/me/get'; 2 | import { createMockRequest } from '../../../testUtils/expressHelpers/createMockRequest'; 3 | import { createMockResponse } from '../../../testUtils/expressHelpers/createMockResponse'; 4 | 5 | describe('admin GET me handler', () => { 6 | it('returns the admin associated with the request', () => { 7 | const admin = { id: 1 }; 8 | const mockRequest = createMockRequest({ 9 | admin: admin as any, 10 | }); 11 | const mockResponse = createMockResponse(); 12 | get(mockRequest as any, mockResponse as any); 13 | expect(mockResponse.send).toBeCalledWith(admin); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/shared/tests/schema/project/contributors/put.test.ts: -------------------------------------------------------------------------------- 1 | import { v4 } from 'uuid'; 2 | import { Schema } from '../../../../src'; 3 | 4 | const validPut = { 5 | projectId: '1', 6 | inviteCode: v4(), 7 | }; 8 | 9 | describe('project contributors put schema', () => { 10 | it('validates a matching object', () => { 11 | expect(Schema.project.contributors.put.safeParse(validPut).success).toBe(true); 12 | }); 13 | 14 | it('does not validate an object with an invalid inviteCode', () => { 15 | expect( 16 | Schema.project.contributors.put.safeParse({ 17 | ...validPut, 18 | inviteCode: `${v4()}-junk`, 19 | }).success, 20 | ).toBe(false); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/api/src/api/criteriaJudgingSession/id/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { get } from './get'; 3 | import { judgeMiddleware } from '../../../middleware/judgeMiddleware'; 4 | import { projects } from './projects'; 5 | import { criteriaJudgingSessionMiddleware } from '../../../middleware/criteriaJudgingSessionMiddleware'; 6 | import { results } from './results'; 7 | import { adminMiddleware } from '../../../middleware/adminMiddleware'; 8 | 9 | export const id = Router({ mergeParams: true }); 10 | 11 | id.use('/results', adminMiddleware, results); 12 | 13 | // Judge routes 14 | id.use(judgeMiddleware, criteriaJudgingSessionMiddleware); 15 | id.get('', get); 16 | id.use('/projects', projects); 17 | -------------------------------------------------------------------------------- /packages/api/src/api/expoJudgingSession/id/get.ts: -------------------------------------------------------------------------------- 1 | import { ExpoJudgingSession } from '@hangar/database'; 2 | import { Request, Response } from 'express'; 3 | import { logger } from '../../../utils/logger'; 4 | 5 | export const get = async (req: Request, res: Response) => { 6 | const { 7 | entityManager: em, 8 | params: { id: ejsId }, 9 | } = req; 10 | 11 | try { 12 | const ejs = await em.findOneOrFail(ExpoJudgingSession, { id: ejsId as string }); 13 | if (!ejs) { 14 | res.sendStatus(404); 15 | return; 16 | } 17 | 18 | res.send(ejs); 19 | } catch (error) { 20 | logger.error('Failed to query Expo Judging Session', error); 21 | res.sendStatus(500); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /packages/api/src/api/criteriaJudgingSession/id/get.ts: -------------------------------------------------------------------------------- 1 | import { CriteriaJudgingSession } from '@hangar/database'; 2 | import { Request, Response } from 'express'; 3 | import { logger } from '../../../utils/logger'; 4 | 5 | export const get = async (req: Request, res: Response) => { 6 | const { 7 | entityManager: em, 8 | params: { id: cjsId }, 9 | } = req; 10 | 11 | try { 12 | const cjs = await em.findOneOrFail( 13 | CriteriaJudgingSession, 14 | { id: cjsId as string }, 15 | { populate: ['criteriaList'] }, 16 | ); 17 | 18 | res.send(cjs); 19 | } catch (error) { 20 | logger.error('Failed to query Expo Judging Session', error); 21 | res.sendStatus(500); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /packages/api/src/api/expoJudgingSession/get.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { ExpoJudgingSession } from '@hangar/database'; 3 | import { QueryOrder } from '@mikro-orm/core'; 4 | import { logger } from '../../utils/logger'; 5 | 6 | export const get = async (req: Request, res: Response) => { 7 | const { entityManager } = req; 8 | try { 9 | const expoJudgingSessions = await entityManager.find( 10 | ExpoJudgingSession, 11 | {}, 12 | { orderBy: { createdAt: QueryOrder.ASC } }, 13 | ); 14 | res.send(expoJudgingSessions); 15 | } catch (error) { 16 | logger.error('Unable to fetch ExpoJudgingSessions from DB: ', error); 17 | res.sendStatus(500); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /packages/api/tests/api/project/id/index.test.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import supertest from 'supertest'; 3 | import { createMockHandler } from '../../../testUtils/expressHelpers/createMockHandler'; 4 | 5 | jest.mock('../../../../src/api/project/id/get', () => ({ 6 | get: createMockHandler(), 7 | })); 8 | 9 | describe('id session router', () => { 10 | it('registers the get route', async () => { 11 | await jest.isolateModulesAsync(async () => { 12 | const { id } = await import('../../../../src/api/project/id'); 13 | 14 | const app = express(); 15 | app.use(id); 16 | const res = await supertest(app).get(''); 17 | expect(res.status).toEqual(200); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /packages/api/src/api/auth/utils/authUrlFormatters/formatPingfedAuthUrl.ts: -------------------------------------------------------------------------------- 1 | import { pingfedAuth } from '../../../../env/auth'; 2 | import { formatRedirectUri } from '../formatRedirectUri'; 3 | import { AuthUrlFormatter } from './types'; 4 | 5 | /** 6 | * 7 | * @returns The URL to redirect to for the initial step of the auth flow 8 | */ 9 | export const formatPingfedAuthUrl: AuthUrlFormatter = ({ returnTo }) => { 10 | const queryArgs = new URLSearchParams({ 11 | redirect_uri: formatRedirectUri({ returnTo }), 12 | client_id: pingfedAuth.clientId, 13 | client_password: pingfedAuth.clientSecret, 14 | response_type: 'code', 15 | }).toString(); 16 | 17 | return `${pingfedAuth.authBaseUrl}?${queryArgs}`; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/api/src/api/criteriaJudgingSession/id/projects/get.ts: -------------------------------------------------------------------------------- 1 | import { CriteriaJudgingSession } from '@hangar/database'; 2 | import { Request, Response } from 'express'; 3 | import { logger } from '../../../../utils/logger'; 4 | 5 | export const get = async (req: Request, res: Response) => { 6 | const { entityManager: em } = req; 7 | const { id: cjsId } = req.params; 8 | 9 | try { 10 | const cjs = await em.findOneOrFail( 11 | CriteriaJudgingSession, 12 | { id: cjsId }, 13 | { populate: ['projects'] }, 14 | ); 15 | res.send(cjs.projects); 16 | } catch (error) { 17 | logger.error('Failed to retrieve projects for criteria judging session', error); 18 | res.sendStatus(500); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /packages/web/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AppProps } from 'next/app'; 3 | import Head from 'next/head'; 4 | import { Chakra } from '../components/Chakra'; 5 | import { defaultPageTitle, pageTitleKey } from '../components/layout/PageContainer'; 6 | import { AppLayout } from '../components/layout/AppLayout'; 7 | import '@fontsource/pacifico/400.css'; 8 | 9 | const App: React.FC = ({ Component, pageProps }) => ( 10 | <> 11 | 12 | {defaultPageTitle} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | 23 | export default App; 24 | -------------------------------------------------------------------------------- /packages/api/src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import * as winston from 'winston'; 2 | 3 | // If `LOG_LEVEL` is not specified, default to `debug` for non-production or `warning` for production 4 | const level = 5 | process.env.LOG_LEVEL || (process.env.NODE_ENV === 'production' ? 'warning' : 'debug'); 6 | 7 | export const logger = winston.createLogger({ 8 | format: winston.format.combine( 9 | winston.format.colorize(), 10 | winston.format.splat(), 11 | winston.format.simple(), 12 | ), 13 | transports: [new winston.transports.Console({ level })], 14 | levels: { 15 | emerg: 0, 16 | alert: 1, 17 | crit: 2, 18 | error: 3, 19 | warning: 4, 20 | notice: 5, 21 | info: 6, 22 | debug: 7, 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /packages/web/src/components/CriteriaJudging/hooks/useCriteriaJudging/fetchProjects.ts: -------------------------------------------------------------------------------- 1 | import { Project, SerializedProject } from '@hangar/shared'; 2 | import axios from 'axios'; 3 | import dayjs from 'dayjs'; 4 | 5 | type FetchProjectsArgs = { 6 | criteriaJudgingSessionId: string; 7 | }; 8 | 9 | export const fetchProjects = async ({ 10 | criteriaJudgingSessionId: id, 11 | }: FetchProjectsArgs): Promise => { 12 | const res = await axios.get(`/api/criteriaJudgingSession/${id}/projects`); 13 | return res.data.map((serializedProject) => ({ 14 | ...serializedProject, 15 | createdAt: dayjs(serializedProject.createdAt), 16 | updatedAt: dayjs(serializedProject.updatedAt), 17 | })); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/web/src/components/Forms/FormHelperHint/FormHelperHint.tsx: -------------------------------------------------------------------------------- 1 | import { FormHelperText } from '@chakra-ui/react'; 2 | import React from 'react'; 3 | import { colors, statusColors } from '../../../theme'; 4 | 5 | type FormHelperHintProps = { 6 | hint?: string; 7 | error?: string | string[]; 8 | }; 9 | 10 | export const FormHelperHint: React.FC = ({ hint, error: errors }) => { 11 | // TODO: Support displaying multiple errors if they exist 12 | const error = Array.isArray(errors) ? (errors[0] as string) : errors ?? null; 13 | 14 | return ( 15 | 16 | {error ?? hint}  17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /packages/api/src/middleware/sessionMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | 3 | /** 4 | * A middleware handler to check to see if the request included a valid session. 5 | * 6 | * Session validity is determined by the presence of an id within signed cookie included with the request. 7 | * If the session is valid, the next function is invoked. 8 | * 9 | * @param req Express Request 10 | * @param res Express Response 11 | * @param next Express NextFunction 12 | */ 13 | export const sessionMiddleware = (req: Request, res: Response, next: NextFunction) => { 14 | if (!req.session?.id) { 15 | // User does not have a valid session 16 | res.sendStatus(401); 17 | return; 18 | } 19 | 20 | next(); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/database/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noErrorTruncation": true, 4 | "module": "commonjs", 5 | "target": "es6", 6 | "moduleResolution": "node", 7 | "outDir": "dist", 8 | "esModuleInterop": true, 9 | "noImplicitAny": true, 10 | "noEmit": true, 11 | "noEmitOnError": true, 12 | "noUncheckedIndexedAccess": true, 13 | "sourceMap": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "lib": ["esnext", "esnext.asynciterable", "dom"], 16 | "resolveJsonModule": true, 17 | "strict": true, 18 | "emitDecoratorMetadata": true, 19 | "experimentalDecorators": true, 20 | "declaration": true 21 | }, 22 | "include": ["**/*.ts"], 23 | "exclude": ["node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/shared/tests/schema/judge/common.test.ts: -------------------------------------------------------------------------------- 1 | import { v4 } from 'uuid'; 2 | import { commonSchema } from '../../../src/schema/judge/common'; 3 | 4 | describe('Judge put/post common schema', () => { 5 | it('validates judge correctly', () => { 6 | expect( 7 | commonSchema.safeParse({ 8 | inviteCode: `${v4()}`, 9 | }).success, 10 | ).toBe(true); 11 | 12 | expect( 13 | commonSchema.safeParse({ 14 | inviteCode: 'some secret', 15 | }).success, 16 | ).toBe(true); 17 | }); 18 | 19 | it('fails on an incorrect uuid', () => { 20 | // incorrect type 21 | expect( 22 | commonSchema.safeParse({ 23 | inviteCode: 45, 24 | }).success, 25 | ).toBe(false); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noErrorTruncation": true, 4 | "module": "commonjs", 5 | "target": "es6", 6 | "moduleResolution": "node", 7 | "outDir": "dist", 8 | "esModuleInterop": true, 9 | "noImplicitAny": true, 10 | "noEmit": true, 11 | "noEmitOnError": true, 12 | "sourceMap": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "lib": ["esnext", "esnext.asynciterable", "dom"], 15 | "resolveJsonModule": true, 16 | "strict": true, 17 | "emitDecoratorMetadata": true, 18 | "experimentalDecorators": true, 19 | "declaration": true, 20 | "noUncheckedIndexedAccess": true 21 | }, 22 | "include": ["**/*.ts"], 23 | "exclude": ["node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/api/tests/testUtils/mockEnv.test.ts: -------------------------------------------------------------------------------- 1 | import { env } from '../../src/env'; 2 | import { mockEnv } from './mockEnv'; 3 | 4 | describe('mockEnv', () => { 5 | it('uses default values', () => { 6 | expect(env.sessionSecret).toBe('tacocat'); 7 | }); 8 | 9 | it('correctly overrides default values', () => { 10 | const myOtherPalindrome = 'race car'; 11 | mockEnv({ sessionSecret: myOtherPalindrome, nodeEnv: 'outer space' }); 12 | expect(env.sessionSecret).toBe(myOtherPalindrome); 13 | }); 14 | 15 | it('resets between tests', () => { 16 | // NOTE: This test must execute last 17 | // Normally a test should never do this 18 | expect(env.sessionSecret).toBe('tacocat'); 19 | expect(env.nodeEnv).toBe('test'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noErrorTruncation": true, 4 | "module": "commonjs", 5 | "target": "es6", 6 | "moduleResolution": "node", 7 | "outDir": "dist", 8 | "esModuleInterop": true, 9 | "noImplicitAny": true, 10 | "noEmit": true, 11 | "noEmitOnError": true, 12 | "noUncheckedIndexedAccess": true, 13 | "sourceMap": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "lib": ["esnext", "esnext.asynciterable", "dom"], 16 | "resolveJsonModule": true, 17 | "strict": true, 18 | "emitDecoratorMetadata": true, 19 | "experimentalDecorators": true 20 | }, 21 | "include": ["**/*.ts"], 22 | "exclude": ["node_modules"], 23 | "files": ["src/@types/index.d.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/api/tests/api/user/me/index.test.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import supertest from 'supertest'; 3 | import { createMockHandler } from '../../../testUtils/expressHelpers/createMockHandler'; 4 | 5 | jest.mock('../../../../src/api/user/me/get', () => ({ 6 | get: createMockHandler(), 7 | })); 8 | 9 | describe('/user/me route registration', () => { 10 | it('uses mountUserMiddleware and registers the route for the me handler', async () => { 11 | await jest.isolateModulesAsync(async () => { 12 | const { me } = await import('../../../../src/api/user/me'); 13 | 14 | const app = express(); 15 | app.use(me); 16 | const res = await supertest(app).get(''); 17 | expect(res.status).toEqual(200); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /packages/api/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | moduleFileExtensions: ['js', 'json', 'ts'], 5 | testMatch: ['**/*.test.ts'], 6 | setupFilesAfterEnv: ['./tests/setupTests.ts'], 7 | clearMocks: true, 8 | testPathIgnorePatterns: ['/node_modules/'], 9 | coverageDirectory: './coverage', 10 | collectCoverageFrom: [ 11 | './src/**/*.ts', 12 | '!./src/@types/**', 13 | '!./src/env.ts', 14 | '!./src/slack/index.ts', 15 | '!./src/index.ts', 16 | '!./src/api/settings.ts', 17 | '!./src/utils/logger.ts', 18 | ], 19 | coverageThreshold: { 20 | global: { 21 | statements: 100, 22 | branches: 100, 23 | functions: 100, 24 | lines: 100, 25 | }, 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /packages/api/src/api/criteriaJudgingSession/get.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { CriteriaJudgingSession } from '@hangar/database'; 3 | import { QueryOrder } from '@mikro-orm/core'; 4 | import { logger } from '../../utils/logger'; 5 | 6 | export const get = async (req: Request, res: Response) => { 7 | const { entityManager } = req; 8 | try { 9 | const criteriaJudgingSessions = await entityManager.find( 10 | CriteriaJudgingSession, 11 | {}, 12 | { orderBy: { createdAt: QueryOrder.ASC }, populate: ['criteriaList'] }, 13 | ); 14 | res.send(criteriaJudgingSessions); 15 | } catch (error) { 16 | logger.error('Unable to fetch CriteriaJudgingSessions from DB: ', error); 17 | res.sendStatus(500); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /packages/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noErrorTruncation": true, 4 | "module": "esnext", 5 | "target": "es5", 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "noImplicitAny": true, 9 | "noEmit": true, 10 | "noEmitOnError": true, 11 | "sourceMap": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "lib": ["dom", "dom.iterable", "esnext"], 14 | "resolveJsonModule": true, 15 | "strict": true, 16 | "skipLibCheck": true, 17 | "jsx": "preserve", 18 | "allowJs": false, 19 | "incremental": true, 20 | "isolatedModules": true, 21 | "noUncheckedIndexedAccess": true 22 | }, 23 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 24 | "exclude": ["node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /packages/api/tests/api/expoJudgingSession/id/skip/index.test.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import supertest from 'supertest'; 3 | import { createMockHandler } from '../../../../testUtils/expressHelpers/createMockHandler'; 4 | 5 | jest.mock('../../../../../src/api/expoJudgingSession/id/skip/post', () => ({ 6 | post: createMockHandler(), 7 | })); 8 | 9 | describe('skip router', () => { 10 | it('registers the post handler', async () => { 11 | await jest.isolateModulesAsync(async () => { 12 | const { skip } = await import('../../../../../src/api/expoJudgingSession/id/skip'); 13 | 14 | const app = express(); 15 | app.use(skip); 16 | const res = await supertest(app).post(''); 17 | expect(res.status).toEqual(200); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /packages/database/seeds/seeders/AdminSeeder.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | import { EntityManager, ref } from '@mikro-orm/core'; 3 | import { Seeder } from '@mikro-orm/seeder'; 4 | import { env } from '../env'; 5 | import { Admin, User } from '../../src'; 6 | 7 | export class AdminSeeder extends Seeder { 8 | run = async (em: EntityManager): Promise => { 9 | if (env.primaryUserIsAdmin) { 10 | try { 11 | const initialUser = await em.findOneOrFail(User, { id: '1' }); 12 | const admin = new Admin({ user: ref(initialUser) }); 13 | em.persist(admin); 14 | } catch { 15 | // eslint-disable-next-line no-console 16 | console.error('Failed to create admin for primary user'); 17 | } 18 | } 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: off 4 | patch: off 5 | 6 | flag_management: 7 | individual_flags: 8 | - name: api 9 | paths: 10 | - api/ 11 | statuses: 12 | - type: project 13 | target: 100% 14 | threshold: 1% 15 | - name: web 16 | paths: 17 | - web/ 18 | statuses: 19 | - type: project 20 | target: 100% 21 | threshold: 1% 22 | - name: database 23 | paths: 24 | - database/ 25 | statuses: 26 | - type: project 27 | target: 100% 28 | threshold: 1% 29 | - name: shared 30 | paths: 31 | - shared/ 32 | statuses: 33 | - type: project 34 | target: 100% 35 | threshold: 1% 36 | -------------------------------------------------------------------------------- /packages/api/src/api/auth/utils/authUrlFormatters/formatSlackAuthUrl.ts: -------------------------------------------------------------------------------- 1 | import { slackAuth } from '../../../../env/auth'; 2 | import { formatRedirectUri } from '../formatRedirectUri'; 3 | import { AuthUrlFormatter } from './types'; 4 | 5 | export const slackAuthBaseUrl: string = 6 | 'https://slack.com/openid/connect/authorize?scope=openid%20email%20profile&response_type=code&'; 7 | 8 | /** 9 | * 10 | * @returns {string} The URL to redirect to for the initial step of the auth flow 11 | */ 12 | export const formatSlackAuthUrl: AuthUrlFormatter = ({ returnTo }) => { 13 | const queryArgs = new URLSearchParams({ 14 | redirect_uri: formatRedirectUri({ returnTo }), 15 | client_id: slackAuth.clientId, 16 | }).toString(); 17 | 18 | return `${slackAuthBaseUrl}${queryArgs}`; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/api/tests/api/auth/logout/get.test.ts: -------------------------------------------------------------------------------- 1 | import { get } from '../../../../src/api/auth/logout/get'; 2 | import { createMockRequest } from '../../../testUtils/expressHelpers/createMockRequest'; 3 | import { createMockResponse } from '../../../testUtils/expressHelpers/createMockResponse'; 4 | 5 | describe('logout GET handler', () => { 6 | it('destroys the session and redirects home', () => { 7 | const mockSession = { id: 1 }; 8 | const mockReq = createMockRequest({ session: mockSession as any }); 9 | const mockRes = createMockResponse(); 10 | 11 | get(mockReq as any, mockRes as any); 12 | 13 | expect(mockRes.redirect).toHaveBeenCalledTimes(1); 14 | expect(mockRes.redirect).toHaveBeenCalledWith('/'); 15 | expect(mockReq.session).toBeNull(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/api/tests/api/expoJudgingSession/id/projects/index.test.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import supertest from 'supertest'; 3 | import { createMockHandler } from '../../../../testUtils/expressHelpers/createMockHandler'; 4 | 5 | jest.mock('../../../../../src/api/expoJudgingSession/id/projects/get', () => ({ 6 | get: createMockHandler(), 7 | })); 8 | 9 | describe('id router', () => { 10 | it('registers the get handler', async () => { 11 | await jest.isolateModulesAsync(async () => { 12 | const { projects } = await import('../../../../../src/api/expoJudgingSession/id/projects'); 13 | const app = express(); 14 | app.use(projects); 15 | 16 | const res = await supertest(app).get(''); 17 | expect(res.status).toEqual(200); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /packages/api/src/api/auth/get.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { Config } from '@hangar/shared'; 3 | import { formatSlackAuthUrl, formatPingfedAuthUrl } from './utils/authUrlFormatters'; 4 | 5 | const { method: authMethod } = Config.Auth; 6 | 7 | /** 8 | * Express handler that redirects to the appropriate auth url based on the auth method 9 | */ 10 | export const get = async (req: Request, res: Response) => { 11 | const { [Config.global.authReturnUriParamName]: returnTo } = req.query as Record; 12 | 13 | let authUrl: string; 14 | if (authMethod === 'slack') { 15 | authUrl = formatSlackAuthUrl({ returnTo }); 16 | } else { 17 | // Pingfed 18 | authUrl = formatPingfedAuthUrl({ returnTo }); 19 | } 20 | 21 | res.redirect(authUrl); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/api/src/api/expoJudgingSession/id/skip/post.ts: -------------------------------------------------------------------------------- 1 | import { ExpoJudgingSession } from '@hangar/database'; 2 | import { Request, Response } from 'express'; 3 | import { logger } from '../../../../utils/logger'; 4 | 5 | export const post = async (req: Request, res: Response) => { 6 | const { 7 | judge, 8 | entityManager: em, 9 | params: { id: ejsId }, 10 | } = req; 11 | 12 | try { 13 | const ejs = await em.findOneOrFail(ExpoJudgingSession, { id: ejsId as string }); 14 | if (!ejs) { 15 | res.sendStatus(404); 16 | return; 17 | } 18 | 19 | await judge.skip({ entityManager: em, expoJudgingSession: ejs }); 20 | res.sendStatus(204); 21 | } catch (error) { 22 | logger.error('Failed to skip project', error); 23 | res.sendStatus(500); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /packages/api/tests/api/expoJudgingSession/id/results/index.test.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import supertest from 'supertest'; 3 | import { createMockHandler } from '../../../../testUtils/expressHelpers/createMockHandler'; 4 | 5 | jest.mock('../../../../../src/api/expoJudgingSession/id/results/get', () => ({ 6 | get: createMockHandler(), 7 | })); 8 | 9 | describe('results session router', () => { 10 | it('registers the get route', async () => { 11 | await jest.isolateModulesAsync(async () => { 12 | const { results } = await import('../../../../../src/api/expoJudgingSession/id/results'); 13 | 14 | const app = express(); 15 | app.use(results); 16 | const res = await supertest(app).get(''); 17 | expect(res.status).toEqual(200); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /packages/database/src/migrations/0003-add-the-admin-table.ts: -------------------------------------------------------------------------------- 1 | import { Migration } from '@mikro-orm/migrations'; 2 | 3 | export class Migration20230828193501 extends Migration { 4 | 5 | async up(): Promise { 6 | this.addSql('create table "Admin" ("id" bigserial primary key, "createdAt" timestamptz not null default clock_timestamp(), "updatedAt" timestamptz not null default clock_timestamp(), "user" bigint not null);'); 7 | this.addSql('alter table "Admin" add constraint "Admin_user_unique" unique ("user");'); 8 | 9 | this.addSql('alter table "Admin" add constraint "Admin_user_foreign" foreign key ("user") references "User" ("id") on update cascade;'); 10 | } 11 | 12 | async down(): Promise { 13 | this.addSql('drop table if exists "Admin" cascade;'); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /packages/web/src/components/JoinSlackButton/JoinSlackButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button, ButtonProps, LinkProps } from '@chakra-ui/react'; 2 | import React from 'react'; 3 | import { FaSlack } from 'react-icons/fa'; 4 | import { useUserStore } from '../../stores/user'; 5 | import { env } from '../../env'; 6 | 7 | type JoinSlackButtonProps = ButtonProps & LinkProps; 8 | 9 | export const JoinSlackButton: React.FC = ({ ...style }) => { 10 | const { user } = useUserStore(); 11 | 12 | if (user || !env.slackInviteUrl) return null; 13 | 14 | return ( 15 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /packages/web/src/index.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | 3 | // THIS FILE IS ONLY USED WHEN RUNNING THE WEB hangar INDEPENDENTLY 4 | import path from 'path'; 5 | import type { Handler } from 'express'; 6 | import next from 'next'; 7 | 8 | interface WebOptions { 9 | dev: boolean; 10 | } 11 | 12 | export const web = async ({ dev }: WebOptions): Promise => { 13 | const nextApp = next({ dev, dir: path.join(__dirname, '..') }); 14 | const handle = nextApp.getRequestHandler(); 15 | 16 | const nextAppPreparePromise = nextApp.prepare(); 17 | 18 | // We don't want to start the server if next is still preparing 19 | if (!dev) await nextAppPreparePromise; 20 | 21 | return async (req, res) => { 22 | await nextAppPreparePromise; 23 | 24 | return handle(req, res); 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /packages/api/tests/api/criteriaJudgingSession/id/results/index.test.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import supertest from 'supertest'; 3 | import { createMockHandler } from '../../../../testUtils/expressHelpers/createMockHandler'; 4 | 5 | jest.mock('../../../../../src/api/criteriaJudgingSession/id/results/get', () => ({ 6 | get: createMockHandler(), 7 | })); 8 | 9 | describe('results router', () => { 10 | it('registers a get route', async () => { 11 | await jest.isolateModulesAsync(async () => { 12 | const { results } = await import('../../../../../src/api/criteriaJudgingSession/id/results'); 13 | 14 | const app = express(); 15 | app.use(results); 16 | const res = await supertest(app).get(''); 17 | expect(res.status).toEqual(200); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /packages/web/src/components/Prizes/Prizes.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Flex, Heading, UnorderedList } from '@chakra-ui/react'; 3 | import { usePrizesStore } from '../../stores/prizes'; 4 | import { PrizeCard } from './PrizeCard'; 5 | 6 | type PrizesProps = {}; 7 | 8 | export const Prizes: React.FC = () => { 9 | const { prizes } = usePrizesStore(); 10 | 11 | if (!prizes?.length) return null; 12 | 13 | return ( 14 | 15 | Prizes 16 | 17 | 18 | {prizes.map((prize) => ( 19 | 20 | ))} 21 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/api/src/api/expoJudgingSession/id/continueSession/post.ts: -------------------------------------------------------------------------------- 1 | import { ExpoJudgingSession } from '@hangar/database'; 2 | import { Request, Response } from 'express'; 3 | import { logger } from '../../../../utils/logger'; 4 | 5 | export const post = async (req: Request, res: Response) => { 6 | const { 7 | judge, 8 | entityManager: em, 9 | params: { id: ejsId }, 10 | } = req; 11 | 12 | try { 13 | const ejs = await em.findOneOrFail(ExpoJudgingSession, { id: ejsId as string }); 14 | 15 | await judge.continue({ entityManager: em, expoJudgingSession: ejs }); 16 | res.sendStatus(204); 17 | } catch (error) { 18 | // TODO: Error handling for various continue client errors 19 | 20 | logger.error('Failed to continue Project', error); 21 | res.sendStatus(500); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /packages/shared/tests/schema/project/put.test.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from '../../../src'; 2 | 3 | const validProject = { 4 | name: 'Coders Hub', 5 | description: 'Where coding meets coffee, creating chaos and laughter on line code at a time!', 6 | location: 'cloud 9 3/4', 7 | repoUrl: 'https://github.com/', 8 | }; 9 | 10 | describe('project put schema', () => { 11 | it('validates matching object', () => { 12 | expect(Schema.project.put.safeParse(validProject).success).toBe(true); 13 | }); 14 | 15 | it('validates a matching object without a location', () => { 16 | const projectWithoutLocation = { ...validProject } as Record; 17 | delete projectWithoutLocation.location; 18 | 19 | expect(Schema.project.put.safeParse(projectWithoutLocation).success).toBe(true); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/database/src/initDatabase.ts: -------------------------------------------------------------------------------- 1 | import { MikroORM, Options } from '@mikro-orm/core'; 2 | import { PostgreSqlDriver } from '@mikro-orm/postgresql'; 3 | 4 | type InitDatabaseOptions = { 5 | migrate?: boolean; 6 | config: Options; 7 | }; 8 | 9 | export const initDatabase = async ({ 10 | migrate, 11 | config, 12 | }: InitDatabaseOptions): Promise> => { 13 | const orm = await MikroORM.init(config); 14 | if (migrate) { 15 | const migrator = orm.getMigrator(); 16 | await migrator.up(); // Run all migrations (make sure the database is in the correct state, will crash app on failure) 17 | } 18 | 19 | if (!(await orm.isConnected())) { 20 | throw new Error('Failed to connect to database'); 21 | } 22 | 23 | return orm as MikroORM; 24 | }; 25 | -------------------------------------------------------------------------------- /packages/shared/tests/schema/project/post.test.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from '../../../src'; 2 | 3 | const validProject = { 4 | name: 'Coders Hub', 5 | description: 'Where coding meets coffee, creating chaos and laughter on line code at a time!!', 6 | location: 'cloud 9 3/4', 7 | repoUrl: 'https://github.com/', 8 | }; 9 | 10 | describe('project post schema', () => { 11 | it('validates matching object', () => { 12 | expect(Schema.project.post.safeParse(validProject).success).toBe(true); 13 | }); 14 | 15 | it('validates a matching object without a location', () => { 16 | const projectWithoutLocation = { ...validProject } as Record; 17 | delete projectWithoutLocation.location; 18 | 19 | expect(Schema.project.post.safeParse(projectWithoutLocation).success).toBe(true); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/api/tests/api/criteriaJudgingSession/id/projects/index.test.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import supertest from 'supertest'; 3 | import { createMockHandler } from '../../../../testUtils/expressHelpers/createMockHandler'; 4 | 5 | jest.mock('../../../../../src/api/criteriaJudgingSession/id/projects/get', () => ({ 6 | get: createMockHandler(), 7 | })); 8 | 9 | describe('projects router', () => { 10 | it('handles routes', async () => { 11 | await jest.isolateModulesAsync(async () => { 12 | const { projects } = await import( 13 | '../../../../../src/api/criteriaJudgingSession/id/projects' 14 | ); 15 | 16 | const app = express(); 17 | app.use(projects); 18 | const res = await supertest(app).get(''); 19 | expect(res.status).toEqual(200); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/web/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint", "react"], 4 | "extends": [ 5 | "airbnb-base", 6 | "airbnb-typescript/base", 7 | "prettier", 8 | "next/core-web-vitals", 9 | "../../.eslintrc" 10 | ], 11 | "globals": { 12 | "Atomics": "readonly", 13 | "SharedArrayBuffer": "readonly" 14 | }, 15 | "parserOptions": { 16 | "ecmaVersion": 2018, 17 | "project": "./tsconfig.json" 18 | }, 19 | "ignorePatterns": ["node_modules", "**/*.js"], 20 | "rules": { 21 | "import/no-cycle": ["off"], 22 | "import/order": ["error"], 23 | "prefer-template": ["error"], 24 | "no-plusplus": ["error"], 25 | "no-nested-ternary": ["error"], 26 | "no-unused-expressions": ["error"], 27 | "react/no-array-index-key": ["error"] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/database/seeds/seeders/UserSeeder.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | import { EntityManager } from '@mikro-orm/core'; 3 | import { Seeder } from '@mikro-orm/seeder'; 4 | import { env } from '../env'; 5 | import { User } from '../../src'; 6 | import { UserFactory } from '../factories/UserFactory'; 7 | 8 | const usersToMake = 40; 9 | 10 | export class UserSeeder extends Seeder { 11 | run = async (em: EntityManager): Promise => { 12 | if (env.primaryUserEmail) { 13 | // Primary user email was provided; 14 | const user = new User({ 15 | firstName: env.primaryUserFirstName ?? 'Jane', 16 | lastName: env.primaryUserLastName ?? 'Doe', 17 | email: env.primaryUserEmail, 18 | }); 19 | em.persist(user); 20 | } 21 | new UserFactory(em).make(usersToMake); 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /packages/web/src/components/Chakra.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { GetServerSideProps } from 'next'; 3 | import { ChakraProps, ChakraProvider } from '@chakra-ui/react'; 4 | import { theme } from '../theme'; 5 | 6 | export const Chakra: React.FC = ({ children }) => ( 7 | {children} 8 | ); 9 | 10 | // Forces the page to ALWAYS be server-rendered 11 | // Any page without this will be rendered at build time 12 | // Usage: 13 | // export { getServerSideProps } from '../components/Chakra'; 14 | // NOTE: This impacts performance and will cause the page to be re-rendered on every request 15 | // The alternative is a page-flash on load by removing this 16 | export const getServerSideProps: GetServerSideProps = async () => ({ 17 | props: {}, 18 | }); 19 | -------------------------------------------------------------------------------- /packages/database/src/entities/Node.ts: -------------------------------------------------------------------------------- 1 | import { AnyEntity, BaseEntity, BigIntType, PrimaryKey, Property } from '@mikro-orm/core'; 2 | 3 | export abstract class Node extends BaseEntity { 4 | @PrimaryKey({ type: new BigIntType('string') }) 5 | public id!: string; 6 | 7 | @Property({ defaultRaw: 'clock_timestamp()' }) 8 | public createdAt!: Date; 9 | 10 | @Property({ 11 | defaultRaw: 'clock_timestamp()', 12 | onUpdate: /* istanbul ignore next */ () => new Date(), 13 | }) 14 | public updatedAt!: Date; 15 | 16 | constructor(extraFields: Partial = {}) { 17 | super(); 18 | 19 | for (let i = 0; i < Object.entries(extraFields).length; i += 1) { 20 | const [key, value] = Object.entries(extraFields)[i] as [keyof T, T[keyof T]]; 21 | (this as unknown as T)[key] = value; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/web/src/pages/error.tsx: -------------------------------------------------------------------------------- 1 | import { VStack } from '@chakra-ui/react'; 2 | import { NextPage } from 'next'; 3 | import { useRouter } from 'next/router'; 4 | import { ErrorPageContent } from '../components/ErrorPageContent'; 5 | import { PageContainer } from '../components/layout/PageContainer'; 6 | 7 | const Error: NextPage = () => { 8 | const router = useRouter(); 9 | const { statusCode, description } = router.query; 10 | const errorReason = description ?? statusCode; 11 | 12 | return ( 13 | 17 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default Error; 25 | -------------------------------------------------------------------------------- /packages/shared/src/schema/criteriaJudgingSession/post.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { criteria } from '../criteria'; 3 | 4 | export const PostValidation = { 5 | MIN_TITLE_LENGTH: 5, 6 | MAX_TITLE_LENGTH: 50, 7 | MIN_DESCRIPTION_LENGTH: 20, 8 | MAX_DESCRIPTION_LENGTH: 200, 9 | 10 | // Criteria 11 | MIN_CRITERIA: 1, 12 | MAX_CRITERIA: 7, 13 | }; 14 | 15 | export const post = z.object({ 16 | title: z.string().min(PostValidation.MIN_TITLE_LENGTH).max(PostValidation.MAX_TITLE_LENGTH), 17 | description: z 18 | .string() 19 | .min(PostValidation.MIN_DESCRIPTION_LENGTH) 20 | .max(PostValidation.MAX_DESCRIPTION_LENGTH), 21 | criteriaList: z.array(criteria).min(PostValidation.MIN_CRITERIA).max(PostValidation.MAX_CRITERIA), 22 | // TODO: Add special criteria to evaluate the total weight to make sure it's equal to 1 23 | }); 24 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignorePaths": [ 3 | "cspell.json", 4 | "Authors.md", 5 | "*.log", 6 | "*.code-workspace", 7 | "*.svg", 8 | "*package.json", 9 | "*.drawio", 10 | "**/coverage", 11 | "**/dist", 12 | "**/.next", 13 | "**/migrations", 14 | "**/node_modules", 15 | "**/tsconfig*" 16 | ], 17 | "words": [ 18 | "americanairlines", 19 | "chakra", 20 | "cooldown", 21 | "devs", 22 | "DTO", 23 | "ejsc", 24 | "emerg", 25 | "fontsource", 26 | "formik", 27 | "gifs", 28 | "hackathon", 29 | "hackathons", 30 | "mikro", 31 | "mrkdwn", 32 | "msapplication", 33 | "pingfed", 34 | "seedrandom", 35 | "SSRF", 36 | "tablist", 37 | "tacocat", 38 | "unauthed", 39 | "xoxb", 40 | "xoxp", 41 | "zustand" 42 | ], 43 | "enabled": true 44 | } 45 | -------------------------------------------------------------------------------- /packages/api/src/api/expoJudgingSession/id/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { get } from './get'; 3 | import { projects } from './projects'; 4 | import { skip } from './skip'; 5 | import { continueSession } from './continueSession'; 6 | import { results } from './results'; 7 | import { adminMiddleware } from '../../../middleware/adminMiddleware'; 8 | import { judgeMiddleware } from '../../../middleware/judgeMiddleware'; 9 | import { expoJudgeAccessMiddleware } from '../../../middleware/expoJudgeAccessMiddleware'; 10 | 11 | export const id = Router({ mergeParams: true }); 12 | 13 | id.use('/results', adminMiddleware, results); 14 | 15 | // Judge routes 16 | id.use(judgeMiddleware, expoJudgeAccessMiddleware); 17 | id.get('', get); 18 | id.use('/projects', projects); 19 | id.use('/skip', skip); 20 | id.use('/continueSession', continueSession); 21 | -------------------------------------------------------------------------------- /packages/web/src/pageUtils/judgingSession/createOrUpdateJudge.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from '@hangar/shared'; 2 | import axios, { isAxiosError } from 'axios'; 3 | import { z } from 'zod'; 4 | 5 | type CreateOrUpdateJudgeArgs = { 6 | inviteCode: string; 7 | }; 8 | 9 | export const createOrUpdateJudge = async ({ inviteCode }: CreateOrUpdateJudgeArgs) => { 10 | const query: z.infer = { inviteCode }; 11 | const queryString = new URLSearchParams(query).toString(); 12 | const judgeEndpoint = `/api/judge?${queryString}`; 13 | 14 | try { 15 | await axios.post(judgeEndpoint); 16 | } catch (postError) { 17 | if (!isAxiosError(postError) || postError.response?.status !== 409) { 18 | throw postError; 19 | } 20 | 21 | // Judge already exists; update the current judge 22 | await axios.put(judgeEndpoint); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /packages/api/tests/api/expoJudgingSession/id/continueSession/index.test.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import supertest from 'supertest'; 3 | import { createMockHandler } from '../../../../testUtils/expressHelpers/createMockHandler'; 4 | 5 | jest.mock('../../../../../src/api/expoJudgingSession/id/continueSession/post', () => ({ 6 | post: createMockHandler(), 7 | })); 8 | 9 | describe('continue session router', () => { 10 | it('registers the post route', async () => { 11 | await jest.isolateModulesAsync(async () => { 12 | const { continueSession } = await import( 13 | '../../../../../src/api/expoJudgingSession/id/continueSession' 14 | ); 15 | 16 | const app = express(); 17 | app.use(continueSession); 18 | const res = await supertest(app).post(''); 19 | expect(res.status).toEqual(200); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /.github/workflows/quality.yaml: -------------------------------------------------------------------------------- 1 | name: Code Quality 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | merge_group: 9 | branches: [main] 10 | 11 | env: 12 | NODE_VERSION: 18 13 | 14 | jobs: 15 | checks: 16 | name: Lint and Style 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v3 21 | 22 | - name: Setup Node.js ${{ env.NODE_VERSION }} 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: ${{ env.NODE_VERSION }} 26 | cache: yarn 27 | 28 | - name: Install dependencies 29 | run: yarn install --frozen-lockfile 30 | 31 | - name: ESLint 32 | run: yarn lint 33 | 34 | - name: Prettier 35 | run: yarn prettier 36 | 37 | - name: Spellcheck 38 | run: yarn spellcheck 39 | -------------------------------------------------------------------------------- /packages/web/src/theme/colors.ts: -------------------------------------------------------------------------------- 1 | type Colors = Record; 2 | 3 | const brandPrimary = '#00467F'; 4 | const brandPrimaryLight = '#0078D2'; 5 | const brandPrimaryDark = '#36495A'; 6 | const brandTertiary = '#FAAF00'; 7 | const brandCta = '#FF7318'; 8 | const grayscaleLight = '#D0DAE0'; 9 | const grayscale = '#9DA6AB'; 10 | 11 | export const statusColors: Colors = { 12 | error: '#E53E3E', // not in brand 13 | errorFaint: '#FF8888', // not in brand 14 | errorDark: '#C30019', 15 | warning: '#FAAF00', 16 | success: '#00B989', 17 | alert: brandCta, 18 | } as const; 19 | 20 | export const colors: Colors = { 21 | brandPrimary, 22 | brandPrimaryLight, 23 | brandPrimaryDark, 24 | brandTertiary, 25 | brandCta, 26 | grayscaleLight, 27 | grayscale, 28 | white: '#FFFFFF', 29 | black: '#131313', 30 | muted: '#7B8085', 31 | ...statusColors, 32 | } as const; 33 | -------------------------------------------------------------------------------- /packages/database/src/entities/Event.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | import { Entity, Property, EntityDTO } from '@mikro-orm/core'; 3 | import { ConstructorValues } from '../types/ConstructorValues'; 4 | import { Node } from './Node'; 5 | 6 | export type EventDTO = EntityDTO; 7 | 8 | export type EventConstructorValues = ConstructorValues; 9 | 10 | @Entity() 11 | export class Event extends Node { 12 | @Property({ columnType: 'text' }) 13 | name: string; 14 | 15 | @Property() 16 | start: Date; 17 | 18 | @Property() 19 | end: Date; 20 | 21 | @Property({ columnType: 'text', nullable: true }) 22 | description?: string; 23 | 24 | constructor({ name, start, end, ...extraValues }: EventConstructorValues) { 25 | super(extraValues); 26 | 27 | this.name = name; 28 | this.start = start; 29 | this.end = end; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/api/tests/api/event/index.test.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import supertest from 'supertest'; 3 | import { createMockHandler } from '../../testUtils/expressHelpers/createMockHandler'; 4 | import { getMock } from '../../testUtils/getMock'; 5 | import { get } from '../../../src/api/event/get'; 6 | 7 | jest.mock('../../../src/api/event/get', () => ({ 8 | get: createMockHandler(), 9 | })); 10 | 11 | const mockGet = getMock(get); 12 | 13 | describe('api/event route', () => { 14 | it('registers the get endpoint', async () => { 15 | await jest.isolateModulesAsync(async () => { 16 | const { event } = await import('../../../src/api/event'); 17 | 18 | const app = express(); 19 | app.use(event); 20 | const res = await supertest(app).get(''); 21 | expect(res.status).toEqual(200); 22 | expect(mockGet).toBeCalled(); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/shared/tests/schema/criteria/criteria.test.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { criteria } from '../../../src/schema/criteria'; 3 | 4 | export const validCriteria: z.infer = { 5 | title: 'Some Criteria', 6 | description: 'A criteria that has a purpose', 7 | weight: 0.3, 8 | scaleMin: 0, 9 | scaleMax: 3, 10 | scaleDescription: '0 = bad, 3 = good', 11 | }; 12 | 13 | describe('criteria', () => { 14 | it('validates a correct object', () => { 15 | const result = criteria.safeParse({ ...validCriteria }); 16 | expect(result.success).toBe(true); 17 | }); 18 | 19 | describe('invalid parsing', () => { 20 | it('rejects a missing title', () => { 21 | const { title, ...invalidCriteria } = validCriteria; 22 | const result = criteria.safeParse(invalidCriteria); 23 | expect(result.success).toBe(false); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/api/src/api/auth/utils/formatRedirectUri.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '@hangar/shared'; 2 | import { env } from '../../../env'; 3 | 4 | type FormatSlackRedirectUriArgs = { 5 | returnTo?: string; 6 | }; 7 | 8 | const { method } = Config.Auth; 9 | 10 | /** 11 | * A method to format a URL encoded redirect URI based on the configured auth method 12 | * @param {string} args.returnTo the uri to return the user to post-auth 13 | * @returns 14 | */ 15 | export const formatRedirectUri = ({ returnTo }: FormatSlackRedirectUriArgs = {}) => { 16 | const params = new URLSearchParams(); 17 | if (returnTo) { 18 | params.append(Config.global.authReturnUriParamName, returnTo); 19 | } 20 | 21 | const paramsString = params.toString(); 22 | const returnToQuery = paramsString ? `?${paramsString}` : ''; 23 | 24 | return `${env.baseUrl ?? ''}/api/auth/callback/${method}/${returnToQuery}`; 25 | }; 26 | -------------------------------------------------------------------------------- /packages/api/tests/api/prizes/index.test.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import supertest from 'supertest'; 3 | import { createMockHandler } from '../../testUtils/expressHelpers/createMockHandler'; 4 | import { getMock } from '../../testUtils/getMock'; 5 | import { get } from '../../../src/api/prize/get'; 6 | 7 | jest.mock('../../../src/api/prize/get', () => ({ 8 | get: createMockHandler(), 9 | })); 10 | 11 | const mockGet = getMock(get); 12 | 13 | describe('api/prizes route', () => { 14 | it('registers the get endpoint', async () => { 15 | await jest.isolateModulesAsync(async () => { 16 | const { prize } = await import('../../../src/api/prize'); 17 | 18 | const app = express(); 19 | app.use(prize); 20 | const res = await supertest(app).get(''); 21 | expect(res.status).toEqual(200); 22 | expect(mockGet).toBeCalled(); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/api/tests/api/auth/index.test.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import supertest from 'supertest'; 3 | import { createMockHandler } from '../../testUtils/expressHelpers/createMockHandler'; 4 | import { get } from '../../../src/api/auth/get'; 5 | import { getMock } from '../../testUtils/getMock'; 6 | 7 | jest.mock('../../../src/api/auth/get', () => ({ 8 | get: createMockHandler(), 9 | })); 10 | const mockGet = getMock(get); 11 | 12 | describe('slack auth declarations', () => { 13 | it('registers the auth handler', async () => { 14 | await jest.isolateModulesAsync(async () => { 15 | // Import auth for the first time AFTER the slack method is mocked 16 | const { auth } = await import('../../../src/api/auth'); 17 | const app = express(); 18 | app.use(auth); 19 | 20 | await supertest(app).get('/'); 21 | 22 | expect(mockGet).toBeCalledTimes(1); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/database/seeds/factories/ProjectFactory.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | import seedrandom from 'seedrandom'; 3 | import { faker } from '@faker-js/faker'; 4 | import { Factory } from '@mikro-orm/seeder'; 5 | import { v4 } from 'uuid'; 6 | import { FakerEntity } from '../types/FakerEntity'; 7 | import { Project } from '../../src'; 8 | 9 | const locationSeeder = seedrandom('location'); 10 | 11 | export class ProjectFactory extends Factory { 12 | model = Project; 13 | 14 | definition = (): FakerEntity< 15 | Project, 16 | 'contributors' | 'incrementActiveJudgeCount' | 'decrementActiveJudgeCount' 17 | > => ({ 18 | name: faker.lorem.words(), 19 | description: faker.lorem.paragraph(), 20 | activeJudgeCount: 0, 21 | inviteCode: v4(), 22 | repoUrl: faker.internet.url(), 23 | location: locationSeeder() > 0.5 ? v4().substring(0, 5) : undefined, 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18 2 | WORKDIR /usr/src/app 3 | 4 | COPY yarn.lock package.json ./ 5 | # Uncomment if packages need patches via patch-package 6 | COPY patches ./patches 7 | COPY packages/shared/package.json packages/shared/package.json 8 | COPY packages/api/package.json packages/api/package.json 9 | COPY packages/web/package.json packages/web/package.json 10 | COPY packages/database/package.json packages/database/package.json 11 | 12 | # Install all dependencies 13 | RUN yarn install --frozen-lockfile 14 | 15 | ARG NEXT_PUBLIC_BASE_URL 16 | ARG NEXT_PUBLIC_SLACK_WORKSPACE_NAME 17 | ARG NEXT_PUBLIC_SLACK_INVITE_URL 18 | 19 | ENV NODE_ENV production 20 | ENV PORT 8080 21 | 22 | COPY . . 23 | 24 | RUN yarn build 25 | 26 | # Install again to remove devDependencies because NODE_ENV is set to production 27 | RUN yarn install --frozen-lockfile 28 | 29 | # TODO: Remove src files 30 | 31 | EXPOSE 8080 32 | ENTRYPOINT [ "yarn", "start" ] 33 | -------------------------------------------------------------------------------- /packages/database/src/entities/CriteriaJudgingSession.ts: -------------------------------------------------------------------------------- 1 | import { Collection, Entity, EntityDTO, ManyToMany, Property } from '@mikro-orm/core'; 2 | import { JudgingSession } from './JudgingSession'; 3 | import { Criteria } from './Criteria'; 4 | import { ConstructorValues } from '../types/ConstructorValues'; 5 | 6 | export type CriteriaJudgingSessionDTO = EntityDTO; 7 | type ConstructorArgs = ConstructorValues< 8 | CriteriaJudgingSession, 9 | 'criteriaList' | 'projects' | 'inviteCode' 10 | >; 11 | 12 | @Entity() 13 | export class CriteriaJudgingSession extends JudgingSession { 14 | @Property({ columnType: 'text' }) 15 | description: string; 16 | 17 | @ManyToMany({ entity: () => Criteria }) 18 | criteriaList = new Collection(this); 19 | 20 | constructor({ title, description, createdBy }: ConstructorArgs) { 21 | super({ title, createdBy }); 22 | 23 | this.description = description; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/api/tests/api/admin/me/index.test.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import supertest from 'supertest'; 3 | import { createMockHandler } from '../../../testUtils/expressHelpers/createMockHandler'; 4 | import { getMock } from '../../../testUtils/getMock'; 5 | import { get } from '../../../../src/api/admin/me/get'; 6 | 7 | jest.mock('../../../../src/api/admin/me/get', () => ({ 8 | get: createMockHandler(), 9 | })); 10 | 11 | const mockGet = getMock(get); 12 | 13 | describe('/admin/me route', () => { 14 | it('uses adminMiddleware and registers the admin for the me handler', async () => { 15 | await jest.isolateModulesAsync(async () => { 16 | const { me } = await import('../../../../src/api/admin/me'); 17 | 18 | const app = express(); 19 | app.use(me); 20 | const res = await supertest(app).get(''); 21 | expect(res.status).toEqual(200); 22 | expect(mockGet).toBeCalled(); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/api/tests/api/health/index.test.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from 'express'; 2 | import supertest from 'supertest'; 3 | import { get } from '../../../src/api/health/get'; 4 | import { getMock } from '../../testUtils/getMock'; 5 | 6 | jest.mock('../../../src/api/health/get', () => ({ 7 | get: jest.fn().mockImplementation(async (req: Request, res: Response) => { 8 | res.sendStatus(200); 9 | }), 10 | })); 11 | const mockGet = getMock(get); 12 | 13 | describe('health route declarations', () => { 14 | it('registers the GET handler', async () => { 15 | await jest.isolateModulesAsync(async () => { 16 | // Import health for the first time AFTER the get method is mocked 17 | const { health } = await import('../../../src/api/health'); 18 | const app = express(); 19 | app.use(health); 20 | 21 | await supertest(app).get(''); 22 | 23 | expect(mockGet).toBeCalledTimes(1); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/api/src/middleware/adminMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import { Admin } from '@hangar/database'; 3 | 4 | /** 5 | * Middleware that evaluates the admins's validity. 6 | * 7 | * Paths: 8 | * - Next function invoked: user was identified and matching object mounted to request 9 | * - 403: Admin not present 10 | * - 500: An error occurred trying to identify the Admin 11 | */ 12 | export const adminMiddleware = async (req: Request, res: Response, next: NextFunction) => { 13 | let userAdmin; 14 | try { 15 | userAdmin = await req.entityManager.findOne(Admin, { user: req.session?.id }); 16 | } catch { 17 | res.sendStatus(500); 18 | return; 19 | } 20 | 21 | if (userAdmin) { 22 | req.admin = userAdmin; 23 | } else { 24 | // Admin does not exist in the database 25 | res.status(403).send('Admin validation failed for user'); 26 | return; 27 | } 28 | 29 | next(); 30 | }; 31 | -------------------------------------------------------------------------------- /packages/web/src/pages/admin/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NextPage } from 'next'; 3 | import { useRouter } from 'next/router'; 4 | import { PageContainer } from '../../components/layout/PageContainer'; 5 | import { useAdminStore } from '../../stores/admin'; 6 | import { JudgingSessionList } from '../../components/Admin'; 7 | 8 | const AdminDashboard: NextPage = () => { 9 | const router = useRouter(); 10 | const { admin, doneLoading: adminDoneLoading } = useAdminStore((state) => state); 11 | 12 | React.useEffect(() => { 13 | if (adminDoneLoading && !admin) { 14 | void router.push('/'); 15 | } 16 | }, [admin, adminDoneLoading, router]); 17 | 18 | return ( 19 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default AdminDashboard; 30 | -------------------------------------------------------------------------------- /packages/api/tests/api/auth/logout/index.test.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import supertest from 'supertest'; 3 | import { createMockHandler } from '../../../testUtils/expressHelpers/createMockHandler'; 4 | import { getMock } from '../../../testUtils/getMock'; 5 | import { get } from '../../../../src/api/auth/logout/get'; 6 | 7 | jest.mock('../../../../src/api/auth/logout/get', () => ({ 8 | get: createMockHandler(), 9 | })); 10 | const mockGet = getMock(get); 11 | 12 | describe('logout route registrations', () => { 13 | it('registers the auth handler', async () => { 14 | await jest.isolateModulesAsync(async () => { 15 | // Import auth for the first time AFTER the slack method is mocked 16 | const { logout } = await import('../../../../src/api/auth/logout'); 17 | const app = express(); 18 | app.use(logout); 19 | 20 | await supertest(app).get('/'); 21 | 22 | expect(mockGet).toBeCalledTimes(1); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/web/src/components/Admin/JudgingSessionList/JudgingSessionsOptionsButton/JudgingSessionOptionsButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, IconButton, Menu, MenuButton, MenuList, MenuItem } from '@chakra-ui/react'; 3 | import NextLink from 'next/link'; 4 | import { BsThreeDots } from 'react-icons/bs'; 5 | 6 | export const JudgingSessionOptionsButton: React.FC = () => ( 7 | 8 | } /> 9 | 10 | 11 | 12 | Create Expo Judging Session 13 | 14 | 15 | 16 | 17 | 18 | Create Criteria Judging Session 19 | 20 | 21 | 22 | 23 | ); 24 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | merge_group: 9 | branches: [main] 10 | 11 | env: 12 | NODE_VERSION: 18 13 | 14 | jobs: 15 | tests: 16 | name: Tests & Code Coverage 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v3 21 | 22 | - name: Setup Node.js ${{ env.NODE_VERSION }} 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: ${{ env.NODE_VERSION }} 26 | cache: yarn 27 | 28 | - name: Install dependencies 29 | run: yarn install --frozen-lockfile 30 | 31 | - name: Build 32 | run: yarn build 33 | 34 | - name: Run Test Coverage 35 | run: yarn test:coverage 36 | 37 | - name: Upload coverage to Codecov 38 | uses: codecov/codecov-action@v3 39 | with: 40 | token: ${{ secrets.CODECOV_TOKEN }} 41 | -------------------------------------------------------------------------------- /packages/api/tests/api/project/index.test.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import supertest from 'supertest'; 3 | import { createMockHandler } from '../../testUtils/expressHelpers/createMockHandler'; 4 | import { createMockNext } from '../../testUtils/expressHelpers/createMockNext'; 5 | 6 | jest.mock('../../../src/api/project/post', () => ({ 7 | post: createMockHandler(), 8 | })); 9 | 10 | jest.mock('../../../src/middleware/mountUserMiddleware', () => ({ 11 | mountUserMiddleware: createMockNext(), 12 | })); 13 | 14 | describe('/project post endpoint registration', () => { 15 | it('uses mountUserMiddleware and registers the route for the me handler', async () => { 16 | await jest.isolateModulesAsync(async () => { 17 | const { project } = await import('../../../src/api/project'); 18 | 19 | const app = express(); 20 | app.use(project); 21 | const res = await supertest(app).post(''); 22 | expect(res.status).toEqual(200); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/api/src/api/expoJudgingSession/id/results/get.ts: -------------------------------------------------------------------------------- 1 | import { ExpoJudgingSession, ExpoJudgingVote, insufficientVoteCountError } from '@hangar/database'; 2 | import { Request, Response } from 'express'; 3 | import { logger } from '../../../../utils/logger'; 4 | 5 | export const get = async (req: Request, res: Response) => { 6 | const { 7 | entityManager: em, 8 | params: { id: ejsId }, 9 | } = req; 10 | 11 | try { 12 | const ejs = await em.findOne(ExpoJudgingSession, { id: ejsId as string }); 13 | if (!ejs) { 14 | res.sendStatus(404); 15 | return; 16 | } 17 | 18 | const results = await ExpoJudgingVote.tabulate({ entityManager: em, expoJudgingSession: ejs }); 19 | res.send(results); 20 | } catch (error) { 21 | if ((error as Error).cause === insufficientVoteCountError) { 22 | res.status(409).send((error as Error).message); 23 | return; 24 | } 25 | logger.error('Failed to calculate results', error); 26 | res.sendStatus(500); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /packages/web/src/pages/expoJudgingSession/[id]/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NextPage } from 'next'; 3 | import { PageContainer } from '../../../components/layout/PageContainer'; 4 | import { useJudgingSessionFetcher } from '../../../pageUtils/judgingSession'; 5 | import { ProjectCardsContainer, useExpoJudging } from '../../../components/ExpoJudging'; 6 | 7 | const ExpoJudgingSessionDetails: NextPage = () => { 8 | const { expoJudgingSession } = useJudgingSessionFetcher({ sessionType: 'expo' }); 9 | React.useEffect(() => { 10 | if (expoJudgingSession) { 11 | useExpoJudging.getState().init({ expoJudgingSessionId: expoJudgingSession.id }); 12 | } 13 | }, [expoJudgingSession]); 14 | 15 | return ( 16 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default ExpoJudgingSessionDetails; 27 | -------------------------------------------------------------------------------- /packages/web/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Hangar", 3 | "icons": [ 4 | { 5 | "src": "/android-icon-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": "/android-icon-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": "/android-icon-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": "/android-icon-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image/png", 26 | "density": "2.0" 27 | }, 28 | { 29 | "src": "/android-icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image/png", 32 | "density": "3.0" 33 | }, 34 | { 35 | "src": "/android-icon-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image/png", 38 | "density": "4.0" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /packages/api/tests/api/project/contributors/index.test.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import supertest from 'supertest'; 3 | import { createMockHandler } from '../../../testUtils/expressHelpers/createMockHandler'; 4 | import { createMockNext } from '../../../testUtils/expressHelpers/createMockNext'; 5 | 6 | jest.mock('../../../../src/api/project/contributors/put', () => ({ 7 | put: createMockHandler(), 8 | })); 9 | 10 | jest.mock('../../../../src/middleware/mountUserMiddleware', () => ({ 11 | mountUserMiddleware: createMockNext(), 12 | })); 13 | 14 | describe('/project put endpoint registration', () => { 15 | it('registers the route for the put handler', async () => { 16 | await jest.isolateModulesAsync(async () => { 17 | const { contributors } = await import('../../../../src/api/project/contributors'); 18 | 19 | const app = express(); 20 | app.use(contributors); 21 | const res = await supertest(app).put(''); 22 | expect(res.status).toEqual(200); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/web/src/components/layout/RedirectToAuthModal/PingfedContent/PingfedContent.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, Heading, Button } from '@chakra-ui/react'; 2 | 3 | type PingfedContentProps = { 4 | secondsRemaining?: number; 5 | onContinue: () => void; 6 | }; 7 | 8 | export const PingfedContent: React.FC = ({ secondsRemaining, onContinue }) => ( 9 | 17 | 18 | {secondsRemaining !== undefined 19 | ? `Redirecting to login in ${secondsRemaining} seconds...` 20 | : 'Redirecting...'} 21 | 22 | 34 | 35 | ); 36 | -------------------------------------------------------------------------------- /packages/shared/src/schema/project/core.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const validation = { 4 | MIN_NAME_LENGTH: 5, 5 | MAX_NAME_LENGTH: 50, 6 | MIN_DESCRIPTION_LENGTH: 20, 7 | MAX_DESCRIPTION_LENGTH: 200, 8 | MAX_LOCATION_LENGTH: 100, 9 | }; 10 | 11 | export const core = z.object({ 12 | name: z.string().trim().min(validation.MIN_NAME_LENGTH).max(validation.MAX_NAME_LENGTH), 13 | description: z 14 | .string() 15 | .trim() 16 | .min(validation.MIN_DESCRIPTION_LENGTH) 17 | .max(validation.MAX_DESCRIPTION_LENGTH), 18 | location: z 19 | .string() 20 | .trim() 21 | .max(validation.MAX_LOCATION_LENGTH) 22 | .transform((data) => (data !== '' ? data : undefined)) // Make sure an empty string is always coerced to undefined 23 | .optional(), 24 | repoUrl: z 25 | .string() 26 | .trim() 27 | .url() 28 | .refine( 29 | (url) => url.startsWith('https://github.com/') || url.startsWith('https://gitlabs.com/'), 30 | 'Not a supported repo hosting platform', 31 | ), 32 | }); 33 | -------------------------------------------------------------------------------- /packages/api/tests/api/auth/callback/slack/index.test.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from 'express'; 2 | import supertest from 'supertest'; 3 | import { get } from '../../../../../src/api/auth/callback/slack/get'; 4 | import { getMock } from '../../../../testUtils/getMock'; 5 | 6 | jest.mock('../../../../../src/api/auth/callback/slack/get', () => ({ 7 | get: jest.fn().mockImplementation(async (req: Request, res: Response) => { 8 | res.sendStatus(200); 9 | }), 10 | })); 11 | const mockGet = getMock(get); 12 | 13 | describe('slack callback declarations', () => { 14 | it('registers the callback handler', async () => { 15 | await jest.isolateModulesAsync(async () => { 16 | // Import callback for the first time AFTER the slack method is mocked 17 | const { slack } = await import('../../../../../src/api/auth/callback/slack'); 18 | 19 | const app = express(); 20 | app.use(slack); 21 | 22 | await supertest(app).get('/'); 23 | 24 | expect(mockGet).toBeCalledTimes(1); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/api/src/api/settings.ts: -------------------------------------------------------------------------------- 1 | import express, { Handler } from 'express'; 2 | import rateLimit from 'express-rate-limit'; 3 | 4 | const maxFileSizeInMbs = 20; 5 | export const enforceMaxFileSize = express.text({ limit: `${maxFileSizeInMbs}mb` }); 6 | 7 | /** 8 | * Allows 20 requests per minute to /api routes, only when `NODE_ENV` is `development`. 9 | * Cloudflare rate limits stage & prod before it hits the app 10 | */ 11 | export const enforceRateLimiting: Handler = rateLimit({ 12 | windowMs: 60 * 1000, 13 | max: 100, 14 | // Return rate limit info in `RateLimit-*` headers 15 | standardHeaders: true, 16 | // Return rate limit info in `X-RateLimit-*` headers 17 | legacyHeaders: false, 18 | }); 19 | 20 | export const customParsingSettings: Handler = (req, res, next) => { 21 | if (req.headers['stripe-signature']) { 22 | // Path requires raw body; do avoid other parsing 23 | express.raw({ type: 'application/json' })(req, res, next); 24 | return; 25 | } 26 | // All other requests; parse body 27 | express.json()(req, res, next); 28 | }; 29 | -------------------------------------------------------------------------------- /packages/database/src/entities/User.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | import { Entity, Property, ManyToOne, Ref, EntityDTO, HiddenProps } from '@mikro-orm/core'; 3 | import { ConstructorValues } from '../types/ConstructorValues'; 4 | import { Project } from './Project'; 5 | import { Node } from './Node'; 6 | 7 | export type UserDTO = EntityDTO; 8 | 9 | export type UserConstructorValues = ConstructorValues; 10 | 11 | @Entity() 12 | export class User extends Node { 13 | [HiddenProps]?: 'email'; 14 | 15 | @Property({ columnType: 'text' }) 16 | firstName: string; 17 | 18 | @Property({ columnType: 'text' }) 19 | lastName: string; 20 | 21 | @Property({ columnType: 'text', unique: true, hidden: true }) 22 | email: string; 23 | 24 | @ManyToOne({ entity: () => Project, nullable: true, ref: true }) 25 | project?: Ref; 26 | 27 | constructor({ firstName, lastName, email }: UserConstructorValues) { 28 | super(); 29 | 30 | this.firstName = firstName; 31 | this.lastName = lastName; 32 | this.email = email; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/web/src/pageUtils/expoJudgingSession/[id]/fetchExpoJudgingSessionResults.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosResponse, isAxiosError } from 'axios'; 2 | 3 | type FetchExpoJudgingSessionArgs = { 4 | expoJudgingSessionId: string; 5 | }; 6 | type FetchExpoJudgingSessionError = Pick; 7 | 8 | export type ExpoJudgingSessionResult = { 9 | id: string; 10 | score: number; 11 | name: string; 12 | }; 13 | 14 | export const fetchExpoJudgingSessionResults = async ({ 15 | expoJudgingSessionId, 16 | }: FetchExpoJudgingSessionArgs): Promise< 17 | ExpoJudgingSessionResult[] | FetchExpoJudgingSessionError 18 | > => { 19 | try { 20 | const res = await axios.get( 21 | `/api/expoJudgingSession/${expoJudgingSessionId}/results`, 22 | ); 23 | return res.data; 24 | } catch (error) { 25 | const defaultError: FetchExpoJudgingSessionError = { status: 500, data: undefined }; 26 | if (isAxiosError(error)) { 27 | return error.response ?? defaultError; 28 | } 29 | return defaultError; 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /packages/web/src/stores/expoJudgingSession/fetchExpoJudgingSessions.ts: -------------------------------------------------------------------------------- 1 | import { ExpoJudgingSession, SerializedExpoJudgingSession } from '@hangar/shared'; 2 | import axios from 'axios'; 3 | import dayjs from 'dayjs'; 4 | import { openErrorToast } from '../../components/utils/CustomToast'; 5 | 6 | export const fetchExpoJudgingSessions = async () => { 7 | try { 8 | const res = await axios.get(`/api/expoJudgingSession`); 9 | return res.data.map( 10 | ({ createdAt, updatedAt, ...rest }) => 11 | ({ 12 | ...rest, 13 | createdAt: dayjs(createdAt), 14 | updatedAt: dayjs(updatedAt), 15 | } as ExpoJudgingSession), 16 | ); 17 | } catch (error) { 18 | if (!axios.isAxiosError(error) || error.status !== 401) { 19 | // eslint-disable-next-line no-console 20 | console.error(error); 21 | openErrorToast({ 22 | title: 'Failed to fetch Expo Judging Session details', 23 | description: (error as Error).message, 24 | }); 25 | } 26 | } 27 | 28 | return []; 29 | }; 30 | -------------------------------------------------------------------------------- /packages/database/src/migrations/0012-adding-criteria-judging-session-judges-table.ts: -------------------------------------------------------------------------------- 1 | import { Migration } from '@mikro-orm/migrations'; 2 | 3 | export class Migration20231023184711 extends Migration { 4 | 5 | async up(): Promise { 6 | this.addSql('create table "Judge_criteriaJudgingSessions" ("judge" bigint not null, "criteriaJudgingSession" bigint not null, constraint "Judge_criteriaJudgingSessions_pkey" primary key ("judge", "criteriaJudgingSession"));'); 7 | 8 | this.addSql('alter table "Judge_criteriaJudgingSessions" add constraint "Judge_criteriaJudgingSessions_judge_foreign" foreign key ("judge") references "Judge" ("id") on update cascade on delete cascade;'); 9 | this.addSql('alter table "Judge_criteriaJudgingSessions" add constraint "Judge_criteriaJudgingSessions_criteriaJudgingSession_foreign" foreign key ("criteriaJudgingSession") references "CriteriaJudgingSession" ("id") on update cascade on delete cascade;'); 10 | } 11 | 12 | async down(): Promise { 13 | this.addSql('drop table if exists "Judge_criteriaJudgingSessions" cascade;'); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /packages/database/src/entities/CriteriaScore.ts: -------------------------------------------------------------------------------- 1 | import { Entity, EntityDTO, Unique, ManyToOne, Property, Ref } from '@mikro-orm/core'; 2 | import { Node } from './Node'; 3 | import { Criteria } from './Criteria'; 4 | import { ConstructorValues } from '../types/ConstructorValues'; 5 | import { CriteriaJudgingSubmission } from './CriteriaJudgingSubmission'; 6 | 7 | export type CriteriaScoreDTO = EntityDTO; 8 | type ConstructorArgs = ConstructorValues; 9 | 10 | @Entity() 11 | @Unique({ properties: ['submission', 'criteria'] }) 12 | export class CriteriaScore extends Node { 13 | @ManyToOne({ entity: () => CriteriaJudgingSubmission, ref: true }) 14 | submission: Ref; 15 | 16 | @ManyToOne({ entity: () => Criteria, ref: true }) 17 | criteria: Ref; 18 | 19 | @Property({ columnType: 'int' }) 20 | score: number; 21 | 22 | constructor({ submission, criteria, score }: ConstructorArgs) { 23 | super(); 24 | 25 | this.submission = submission; 26 | this.criteria = criteria; 27 | this.score = score; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/web/src/stores/criteriaJudgingSession/fetchCriteriaJudgingSessions.ts: -------------------------------------------------------------------------------- 1 | import { CriteriaJudgingSession, SerializedCriteriaJudgingSession } from '@hangar/shared'; 2 | import axios from 'axios'; 3 | import dayjs from 'dayjs'; 4 | import { openErrorToast } from '../../components/utils/CustomToast'; 5 | 6 | export const fetchCriteriaJudgingSessions = async () => { 7 | try { 8 | const res = await axios.get(`/api/criteriaJudgingSession`); 9 | return res.data.map( 10 | ({ createdAt, updatedAt, ...rest }) => 11 | ({ 12 | ...rest, 13 | createdAt: dayjs(createdAt), 14 | updatedAt: dayjs(updatedAt), 15 | } as CriteriaJudgingSession), 16 | ); 17 | } catch (error) { 18 | if (!axios.isAxiosError(error) || error.status !== 401) { 19 | // eslint-disable-next-line no-console 20 | console.error(error); 21 | openErrorToast({ 22 | title: 'Failed to fetch Criteria Judging Sessions', 23 | description: (error as Error).message, 24 | }); 25 | } 26 | } 27 | 28 | return []; 29 | }; 30 | -------------------------------------------------------------------------------- /packages/web/src/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { extendTheme, ThemeConfig, ThemeOverride } from '@chakra-ui/react'; 2 | import { colors } from './colors'; 3 | import { components } from './components'; 4 | 5 | // https://chakra-ui.com/docs/styled-system/color-mode 6 | export const forcedColorMode = 'dark'; 7 | const config: ThemeConfig = { 8 | initialColorMode: forcedColorMode, 9 | useSystemColorMode: false, 10 | }; 11 | 12 | export const theme = extendTheme({ 13 | config, 14 | colors, 15 | components, 16 | fonts: { 17 | // heading: 'Inter', 18 | // body: 'Inter', 19 | }, 20 | styles: { 21 | global: () => ({ 22 | '*': { 23 | borderColor: 'gray.300', 24 | '::-webkit-scrollbar': { display: 'none' }, 25 | }, 26 | heading: { 27 | // fontFamily: 'Inter', 28 | // fontWeight: 400, // When adding additional weights, make sure to add the import to _app.tsx 29 | }, 30 | body: { 31 | color: colors.white, 32 | backgroundColor: colors.black, 33 | }, 34 | }), 35 | }, 36 | } as ThemeOverride); 37 | 38 | export * from './colors'; 39 | -------------------------------------------------------------------------------- /packages/database/src/entities/JudgingSession.ts: -------------------------------------------------------------------------------- 1 | import { Collection, EntityDTO, ManyToMany, ManyToOne, Property, Ref } from '@mikro-orm/core'; 2 | import { v4 } from 'uuid'; 3 | import { Node } from './Node'; 4 | import { ConstructorValues } from '../types/ConstructorValues'; 5 | import { User } from './User'; 6 | import { Project } from './Project'; 7 | 8 | export type JudgingSessionDTO = EntityDTO; 9 | 10 | export type JudgingSessionConstructorValues = ConstructorValues< 11 | JudgingSession, 12 | 'inviteCode' | 'projects' 13 | >; 14 | 15 | export abstract class JudgingSession extends Node { 16 | @Property({ columnType: 'text' }) 17 | title: string; 18 | 19 | @Property({ unique: true }) 20 | inviteCode: string = v4(); 21 | 22 | @ManyToOne({ entity: () => User, ref: true }) 23 | createdBy: Ref; 24 | 25 | @ManyToMany({ entity: () => Project }) 26 | projects = new Collection(this); 27 | 28 | constructor({ title, createdBy }: JudgingSessionConstructorValues) { 29 | super(); 30 | 31 | this.title = title; 32 | this.createdBy = createdBy; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/database/src/migrations/0008-drops-judge_expojudgingsessioncontexts-table.ts: -------------------------------------------------------------------------------- 1 | import { Migration } from '@mikro-orm/migrations'; 2 | 3 | export class Migration20231002213723 extends Migration { 4 | 5 | async up(): Promise { 6 | this.addSql('drop table if exists "Judge_expoJudgingSessionContexts" cascade;'); 7 | } 8 | 9 | async down(): Promise { 10 | this.addSql('create table "Judge_expoJudgingSessionContexts" ("judge" bigint not null, "expoJudgingSessionContext" bigint not null, constraint "Judge_expoJudgingSessionContexts_pkey" primary key ("judge", "expoJudgingSessionContext"));'); 11 | 12 | this.addSql('alter table "Judge_expoJudgingSessionContexts" add constraint "Judge_expoJudgingSessionContexts_judge_foreign" foreign key ("judge") references "Judge" ("id") on update cascade on delete cascade;'); 13 | this.addSql('alter table "Judge_expoJudgingSessionContexts" add constraint "Judge_expoJudgingSessionContexts_expoJudgingSessi_dd190_foreign" foreign key ("expoJudgingSessionContext") references "ExpoJudgingSessionContext" ("id") on update cascade on delete cascade;'); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /packages/api/tests/testUtils/createMockEntityManager.ts: -------------------------------------------------------------------------------- 1 | import { EntityManager } from '@mikro-orm/postgresql'; 2 | 3 | type MockEntityManagerDefaults = Partial>; 4 | 5 | export const createMockEntityManager = (defaults?: MockEntityManagerDefaults) => { 6 | const em = { 7 | count: jest.fn(), 8 | getConnection: jest.fn().mockReturnValue({ isConnected: jest.fn().mockReturnValue(true) }), 9 | find: jest.fn(), 10 | findOne: jest.fn(), 11 | findOneOrFail: jest.fn(), 12 | flush: jest.fn(), 13 | persist: jest.fn(), 14 | persistAndFlush: jest.fn(), 15 | transactional: jest.fn(), 16 | populate: jest.fn(), 17 | ...defaults, 18 | }; 19 | 20 | if (!(defaults ?? {}).transactional) { 21 | // The transaction needs a reference to an entity manager 22 | // so we need to implement it AFTER initialization so 23 | // the em can be used within the transaction itself 24 | em.transactional.mockImplementation((async ( 25 | transaction: (em: EntityManager) => Promise, 26 | ) => transaction(em as any)) as jest.Mock); 27 | } 28 | 29 | return em; 30 | }; 31 | -------------------------------------------------------------------------------- /packages/database/seeds/seeders/EventSeeder.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | import { EntityManager } from '@mikro-orm/core'; 3 | import dayjs from 'dayjs'; 4 | import { Seeder } from '@mikro-orm/seeder'; 5 | import { EventFactory } from '../factories/EventFactory'; 6 | 7 | const eventsToMake = 20; 8 | 9 | export class EventSeeder extends Seeder { 10 | run = async (em: EntityManager): Promise => { 11 | const eventFactory = new EventFactory(em); 12 | 13 | let now = dayjs().subtract(4, 'hours').startOf('hour'); 14 | 15 | for (let i = 0; i < eventsToMake; i += 1) { 16 | const duration = Math.round(Math.random()) * 30 + 30; // 30 or 60 mins 17 | const randStartTimeShift = Math.random() > 0.8 ? 30 * Math.ceil(Math.random() * 5) : 0; // Random amount of time between 0 mins and 150 mins 18 | const start = now.add(randStartTimeShift, 'minutes'); // Shift the start time by a random amount 20% of the time 19 | const end = start.add(duration, 'minutes'); 20 | now = end; 21 | em.persist(eventFactory.makeOne({ start: start.toDate(), end: end.toDate() })); 22 | } 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/web/src/components/Prizes/PrizeCard.tsx: -------------------------------------------------------------------------------- 1 | import { ListItem, Heading, Flex, Text, Button } from '@chakra-ui/react'; 2 | import { Prize } from '@hangar/shared'; 3 | import NextLink from 'next/link'; 4 | import { colors } from '../../theme'; 5 | 6 | type PrizeCardProps = { 7 | prize: Prize; 8 | }; 9 | 10 | const RANKING = ['🥇', '🥈', '🥉']; 11 | const BONUS = '✨'; 12 | 13 | export const PrizeCard: React.FC = ({ prize }) => { 14 | const { name, description, position, isBonus, winner } = prize; 15 | 16 | return ( 17 | 18 | 19 | 20 | 21 | {isBonus ? BONUS : RANKING[position]} 22 | {name} 23 | 24 | 25 | 26 | {description} 27 | {winner && ( 28 | 29 | 30 | 31 | )} 32 | 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /packages/database/seeds/DatabaseSeeder.ts: -------------------------------------------------------------------------------- 1 | import { Seeder } from '@mikro-orm/seeder'; 2 | import { EntityManager } from '@mikro-orm/postgresql'; 3 | import { UserSeeder } from './seeders/UserSeeder'; 4 | import { AdminSeeder } from './seeders/AdminSeeder'; 5 | import { EventSeeder } from './seeders/EventSeeder'; 6 | import { PrizeSeeder } from './seeders/PrizeSeeder'; 7 | import { JudgeSeeder } from './seeders/JudgeSeeder'; 8 | import { ProjectSeeder } from './seeders/ProjectSeeder'; 9 | import { ExpoJudgingSessionSeeder } from './seeders/ExpoJudgingSessionSeeder'; 10 | import { ExpoJudgingVoteSeeder } from './seeders/ExpoJudgingVoteSeeder'; 11 | import { ExpoJudgingSessionContextSeeder } from './seeders/ExpoJudgingSessionContextSeeder'; 12 | 13 | export class DatabaseSeeder extends Seeder { 14 | run = async (em: EntityManager): Promise => 15 | this.call(em, [ 16 | UserSeeder, 17 | AdminSeeder, 18 | ProjectSeeder, 19 | ExpoJudgingSessionSeeder, 20 | JudgeSeeder, 21 | ExpoJudgingSessionContextSeeder, 22 | ExpoJudgingVoteSeeder, 23 | EventSeeder, 24 | PrizeSeeder, 25 | ]); 26 | } 27 | -------------------------------------------------------------------------------- /packages/shared/src/schema/criteria/criteria.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const Validation = { 4 | TITLE_MIN_LENGTH: 3, 5 | TITLE_MAX_LENGTH: 30, 6 | DESCRIPTION_MIN_LENGTH: 0, 7 | DESCRIPTION_MAX_LENGTH: 100, 8 | WEIGHT_MIN: 0.1, 9 | WEIGHT_MAX: 1, 10 | SCALE_MIN: 0, 11 | SCALE_MAX: 7, 12 | SCALE_DESCRIPTION_MAX_LENGTH: 500, 13 | }; 14 | 15 | const integerError = 'Number must be an integer'; 16 | 17 | export const criteria = z.object({ 18 | title: z.string().min(Validation.TITLE_MIN_LENGTH).max(Validation.TITLE_MAX_LENGTH), 19 | description: z 20 | .string() 21 | .min(Validation.DESCRIPTION_MIN_LENGTH) 22 | .max(Validation.DESCRIPTION_MAX_LENGTH), 23 | weight: z.coerce.number().min(Validation.WEIGHT_MIN).max(Validation.WEIGHT_MAX), 24 | scaleMin: z.coerce.number().int(integerError).min(Validation.SCALE_MIN).max(Validation.SCALE_MAX), 25 | scaleMax: z.coerce.number().int(integerError).min(Validation.SCALE_MIN).max(Validation.SCALE_MAX), 26 | scaleDescription: z 27 | .string() 28 | .min(Validation.DESCRIPTION_MIN_LENGTH) 29 | .max(Validation.SCALE_DESCRIPTION_MAX_LENGTH), 30 | }); 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 American Airlines 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 | -------------------------------------------------------------------------------- /Authors.md: -------------------------------------------------------------------------------- 1 | # Authors 2 | 3 | - Spencer Kaiser (spencer.kaiser@aa.com) 4 | - John Kahn (john.kahn@aa.com) 5 | - Jeremy Moorman (jeremy.moorman@aa.com) 6 | - Melinda Malmgren (melinda.malmgren@aa.com) 7 | - Charlie Albright (charlie.albright@aa.com) 8 | - Smit Shah (shah.smit@hotmail.com) 9 | - Austin Slobodnik (austinslobodnik@gmail.com) 10 | - Osman Findik (osmanfindik1@windowslive.com) 11 | - Maha Siddique (mxs151731@utdallas.edu) 12 | - Dhruv Tailor (dhruv6198@gmail.com) 13 | - Joseph Nalepka (jwn170030@utdallas.edu) 14 | - Tristen Even (tristengeven@gmail.com) 15 | - Tatum Hutton (tatumhutton@gmail.com) 16 | - Michael Nguyen (mkn170030@utdallas.edu) 17 | - Razvan Preotu (rnp170130@utdallas.edu) 18 | - Evelio Sosa (evsosa@tamu.edu) 19 | - Yun Phelps (phelps.yun@tamu.edu) 20 | - Allyson King (allysonmking@tamu.edu) 21 | - Olin Zhou (olin@tamu.edu) 22 | - Natalie Martinez (mart125074@tamu.edu) 23 | - Turner Levey (turnerlevey@tamu.edu) 24 | - Demondre Livinston (demondre.livingston@aa.com) 25 | - Ansaar Syed (ansaar.syed@aa.com) 26 | - Alexander Allen (alexander.allen@aa.com) 27 | - Grant Hill (grant.hill1@aa.com) 28 | - Saraah Cooper (saraah.cooper@aa.com) 29 | -------------------------------------------------------------------------------- /packages/api/src/middleware/expoJudgeAccessMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { ExpoJudgingSession, ExpoJudgingSessionContext } from '@hangar/database'; 2 | import { NextFunction, Request, Response } from 'express'; 3 | import { logger } from '../utils/logger'; 4 | 5 | export const expoJudgeAccessMiddleware = async ( 6 | req: Request, 7 | res: Response, 8 | next: NextFunction, 9 | ) => { 10 | const { 11 | judge, 12 | entityManager: em, 13 | params: { id: ejsId }, 14 | } = req; 15 | 16 | try { 17 | const ejsCount = await em.count(ExpoJudgingSession, { id: ejsId as string }); 18 | if (ejsCount === 0) { 19 | res.sendStatus(404); 20 | return; 21 | } 22 | 23 | const contextCount = await em.count(ExpoJudgingSessionContext, { 24 | expoJudgingSession: { 25 | id: ejsId, 26 | }, 27 | judge: { 28 | id: judge.id, 29 | }, 30 | }); 31 | 32 | if (!contextCount) { 33 | res.sendStatus(403); 34 | return; 35 | } 36 | 37 | next(); 38 | } catch (error) { 39 | logger.error('Failed to evaluate expo judge access', error); 40 | res.sendStatus(500); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /packages/web/src/components/Admin/CriteriaJudgingSessionForm/CriteriaList/CriteriaList.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, FormControl, FormLabel, Link } from '@chakra-ui/react'; 2 | import { Schema } from '@hangar/shared'; 3 | import { CriteriaFields, CriteriaFieldsProps } from './CriteriaFields'; 4 | 5 | export type CriteriaListProps = Omit & {}; 6 | 7 | export const CriteriaList: React.FC = (props) => ( 8 | 9 | Criteria 10 | 11 | {props.formik.values.criteriaList.map((criteria, index) => ( 12 | // eslint-disable-next-line react/no-array-index-key 13 | 14 | ))} 15 | 16 | 17 | = 20 | Schema.criteriaJudgingSession.PostValidation.MAX_CRITERIA 21 | } 22 | onClick={props.addCriteria} 23 | > 24 | Add Criteria 25 | 26 | 27 | 28 | ); 29 | -------------------------------------------------------------------------------- /packages/web/src/components/ExpoJudging/hooks/useExpoJudging/fetchProjects.ts: -------------------------------------------------------------------------------- 1 | import { ExpoJudgingSessionProjects, SerializedExpoJudgingSessionProjects } from '@hangar/shared'; 2 | import axios from 'axios'; 3 | import dayjs from 'dayjs'; 4 | 5 | type FetchProjectsArgs = { 6 | expoJudgingSessionId: string; 7 | }; 8 | 9 | export const fetchProjects = async ({ 10 | expoJudgingSessionId: id, 11 | }: FetchProjectsArgs): Promise => { 12 | const res = await axios.get( 13 | `/api/expoJudgingSession/${id}/projects`, 14 | ); 15 | const { currentProject, previousProject } = res.data; 16 | return { 17 | currentProject: currentProject 18 | ? { 19 | ...currentProject, 20 | createdAt: dayjs(currentProject.createdAt), 21 | updatedAt: dayjs(currentProject.updatedAt), 22 | } 23 | : undefined, 24 | previousProject: previousProject 25 | ? { 26 | ...previousProject, 27 | createdAt: dayjs(previousProject.createdAt), 28 | updatedAt: dayjs(previousProject.updatedAt), 29 | } 30 | : undefined, 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /packages/api/.env.sample: -------------------------------------------------------------------------------- 1 | # cSpell:disable 2 | 3 | # NOTE: All commented out variables below are optional but some are required when running docker locally 4 | 5 | #### Generic options #### 6 | NODE_ENV=development 7 | PORT=3000 8 | SESSION_SECRET=XXXXX 9 | 10 | # No ending slash 11 | NEXT_PUBLIC_BASE_URL="http://localhost:3000" 12 | 13 | #### Database #### 14 | DATABASE_URL=postgresql://localhost:5432/hangar 15 | DB_LOGGING_ENABLED=true 16 | DISABLE_DATABASE_SSL=true 17 | # REQUIRED FOR PC DEVS AND DOCKER, macOS DEVS MAY NEED USER: 18 | # DATABASE_PASS= 19 | # DATABASE_USER= 20 | 21 | #### Ping Federate Auth #### 22 | PINGFED_CLIENT_ID="XXXXXXXXXXXX" 23 | PINGFED_CLIENT_SECRET="XXXXXXXX" 24 | PINGFED_AUTH_BASE_URL="XXXXXXXX" 25 | PINGFED_TOKEN_BASE_URL="XXXXXXX" 26 | 27 | #### Slack Workspace Invite Button #### 28 | # Set if you'd like a "Join Slack" button in the UI 29 | # NEXT_PUBLIC_SLACK_INVITE_URL=https://join.slack.com/XXX 30 | 31 | #### Slack Auth - See README #### 32 | # NEXT_PUBLIC_SLACK_WORKSPACE_NAME=your-slack-workspace 33 | # SLACK_BOT_TOKEN="xoxb-XXXXXXXX" 34 | # SLACK_SIGNING_SECRET="XXXXX" 35 | # SLACK_CLIENT_ID="XXXXX" 36 | # SLACK_CLIENT_SECRET="XXXXXX" 37 | 38 | -------------------------------------------------------------------------------- /packages/api/src/middleware/judgeMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import { Judge } from '@hangar/database'; 3 | import { logger } from '../utils/logger'; 4 | 5 | /** 6 | * Middleware that evaluates the judge's validity 7 | * 8 | * Paths: 9 | * - Next function invoked: judge was identified and matching object mounted to request 10 | * - 401: Valid Session not found 11 | * - 403: Judge not found 12 | * - 500: Error occurred trying to identify 13 | */ 14 | 15 | export const judgeMiddleware = async (req: Request, res: Response, next: NextFunction) => { 16 | let judge; 17 | 18 | if (!req.session?.id) { 19 | res.sendStatus(401); 20 | return; 21 | } 22 | 23 | try { 24 | judge = await req.entityManager.findOne(Judge, { user: req.session.id }); 25 | } catch { 26 | res.sendStatus(500); 27 | return; 28 | } 29 | 30 | if (!judge) { 31 | // Judge does not exits in database 32 | logger.debug(`JudgeMiddleware failed to authenticate user - ${req.originalUrl}`); 33 | res.status(403).send('Judge validation failed for user'); 34 | return; 35 | } 36 | req.judge = judge; 37 | next(); 38 | }; 39 | -------------------------------------------------------------------------------- /packages/database/seeds/seeders/ExpoJudgingSessionSeeder.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | import { EntityManager, ref } from '@mikro-orm/core'; 3 | import { Seeder } from '@mikro-orm/seeder'; 4 | import { env } from '../env'; 5 | import { ExpoJudgingSession, Project, User } from '../../src'; 6 | 7 | const numSessions = 2; 8 | 9 | export class ExpoJudgingSessionSeeder extends Seeder { 10 | run = async (em: EntityManager): Promise => { 11 | if (env.primaryUserIsAdmin) { 12 | try { 13 | const projects = await em.find(Project, {}); 14 | const admin = await em.findOneOrFail(User, { id: '1' }); 15 | 16 | for (let i = 0; i < numSessions; i += 1) { 17 | const expoJudgingSession = new ExpoJudgingSession({ 18 | createdBy: ref(admin), 19 | title: `Expo Judging Session ${i + 1}`, 20 | }); 21 | expoJudgingSession.projects.set(projects); 22 | 23 | em.persist(expoJudgingSession); 24 | } 25 | } catch { 26 | // eslint-disable-next-line no-console 27 | console.error('Failed to create a judging session'); 28 | } 29 | } 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /packages/web/src/pageUtils/admin/criteriaJudgingSession/[id]/fetchCriteriaJudgingSessionResults.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CriteriaJudgingSessionResults, 3 | SerializedCriteriaJudgingSessionResults, 4 | } from '@hangar/shared'; 5 | import dayjs from 'dayjs'; 6 | import axios from 'axios'; 7 | import { useCustomToast } from '../../../../components/utils/CustomToast'; 8 | 9 | type FetchExpoJudgingSessionArgs = { 10 | criteriaJudgingSessionId: string; 11 | }; 12 | 13 | export const fetchCriteriaJudgingSessionResults = async ({ 14 | criteriaJudgingSessionId, 15 | }: FetchExpoJudgingSessionArgs): Promise => { 16 | try { 17 | const res = await axios.get( 18 | `/api/criteriaJudgingSession/${criteriaJudgingSessionId}/results`, 19 | ); 20 | return res.data.map((p) => ({ 21 | ...p, 22 | createdAt: dayjs(p.createdAt), 23 | updatedAt: dayjs(p.updatedAt), 24 | })); 25 | } catch (error) { 26 | useCustomToast.getState().openErrorToast({ title: 'Failed to fetch results' }); 27 | // eslint-disable-next-line no-console 28 | console.error(error); 29 | return []; 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /packages/web/src/components/CriteriaJudging/CriteriaJudgingForm/ProjectDetails.tsx: -------------------------------------------------------------------------------- 1 | import { Text, Flex, Heading, Button, Box } from '@chakra-ui/react'; 2 | import { Config, Project } from '@hangar/shared'; 3 | import { colors } from '../../../theme'; 4 | 5 | type ProjectDetailsProps = { 6 | project: Project; 7 | }; 8 | 9 | export const ProjectDetails: React.FC = ({ project }) => ( 10 | 11 | {project.name} 12 | 13 | {project.description} 14 | 15 | 16 | 21 | {project.location && ( 22 | 23 | {Config.project.locationLabel}: {project.location} 24 | 25 | )} 26 | 27 | 28 | 35 | 36 | 37 | 38 | ); 39 | -------------------------------------------------------------------------------- /packages/web/src/components/EventsList/EventsList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Event } from '@hangar/shared'; 3 | import dayjs from 'dayjs'; 4 | import isBetween from 'dayjs/plugin/isBetween'; 5 | import { UnorderedList } from '@chakra-ui/react'; 6 | import { EventCard } from './EventCard'; 7 | 8 | dayjs.extend(isBetween); 9 | 10 | export const EventsList: React.FC<{ events: Event[] }> = ({ events }) => { 11 | const nextEventCardRef = React.useRef(null); 12 | 13 | const nextEventId = React.useMemo( 14 | () => events.find((event) => dayjs().isBefore(event.end))?.id, // Grab the id of the first event that isn't in the past 15 | [events], 16 | ); 17 | 18 | React.useEffect(() => { 19 | nextEventCardRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }); 20 | }, [nextEventId]); 21 | 22 | return ( 23 | 24 | {events.map((event) => ( 25 | 30 | ))} 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/shared/tests/schema/criteriaJudgingSession/criteriaJudgingSession.test.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { post } from '../../../src/schema/criteriaJudgingSession'; 3 | import { validCriteria } from '../criteria/criteria.test'; 4 | 5 | export const validCriteriaJudgingSession: z.infer = { 6 | title: 'Some Criteria', 7 | description: 'A criteria that has a purpose', 8 | criteriaList: [{ ...validCriteria }], 9 | }; 10 | 11 | describe('criteriaJudgingSession', () => { 12 | it('validates a correct object', () => { 13 | const result = post.safeParse({ ...validCriteriaJudgingSession }); 14 | expect(result.success).toBe(true); 15 | }); 16 | 17 | describe('invalid parsing', () => { 18 | it('rejects a missing title', () => { 19 | const { title, ...invalidCriteriaJudgingSession } = validCriteriaJudgingSession; 20 | const result = post.safeParse(invalidCriteriaJudgingSession); 21 | expect(result.success).toBe(false); 22 | }); 23 | 24 | it('rejects missing criteria', () => { 25 | const result = post.safeParse({ ...validCriteriaJudgingSession, criteriaList: [] }); 26 | expect(result.success).toBe(false); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/web/src/components/Admin/CriteriaJudgingSessionProjectResults/CriteriaJudgingSessionProjectResults.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Flex, Heading, Stat, StatLabel, StatNumber, Text } from '@chakra-ui/react'; 2 | import { CriteriaJudgingSessionResult } from '@hangar/shared'; 3 | import { colors } from '../../../theme'; 4 | 5 | type CriteriaJudgingSessionProjectResultsProps = { 6 | projectWithResults: CriteriaJudgingSessionResult; 7 | }; 8 | 9 | export const CriteriaJudgingSessionProjectResults: React.FC< 10 | CriteriaJudgingSessionProjectResultsProps 11 | > = ({ projectWithResults: project }) => { 12 | if (project.results === undefined) return null; 13 | 14 | return ( 15 | 16 | 17 | {project.name} 18 | 19 | 20 | Score 21 | {project.results.score.toFixed(2)} 22 | 23 | 24 | 25 | {project.description} 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /packages/web/src/stores/expoJudgingSession/createExpoJudgingSession.ts: -------------------------------------------------------------------------------- 1 | import { SerializedExpoJudgingSession, Schema, ExpoJudgingSession } from '@hangar/shared'; 2 | import z from 'zod'; 3 | import axios from 'axios'; 4 | import dayjs from 'dayjs'; 5 | import { openErrorToast } from '../../components/utils/CustomToast'; 6 | 7 | type CreateExpoJudgingSessionArgs = z.infer; 8 | 9 | export const createExpoJudgingSession = async (args: CreateExpoJudgingSessionArgs) => { 10 | try { 11 | const { data } = await axios.post( 12 | `/api/expoJudgingSession`, 13 | args, 14 | ); 15 | const { createdAt, updatedAt, ...rest } = data; 16 | return { 17 | ...rest, 18 | createdAt: dayjs(createdAt), 19 | updatedAt: dayjs(updatedAt), 20 | } as ExpoJudgingSession; 21 | } catch (error) { 22 | if (!axios.isAxiosError(error) || error.status !== 401) { 23 | // eslint-disable-next-line no-console 24 | console.error(error); 25 | openErrorToast({ 26 | title: 'Failed to create Expo Judging Session', 27 | description: (error as Error).message, 28 | }); 29 | } 30 | throw error; 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /packages/web/src/stores/expoJudgingSession/expoJudgingSession.ts: -------------------------------------------------------------------------------- 1 | import { ExpoJudgingSession, wait } from '@hangar/shared'; 2 | import { create } from 'zustand'; 3 | import { fetchExpoJudgingSessions } from './fetchExpoJudgingSessions'; 4 | import { createExpoJudgingSession } from './createExpoJudgingSession'; 5 | 6 | type ExpoJudgingSessionStore = { 7 | expoJudgingSessions?: ExpoJudgingSession[]; 8 | doneLoading: boolean; 9 | fetchExpoJudgingSessions: () => Promise; 10 | addExpoJudgingSession: (...args: Parameters) => Promise; 11 | }; 12 | 13 | export const useExpoJudgingSessionStore = create((set) => ({ 14 | expoJudgingSessions: undefined, 15 | doneLoading: false, 16 | fetchExpoJudgingSessions: async () => { 17 | const [expoJudgingSessions] = await Promise.all([fetchExpoJudgingSessions(), wait(1000)]); 18 | set({ 19 | expoJudgingSessions, 20 | doneLoading: true, 21 | }); 22 | }, 23 | addExpoJudgingSession: async (...args) => { 24 | const newEjs = await createExpoJudgingSession(...args); 25 | 26 | set((state) => ({ 27 | expoJudgingSessions: [...(state.expoJudgingSessions ?? []), newEjs], 28 | })); 29 | }, 30 | })); 31 | -------------------------------------------------------------------------------- /packages/web/src/stores/admin.ts: -------------------------------------------------------------------------------- 1 | import { SerializedAdmin, Admin } from '@hangar/shared'; 2 | import axios from 'axios'; 3 | import dayjs from 'dayjs'; 4 | import { create } from 'zustand'; 5 | import { openErrorToast } from '../components/utils/CustomToast'; 6 | 7 | type AdminStore = { 8 | admin?: Admin; 9 | doneLoading: boolean; 10 | fetchAdmin: () => Promise; 11 | }; 12 | 13 | export const useAdminStore = create((set) => ({ 14 | admin: undefined, 15 | doneLoading: false, 16 | fetchAdmin: async () => { 17 | let admin: Admin | undefined; 18 | try { 19 | const res = await axios.get('/api/admin/me'); 20 | const { createdAt, updatedAt, ...rest } = res.data; 21 | admin = { ...rest, createdAt: dayjs(createdAt), updatedAt: dayjs(updatedAt) }; 22 | } catch (error) { 23 | if (!axios.isAxiosError(error) || ![401, 403].includes(error.response?.status as number)) { 24 | // eslint-disable-next-line no-console 25 | console.error(error); 26 | openErrorToast({ 27 | title: 'Failed to fetch admin details', 28 | description: (error as Error).message, 29 | }); 30 | } 31 | } 32 | set({ admin, doneLoading: true }); 33 | }, 34 | })); 35 | --------------------------------------------------------------------------------