diff --git a/apps/site/.eslintrc.json b/apps/site/.eslintrc.json index 0c0979dd..3508f915 100644 --- a/apps/site/.eslintrc.json +++ b/apps/site/.eslintrc.json @@ -4,10 +4,14 @@ "plugin:@typescript-eslint/recommended", "next/core-web-vitals" ], + "parserOptions": { + "project": "./tsconfig.json" + }, "rules": { "eqeqeq": "error", "no-unused-expressions": "warn", "@typescript-eslint/no-unused-vars": "warn", + "@typescript-eslint/switch-exhaustiveness-check": "error", "react/button-has-type": "error", "react/no-danger": "warn", "react/no-unstable-nested-components": "error", diff --git a/apps/site/src/app/(main)/portal/@applicant/ApplicantPortal.tsx b/apps/site/src/app/(main)/portal/@applicant/ApplicantPortal.tsx index 0aa99e4c..11c1d2ae 100644 --- a/apps/site/src/app/(main)/portal/@applicant/ApplicantPortal.tsx +++ b/apps/site/src/app/(main)/portal/@applicant/ApplicantPortal.tsx @@ -3,6 +3,7 @@ import { redirect } from "next/navigation"; import useUserIdentity from "@/lib/utils/useUserIdentity"; +import { Status } from "@/lib/userRecord"; import ConfirmAttendance from "./components/ConfirmAttendance"; import Message from "./components/Message"; @@ -11,20 +12,6 @@ import ReturnHome from "./components/ReturnHome"; import VerticalTimeline from "./components/timeline/VerticalTimeline"; import BackgroundStars from "./components/BackgroundStars"; -// TODO: use common Status enum from userRecord.ts - -export const enum PortalStatus { - pending = "PENDING_REVIEW", - reviewed = "REVIEWED", - accepted = "ACCEPTED", - rejected = "REJECTED", - waitlisted = "WAITLISTED", - waived = "WAIVER_SIGNED", - confirmed = "CONFIRMED", - attending = "ATTENDING", - void = "VOID", -} - const rolesArray = ["Mentor", "Hacker", "Volunteer"]; function Portal() { @@ -45,12 +32,12 @@ function Portal() { ); const submittedWaiver = - status === PortalStatus.waived || - status === PortalStatus.confirmed || - status === PortalStatus.attending; + status === Status.Signed || + status === Status.Confirmed || + status === Status.Attending; - const needsToSignWaiver = status === PortalStatus.accepted; - const rejected = status === PortalStatus.rejected; + const needsToSignWaiver = status === Status.Accepted; + const rejected = status === Status.Rejected; return (
@@ -59,12 +46,10 @@ function Portal() {

{roleToDisplay} Application Status

- - + + {needsToSignWaiver && } - {submittedWaiver && ( - - )} + {submittedWaiver && } {rejected && }
diff --git a/apps/site/src/app/(main)/portal/@applicant/components/ConfirmAttendance.tsx b/apps/site/src/app/(main)/portal/@applicant/components/ConfirmAttendance.tsx index db19b1ec..10b7673e 100644 --- a/apps/site/src/app/(main)/portal/@applicant/components/ConfirmAttendance.tsx +++ b/apps/site/src/app/(main)/portal/@applicant/components/ConfirmAttendance.tsx @@ -1,13 +1,13 @@ -import { PortalStatus } from "../ApplicantPortal"; +import { Status } from "@/lib/userRecord"; import RsvpForm from "./RsvpForm"; interface ConfirmAttendanceProps { - status: PortalStatus; + status: Status; } function ConfirmAttendance({ status }: ConfirmAttendanceProps) { const buttonText = - status === PortalStatus.confirmed || status === PortalStatus.attending + status === Status.Confirmed || status === Status.Attending ? "I am no longer able to attend IrvineHacks 2025" : "I will be attending IrvineHacks 2025"; @@ -16,15 +16,14 @@ function ConfirmAttendance({ status }: ConfirmAttendanceProps) {

RSVP

- {status === PortalStatus.confirmed || - status === PortalStatus.attending ? ( + {status === Status.Confirmed || status === Status.Attending ? ( <>

Thank you for confirming your attendance! We look forward to seeing you at IrvineHacks! If you are no longer able to attend, please let us know using the button below.

- {status === PortalStatus.attending && ( + {status === Status.Attending && ( WARNING: After clicking the button below, you will{" "} NOT be able to RSVP again. @@ -40,7 +39,7 @@ function ConfirmAttendance({ status }: ConfirmAttendanceProps) {
diff --git a/apps/site/src/app/(main)/portal/@applicant/components/Message.tsx b/apps/site/src/app/(main)/portal/@applicant/components/Message.tsx index 45c35386..6d008aa0 100644 --- a/apps/site/src/app/(main)/portal/@applicant/components/Message.tsx +++ b/apps/site/src/app/(main)/portal/@applicant/components/Message.tsx @@ -1,53 +1,77 @@ -import { PortalStatus } from "../ApplicantPortal"; +import { Status } from "@/lib/userRecord"; interface MessageProps { - status: PortalStatus; + status: Status; } function Message({ status }: MessageProps) { - const submittedMessage = ( -

- Thank you for submitting your application! We are currently reviewing - applications on a rolling basis, and you will hear back from us soon! -

- ); + let message: JSX.Element; + + switch (status) { + case Status.Pending: + case Status.Reviewed: { + message = ( +

+ Thank you for submitting your application! We are currently reviewing + applications on a rolling basis, and you will hear back from us soon! +

+ ); + break; + } + + case Status.Rejected: { + message = ( +

+ Thank you for applying to IrvineHacks this year. We have read through + many applications so far, and unfortunately are unable to offer you a + spot at our event. We highly encourage you to continue developing your + skills and passion for technology. We would love to see you apply + again next year! +

+ ); + break; + } + + case Status.Waitlisted: { + message = ( +

+ Thank you for applying to IrvineHacks this year. We have read through + many applications so far, and are able to offer you a spot on the + event waitlist. Please check your email for more info about the + waitlist and waitlist walk-ins! +

+ ); + break; + } + + case Status.Accepted: + case Status.Signed: + case Status.Confirmed: + case Status.Attending: { + message = <>; + break; + } + + case Status.Void: { + message = ( +

+ Unfortunately, you are not able to RSVP for IrvineHacks at this time + and will not be able to come to the event. However, we would love to + see you apply again next year! +

+ ); + break; + } - const messages: Record = { - [PortalStatus.pending]: submittedMessage, - [PortalStatus.reviewed]: submittedMessage, - [PortalStatus.rejected]: ( -

- Thank you for applying to IrvineHacks this year. We have read through - many applications so far, and unfortunately are unable to offer you a - spot at our event. We highly encourage you to continue developing your - skills and passion for technology. We would love to see you apply again - next year! -

- ), - [PortalStatus.waitlisted]: ( -

- Thank you for applying to IrvineHacks this year. We have read through - many applications so far, and are able to offer you a spot on the event - waitlist. Please check your email for more info about the waitlist and - waitlist walk-ins! -

- ), - [PortalStatus.accepted]: <>, - [PortalStatus.waived]: <>, - [PortalStatus.confirmed]: <>, - [PortalStatus.attending]: <>, - [PortalStatus.void]: ( -

- Unfortunately, you are not able to RSVP for IrvineHacks at this time and - will not be able to come to the event. However, we would love to see you - apply again next year! -

- ), - }; + default: { + const exhaustiveCheck: never = status; + throw new Error(`Unhandled status: ${exhaustiveCheck}`); + } + } return (
- {messages[status]} + {message}
); } diff --git a/apps/site/src/app/(main)/portal/@applicant/components/timeline/RSVPComponent.tsx b/apps/site/src/app/(main)/portal/@applicant/components/timeline/RSVPComponent.tsx index 9603b96b..fd493dee 100644 --- a/apps/site/src/app/(main)/portal/@applicant/components/timeline/RSVPComponent.tsx +++ b/apps/site/src/app/(main)/portal/@applicant/components/timeline/RSVPComponent.tsx @@ -1,26 +1,23 @@ import { TimelineComponent } from "./TimelineComponent"; -import { PortalStatus } from "../.././ApplicantPortal"; +import { Status } from "@/lib/userRecord"; import { StatusImageProps } from "./StatusImage"; -export const RSVPComponent = ({ status }: { status: PortalStatus }) => { +export const RSVPComponent = ({ status }: { status: Status }) => { let verdict = null; - if (status === PortalStatus.accepted || status === PortalStatus.waived) { + if (status === Status.Accepted || status === Status.Signed) { verdict = { text: "Confirm Attendance", finished: false, statusIcon: "Pending", }; - } else if ( - status === PortalStatus.confirmed || - status === PortalStatus.attending - ) { + } else if (status === Status.Confirmed || status === Status.Attending) { verdict = { text: "Attendance Confirmed", finished: true, statusIcon: "Accepted", }; - } else if (status === PortalStatus.void) { + } else if (status === Status.Void) { verdict = { text: "No RSVP Indicated", finished: false, diff --git a/apps/site/src/app/(main)/portal/@applicant/components/timeline/VerdictComponent.tsx b/apps/site/src/app/(main)/portal/@applicant/components/timeline/VerdictComponent.tsx index 42ad2407..39e6d431 100644 --- a/apps/site/src/app/(main)/portal/@applicant/components/timeline/VerdictComponent.tsx +++ b/apps/site/src/app/(main)/portal/@applicant/components/timeline/VerdictComponent.tsx @@ -1,50 +1,67 @@ import { TimelineComponent } from "./TimelineComponent"; -import { PortalStatus } from "../.././ApplicantPortal"; +import { Status } from "@/lib/userRecord"; import { StatusImageProps } from "./StatusImage"; -export const VerdictComponent = ({ status }: { status: PortalStatus }) => { - let verdict = null; +export const VerdictComponent = ({ status }: { status: Status }) => { + let verdict: { + text: string; + finished: boolean; + statusIcon: StatusImageProps["statusIcon"]; + } | null = null; - if ( - status === PortalStatus.accepted || - status === PortalStatus.void || - status === PortalStatus.waived || - status === PortalStatus.confirmed || - status === PortalStatus.attending - ) { - verdict = { - text: "Application Accepted", - finished: true, - statusIcon: "Accepted", - }; - } else if (status === PortalStatus.rejected) { - verdict = { - text: "Application Rejected", - finished: true, - statusIcon: "Rejected", - }; - } else if (status === PortalStatus.waitlisted) { - verdict = { - text: "Application Waitlisted", - finished: true, - statusIcon: "Pending", - }; - } else if ( - status === PortalStatus.pending || - status === PortalStatus.reviewed - ) { - verdict = { - text: "Application Under Review", - finished: true, - statusIcon: "Pending", - }; + switch (status) { + case Status.Accepted: + case Status.Void: + case Status.Signed: + case Status.Confirmed: + case Status.Attending: { + verdict = { + text: "Application Accepted", + finished: true, + statusIcon: "Accepted", + }; + break; + } + + case Status.Rejected: { + verdict = { + text: "Application Rejected", + finished: true, + statusIcon: "Rejected", + }; + break; + } + + case Status.Waitlisted: { + verdict = { + text: "Application Waitlisted", + finished: true, + statusIcon: "Pending", + }; + break; + } + + case Status.Pending: + case Status.Reviewed: { + verdict = { + text: "Application Under Review", + finished: true, + statusIcon: "Pending", + }; + break; + } + + default: { + const exhaustiveCheck: never = status; + throw new Error(`Unhandled status: ${exhaustiveCheck}`); + } } return verdict ? ( ) : null; }; diff --git a/apps/site/src/app/(main)/portal/@applicant/components/timeline/VerticalTimeline.tsx b/apps/site/src/app/(main)/portal/@applicant/components/timeline/VerticalTimeline.tsx index 4a91fe1a..ccc869c9 100644 --- a/apps/site/src/app/(main)/portal/@applicant/components/timeline/VerticalTimeline.tsx +++ b/apps/site/src/app/(main)/portal/@applicant/components/timeline/VerticalTimeline.tsx @@ -1,12 +1,12 @@ import React from "react"; -import { PortalStatus } from "../../ApplicantPortal"; +import { Status } from "@/lib/userRecord"; import { SubmissionComponent } from "./SubmissionComponent"; import { VerdictComponent } from "./VerdictComponent"; import { WaiverComponent } from "./WaiverComponent"; import { RSVPComponent } from "./RSVPComponent"; interface VerticalTimelineProps { - status: PortalStatus; + status: Status; } function VerticalTimeline({ status }: VerticalTimelineProps) { diff --git a/apps/site/src/app/(main)/portal/@applicant/components/timeline/WaiverComponent.tsx b/apps/site/src/app/(main)/portal/@applicant/components/timeline/WaiverComponent.tsx index 8ba9cf64..a27fcb78 100644 --- a/apps/site/src/app/(main)/portal/@applicant/components/timeline/WaiverComponent.tsx +++ b/apps/site/src/app/(main)/portal/@applicant/components/timeline/WaiverComponent.tsx @@ -1,20 +1,20 @@ import { TimelineComponent } from "./TimelineComponent"; -import { PortalStatus } from "../.././ApplicantPortal"; +import { Status } from "@/lib/userRecord"; import { StatusImageProps } from "./StatusImage"; -export const WaiverComponent = ({ status }: { status: PortalStatus }) => { +export const WaiverComponent = ({ status }: { status: Status }) => { let verdict = null; - if (status === PortalStatus.accepted) { + if (status === Status.Accepted) { verdict = { text: "Sign Waiver", finished: false, statusIcon: "Pending", }; } else if ( - status === PortalStatus.waived || - status === PortalStatus.attending || - status === PortalStatus.confirmed + status === Status.Signed || + status === Status.Attending || + status === Status.Confirmed ) { verdict = { text: "Waiver Signed", diff --git a/apps/site/src/app/admin/applicants/components/ApplicantActions.tsx b/apps/site/src/app/admin/applicants/components/ApplicantActions.tsx index a370b0e9..88058017 100644 --- a/apps/site/src/app/admin/applicants/components/ApplicantActions.tsx +++ b/apps/site/src/app/admin/applicants/components/ApplicantActions.tsx @@ -38,19 +38,19 @@ function ApplicantActions({ applicant, submitReview }: ApplicantActionsProps) { const dropdownItems: ReviewButtonItems = [ { text: "Accept", - id: Decision.accepted, + id: Decision.Accepted, iconName: "status-positive", description: "Accept the applicant", }, { text: "Waitlist", - id: Decision.waitlisted, + id: Decision.Waitlisted, iconName: "status-pending", description: "Waitlist the applicant", }, { text: "Reject", - id: Decision.rejected, + id: Decision.Rejected, iconName: "status-negative", description: "Reject the applicant", }, diff --git a/apps/site/src/app/admin/applicants/components/ApplicantFilters.tsx b/apps/site/src/app/admin/applicants/components/ApplicantFilters.tsx index 5f5c2714..7c84deb6 100644 --- a/apps/site/src/app/admin/applicants/components/ApplicantFilters.tsx +++ b/apps/site/src/app/admin/applicants/components/ApplicantFilters.tsx @@ -31,16 +31,15 @@ interface ApplicantFiltersProps { } const StatusIcons: Record = { - [ReviewStatus.pending]: "status-pending", - [ReviewStatus.reviewed]: "status-in-progress", - [ReviewStatus.released]: "status-positive", - [Decision.accepted]: "status-positive", - [Decision.rejected]: "status-pending", - [Decision.waitlisted]: "status-negative", - [PostAcceptedStatus.signed]: "status-in-progress", - [PostAcceptedStatus.confirmed]: "status-positive", - [PostAcceptedStatus.attending]: "status-positive", - [PostAcceptedStatus.void]: "status-negative", + [ReviewStatus.Pending]: "status-pending", + [ReviewStatus.Reviewed]: "status-in-progress", + [Decision.Accepted]: "status-positive", + [Decision.Rejected]: "status-pending", + [Decision.Waitlisted]: "status-negative", + [PostAcceptedStatus.Signed]: "status-in-progress", + [PostAcceptedStatus.Confirmed]: "status-positive", + [PostAcceptedStatus.Attending]: "status-positive", + [PostAcceptedStatus.Void]: "status-negative", }; const statusOption = (status: Status): MultiselectProps.Option => ({ diff --git a/apps/site/src/app/admin/applicants/components/ApplicantStatus.tsx b/apps/site/src/app/admin/applicants/components/ApplicantStatus.tsx index 7e6441a2..e416434a 100644 --- a/apps/site/src/app/admin/applicants/components/ApplicantStatus.tsx +++ b/apps/site/src/app/admin/applicants/components/ApplicantStatus.tsx @@ -5,29 +5,27 @@ import StatusIndicator, { import { Status } from "@/lib/userRecord"; export const StatusLabels = { - [Status.accepted]: "accepted", - [Status.rejected]: "rejected", - [Status.waitlisted]: "waitlisted", - [Status.pending]: "needs review", - [Status.reviewed]: "reviewed", - [Status.released]: "released", - [Status.signed]: "waiver signed", - [Status.confirmed]: "confirmed", - [Status.attending]: "attending", - [Status.void]: "void", + [Status.Accepted]: "accepted", + [Status.Rejected]: "rejected", + [Status.Waitlisted]: "waitlisted", + [Status.Pending]: "needs review", + [Status.Reviewed]: "reviewed", + [Status.Signed]: "waiver signed", + [Status.Confirmed]: "confirmed", + [Status.Attending]: "attending", + [Status.Void]: "void", }; const StatusTypes: Record = { - [Status.accepted]: "success", - [Status.rejected]: "error", - [Status.waitlisted]: "pending", - [Status.pending]: "pending", - [Status.reviewed]: "in-progress", - [Status.released]: "success", - [Status.signed]: "in-progress", - [Status.confirmed]: "info", - [Status.attending]: "success", - [Status.void]: "stopped", + [Status.Accepted]: "success", + [Status.Rejected]: "error", + [Status.Waitlisted]: "pending", + [Status.Pending]: "pending", + [Status.Reviewed]: "in-progress", + [Status.Signed]: "in-progress", + [Status.Confirmed]: "info", + [Status.Attending]: "success", + [Status.Void]: "stopped", }; interface ApplicantStatusProps { @@ -38,7 +36,7 @@ function ApplicantStatus({ status }: ApplicantStatusProps) { return ( {StatusLabels[status]} diff --git a/apps/site/src/app/admin/dashboard/components/ApplicantSummary.tsx b/apps/site/src/app/admin/dashboard/components/ApplicantSummary.tsx index 90aedab7..1a44a5e6 100644 --- a/apps/site/src/app/admin/dashboard/components/ApplicantSummary.tsx +++ b/apps/site/src/app/admin/dashboard/components/ApplicantSummary.tsx @@ -11,13 +11,15 @@ function ApplicantSummary() { const totalApplicants = Object.values(summary).reduce((s, v) => s + v, 0); const orderedData = [ - Status.rejected, - Status.waitlisted, - Status.accepted, - Status.signed, - Status.confirmed, - Status.attending, - Status.void, + Status.Pending, + Status.Reviewed, + Status.Rejected, + Status.Waitlisted, + Status.Accepted, + Status.Signed, + Status.Confirmed, + Status.Attending, + Status.Void, ].map((status) => ({ title: status, value: summary[status] ?? 0, diff --git a/apps/site/src/app/admin/participants/components/ParticipantAction.tsx b/apps/site/src/app/admin/participants/components/ParticipantAction.tsx index 50a8829c..93cc7f7b 100644 --- a/apps/site/src/app/admin/participants/components/ParticipantAction.tsx +++ b/apps/site/src/app/admin/participants/components/ParticipantAction.tsx @@ -38,8 +38,8 @@ function ParticipantAction({ const { roles } = useContext(UserContext); const canPromote = isCheckInLead(roles); - const isWaiverSigned = participant.status === Status.signed; - const isAccepted = participant.status === Status.accepted; + const isWaiverSigned = participant.status === Status.Signed; + const isAccepted = participant.status === Status.Accepted; const nonHacker = isNonHacker(participant.roles); const promoteButton = ( @@ -79,17 +79,17 @@ function ParticipantAction({ const content = !canPromote ? "Only check-in leads can confirm non-hackers." : "Must sign waiver first."; - if (!canPromote || participant.status === ReviewStatus.reviewed) { + if (!canPromote || participant.status === ReviewStatus.Reviewed) { return ( {confirmButton} ); - } else if (participant.status === Status.signed) { + } else if (participant.status === Status.Signed) { return confirmButton; } return checkinButton; - } else if (participant.status === Status.waitlisted) { + } else if (participant.status === Status.Waitlisted) { if (!canPromote) { return ( diff --git a/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx b/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx index b6c53a2d..05712d62 100644 --- a/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx +++ b/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx @@ -33,16 +33,15 @@ interface ParticipantsFiltersProps { } const StatusIcons: Record = { - [ReviewStatus.pending]: "status-pending", - [ReviewStatus.reviewed]: "status-in-progress", - [ReviewStatus.released]: "status-positive", - [Decision.accepted]: "status-positive", - [Decision.rejected]: "status-pending", - [Decision.waitlisted]: "status-negative", - [PostAcceptedStatus.signed]: "status-in-progress", - [PostAcceptedStatus.confirmed]: "status-positive", - [PostAcceptedStatus.attending]: "status-positive", - [PostAcceptedStatus.void]: "status-negative", + [ReviewStatus.Pending]: "status-pending", + [ReviewStatus.Reviewed]: "status-in-progress", + [Decision.Accepted]: "status-positive", + [Decision.Rejected]: "status-pending", + [Decision.Waitlisted]: "status-negative", + [PostAcceptedStatus.Signed]: "status-in-progress", + [PostAcceptedStatus.Confirmed]: "status-positive", + [PostAcceptedStatus.Attending]: "status-positive", + [PostAcceptedStatus.Void]: "status-negative", }; const statusOption = (status: MultiselectProps.Option) => { diff --git a/apps/site/src/lib/decisionScores.ts b/apps/site/src/lib/decisionScores.ts index a2b03f2e..156382ec 100644 --- a/apps/site/src/lib/decisionScores.ts +++ b/apps/site/src/lib/decisionScores.ts @@ -6,13 +6,13 @@ const waitlistScore = -2; const rejectScore = 0; export const decisionsToScores: Record = { - [Decision.accepted]: acceptScore, - [Decision.waitlisted]: waitlistScore, - [Decision.rejected]: rejectScore, + [Decision.Accepted]: acceptScore, + [Decision.Waitlisted]: waitlistScore, + [Decision.Rejected]: rejectScore, }; export const scoresToDecisions: Record = { - [acceptScore]: Decision.accepted, - [waitlistScore]: Decision.waitlisted, - [rejectScore]: Decision.rejected, + [acceptScore]: Decision.Accepted, + [waitlistScore]: Decision.Waitlisted, + [rejectScore]: Decision.Rejected, }; diff --git a/apps/site/src/lib/userRecord.ts b/apps/site/src/lib/userRecord.ts index da379c11..44d8b9cb 100644 --- a/apps/site/src/lib/userRecord.ts +++ b/apps/site/src/lib/userRecord.ts @@ -37,24 +37,23 @@ export type Role = ParticipantRole | AdminRole; * An applicant's decision becomes their status when released. */ export enum Decision { - accepted = "ACCEPTED", - rejected = "REJECTED", - waitlisted = "WAITLISTED", + Accepted = "ACCEPTED", + Rejected = "REJECTED", + Waitlisted = "WAITLISTED", } /** The possible statuses for an applicant without a released decision. */ export enum ReviewStatus { - pending = "PENDING_REVIEW", - reviewed = "REVIEWED", - released = "RELEASED", + Pending = "PENDING_REVIEW", + Reviewed = "REVIEWED", } /** The possible status after an applicant has been accepted. */ export enum PostAcceptedStatus { - signed = "WAIVER_SIGNED", - confirmed = "CONFIRMED", - attending = "ATTENDING", - void = "VOID", + Signed = "WAIVER_SIGNED", + Confirmed = "CONFIRMED", + Attending = "ATTENDING", + Void = "VOID", } /** All of the different possible status values. */