From 3643969fb90b79cf007986dc703faecbe7c1a2e9 Mon Sep 17 00:00:00 2001 From: Taesung Hwang Date: Wed, 3 Jan 2024 22:41:58 -0800 Subject: [PATCH 01/17] Import last year's Admin site components as-is - Import Admin site components of HackAtUCI/HackUCI-Site exactly as-is - Includes page views, subcomponents, hooks, and layout - Organize components into approximate locations but without changes --- .../src/app/admin/applicants/Applicants.tsx | 104 ++++++++++++++++++ .../app/admin/applicants/[uid]/Applicant.tsx | 59 ++++++++++ .../[uid]/components/ApplicantActions.tsx | 63 +++++++++++ .../[uid]/components/ApplicantOverview.tsx | 41 +++++++ .../[uid]/components/Application.tsx | 46 ++++++++ .../[uid]/components/ApplicationReviews.tsx | 43 ++++++++ .../[uid]/components/ApplicationSection.tsx | 76 +++++++++++++ .../components/ApplicantFilters.tsx | 67 +++++++++++ .../applicants/components/ApplicantStatus.tsx | 37 +++++++ .../app/admin/dashboard/AdminDashboard.tsx | 5 + .../site/src/app/admin/layout/AdminLayout.tsx | 45 ++++++++ .../src/app/admin/layout/AdminSidebar.tsx | 28 +++++ .../site/src/app/admin/layout/Breadcrumbs.tsx | 60 ++++++++++ apps/site/src/app/admin/layout/common.tsx | 18 +++ apps/site/src/lib/admin/useApplicant.ts | 79 +++++++++++++ apps/site/src/lib/admin/useApplicants.ts | 32 ++++++ 16 files changed, 803 insertions(+) create mode 100644 apps/site/src/app/admin/applicants/Applicants.tsx create mode 100644 apps/site/src/app/admin/applicants/[uid]/Applicant.tsx create mode 100644 apps/site/src/app/admin/applicants/[uid]/components/ApplicantActions.tsx create mode 100644 apps/site/src/app/admin/applicants/[uid]/components/ApplicantOverview.tsx create mode 100644 apps/site/src/app/admin/applicants/[uid]/components/Application.tsx create mode 100644 apps/site/src/app/admin/applicants/[uid]/components/ApplicationReviews.tsx create mode 100644 apps/site/src/app/admin/applicants/[uid]/components/ApplicationSection.tsx create mode 100644 apps/site/src/app/admin/applicants/components/ApplicantFilters.tsx create mode 100644 apps/site/src/app/admin/applicants/components/ApplicantStatus.tsx create mode 100644 apps/site/src/app/admin/dashboard/AdminDashboard.tsx create mode 100644 apps/site/src/app/admin/layout/AdminLayout.tsx create mode 100644 apps/site/src/app/admin/layout/AdminSidebar.tsx create mode 100644 apps/site/src/app/admin/layout/Breadcrumbs.tsx create mode 100644 apps/site/src/app/admin/layout/common.tsx create mode 100644 apps/site/src/lib/admin/useApplicant.ts create mode 100644 apps/site/src/lib/admin/useApplicants.ts diff --git a/apps/site/src/app/admin/applicants/Applicants.tsx b/apps/site/src/app/admin/applicants/Applicants.tsx new file mode 100644 index 00000000..81d3157d --- /dev/null +++ b/apps/site/src/app/admin/applicants/Applicants.tsx @@ -0,0 +1,104 @@ +import Link from "next/link"; +import { useState } from "react"; + +import Box from "@cloudscape-design/components/box"; +import Cards from "@cloudscape-design/components/cards"; +import Header from "@cloudscape-design/components/header"; +import { OptionDefinition } from "@cloudscape-design/components/internal/components/option/interfaces"; + +import { ApplicantStatus } from "admin/components"; +import useApplicants, { ApplicantSummary } from "admin/utils/useApplicants"; + +import ApplicantFilters from "./components/ApplicantFilters"; + +function Applicants() { + const [selectedStatuses, setSelectedStatuses] = useState< + readonly OptionDefinition[] + >([]); + const [selectedDecisions, setSelectedDecisions] = useState< + readonly OptionDefinition[] + >([]); + const { applicantList, loading } = useApplicants(); + + const selectedStatusValues = selectedStatuses.map(({ value }) => value); + const selectedDecisionValues = selectedDecisions.map(({ value }) => value); + + const filteredApplicants = applicantList.filter( + (applicant) => + (selectedStatuses.length === 0 || + selectedStatusValues.includes(applicant.status)) && + (selectedDecisions.length === 0 || + selectedDecisionValues.includes(applicant.decision || "-")) + ); + + const items = filteredApplicants; + + const counter = + selectedStatuses.length > 0 || selectedDecisions.length > 0 + ? `(${items.length}/${applicantList.length})` + : `(${applicantList.length})`; + + const emptyContent = ( + + No applicants + + ); + + return ( + application_data.university, + }, + { + id: "status", + header: "Status", + content: ApplicantStatus, + }, + { + id: "submission_time", + header: "Applied", + content: ({ application_data }) => + new Date(application_data.submission_time).toLocaleDateString(), + }, + { + id: "decision", + header: "Decision", + content: DecisionStatus, + }, + ], + }} + // visibleSections={preferences.visibleContent} + loading={loading} + loadingText="Loading applicants" + items={items} + trackBy="_id" + variant="full-page" + filter={ + + } + empty={emptyContent} + header={
Applicants
} + /> + ); +} + +const CardHeader = ({ _id, application_data }: ApplicantSummary) => ( + + {application_data.first_name} {application_data.last_name} + +); + +const DecisionStatus = ({ decision }: ApplicantSummary) => + decision ? : "-"; + +export default Applicants; diff --git a/apps/site/src/app/admin/applicants/[uid]/Applicant.tsx b/apps/site/src/app/admin/applicants/[uid]/Applicant.tsx new file mode 100644 index 00000000..ddec8a0e --- /dev/null +++ b/apps/site/src/app/admin/applicants/[uid]/Applicant.tsx @@ -0,0 +1,59 @@ +import { useRouter } from "next/router"; + +import ContentLayout from "@cloudscape-design/components/content-layout"; +import Header from "@cloudscape-design/components/header"; +import SpaceBetween from "@cloudscape-design/components/space-between"; +import Spinner from "@cloudscape-design/components/spinner"; + +import { ApplicantActions, Application } from "admin/components"; +import useApplicant from "admin/utils/useApplicant"; + +import ApplicantOverview from "./components/ApplicantOverview"; + +function Applicant() { + const router = useRouter(); + const { uid } = router.query; + + if (typeof uid === "string") { + throw TypeError(); + } + + const { applicant, loading, submitReview } = useApplicant(uid ? uid[0] : ""); + + if (loading || !applicant) { + return ( + }> + + + ); + } + + const { application_data } = applicant; + const { first_name, last_name } = application_data; + + return ( + + } + > + {first_name} {last_name} + + } + > + + + + + + ); +} + +export default Applicant; diff --git a/apps/site/src/app/admin/applicants/[uid]/components/ApplicantActions.tsx b/apps/site/src/app/admin/applicants/[uid]/components/ApplicantActions.tsx new file mode 100644 index 00000000..1b2bd576 --- /dev/null +++ b/apps/site/src/app/admin/applicants/[uid]/components/ApplicantActions.tsx @@ -0,0 +1,63 @@ +import { useContext } from "react"; + +import ButtonDropdown, { + ButtonDropdownProps, +} from "@cloudscape-design/components/button-dropdown"; + +import { Decision, submitReview, uid } from "admin/utils/useApplicant"; +import UserContext from "utils/userContext"; + +interface ApplicantActionsProps { + applicant: uid; + submitReview: submitReview; +} + +interface ReviewButtonItem extends ButtonDropdownProps.Item { + id: Decision; +} + +type ReviewButtonItems = ReviewButtonItem[]; + +function ApplicantActions({ applicant, submitReview }: ApplicantActionsProps) { + const { role } = useContext(UserContext); + + if (role !== "reviewer") { + return null; + } + + const handleClick = ( + event: CustomEvent + ) => { + const review = event.detail.id; + submitReview(applicant, review as Decision); + }; + + const dropdownItems: ReviewButtonItems = [ + { + text: "Accept", + id: Decision.accepted, + iconName: "status-positive", + description: "Accept the applicant", + }, + { + text: "Waitlist", + id: Decision.waitlisted, + iconName: "status-pending", + description: "Waitlist the applicant", + }, + { + text: "Reject", + id: Decision.rejected, + iconName: "status-negative", + description: "Reject the applicant", + }, + ]; + + return ( + + Review + + ); +} + +export default ApplicantActions; diff --git a/apps/site/src/app/admin/applicants/[uid]/components/ApplicantOverview.tsx b/apps/site/src/app/admin/applicants/[uid]/components/ApplicantOverview.tsx new file mode 100644 index 00000000..7028977f --- /dev/null +++ b/apps/site/src/app/admin/applicants/[uid]/components/ApplicantOverview.tsx @@ -0,0 +1,41 @@ +import Box from "@cloudscape-design/components/box"; +import ColumnLayout from "@cloudscape-design/components/column-layout"; +import Container from "@cloudscape-design/components/container"; +import Header from "@cloudscape-design/components/header"; + +import { ApplicantStatus } from "admin/components"; +import { Applicant } from "admin/utils/useApplicant"; + +import ApplicationReviews from "./ApplicationReviews"; + +interface ApplicantOverviewProps { + applicant: Applicant; +} + +function ApplicantOverview({ applicant }: ApplicantOverviewProps) { + const { application_data, status } = applicant; + const { submission_time, reviews } = application_data; + + const submittedDate = new Date(submission_time).toDateString(); + + return ( + Overview}> + +
+ Submitted + {submittedDate} +
+
+ Status + +
+
+ Reviews + +
+
+
+ ); +} + +export default ApplicantOverview; diff --git a/apps/site/src/app/admin/applicants/[uid]/components/Application.tsx b/apps/site/src/app/admin/applicants/[uid]/components/Application.tsx new file mode 100644 index 00000000..1f7c81f9 --- /dev/null +++ b/apps/site/src/app/admin/applicants/[uid]/components/Application.tsx @@ -0,0 +1,46 @@ +import Container from "@cloudscape-design/components/container"; +import Header from "@cloudscape-design/components/header"; +import SpaceBetween from "@cloudscape-design/components/space-between"; + +import { Applicant, ApplicationQuestion } from "admin/utils/useApplicant"; + +import ApplicationSection from "./ApplicationSection"; + +interface ApplicationSections { + [key: string]: ApplicationQuestion[]; +} + +const APPLICATION_SECTIONS: ApplicationSections = { + "Personal Information": ["gender", "pronouns", "ethnicity", "is_18_older"], + Education: ["university", "education_level", "major", "is_first_hackathon"], + Experience: ["portfolio_link", "linkedin_link", "resume_url"], + "Free Response Questions": [ + "stress_relief_question", + "company_specialize_question", + ], +}; + +interface ApplicationProps { + applicant: Applicant; +} + +function Application({ applicant }: ApplicationProps) { + const { application_data } = applicant; + + return ( + Application}> + + {Object.entries(APPLICATION_SECTIONS).map(([section, questions]) => ( + + ))} + + + ); +} + +export default Application; diff --git a/apps/site/src/app/admin/applicants/[uid]/components/ApplicationReviews.tsx b/apps/site/src/app/admin/applicants/[uid]/components/ApplicationReviews.tsx new file mode 100644 index 00000000..76e89d98 --- /dev/null +++ b/apps/site/src/app/admin/applicants/[uid]/components/ApplicationReviews.tsx @@ -0,0 +1,43 @@ +import { useContext } from "react"; + +import { ApplicantStatus } from "admin/components"; +import { Review, uid } from "admin/utils/useApplicant"; +import UserContext from "utils/userContext"; + +interface ApplicationReviewsProps { + reviews: Review[]; +} + +function ApplicationReviews({ reviews }: ApplicationReviewsProps) { + const { uid } = useContext(UserContext); + + if (reviews.length === 0) { + return

-

; + } + + const formatUid = (uid: uid) => uid.split(".").at(-1); + const formatDate = (timestamp: string) => + new Date(timestamp).toLocaleDateString(); + + return ( +
    + {reviews.map(([date, reviewer, decision]) => + reviewer === uid ? ( +
  • + <> + You reviewed as on{" "} + {formatDate(date)} + +
  • + ) : ( +
  • + {formatUid(reviewer)} reviewed this application on{" "} + {formatDate(date)} +
  • + ) + )} +
+ ); +} + +export default ApplicationReviews; diff --git a/apps/site/src/app/admin/applicants/[uid]/components/ApplicationSection.tsx b/apps/site/src/app/admin/applicants/[uid]/components/ApplicationSection.tsx new file mode 100644 index 00000000..cb10e2c0 --- /dev/null +++ b/apps/site/src/app/admin/applicants/[uid]/components/ApplicationSection.tsx @@ -0,0 +1,76 @@ +import ColumnLayout from "@cloudscape-design/components/column-layout"; +import TextContent from "@cloudscape-design/components/text-content"; + +import { ApplicationData, ApplicationQuestion } from "admin/utils/useApplicant"; + +interface ApplicationResponseProps { + value: string | boolean | string[] | null; +} + +const titleCase = (str: string) => + str.charAt(0).toUpperCase() + str.substring(1); + +const formatQuestion = (q: string) => q.split("_").map(titleCase).join(" "); + +function ApplicationResponse({ value }: ApplicationResponseProps) { + if (value === null) { + return

Not provided

; + } + + switch (typeof value) { + case "boolean": + return

{value ? "Yes" : "No"}

; + case "string": + if (value.startsWith("http")) { + return ( +

+ + {value} + +

+ ); + } + return

{value}

; + case "object": + return ( +
    + {value.map((v) => ( +
  • {v}
  • + ))} +
+ ); + default: + return

; + } +} + +interface ApplicationSectionProps { + title: string; + data: Omit; + propsToShow: ApplicationQuestion[]; +} + +function ApplicationSection({ + title, + data, + propsToShow, +}: ApplicationSectionProps) { + return ( + +

{title}

+ + {propsToShow.map((prop) => ( +
+

{formatQuestion(prop)}

+ +
+ ))} +
+ + ); +} + +export default ApplicationSection; diff --git a/apps/site/src/app/admin/applicants/components/ApplicantFilters.tsx b/apps/site/src/app/admin/applicants/components/ApplicantFilters.tsx new file mode 100644 index 00000000..ed71e19b --- /dev/null +++ b/apps/site/src/app/admin/applicants/components/ApplicantFilters.tsx @@ -0,0 +1,67 @@ +import { Dispatch, SetStateAction } from "react"; + +import ColumnLayout from "@cloudscape-design/components/column-layout"; +import { IconProps } from "@cloudscape-design/components/icon"; +import { OptionDefinition } from "@cloudscape-design/components/internal/components/option/interfaces"; +import Multiselect from "@cloudscape-design/components/multiselect"; + +import { StatusLabels } from "admin/components/ApplicantStatus/ApplicantStatus"; +import { Decision, ReviewStatus, Status } from "admin/utils/useApplicant"; + +interface ApplicantFiltersProps { + selectedStatuses: readonly OptionDefinition[]; + setSelectedStatuses: Dispatch>; + selectedDecisions: readonly OptionDefinition[]; + setSelectedDecisions: Dispatch>; +} + +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", +}; + +const statusOption = (status: Status) => ({ + label: StatusLabels[status], + value: status, + iconName: StatusIcons[status], +}); + +const STATUS_OPTIONS: OptionDefinition[] = + Object.values(ReviewStatus).map(statusOption); + +const DECISION_OPTIONS: OptionDefinition[] = + Object.values(Decision).map(statusOption); + +function ApplicantFilters({ + selectedStatuses, + setSelectedStatuses, + selectedDecisions, + setSelectedDecisions, +}: ApplicantFiltersProps) { + return ( + + setSelectedStatuses(detail.selectedOptions)} + deselectAriaLabel={(e) => `Remove ${e.label}`} + options={STATUS_OPTIONS} + placeholder="Choose statuses" + selectedAriaLabel="Selected" + /> + setSelectedDecisions(detail.selectedOptions)} + deselectAriaLabel={(e) => `Remove ${e.label}`} + options={DECISION_OPTIONS} + placeholder="Choose reviews" + selectedAriaLabel="Selected" + /> + + ); +} + +export default ApplicantFilters; diff --git a/apps/site/src/app/admin/applicants/components/ApplicantStatus.tsx b/apps/site/src/app/admin/applicants/components/ApplicantStatus.tsx new file mode 100644 index 00000000..f604741d --- /dev/null +++ b/apps/site/src/app/admin/applicants/components/ApplicantStatus.tsx @@ -0,0 +1,37 @@ +import StatusIndicator, { + StatusIndicatorProps, +} from "@cloudscape-design/components/status-indicator"; + +import { Status } from "admin/utils/useApplicant"; + +export const StatusLabels = { + [Status.accepted]: "accepted", + [Status.rejected]: "rejected", + [Status.waitlisted]: "waitlisted", + [Status.pending]: "needs review", + [Status.reviewed]: "reviewed", + [Status.released]: "released", +}; + +const StatusTypes: Record = { + [Status.accepted]: "success", + [Status.rejected]: "error", + [Status.waitlisted]: "pending", + [Status.pending]: "pending", + [Status.reviewed]: "in-progress", + [Status.released]: "success", +}; + +interface ApplicantStatusProps { + status: Status; +} + +function ApplicantStatus({ status }: ApplicantStatusProps) { + return ( + + {StatusLabels[status]} + + ); +} + +export default ApplicantStatus; diff --git a/apps/site/src/app/admin/dashboard/AdminDashboard.tsx b/apps/site/src/app/admin/dashboard/AdminDashboard.tsx new file mode 100644 index 00000000..5aff8634 --- /dev/null +++ b/apps/site/src/app/admin/dashboard/AdminDashboard.tsx @@ -0,0 +1,5 @@ +function AdminDashboard() { + return
AdminDashboard
; +} + +export default AdminDashboard; diff --git a/apps/site/src/app/admin/layout/AdminLayout.tsx b/apps/site/src/app/admin/layout/AdminLayout.tsx new file mode 100644 index 00000000..7d1f915b --- /dev/null +++ b/apps/site/src/app/admin/layout/AdminLayout.tsx @@ -0,0 +1,45 @@ +import { useRouter } from "next/router"; +import { PropsWithChildren, useContext, useEffect } from "react"; + +import AppLayout from "@cloudscape-design/components/app-layout"; + +import UserContext from "utils/userContext"; + +import AdminSidebar from "./AdminSidebar"; +import Breadcrumbs from "./Breadcrumbs"; + +const ADMIN_ROLES = ["director", "reviewer"]; + +export function isAdminRole(role: string | null) { + return role !== null && ADMIN_ROLES.includes(role); +} + +function AdminLayout({ children }: PropsWithChildren) { + const { uid, role } = useContext(UserContext); + const router = useRouter(); + + const loggedIn = uid !== null; + const authorized = isAdminRole(role); + + useEffect(() => { + if (!loggedIn) { + router.replace("/login"); + } else if (!authorized) { + router.replace("/unauthorized"); + } + }, [router, loggedIn, authorized]); + + if (!loggedIn || !authorized) { + return null; + } + + return ( + } + breadcrumbs={} + /> + ); +} + +export default AdminLayout; diff --git a/apps/site/src/app/admin/layout/AdminSidebar.tsx b/apps/site/src/app/admin/layout/AdminSidebar.tsx new file mode 100644 index 00000000..3fa5beb6 --- /dev/null +++ b/apps/site/src/app/admin/layout/AdminSidebar.tsx @@ -0,0 +1,28 @@ +import { useRouter } from "next/router"; + +import SideNavigation, { + SideNavigationProps, +} from "@cloudscape-design/components/side-navigation"; + +import { BASE_PATH, followWithNextLink } from "./common"; + +function AdminSidebar() { + const router = useRouter(); + + const navigationItems: SideNavigationProps.Item[] = [ + { type: "link", text: "Applicants", href: "/admin/applicants" }, + { type: "divider" }, + { type: "link", text: "Back to main site", href: "/" }, + ]; + + return ( + + ); +} + +export default AdminSidebar; diff --git a/apps/site/src/app/admin/layout/Breadcrumbs.tsx b/apps/site/src/app/admin/layout/Breadcrumbs.tsx new file mode 100644 index 00000000..f7d1bd8f --- /dev/null +++ b/apps/site/src/app/admin/layout/Breadcrumbs.tsx @@ -0,0 +1,60 @@ +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; + +import BreadcrumbGroup, { + BreadcrumbGroupProps, +} from "@cloudscape-design/components/breadcrumb-group"; + +import { BASE_PATH, followWithNextLink } from "./common"; + +interface PathTitles { + [key: string]: string; +} + +const pathTitles: PathTitles = { + applicants: "Applicants", +}; + +const DEFAULT_ITEMS = [{ text: "HackUCI 2023", href: BASE_PATH }]; + +function Breadcrumbs() { + const router = useRouter(); + const { asPath, isReady } = router; + + const [breadcrumbItems, setBreadcrumbItems] = + useState(DEFAULT_ITEMS); + + useEffect(() => { + if (!isReady) { + return; + } + + const items = [...DEFAULT_ITEMS]; + + if (asPath !== BASE_PATH) { + asPath + .slice("/admin/".length) + .split("/") + .reduce((partial, path) => { + partial += path; + items.push({ + text: pathTitles[path] || path, + href: partial, + }); + return partial; + }, "/admin/"); + } + + setBreadcrumbItems(items); + }, [asPath, isReady]); + + return ( + + ); +} + +export default Breadcrumbs; diff --git a/apps/site/src/app/admin/layout/common.tsx b/apps/site/src/app/admin/layout/common.tsx new file mode 100644 index 00000000..001eba6f --- /dev/null +++ b/apps/site/src/app/admin/layout/common.tsx @@ -0,0 +1,18 @@ +import Router from "next/router"; + +import { BreadcrumbGroupProps } from "@cloudscape-design/components/breadcrumb-group"; +import { SideNavigationProps } from "@cloudscape-design/components/side-navigation"; + +export const BASE_PATH = "/admin/dashboard"; + +type FollowEvent = CustomEvent< + | BreadcrumbGroupProps.ClickDetail + | SideNavigationProps.FollowDetail +>; + +export const followWithNextLink = (event: FollowEvent) => { + if (!event.detail.external) { + event.preventDefault(); + Router.push(event.detail.href); + } +}; diff --git a/apps/site/src/lib/admin/useApplicant.ts b/apps/site/src/lib/admin/useApplicant.ts new file mode 100644 index 00000000..dd7856f0 --- /dev/null +++ b/apps/site/src/lib/admin/useApplicant.ts @@ -0,0 +1,79 @@ +import axios from "axios"; +import useSWR from "swr"; + +export type uid = string; + +export enum Decision { + accepted = "ACCEPTED", + rejected = "REJECTED", + waitlisted = "WAITLISTED", +} + +export type Review = [string, uid, Decision]; + +// The application responses submitted by an applicant +export interface ApplicationData { + first_name: string; + last_name: string; + email: string; + gender: string; + pronouns: string[]; + ethnicity: string; + is_18_older: boolean; + university: string; + education_level: string; + major: string; + is_first_hackathon: boolean; + portfolio_link: string | null; + linkedin_link: string | null; + stress_relief_question: string; + company_specialize_question: string; + resume_url: string; + submission_time: string; + reviews: Review[]; +} + +export type ApplicationQuestion = Exclude; + +export enum ReviewStatus { + pending = "PENDING_REVIEW", + reviewed = "REVIEWED", + released = "RELEASED", +} + +export const Status = { ...ReviewStatus, ...Decision }; +export type Status = ReviewStatus | Decision; + +export interface Applicant { + _id: uid; + role: string; + status: Status; + application_data: ApplicationData; +} + +const fetcher = async ([api, uid]: string[]) => { + if (!uid) { + return null; + } + const res = await axios.get(api + uid); + return res.data; +}; + +function useApplicant(uid: uid) { + const { data, error, isLoading, mutate } = useSWR( + ["/api/admin/applicant/", uid], + fetcher + ); + + async function submitReview(uid: uid, review: Decision) { + await axios.post("/api/admin/review", { applicant: uid, decision: review }); + // TODO: provide success status to display in alert + mutate(); + } + + return { applicant: data, loading: isLoading, error, submitReview }; +} + +export type submitReview = (uid: uid, review: Decision) => Promise; + +export default useApplicant; diff --git a/apps/site/src/lib/admin/useApplicants.ts b/apps/site/src/lib/admin/useApplicants.ts new file mode 100644 index 00000000..e3be5420 --- /dev/null +++ b/apps/site/src/lib/admin/useApplicants.ts @@ -0,0 +1,32 @@ +import axios from "axios"; +import useSWR from "swr"; + +import { Status } from "./useApplicant"; + +export interface ApplicantSummary { + _id: string; + status: Status; + decision: Status | null; + application_data: { + first_name: string; + last_name: string; + university: string; + submission_time: string; + }; +} + +const fetcher = async (url: string) => { + const res = await axios.get(url); + return res.data; +}; + +function useApplicants() { + const { data, error, isLoading } = useSWR( + "/api/admin/applicants", + fetcher + ); + + return { applicantList: data || [], loading: isLoading, error }; +} + +export default useApplicants; From 9589cf1b753c8c8b44ec1077b6c0308e1abeca4d Mon Sep 17 00:00:00 2001 From: Taesung Hwang Date: Wed, 3 Jan 2024 22:47:31 -0800 Subject: [PATCH 02/17] Install cloudscape-design and swr - Install cloudscape-design components and global-styles for Admin site - Install swr for client-side data fetching on Admin site --- apps/site/package.json | 3 + pnpm-lock.yaml | 192 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 194 insertions(+), 1 deletion(-) diff --git a/apps/site/package.json b/apps/site/package.json index 51bed935..d3d37fb6 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -9,6 +9,8 @@ "lint": "next lint" }, "dependencies": { + "@cloudscape-design/components": "^3.0.475", + "@cloudscape-design/global-styles": "^1.0.20", "@fireworks-js/react": "^2.10.7", "@portabletext/react": "^3.0.11", "@radix-ui/react-accordion": "^1.1.2", @@ -31,6 +33,7 @@ "next-sanity": "^5.5.4", "react": "^18.2.0", "react-dom": "^18.2.0", + "swr": "^2.2.4", "three": "^0.158.0", "tunnel-rat": "^0.1.2", "zod": "^3.22.4" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40ea4e5c..430e3049 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,6 +84,12 @@ importers: apps/site: dependencies: + '@cloudscape-design/components': + specifier: ^3.0.475 + version: 3.0.475(react-dom@18.2.0)(react@18.2.0) + '@cloudscape-design/global-styles': + specifier: ^1.0.20 + version: 1.0.20 '@fireworks-js/react': specifier: ^2.10.7 version: 2.10.7(@types/react@18.2.38)(react@18.2.0) @@ -150,6 +156,9 @@ importers: react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + swr: + specifier: ^2.2.4 + version: 2.2.4(react@18.2.0) three: specifier: ^0.158.0 version: 0.158.0 @@ -1506,6 +1515,68 @@ packages: '@babel/helper-validator-identifier': 7.22.20 to-fast-properties: 2.0.0 + /@cloudscape-design/collection-hooks@1.0.32(react@18.2.0): + resolution: {integrity: sha512-wdOGW+W8+5rYYSGPab/v7BOy0P8mC6aUtwwN3w2AWE3n3LrK/Wqzsx77r/m4zlLdtkhZFnsnSBpT14OIw8ve/Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + + /@cloudscape-design/component-toolkit@1.0.0-beta.30: + resolution: {integrity: sha512-RFbLd8YXNz2QFv2Cb0lN7k8USCjE4lgVjSk5Kn6PZn77dtZrh7WGT313qEeOAB1IH40A/ZhtKYt265fC9Q5f/Q==} + dependencies: + '@juggle/resize-observer': 3.4.0 + tslib: 2.5.0 + dev: false + + /@cloudscape-design/components@3.0.475(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-sn7xCQpufgYafK+9RUxNlzoThGoj65b5RzIgKE3VZVNvGfaUmrAXu7p6Vhs4KcVckXkwaE8FqP3IUsGyrnX1RQ==} + peerDependencies: + react: ^16.8 || ^17 || ^18 + react-dom: ^16.8 || ^17 || ^18 + dependencies: + '@cloudscape-design/collection-hooks': 1.0.32(react@18.2.0) + '@cloudscape-design/component-toolkit': 1.0.0-beta.30 + '@cloudscape-design/test-utils-core': 1.0.21 + '@cloudscape-design/theming-runtime': 1.0.39 + '@dnd-kit/core': 6.1.0(react-dom@18.2.0)(react@18.2.0) + '@dnd-kit/sortable': 7.0.2(@dnd-kit/core@6.1.0)(react@18.2.0) + '@dnd-kit/utilities': 3.2.2(react@18.2.0) + '@juggle/resize-observer': 3.4.0 + ace-builds: 1.32.3 + balanced-match: 1.0.2 + clsx: 1.2.1 + d3-shape: 1.3.7 + date-fns: 2.30.0 + intl-messageformat: 10.5.8 + mnth: 2.0.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-keyed-flatten-children: 1.3.0(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + react-virtual: 2.10.4(react@18.2.0) + tslib: 2.5.0 + weekstart: 1.1.0 + dev: false + + /@cloudscape-design/global-styles@1.0.20: + resolution: {integrity: sha512-eEU3o7fZSRtIQVcFj1vRtheDFktRdAMMIidihrpNHqWVZzazFe0Z22vll684Yzy9TQ5R0HXKKupZLgKyC+Ia8w==} + dev: false + + /@cloudscape-design/test-utils-core@1.0.21: + resolution: {integrity: sha512-Kjxtl1ImQLmJ5SJ3PNF0hrtEbKidcZqk3E+iY4dLbJOI3sWh2TwoEpH5i4QEfO2GMtkMNzi41V4mmI4e4sydAw==} + dependencies: + css-selector-tokenizer: 0.8.0 + css.escape: 1.5.1 + dev: false + + /@cloudscape-design/theming-runtime@1.0.39: + resolution: {integrity: sha512-bc3ATkJhQ2PVomnLtBINcT3szYk6daPzBDCM+rwR9RZQ1a7T611R+EsPjyoKm355Z4xn/3hWKtw+IArg6vn0+A==} + dependencies: + tslib: 2.5.0 + dev: false + /@codemirror/autocomplete@6.11.0(@codemirror/language@6.9.2)(@codemirror/state@6.3.1)(@codemirror/view@6.22.0)(@lezer/common@1.1.1): resolution: {integrity: sha512-LCPH3W+hl5vcO7OzEQgX6NpKuKVyiKFLGAy7FXROF6nUpsWUdQEgUb3fe/g7B0E1KZCRFfgzdKASt6Wly2UOBg==} peerDependencies: @@ -2260,6 +2331,40 @@ packages: resolution: {integrity: sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==} dev: false + /@formatjs/ecma402-abstract@1.18.0: + resolution: {integrity: sha512-PEVLoa3zBevWSCZzPIM/lvPCi8P5l4G+NXQMc/CjEiaCWgyHieUoo0nM7Bs0n/NbuQ6JpXEolivQ9pKSBHaDlA==} + dependencies: + '@formatjs/intl-localematcher': 0.5.2 + tslib: 2.5.0 + dev: false + + /@formatjs/fast-memoize@2.2.0: + resolution: {integrity: sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==} + dependencies: + tslib: 2.5.0 + dev: false + + /@formatjs/icu-messageformat-parser@2.7.3: + resolution: {integrity: sha512-X/jy10V9S/vW+qlplqhMUxR8wErQ0mmIYSq4mrjpjDl9mbuGcCILcI1SUYkL5nlM4PJqpc0KOS0bFkkJNPxYRw==} + dependencies: + '@formatjs/ecma402-abstract': 1.18.0 + '@formatjs/icu-skeleton-parser': 1.7.0 + tslib: 2.5.0 + dev: false + + /@formatjs/icu-skeleton-parser@1.7.0: + resolution: {integrity: sha512-Cfdo/fgbZzpN/jlN/ptQVe0lRHora+8ezrEeg2RfrNjyp+YStwBy7cqDY8k5/z2LzXg6O0AdzAV91XS0zIWv+A==} + dependencies: + '@formatjs/ecma402-abstract': 1.18.0 + tslib: 2.5.0 + dev: false + + /@formatjs/intl-localematcher@0.5.2: + resolution: {integrity: sha512-txaaE2fiBMagLrR4jYhxzFO6wEdEG4TPMqrzBAcbr4HFUYzH/YC+lg6OIzKCHm8WgDdyQevxbAAV1OgcXctuGw==} + dependencies: + tslib: 2.5.0 + dev: false + /@hookform/resolvers@3.3.2(react-hook-form@7.48.2): resolution: {integrity: sha512-Tw+GGPnBp+5DOsSg4ek3LCPgkBOuOgS5DsDV7qsWNH9LZc433kgsWICjlsh2J9p04H2K66hsXPPb9qn9ILdUtA==} peerDependencies: @@ -3185,6 +3290,10 @@ packages: '@babel/runtime': 7.23.4 dev: false + /@reach/observe-rect@1.2.0: + resolution: {integrity: sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==} + dev: false + /@react-spring/animated@9.6.1(react@18.2.0): resolution: {integrity: sha512-ls/rJBrAqiAYozjLo5EPPLLOb1LM0lNVQcXODTC1SMtS6DbuBCPaKco5svFUQFMP2dso3O+qcC4k9FsKc0KxMQ==} peerDependencies: @@ -4440,6 +4549,10 @@ packages: deprecated: Use your platform's native atob() and btoa() methods instead dev: false + /ace-builds@1.32.3: + resolution: {integrity: sha512-ptSTUmDEU+LuwGiPY3/qQPmmAWE27vuv5sASL8swLRyLGJb7Ye7a8MrJ4NnAkFh1sJgVUqKTEGWRRFDmqYPw2Q==} + dev: false + /acorn-globals@7.0.1: resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} dependencies: @@ -5105,6 +5218,11 @@ packages: engines: {node: '>=0.8'} dev: true + /clsx@1.2.1: + resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} + engines: {node: '>=6'} + dev: false + /clsx@2.0.0: resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==} engines: {node: '>=6'} @@ -5329,6 +5447,13 @@ packages: engines: {node: '>=4'} dev: false + /css-selector-tokenizer@0.8.0: + resolution: {integrity: sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==} + dependencies: + cssesc: 3.0.0 + fastparse: 1.1.2 + dev: false + /css-to-react-native@3.2.0: resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} dependencies: @@ -5337,11 +5462,14 @@ packages: postcss-value-parser: 4.2.0 dev: false + /css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + dev: false + /cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} hasBin: true - dev: true /cssom@0.3.8: resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} @@ -5365,6 +5493,16 @@ packages: resolution: {integrity: sha512-0sVXIohTfLqVIW3kb/0n6IiWF3Ifj5nm2XaSrLq2DI6fKIGa2fYAZdk917rUneaeLVpYfFcyXE2ft0fe3remsA==} dev: false + /d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + dev: false + + /d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + dependencies: + d3-path: 1.0.9 + dev: false + /damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} dev: true @@ -6563,6 +6701,10 @@ packages: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} dev: true + /fastparse@1.1.2: + resolution: {integrity: sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==} + dev: false + /fastq@1.15.0: resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} dependencies: @@ -7306,6 +7448,15 @@ packages: side-channel: 1.0.4 dev: true + /intl-messageformat@10.5.8: + resolution: {integrity: sha512-NRf0jpBWV0vd671G5b06wNofAN8tp7WWDogMZyaU8GUAsmbouyvgwmFJI7zLjfAMpm3zK+vSwRP3jzaoIcMbaA==} + dependencies: + '@formatjs/ecma402-abstract': 1.18.0 + '@formatjs/fast-memoize': 2.2.0 + '@formatjs/icu-messageformat-parser': 2.7.3 + tslib: 2.5.0 + dev: false + /into-stream@6.0.0: resolution: {integrity: sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==} engines: {node: '>=10'} @@ -8153,6 +8304,13 @@ packages: minimist: 1.2.8 dev: true + /mnth@2.0.0: + resolution: {integrity: sha512-3ZH4UWBGpAwCKdfjynLQpUDVZWMe6vRHwarIpMdGLUp89CVR9hjzgyWERtMyqx+fPEqQ/PsAxFwvwPxLFxW40A==} + engines: {node: '>=12.13.0'} + dependencies: + '@babel/runtime': 7.23.4 + dev: false + /module-alias@2.2.3: resolution: {integrity: sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==} dev: false @@ -9076,6 +9234,15 @@ packages: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} dev: false + /react-keyed-flatten-children@1.3.0(react@18.2.0): + resolution: {integrity: sha512-qB7A6n+NHU0x88qTZGAJw6dsqwI941jcRPBB640c/CyWqjPQQ+YUmXOuzPziuHb7iqplM3xksWAbGYwkQT0tXA==} + peerDependencies: + react: '>=15.0.0' + dependencies: + react: 18.2.0 + react-is: 16.13.1 + dev: false + /react-lifecycles-compat@3.0.4: resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} dev: false @@ -9250,6 +9417,15 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /react-virtual@2.10.4(react@18.2.0): + resolution: {integrity: sha512-Ir6+oPQZTVHfa6+JL9M7cvMILstFZH/H3jqeYeKI4MSUX+rIruVwFC6nGVXw9wqAw8L0Kg2KvfXxI85OvYQdpQ==} + peerDependencies: + react: ^16.6.3 || ^17.0.0 + dependencies: + '@reach/observe-rect': 1.2.0 + react: 18.2.0 + dev: false + /react-virtuoso@4.6.2(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-vvlqvzPif+MvBrJ09+hJJrVY0xJK9yran+A+/1iwY78k0YCVKsyoNPqoLxOxzYPggspNBNXqUXEcvckN29OxyQ==} engines: {node: '>=10'} @@ -10270,6 +10446,16 @@ packages: upper-case: 1.1.3 dev: true + /swr@2.2.4(react@18.2.0): + resolution: {integrity: sha512-njiZ/4RiIhoOlAaLYDqwz5qH/KZXVilRLvomrx83HjzCWTfa+InyfAjv05PSFxnmLzZkNO9ZfvgoqzAaEI4sGQ==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 + dependencies: + client-only: 0.0.1 + react: 18.2.0 + use-sync-external-store: 1.2.0(react@18.2.0) + dev: false + /symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} dev: false @@ -11039,6 +11225,10 @@ packages: engines: {node: '>=12'} dev: false + /weekstart@1.1.0: + resolution: {integrity: sha512-ZO3I7c7J9nwGN1PZKZeBYAsuwWEsCOZi5T68cQoVNYrzrpp5Br0Bgi0OF4l8kH/Ez7nKfxa5mSsXjsgris3+qg==} + dev: false + /whatwg-encoding@2.0.0: resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} engines: {node: '>=12'} From abbfd471335040288f51dcaaf028de3621e933e1 Mon Sep 17 00:00:00 2001 From: Taesung Hwang Date: Wed, 3 Jan 2024 23:11:38 -0800 Subject: [PATCH 03/17] Fix first-party imports of Admin components --- apps/site/src/app/admin/applicants/Applicants.tsx | 4 ++-- apps/site/src/app/admin/applicants/[uid]/Applicant.tsx | 5 +++-- .../admin/applicants/[uid]/components/ApplicantActions.tsx | 2 +- .../admin/applicants/[uid]/components/ApplicantOverview.tsx | 4 ++-- .../app/admin/applicants/[uid]/components/Application.tsx | 2 +- .../admin/applicants/[uid]/components/ApplicationReviews.tsx | 4 ++-- .../admin/applicants/[uid]/components/ApplicationSection.tsx | 2 +- .../src/app/admin/applicants/components/ApplicantFilters.tsx | 5 +++-- .../src/app/admin/applicants/components/ApplicantStatus.tsx | 2 +- 9 files changed, 16 insertions(+), 14 deletions(-) diff --git a/apps/site/src/app/admin/applicants/Applicants.tsx b/apps/site/src/app/admin/applicants/Applicants.tsx index 81d3157d..eacd8a34 100644 --- a/apps/site/src/app/admin/applicants/Applicants.tsx +++ b/apps/site/src/app/admin/applicants/Applicants.tsx @@ -6,10 +6,10 @@ import Cards from "@cloudscape-design/components/cards"; import Header from "@cloudscape-design/components/header"; import { OptionDefinition } from "@cloudscape-design/components/internal/components/option/interfaces"; -import { ApplicantStatus } from "admin/components"; -import useApplicants, { ApplicantSummary } from "admin/utils/useApplicants"; +import useApplicants, { ApplicantSummary } from "@/lib/admin/useApplicants"; import ApplicantFilters from "./components/ApplicantFilters"; +import ApplicantStatus from "./components/ApplicantStatus"; function Applicants() { const [selectedStatuses, setSelectedStatuses] = useState< diff --git a/apps/site/src/app/admin/applicants/[uid]/Applicant.tsx b/apps/site/src/app/admin/applicants/[uid]/Applicant.tsx index ddec8a0e..d1310a6b 100644 --- a/apps/site/src/app/admin/applicants/[uid]/Applicant.tsx +++ b/apps/site/src/app/admin/applicants/[uid]/Applicant.tsx @@ -5,10 +5,11 @@ import Header from "@cloudscape-design/components/header"; import SpaceBetween from "@cloudscape-design/components/space-between"; import Spinner from "@cloudscape-design/components/spinner"; -import { ApplicantActions, Application } from "admin/components"; -import useApplicant from "admin/utils/useApplicant"; +import useApplicant from "@/lib/admin/useApplicant"; +import ApplicantActions from "./components/ApplicantActions"; import ApplicantOverview from "./components/ApplicantOverview"; +import Application from "./components/Application"; function Applicant() { const router = useRouter(); diff --git a/apps/site/src/app/admin/applicants/[uid]/components/ApplicantActions.tsx b/apps/site/src/app/admin/applicants/[uid]/components/ApplicantActions.tsx index 1b2bd576..0c48e9dd 100644 --- a/apps/site/src/app/admin/applicants/[uid]/components/ApplicantActions.tsx +++ b/apps/site/src/app/admin/applicants/[uid]/components/ApplicantActions.tsx @@ -4,7 +4,7 @@ import ButtonDropdown, { ButtonDropdownProps, } from "@cloudscape-design/components/button-dropdown"; -import { Decision, submitReview, uid } from "admin/utils/useApplicant"; +import { Decision, submitReview, uid } from "@/lib/admin/useApplicant"; import UserContext from "utils/userContext"; interface ApplicantActionsProps { diff --git a/apps/site/src/app/admin/applicants/[uid]/components/ApplicantOverview.tsx b/apps/site/src/app/admin/applicants/[uid]/components/ApplicantOverview.tsx index 7028977f..94aa7d64 100644 --- a/apps/site/src/app/admin/applicants/[uid]/components/ApplicantOverview.tsx +++ b/apps/site/src/app/admin/applicants/[uid]/components/ApplicantOverview.tsx @@ -3,8 +3,8 @@ import ColumnLayout from "@cloudscape-design/components/column-layout"; import Container from "@cloudscape-design/components/container"; import Header from "@cloudscape-design/components/header"; -import { ApplicantStatus } from "admin/components"; -import { Applicant } from "admin/utils/useApplicant"; +import ApplicantStatus from "@/app/admin/applicants/components/ApplicantStatus"; +import { Applicant } from "@/lib/admin/useApplicant"; import ApplicationReviews from "./ApplicationReviews"; diff --git a/apps/site/src/app/admin/applicants/[uid]/components/Application.tsx b/apps/site/src/app/admin/applicants/[uid]/components/Application.tsx index 1f7c81f9..5519363d 100644 --- a/apps/site/src/app/admin/applicants/[uid]/components/Application.tsx +++ b/apps/site/src/app/admin/applicants/[uid]/components/Application.tsx @@ -2,7 +2,7 @@ import Container from "@cloudscape-design/components/container"; import Header from "@cloudscape-design/components/header"; import SpaceBetween from "@cloudscape-design/components/space-between"; -import { Applicant, ApplicationQuestion } from "admin/utils/useApplicant"; +import { Applicant, ApplicationQuestion } from "@/lib/admin/useApplicant"; import ApplicationSection from "./ApplicationSection"; diff --git a/apps/site/src/app/admin/applicants/[uid]/components/ApplicationReviews.tsx b/apps/site/src/app/admin/applicants/[uid]/components/ApplicationReviews.tsx index 76e89d98..8e863dca 100644 --- a/apps/site/src/app/admin/applicants/[uid]/components/ApplicationReviews.tsx +++ b/apps/site/src/app/admin/applicants/[uid]/components/ApplicationReviews.tsx @@ -1,7 +1,7 @@ import { useContext } from "react"; -import { ApplicantStatus } from "admin/components"; -import { Review, uid } from "admin/utils/useApplicant"; +import ApplicantStatus from "@/app/admin/applicants/components/ApplicantStatus"; +import { Review, uid } from "@/lib/admin/useApplicant"; import UserContext from "utils/userContext"; interface ApplicationReviewsProps { diff --git a/apps/site/src/app/admin/applicants/[uid]/components/ApplicationSection.tsx b/apps/site/src/app/admin/applicants/[uid]/components/ApplicationSection.tsx index cb10e2c0..f4b52845 100644 --- a/apps/site/src/app/admin/applicants/[uid]/components/ApplicationSection.tsx +++ b/apps/site/src/app/admin/applicants/[uid]/components/ApplicationSection.tsx @@ -1,7 +1,7 @@ import ColumnLayout from "@cloudscape-design/components/column-layout"; import TextContent from "@cloudscape-design/components/text-content"; -import { ApplicationData, ApplicationQuestion } from "admin/utils/useApplicant"; +import { ApplicationData, ApplicationQuestion } from "@/lib/admin/useApplicant"; interface ApplicationResponseProps { value: string | boolean | string[] | null; diff --git a/apps/site/src/app/admin/applicants/components/ApplicantFilters.tsx b/apps/site/src/app/admin/applicants/components/ApplicantFilters.tsx index ed71e19b..5bee09fc 100644 --- a/apps/site/src/app/admin/applicants/components/ApplicantFilters.tsx +++ b/apps/site/src/app/admin/applicants/components/ApplicantFilters.tsx @@ -5,8 +5,9 @@ import { IconProps } from "@cloudscape-design/components/icon"; import { OptionDefinition } from "@cloudscape-design/components/internal/components/option/interfaces"; import Multiselect from "@cloudscape-design/components/multiselect"; -import { StatusLabels } from "admin/components/ApplicantStatus/ApplicantStatus"; -import { Decision, ReviewStatus, Status } from "admin/utils/useApplicant"; +import { Decision, ReviewStatus, Status } from "@/lib/admin/useApplicant"; + +import { StatusLabels } from "./ApplicantStatus"; interface ApplicantFiltersProps { selectedStatuses: readonly OptionDefinition[]; diff --git a/apps/site/src/app/admin/applicants/components/ApplicantStatus.tsx b/apps/site/src/app/admin/applicants/components/ApplicantStatus.tsx index f604741d..bd0a4a74 100644 --- a/apps/site/src/app/admin/applicants/components/ApplicantStatus.tsx +++ b/apps/site/src/app/admin/applicants/components/ApplicantStatus.tsx @@ -2,7 +2,7 @@ import StatusIndicator, { StatusIndicatorProps, } from "@cloudscape-design/components/status-indicator"; -import { Status } from "admin/utils/useApplicant"; +import { Status } from "@/lib/admin/useApplicant"; export const StatusLabels = { [Status.accepted]: "accepted", From b61e9fb2c392a3ccc0154c72ad776de3d42bf9cd Mon Sep 17 00:00:00 2001 From: Taesung Hwang Date: Wed, 3 Jan 2024 23:24:56 -0800 Subject: [PATCH 04/17] Fix missing `OptionDefinition` type - Extract `MultiselectProps.Option` to use instead of `OptionDefinition` --- .../src/app/admin/applicants/Applicants.tsx | 11 +++------ .../components/ApplicantFilters.tsx | 23 ++++++++++--------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/apps/site/src/app/admin/applicants/Applicants.tsx b/apps/site/src/app/admin/applicants/Applicants.tsx index eacd8a34..a9c5b372 100644 --- a/apps/site/src/app/admin/applicants/Applicants.tsx +++ b/apps/site/src/app/admin/applicants/Applicants.tsx @@ -4,20 +4,15 @@ import { useState } from "react"; import Box from "@cloudscape-design/components/box"; import Cards from "@cloudscape-design/components/cards"; import Header from "@cloudscape-design/components/header"; -import { OptionDefinition } from "@cloudscape-design/components/internal/components/option/interfaces"; import useApplicants, { ApplicantSummary } from "@/lib/admin/useApplicants"; -import ApplicantFilters from "./components/ApplicantFilters"; +import ApplicantFilters, { Options } from "./components/ApplicantFilters"; import ApplicantStatus from "./components/ApplicantStatus"; function Applicants() { - const [selectedStatuses, setSelectedStatuses] = useState< - readonly OptionDefinition[] - >([]); - const [selectedDecisions, setSelectedDecisions] = useState< - readonly OptionDefinition[] - >([]); + const [selectedStatuses, setSelectedStatuses] = useState([]); + const [selectedDecisions, setSelectedDecisions] = useState([]); const { applicantList, loading } = useApplicants(); const selectedStatusValues = selectedStatuses.map(({ value }) => value); diff --git a/apps/site/src/app/admin/applicants/components/ApplicantFilters.tsx b/apps/site/src/app/admin/applicants/components/ApplicantFilters.tsx index 5bee09fc..ff12a5ea 100644 --- a/apps/site/src/app/admin/applicants/components/ApplicantFilters.tsx +++ b/apps/site/src/app/admin/applicants/components/ApplicantFilters.tsx @@ -2,18 +2,21 @@ import { Dispatch, SetStateAction } from "react"; import ColumnLayout from "@cloudscape-design/components/column-layout"; import { IconProps } from "@cloudscape-design/components/icon"; -import { OptionDefinition } from "@cloudscape-design/components/internal/components/option/interfaces"; -import Multiselect from "@cloudscape-design/components/multiselect"; +import Multiselect, { + MultiselectProps, +} from "@cloudscape-design/components/multiselect"; import { Decision, ReviewStatus, Status } from "@/lib/admin/useApplicant"; import { StatusLabels } from "./ApplicantStatus"; +export type Options = ReadonlyArray; + interface ApplicantFiltersProps { - selectedStatuses: readonly OptionDefinition[]; - setSelectedStatuses: Dispatch>; - selectedDecisions: readonly OptionDefinition[]; - setSelectedDecisions: Dispatch>; + selectedStatuses: Options; + setSelectedStatuses: Dispatch>; + selectedDecisions: Options; + setSelectedDecisions: Dispatch>; } const StatusIcons: Record = { @@ -25,17 +28,15 @@ const StatusIcons: Record = { [Decision.waitlisted]: "status-negative", }; -const statusOption = (status: Status) => ({ +const statusOption = (status: Status): MultiselectProps.Option => ({ label: StatusLabels[status], value: status, iconName: StatusIcons[status], }); -const STATUS_OPTIONS: OptionDefinition[] = - Object.values(ReviewStatus).map(statusOption); +const STATUS_OPTIONS = Object.values(ReviewStatus).map(statusOption); -const DECISION_OPTIONS: OptionDefinition[] = - Object.values(Decision).map(statusOption); +const DECISION_OPTIONS = Object.values(Decision).map(statusOption); function ApplicantFilters({ selectedStatuses, From 485151a4ad3fbfccbc05c0d2cd17ab5ebb817335 Mon Sep 17 00:00:00 2001 From: Taesung Hwang Date: Thu, 4 Jan 2024 00:37:33 -0800 Subject: [PATCH 05/17] Use `UserContext` for Admin site components - For now, we are using client-side data fetching, so the user identity cannot be fetched by the server and must be provided through context --- .../applicants/[uid]/components/ApplicantActions.tsx | 2 +- .../[uid]/components/ApplicationReviews.tsx | 2 +- apps/site/src/app/admin/layout/AdminLayout.tsx | 2 +- apps/site/src/lib/admin/UserContext.ts | 11 +++++++++++ 4 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 apps/site/src/lib/admin/UserContext.ts diff --git a/apps/site/src/app/admin/applicants/[uid]/components/ApplicantActions.tsx b/apps/site/src/app/admin/applicants/[uid]/components/ApplicantActions.tsx index 0c48e9dd..3598cfbc 100644 --- a/apps/site/src/app/admin/applicants/[uid]/components/ApplicantActions.tsx +++ b/apps/site/src/app/admin/applicants/[uid]/components/ApplicantActions.tsx @@ -5,7 +5,7 @@ import ButtonDropdown, { } from "@cloudscape-design/components/button-dropdown"; import { Decision, submitReview, uid } from "@/lib/admin/useApplicant"; -import UserContext from "utils/userContext"; +import UserContext from "@/lib/admin/UserContext"; interface ApplicantActionsProps { applicant: uid; diff --git a/apps/site/src/app/admin/applicants/[uid]/components/ApplicationReviews.tsx b/apps/site/src/app/admin/applicants/[uid]/components/ApplicationReviews.tsx index 8e863dca..7598c279 100644 --- a/apps/site/src/app/admin/applicants/[uid]/components/ApplicationReviews.tsx +++ b/apps/site/src/app/admin/applicants/[uid]/components/ApplicationReviews.tsx @@ -2,7 +2,7 @@ import { useContext } from "react"; import ApplicantStatus from "@/app/admin/applicants/components/ApplicantStatus"; import { Review, uid } from "@/lib/admin/useApplicant"; -import UserContext from "utils/userContext"; +import UserContext from "@/lib/admin/UserContext"; interface ApplicationReviewsProps { reviews: Review[]; diff --git a/apps/site/src/app/admin/layout/AdminLayout.tsx b/apps/site/src/app/admin/layout/AdminLayout.tsx index 7d1f915b..13f15231 100644 --- a/apps/site/src/app/admin/layout/AdminLayout.tsx +++ b/apps/site/src/app/admin/layout/AdminLayout.tsx @@ -3,7 +3,7 @@ import { PropsWithChildren, useContext, useEffect } from "react"; import AppLayout from "@cloudscape-design/components/app-layout"; -import UserContext from "utils/userContext"; +import UserContext from "@/lib/admin/UserContext"; import AdminSidebar from "./AdminSidebar"; import Breadcrumbs from "./Breadcrumbs"; diff --git a/apps/site/src/lib/admin/UserContext.ts b/apps/site/src/lib/admin/UserContext.ts new file mode 100644 index 00000000..5d572f4b --- /dev/null +++ b/apps/site/src/lib/admin/UserContext.ts @@ -0,0 +1,11 @@ +import { createContext } from "react"; + +import { Identity } from "@/lib/utils/getUserIdentity"; + +const UserContext = createContext({ + uid: null, + role: null, + status: null, +}); + +export default UserContext; From 23a2daa8c61d315b7895e8844a33baa45ae23cc4 Mon Sep 17 00:00:00 2001 From: Taesung Hwang Date: Thu, 4 Jan 2024 00:41:53 -0800 Subject: [PATCH 06/17] Use dynamic segment params for Applicant UID - Replace router with props for dynamic route segment in `Applicant` --- .../src/app/admin/applicants/[uid]/Applicant.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/apps/site/src/app/admin/applicants/[uid]/Applicant.tsx b/apps/site/src/app/admin/applicants/[uid]/Applicant.tsx index d1310a6b..2c6f5ce9 100644 --- a/apps/site/src/app/admin/applicants/[uid]/Applicant.tsx +++ b/apps/site/src/app/admin/applicants/[uid]/Applicant.tsx @@ -1,5 +1,3 @@ -import { useRouter } from "next/router"; - import ContentLayout from "@cloudscape-design/components/content-layout"; import Header from "@cloudscape-design/components/header"; import SpaceBetween from "@cloudscape-design/components/space-between"; @@ -11,15 +9,14 @@ import ApplicantActions from "./components/ApplicantActions"; import ApplicantOverview from "./components/ApplicantOverview"; import Application from "./components/Application"; -function Applicant() { - const router = useRouter(); - const { uid } = router.query; +interface ApplicantProps { + params: { uid: string }; +} - if (typeof uid === "string") { - throw TypeError(); - } +function Applicant({ params }: ApplicantProps) { + const { uid } = params; - const { applicant, loading, submitReview } = useApplicant(uid ? uid[0] : ""); + const { applicant, loading, submitReview } = useApplicant(uid); if (loading || !applicant) { return ( From 82260507186734bd11c02b6509097a3a3b5a7acd Mon Sep 17 00:00:00 2001 From: Taesung Hwang Date: Thu, 4 Jan 2024 00:44:09 -0800 Subject: [PATCH 07/17] Expose Admin site pages for Applicant(s) - Add default exports to `page.tsx` to serve `Applicant`/`Applicants` - Include `use client` directive since fetching data from client by hook --- apps/site/src/app/admin/applicants/Applicants.tsx | 2 ++ apps/site/src/app/admin/applicants/[uid]/Applicant.tsx | 2 ++ apps/site/src/app/admin/applicants/[uid]/page.tsx | 1 + apps/site/src/app/admin/applicants/page.tsx | 1 + 4 files changed, 6 insertions(+) create mode 100644 apps/site/src/app/admin/applicants/[uid]/page.tsx create mode 100644 apps/site/src/app/admin/applicants/page.tsx diff --git a/apps/site/src/app/admin/applicants/Applicants.tsx b/apps/site/src/app/admin/applicants/Applicants.tsx index a9c5b372..0102dc98 100644 --- a/apps/site/src/app/admin/applicants/Applicants.tsx +++ b/apps/site/src/app/admin/applicants/Applicants.tsx @@ -1,3 +1,5 @@ +"use client"; + import Link from "next/link"; import { useState } from "react"; diff --git a/apps/site/src/app/admin/applicants/[uid]/Applicant.tsx b/apps/site/src/app/admin/applicants/[uid]/Applicant.tsx index 2c6f5ce9..a50ec446 100644 --- a/apps/site/src/app/admin/applicants/[uid]/Applicant.tsx +++ b/apps/site/src/app/admin/applicants/[uid]/Applicant.tsx @@ -1,3 +1,5 @@ +"use client"; + import ContentLayout from "@cloudscape-design/components/content-layout"; import Header from "@cloudscape-design/components/header"; import SpaceBetween from "@cloudscape-design/components/space-between"; diff --git a/apps/site/src/app/admin/applicants/[uid]/page.tsx b/apps/site/src/app/admin/applicants/[uid]/page.tsx new file mode 100644 index 00000000..249362ac --- /dev/null +++ b/apps/site/src/app/admin/applicants/[uid]/page.tsx @@ -0,0 +1 @@ +export { default as default } from "./Applicant"; diff --git a/apps/site/src/app/admin/applicants/page.tsx b/apps/site/src/app/admin/applicants/page.tsx new file mode 100644 index 00000000..350cac4a --- /dev/null +++ b/apps/site/src/app/admin/applicants/page.tsx @@ -0,0 +1 @@ +export { default as default } from "./Applicants"; From d14d8555a6b6ef2c61d1c6d1136ab49182dab485 Mon Sep 17 00:00:00 2001 From: Taesung Hwang Date: Thu, 4 Jan 2024 18:44:37 -0800 Subject: [PATCH 08/17] Provide identity through context in Admin layout - Add `useUserIdentity` hook for client-side identity on Admin site - Provide `UserContext` with identity in layout for Admin site --- apps/site/src/app/admin/layout.tsx | 17 +++++++++++----- apps/site/src/lib/admin/useUserIdentity.ts | 23 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 apps/site/src/lib/admin/useUserIdentity.ts diff --git a/apps/site/src/app/admin/layout.tsx b/apps/site/src/app/admin/layout.tsx index 05ae6931..f49fc07f 100644 --- a/apps/site/src/app/admin/layout.tsx +++ b/apps/site/src/app/admin/layout.tsx @@ -1,13 +1,20 @@ -import { Metadata } from "next/types"; +"use client"; import { PropsWithChildren } from "react"; -export const metadata: Metadata = { - title: "Admin | IrvineHacks 2024", -}; +import UserContext from "@/lib/admin/UserContext"; +import useUserIdentity from "@/lib/admin/useUserIdentity"; function Layout({ children }: PropsWithChildren) { - return
{children}
; + const identity = useUserIdentity(); + + if (!identity) { + return "Loading..."; + } + + return ( + {children} + ); } export default Layout; diff --git a/apps/site/src/lib/admin/useUserIdentity.ts b/apps/site/src/lib/admin/useUserIdentity.ts new file mode 100644 index 00000000..b173962b --- /dev/null +++ b/apps/site/src/lib/admin/useUserIdentity.ts @@ -0,0 +1,23 @@ +import { useEffect, useState } from "react"; + +import axios from "axios"; + +import { Identity } from "@/lib/utils/getUserIdentity"; + +function useUserIdentity(): Identity | undefined { + const [identity, setIdentity] = useState(); + + useEffect(() => { + const getIdentity = async () => { + const res = await axios.get(`/api/user/me`); + const identity = res.data; + setIdentity(identity); + }; + + getIdentity(); + }, []); + + return identity; +} + +export default useUserIdentity; From dbc776241463b8c02c365ddadd4dbed646650f5b Mon Sep 17 00:00:00 2001 From: Taesung Hwang Date: Thu, 4 Jan 2024 18:57:17 -0800 Subject: [PATCH 09/17] Migrate Admin navigation to use App Router hooks - Replace old `next/router` hooks with new hooks from `next/navigation` - Redesign `followWithNextLink` into hook since `useRouter` is a hook - Replace state and effect for breadcrumbItems with regular function --- .../site/src/app/admin/layout/AdminLayout.tsx | 10 ++-- .../src/app/admin/layout/AdminSidebar.tsx | 9 +-- .../site/src/app/admin/layout/Breadcrumbs.tsx | 58 +++++++++---------- apps/site/src/app/admin/layout/common.tsx | 20 ++++--- 4 files changed, 49 insertions(+), 48 deletions(-) diff --git a/apps/site/src/app/admin/layout/AdminLayout.tsx b/apps/site/src/app/admin/layout/AdminLayout.tsx index 13f15231..79d63e35 100644 --- a/apps/site/src/app/admin/layout/AdminLayout.tsx +++ b/apps/site/src/app/admin/layout/AdminLayout.tsx @@ -1,4 +1,5 @@ -import { useRouter } from "next/router"; +import { redirect } from "next/navigation"; + import { PropsWithChildren, useContext, useEffect } from "react"; import AppLayout from "@cloudscape-design/components/app-layout"; @@ -16,18 +17,17 @@ export function isAdminRole(role: string | null) { function AdminLayout({ children }: PropsWithChildren) { const { uid, role } = useContext(UserContext); - const router = useRouter(); const loggedIn = uid !== null; const authorized = isAdminRole(role); useEffect(() => { if (!loggedIn) { - router.replace("/login"); + redirect("/login") } else if (!authorized) { - router.replace("/unauthorized"); + redirect("/unauthorized") } - }, [router, loggedIn, authorized]); + }, [loggedIn, authorized]); if (!loggedIn || !authorized) { return null; diff --git a/apps/site/src/app/admin/layout/AdminSidebar.tsx b/apps/site/src/app/admin/layout/AdminSidebar.tsx index 3fa5beb6..b38ba9ed 100644 --- a/apps/site/src/app/admin/layout/AdminSidebar.tsx +++ b/apps/site/src/app/admin/layout/AdminSidebar.tsx @@ -1,13 +1,14 @@ -import { useRouter } from "next/router"; +import { usePathname } from "next/navigation"; import SideNavigation, { SideNavigationProps, } from "@cloudscape-design/components/side-navigation"; -import { BASE_PATH, followWithNextLink } from "./common"; +import { BASE_PATH, useFollowWithNextLink } from "./common"; function AdminSidebar() { - const router = useRouter(); + const pathname = usePathname(); + const followWithNextLink = useFollowWithNextLink(); const navigationItems: SideNavigationProps.Item[] = [ { type: "link", text: "Applicants", href: "/admin/applicants" }, @@ -17,7 +18,7 @@ function AdminSidebar() { return ( (DEFAULT_ITEMS); - - useEffect(() => { - if (!isReady) { - return; - } - - const items = [...DEFAULT_ITEMS]; - - if (asPath !== BASE_PATH) { - asPath - .slice("/admin/".length) - .split("/") - .reduce((partial, path) => { - partial += path; - items.push({ - text: pathTitles[path] || path, - href: partial, - }); - return partial; - }, "/admin/"); - } - - setBreadcrumbItems(items); - }, [asPath, isReady]); + const pathname = usePathname(); + const followWithNextLink = useFollowWithNextLink(); + + const breadcrumbItems = getBreadcrumbItems(pathname); return ( { + partial += path; + items.push({ + text: pathTitles[path] || path, + href: partial, + }); + return partial; + }, "/admin/"); + } + + return items; +} + export default Breadcrumbs; diff --git a/apps/site/src/app/admin/layout/common.tsx b/apps/site/src/app/admin/layout/common.tsx index 001eba6f..25ac0b36 100644 --- a/apps/site/src/app/admin/layout/common.tsx +++ b/apps/site/src/app/admin/layout/common.tsx @@ -1,4 +1,4 @@ -import Router from "next/router"; +import { useRouter } from "next/navigation"; import { BreadcrumbGroupProps } from "@cloudscape-design/components/breadcrumb-group"; import { SideNavigationProps } from "@cloudscape-design/components/side-navigation"; @@ -10,9 +10,15 @@ type FollowEvent = CustomEvent< | SideNavigationProps.FollowDetail >; -export const followWithNextLink = (event: FollowEvent) => { - if (!event.detail.external) { - event.preventDefault(); - Router.push(event.detail.href); - } -}; +export function useFollowWithNextLink(): (event: FollowEvent) => void { + const router = useRouter(); + + const followWithNextLink = (event: FollowEvent) => { + if (!event.detail.external) { + event.preventDefault(); + router.push(event.detail.href); + } + }; + + return followWithNextLink; +} From 8e20fdc7a7964c27dd21f9192cc7ffc131d0be8e Mon Sep 17 00:00:00 2001 From: Taesung Hwang Date: Thu, 4 Jan 2024 23:40:06 -0800 Subject: [PATCH 10/17] Use Server Component for Admin Layout - Metadata cannot be provided from a Client Component - Similar reasoning, if we wanted the Admin Layout to be a Root Layout - Transfer `UserContext.Provider` to `AdminLayout` which is Client - Simplify redirects when not logged in or unauthorized --- apps/site/src/app/admin/layout.tsx | 23 +++-------- .../site/src/app/admin/layout/AdminLayout.tsx | 38 ++++++++++--------- 2 files changed, 27 insertions(+), 34 deletions(-) diff --git a/apps/site/src/app/admin/layout.tsx b/apps/site/src/app/admin/layout.tsx index f49fc07f..7275a135 100644 --- a/apps/site/src/app/admin/layout.tsx +++ b/apps/site/src/app/admin/layout.tsx @@ -1,20 +1,9 @@ -"use client"; +import { Metadata } from "next/types"; -import { PropsWithChildren } from "react"; +import "@cloudscape-design/global-styles/index.css"; -import UserContext from "@/lib/admin/UserContext"; -import useUserIdentity from "@/lib/admin/useUserIdentity"; +export const metadata: Metadata = { + title: "Admin | IrvineHacks 2024", +}; -function Layout({ children }: PropsWithChildren) { - const identity = useUserIdentity(); - - if (!identity) { - return "Loading..."; - } - - return ( - {children} - ); -} - -export default Layout; +export { default as default } from "./layout/AdminLayout"; diff --git a/apps/site/src/app/admin/layout/AdminLayout.tsx b/apps/site/src/app/admin/layout/AdminLayout.tsx index 79d63e35..dde12734 100644 --- a/apps/site/src/app/admin/layout/AdminLayout.tsx +++ b/apps/site/src/app/admin/layout/AdminLayout.tsx @@ -1,10 +1,13 @@ +"use client"; + import { redirect } from "next/navigation"; -import { PropsWithChildren, useContext, useEffect } from "react"; +import { PropsWithChildren } from "react"; import AppLayout from "@cloudscape-design/components/app-layout"; import UserContext from "@/lib/admin/UserContext"; +import useUserIdentity from "@/lib/admin/useUserIdentity"; import AdminSidebar from "./AdminSidebar"; import Breadcrumbs from "./Breadcrumbs"; @@ -16,29 +19,30 @@ export function isAdminRole(role: string | null) { } function AdminLayout({ children }: PropsWithChildren) { - const { uid, role } = useContext(UserContext); + const identity = useUserIdentity(); + + if (!identity) { + return "Loading..."; + } + const { uid, role } = identity; const loggedIn = uid !== null; const authorized = isAdminRole(role); - useEffect(() => { - if (!loggedIn) { - redirect("/login") - } else if (!authorized) { - redirect("/unauthorized") - } - }, [loggedIn, authorized]); - - if (!loggedIn || !authorized) { - return null; + if (!loggedIn) { + redirect("/login"); + } else if (!authorized) { + redirect("/unauthorized"); } return ( - } - breadcrumbs={} - /> + + } + breadcrumbs={} + /> + ); } From d9005b14e26d817161ec4d1378be7c667e6ceef8 Mon Sep 17 00:00:00 2001 From: Taesung Hwang Date: Thu, 4 Jan 2024 23:46:33 -0800 Subject: [PATCH 11/17] Fix `CardHeader` links in Admin `Applicants` view - Using `passHref` and `legacyBehavior` does not work with Cloudscape - Use Cloudscape `Link` with custom `followWithNextLink` router - Simplify event type for `BaseNavigationDetail` --- .../src/app/admin/applicants/Applicants.tsx | 20 +++++++++++++------ apps/site/src/app/admin/layout/common.tsx | 11 ++++------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/apps/site/src/app/admin/applicants/Applicants.tsx b/apps/site/src/app/admin/applicants/Applicants.tsx index 0102dc98..7898647f 100644 --- a/apps/site/src/app/admin/applicants/Applicants.tsx +++ b/apps/site/src/app/admin/applicants/Applicants.tsx @@ -1,12 +1,13 @@ "use client"; -import Link from "next/link"; import { useState } from "react"; import Box from "@cloudscape-design/components/box"; import Cards from "@cloudscape-design/components/cards"; import Header from "@cloudscape-design/components/header"; +import Link from "@cloudscape-design/components/link"; +import { useFollowWithNextLink } from "@/app/admin/layout/common"; import useApplicants, { ApplicantSummary } from "@/lib/admin/useApplicants"; import ApplicantFilters, { Options } from "./components/ApplicantFilters"; @@ -89,11 +90,18 @@ function Applicants() { ); } -const CardHeader = ({ _id, application_data }: ApplicantSummary) => ( - - {application_data.first_name} {application_data.last_name} - -); +const CardHeader = ({ _id, application_data }: ApplicantSummary) => { + const followWithNextLink = useFollowWithNextLink(); + return ( + + {application_data.first_name} {application_data.last_name} + + ); +}; const DecisionStatus = ({ decision }: ApplicantSummary) => decision ? : "-"; diff --git a/apps/site/src/app/admin/layout/common.tsx b/apps/site/src/app/admin/layout/common.tsx index 25ac0b36..47c2da88 100644 --- a/apps/site/src/app/admin/layout/common.tsx +++ b/apps/site/src/app/admin/layout/common.tsx @@ -1,20 +1,17 @@ import { useRouter } from "next/navigation"; -import { BreadcrumbGroupProps } from "@cloudscape-design/components/breadcrumb-group"; -import { SideNavigationProps } from "@cloudscape-design/components/side-navigation"; +import { LinkProps } from "@cloudscape-design/components/link"; export const BASE_PATH = "/admin/dashboard"; -type FollowEvent = CustomEvent< - | BreadcrumbGroupProps.ClickDetail - | SideNavigationProps.FollowDetail ->; +type BaseNavigationDetail = LinkProps.FollowDetail; +type FollowEvent = CustomEvent; export function useFollowWithNextLink(): (event: FollowEvent) => void { const router = useRouter(); const followWithNextLink = (event: FollowEvent) => { - if (!event.detail.external) { + if (!event.detail.external && event.detail.href) { event.preventDefault(); router.push(event.detail.href); } From 5407d7ba20b41afb772b3966c84cd91d68cfc44e Mon Sep 17 00:00:00 2001 From: Taesung Hwang Date: Thu, 4 Jan 2024 23:49:34 -0800 Subject: [PATCH 12/17] Update hackathon name in Admin site layout - Replace "HackUCI 2023" with "IrvineHacks 2024" --- apps/site/src/app/admin/layout/AdminSidebar.tsx | 2 +- apps/site/src/app/admin/layout/Breadcrumbs.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/site/src/app/admin/layout/AdminSidebar.tsx b/apps/site/src/app/admin/layout/AdminSidebar.tsx index b38ba9ed..af159968 100644 --- a/apps/site/src/app/admin/layout/AdminSidebar.tsx +++ b/apps/site/src/app/admin/layout/AdminSidebar.tsx @@ -19,7 +19,7 @@ function AdminSidebar() { return ( diff --git a/apps/site/src/app/admin/layout/Breadcrumbs.tsx b/apps/site/src/app/admin/layout/Breadcrumbs.tsx index a8c167d8..aacf758f 100644 --- a/apps/site/src/app/admin/layout/Breadcrumbs.tsx +++ b/apps/site/src/app/admin/layout/Breadcrumbs.tsx @@ -14,7 +14,7 @@ const pathTitles: PathTitles = { applicants: "Applicants", }; -const DEFAULT_ITEMS = [{ text: "HackUCI 2023", href: BASE_PATH }]; +const DEFAULT_ITEMS = [{ text: "IrvineHacks 2024", href: BASE_PATH }]; function Breadcrumbs() { const pathname = usePathname(); From 3e47fde6fc9794ba4b7d22950322452c65bf8d44 Mon Sep 17 00:00:00 2001 From: Taesung Hwang Date: Fri, 5 Jan 2024 00:14:08 -0800 Subject: [PATCH 13/17] Update `ApplicationData` fields in Admin site - Change `university` to `school` and remove `gender` - Update profile links and FRQ question field names --- apps/site/src/app/admin/applicants/Applicants.tsx | 6 +++--- .../admin/applicants/[uid]/components/Application.tsx | 11 ++++------- apps/site/src/lib/admin/useApplicant.ts | 11 +++++------ apps/site/src/lib/admin/useApplicants.ts | 2 +- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/apps/site/src/app/admin/applicants/Applicants.tsx b/apps/site/src/app/admin/applicants/Applicants.tsx index 7898647f..4cc04fc4 100644 --- a/apps/site/src/app/admin/applicants/Applicants.tsx +++ b/apps/site/src/app/admin/applicants/Applicants.tsx @@ -48,9 +48,9 @@ function Applicants() { header: CardHeader, sections: [ { - id: "university", - header: "University", - content: ({ application_data }) => application_data.university, + id: "school", + header: "School", + content: ({ application_data }) => application_data.school, }, { id: "status", diff --git a/apps/site/src/app/admin/applicants/[uid]/components/Application.tsx b/apps/site/src/app/admin/applicants/[uid]/components/Application.tsx index 5519363d..15b9a797 100644 --- a/apps/site/src/app/admin/applicants/[uid]/components/Application.tsx +++ b/apps/site/src/app/admin/applicants/[uid]/components/Application.tsx @@ -11,13 +11,10 @@ interface ApplicationSections { } const APPLICATION_SECTIONS: ApplicationSections = { - "Personal Information": ["gender", "pronouns", "ethnicity", "is_18_older"], - Education: ["university", "education_level", "major", "is_first_hackathon"], - Experience: ["portfolio_link", "linkedin_link", "resume_url"], - "Free Response Questions": [ - "stress_relief_question", - "company_specialize_question", - ], + "Personal Information": ["pronouns", "ethnicity", "is_18_older"], + Education: ["school", "education_level", "major", "is_first_hackathon"], + Experience: ["portfolio", "linkedin", "resume_url"], + "Free Response Questions": ["frq_collaboration", "frq_dream_job"], }; interface ApplicationProps { diff --git a/apps/site/src/lib/admin/useApplicant.ts b/apps/site/src/lib/admin/useApplicant.ts index dd7856f0..5e1bd157 100644 --- a/apps/site/src/lib/admin/useApplicant.ts +++ b/apps/site/src/lib/admin/useApplicant.ts @@ -16,18 +16,17 @@ export interface ApplicationData { first_name: string; last_name: string; email: string; - gender: string; pronouns: string[]; ethnicity: string; is_18_older: boolean; - university: string; + school: string; education_level: string; major: string; is_first_hackathon: boolean; - portfolio_link: string | null; - linkedin_link: string | null; - stress_relief_question: string; - company_specialize_question: string; + portfolio: string | null; + linkedin: string | null; + frq_collaboration: string; + frq_dream_job: string; resume_url: string; submission_time: string; reviews: Review[]; diff --git a/apps/site/src/lib/admin/useApplicants.ts b/apps/site/src/lib/admin/useApplicants.ts index e3be5420..7b502f75 100644 --- a/apps/site/src/lib/admin/useApplicants.ts +++ b/apps/site/src/lib/admin/useApplicants.ts @@ -10,7 +10,7 @@ export interface ApplicantSummary { application_data: { first_name: string; last_name: string; - university: string; + school: string; submission_time: string; }; } From 6a358ebe12da8aef8fb90ff939446d5be163a8f8 Mon Sep 17 00:00:00 2001 From: Taesung Hwang Date: Fri, 5 Jan 2024 00:29:16 -0800 Subject: [PATCH 14/17] Serve placeholder page for `AdminDashboard` - Remove old `AdminDemo` page --- apps/site/src/app/admin/dashboard/AdminDashboard.tsx | 11 ++++++++++- apps/site/src/app/admin/dashboard/page.tsx | 1 + apps/site/src/app/admin/demo/page.tsx | 5 ----- 3 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 apps/site/src/app/admin/dashboard/page.tsx delete mode 100644 apps/site/src/app/admin/demo/page.tsx diff --git a/apps/site/src/app/admin/dashboard/AdminDashboard.tsx b/apps/site/src/app/admin/dashboard/AdminDashboard.tsx index 5aff8634..5ae9f9c9 100644 --- a/apps/site/src/app/admin/dashboard/AdminDashboard.tsx +++ b/apps/site/src/app/admin/dashboard/AdminDashboard.tsx @@ -1,5 +1,14 @@ +"use client"; + +import Container from "@cloudscape-design/components/container"; +import ContentLayout from "@cloudscape-design/components/content-layout"; + function AdminDashboard() { - return
AdminDashboard
; + return ( + + Admin Dashboard + + ); } export default AdminDashboard; diff --git a/apps/site/src/app/admin/dashboard/page.tsx b/apps/site/src/app/admin/dashboard/page.tsx new file mode 100644 index 00000000..e9d89cbb --- /dev/null +++ b/apps/site/src/app/admin/dashboard/page.tsx @@ -0,0 +1 @@ +export { default as default } from "./AdminDashboard"; diff --git a/apps/site/src/app/admin/demo/page.tsx b/apps/site/src/app/admin/demo/page.tsx deleted file mode 100644 index bbdda987..00000000 --- a/apps/site/src/app/admin/demo/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -function AdminDemo() { - return
AdminDemo
; -} - -export default AdminDemo; From b6c34838256e48c0713438c80802a5725bfe1f29 Mon Sep 17 00:00:00 2001 From: Taesung Hwang Date: Sun, 7 Jan 2024 21:03:16 -0800 Subject: [PATCH 15/17] Reformat Admin site: include all trailing commas --- .prettierrc | 3 +-- apps/site/src/app/admin/applicants/Applicants.tsx | 2 +- .../app/admin/applicants/[uid]/components/ApplicantActions.tsx | 2 +- .../admin/applicants/[uid]/components/ApplicationReviews.tsx | 2 +- apps/site/src/lib/admin/useApplicant.ts | 2 +- apps/site/src/lib/admin/useApplicants.ts | 2 +- 6 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.prettierrc b/.prettierrc index 6a48eb4a..c9590876 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,3 @@ { - "useTabs": true, - "tabWidth": 4 + "useTabs": true } diff --git a/apps/site/src/app/admin/applicants/Applicants.tsx b/apps/site/src/app/admin/applicants/Applicants.tsx index 4cc04fc4..7801a85a 100644 --- a/apps/site/src/app/admin/applicants/Applicants.tsx +++ b/apps/site/src/app/admin/applicants/Applicants.tsx @@ -26,7 +26,7 @@ function Applicants() { (selectedStatuses.length === 0 || selectedStatusValues.includes(applicant.status)) && (selectedDecisions.length === 0 || - selectedDecisionValues.includes(applicant.decision || "-")) + selectedDecisionValues.includes(applicant.decision || "-")), ); const items = filteredApplicants; diff --git a/apps/site/src/app/admin/applicants/[uid]/components/ApplicantActions.tsx b/apps/site/src/app/admin/applicants/[uid]/components/ApplicantActions.tsx index 3598cfbc..e3d9b184 100644 --- a/apps/site/src/app/admin/applicants/[uid]/components/ApplicantActions.tsx +++ b/apps/site/src/app/admin/applicants/[uid]/components/ApplicantActions.tsx @@ -26,7 +26,7 @@ function ApplicantActions({ applicant, submitReview }: ApplicantActionsProps) { } const handleClick = ( - event: CustomEvent + event: CustomEvent, ) => { const review = event.detail.id; submitReview(applicant, review as Decision); diff --git a/apps/site/src/app/admin/applicants/[uid]/components/ApplicationReviews.tsx b/apps/site/src/app/admin/applicants/[uid]/components/ApplicationReviews.tsx index 7598c279..6f68332c 100644 --- a/apps/site/src/app/admin/applicants/[uid]/components/ApplicationReviews.tsx +++ b/apps/site/src/app/admin/applicants/[uid]/components/ApplicationReviews.tsx @@ -34,7 +34,7 @@ function ApplicationReviews({ reviews }: ApplicationReviewsProps) { {formatUid(reviewer)} reviewed this application on{" "} {formatDate(date)} - ) + ), )} ); diff --git a/apps/site/src/lib/admin/useApplicant.ts b/apps/site/src/lib/admin/useApplicant.ts index 5e1bd157..294e02e7 100644 --- a/apps/site/src/lib/admin/useApplicant.ts +++ b/apps/site/src/lib/admin/useApplicant.ts @@ -61,7 +61,7 @@ const fetcher = async ([api, uid]: string[]) => { function useApplicant(uid: uid) { const { data, error, isLoading, mutate } = useSWR( ["/api/admin/applicant/", uid], - fetcher + fetcher, ); async function submitReview(uid: uid, review: Decision) { diff --git a/apps/site/src/lib/admin/useApplicants.ts b/apps/site/src/lib/admin/useApplicants.ts index 7b502f75..90d2b68e 100644 --- a/apps/site/src/lib/admin/useApplicants.ts +++ b/apps/site/src/lib/admin/useApplicants.ts @@ -23,7 +23,7 @@ const fetcher = async (url: string) => { function useApplicants() { const { data, error, isLoading } = useSWR( "/api/admin/applicants", - fetcher + fetcher, ); return { applicantList: data || [], loading: isLoading, error }; From 8a9fad7331f60a650761053c7011e0c3a3a3fdff Mon Sep 17 00:00:00 2001 From: Taesung Hwang Date: Sun, 7 Jan 2024 21:31:41 -0800 Subject: [PATCH 16/17] Fix type error with `fetcher` in `useApplicant` - Specify explicit `SWRKey` generic type --- apps/site/src/lib/admin/useApplicant.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/site/src/lib/admin/useApplicant.ts b/apps/site/src/lib/admin/useApplicant.ts index 294e02e7..2201ef9f 100644 --- a/apps/site/src/lib/admin/useApplicant.ts +++ b/apps/site/src/lib/admin/useApplicant.ts @@ -50,7 +50,7 @@ export interface Applicant { application_data: ApplicationData; } -const fetcher = async ([api, uid]: string[]) => { +const fetcher = async ([api, uid]: [string, uid]) => { if (!uid) { return null; } @@ -59,10 +59,11 @@ const fetcher = async ([api, uid]: string[]) => { }; function useApplicant(uid: uid) { - const { data, error, isLoading, mutate } = useSWR( - ["/api/admin/applicant/", uid], - fetcher, - ); + const { data, error, isLoading, mutate } = useSWR< + Applicant | null, + unknown, + [string, uid] + >(["/api/admin/applicant/", uid], fetcher); async function submitReview(uid: uid, review: Decision) { await axios.post("/api/admin/review", { applicant: uid, decision: review }); From 0aaf48a7dcf321cb8d885806acc6033a263e222c Mon Sep 17 00:00:00 2001 From: Taesung Hwang Date: Sun, 7 Jan 2024 23:21:53 -0800 Subject: [PATCH 17/17] Capitalize type alias `Uid` --- .../[uid]/components/ApplicantActions.tsx | 4 ++-- .../[uid]/components/ApplicationReviews.tsx | 4 ++-- apps/site/src/lib/admin/useApplicant.ts | 16 ++++++++-------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/site/src/app/admin/applicants/[uid]/components/ApplicantActions.tsx b/apps/site/src/app/admin/applicants/[uid]/components/ApplicantActions.tsx index e3d9b184..5a006614 100644 --- a/apps/site/src/app/admin/applicants/[uid]/components/ApplicantActions.tsx +++ b/apps/site/src/app/admin/applicants/[uid]/components/ApplicantActions.tsx @@ -4,11 +4,11 @@ import ButtonDropdown, { ButtonDropdownProps, } from "@cloudscape-design/components/button-dropdown"; -import { Decision, submitReview, uid } from "@/lib/admin/useApplicant"; +import { Decision, submitReview, Uid } from "@/lib/admin/useApplicant"; import UserContext from "@/lib/admin/UserContext"; interface ApplicantActionsProps { - applicant: uid; + applicant: Uid; submitReview: submitReview; } diff --git a/apps/site/src/app/admin/applicants/[uid]/components/ApplicationReviews.tsx b/apps/site/src/app/admin/applicants/[uid]/components/ApplicationReviews.tsx index 6f68332c..60afdfb3 100644 --- a/apps/site/src/app/admin/applicants/[uid]/components/ApplicationReviews.tsx +++ b/apps/site/src/app/admin/applicants/[uid]/components/ApplicationReviews.tsx @@ -1,7 +1,7 @@ import { useContext } from "react"; import ApplicantStatus from "@/app/admin/applicants/components/ApplicantStatus"; -import { Review, uid } from "@/lib/admin/useApplicant"; +import { Review, Uid } from "@/lib/admin/useApplicant"; import UserContext from "@/lib/admin/UserContext"; interface ApplicationReviewsProps { @@ -15,7 +15,7 @@ function ApplicationReviews({ reviews }: ApplicationReviewsProps) { return

-

; } - const formatUid = (uid: uid) => uid.split(".").at(-1); + const formatUid = (uid: Uid) => uid.split(".").at(-1); const formatDate = (timestamp: string) => new Date(timestamp).toLocaleDateString(); diff --git a/apps/site/src/lib/admin/useApplicant.ts b/apps/site/src/lib/admin/useApplicant.ts index 2201ef9f..78545fcc 100644 --- a/apps/site/src/lib/admin/useApplicant.ts +++ b/apps/site/src/lib/admin/useApplicant.ts @@ -1,7 +1,7 @@ import axios from "axios"; import useSWR from "swr"; -export type uid = string; +export type Uid = string; export enum Decision { accepted = "ACCEPTED", @@ -9,7 +9,7 @@ export enum Decision { waitlisted = "WAITLISTED", } -export type Review = [string, uid, Decision]; +export type Review = [string, Uid, Decision]; // The application responses submitted by an applicant export interface ApplicationData { @@ -44,13 +44,13 @@ export const Status = { ...ReviewStatus, ...Decision }; export type Status = ReviewStatus | Decision; export interface Applicant { - _id: uid; + _id: Uid; role: string; status: Status; application_data: ApplicationData; } -const fetcher = async ([api, uid]: [string, uid]) => { +const fetcher = async ([api, uid]: [string, Uid]) => { if (!uid) { return null; } @@ -58,14 +58,14 @@ const fetcher = async ([api, uid]: [string, uid]) => { return res.data; }; -function useApplicant(uid: uid) { +function useApplicant(uid: Uid) { const { data, error, isLoading, mutate } = useSWR< Applicant | null, unknown, - [string, uid] + [string, Uid] >(["/api/admin/applicant/", uid], fetcher); - async function submitReview(uid: uid, review: Decision) { + async function submitReview(uid: Uid, review: Decision) { await axios.post("/api/admin/review", { applicant: uid, decision: review }); // TODO: provide success status to display in alert mutate(); @@ -74,6 +74,6 @@ function useApplicant(uid: uid) { return { applicant: data, loading: isLoading, error, submitReview }; } -export type submitReview = (uid: uid, review: Decision) => Promise; +export type submitReview = (uid: Uid, review: Decision) => Promise; export default useApplicant;