Skip to content

Commit

Permalink
Merge pull request #195 from HackAtUCI/feature/admin-site
Browse files Browse the repository at this point in the history
Import Admin site from last year
  • Loading branch information
taesungh authored Jan 9, 2024
2 parents 6d8d9b2 + 0aaf48a commit 68bd937
Show file tree
Hide file tree
Showing 26 changed files with 1,052 additions and 14 deletions.
3 changes: 1 addition & 2 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
{
"useTabs": true,
"tabWidth": 4
"useTabs": true
}
3 changes: 3 additions & 0 deletions apps/site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
Expand Down
109 changes: 109 additions & 0 deletions apps/site/src/app/admin/applicants/Applicants.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"use client";

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";
import ApplicantStatus from "./components/ApplicantStatus";

function Applicants() {
const [selectedStatuses, setSelectedStatuses] = useState<Options>([]);
const [selectedDecisions, setSelectedDecisions] = useState<Options>([]);
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 = (
<Box textAlign="center" color="inherit">
No applicants
</Box>
);

return (
<Cards
cardDefinition={{
header: CardHeader,
sections: [
{
id: "school",
header: "School",
content: ({ application_data }) => application_data.school,
},
{
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={
<ApplicantFilters
selectedStatuses={selectedStatuses}
setSelectedStatuses={setSelectedStatuses}
selectedDecisions={selectedDecisions}
setSelectedDecisions={setSelectedDecisions}
/>
}
empty={emptyContent}
header={<Header counter={counter}>Applicants</Header>}
/>
);
}

const CardHeader = ({ _id, application_data }: ApplicantSummary) => {
const followWithNextLink = useFollowWithNextLink();
return (
<Link
href={`/admin/applicants/${_id}`}
fontSize="inherit"
onFollow={followWithNextLink}
>
{application_data.first_name} {application_data.last_name}
</Link>
);
};

const DecisionStatus = ({ decision }: ApplicantSummary) =>
decision ? <ApplicantStatus status={decision} /> : "-";

export default Applicants;
59 changes: 59 additions & 0 deletions apps/site/src/app/admin/applicants/[uid]/Applicant.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"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";
import Spinner from "@cloudscape-design/components/spinner";

import useApplicant from "@/lib/admin/useApplicant";

import ApplicantActions from "./components/ApplicantActions";
import ApplicantOverview from "./components/ApplicantOverview";
import Application from "./components/Application";

interface ApplicantProps {
params: { uid: string };
}

function Applicant({ params }: ApplicantProps) {
const { uid } = params;

const { applicant, loading, submitReview } = useApplicant(uid);

if (loading || !applicant) {
return (
<ContentLayout header={<Header />}>
<Spinner variant="inverted" />
</ContentLayout>
);
}

const { application_data } = applicant;
const { first_name, last_name } = application_data;

return (
<ContentLayout
header={
<Header
variant="h1"
description="Applicant"
actions={
<ApplicantActions
applicant={applicant._id}
submitReview={submitReview}
/>
}
>
{first_name} {last_name}
</Header>
}
>
<SpaceBetween direction="vertical" size="l">
<ApplicantOverview applicant={applicant} />
<Application applicant={applicant} />
</SpaceBetween>
</ContentLayout>
);
}

export default Applicant;
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useContext } from "react";

import ButtonDropdown, {
ButtonDropdownProps,
} from "@cloudscape-design/components/button-dropdown";

import { Decision, submitReview, Uid } from "@/lib/admin/useApplicant";
import UserContext from "@/lib/admin/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<ButtonDropdownProps.ItemClickDetails>,
) => {
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 (
<ButtonDropdown items={dropdownItems} onItemClick={handleClick}>
Review
</ButtonDropdown>
);
}

export default ApplicantActions;
Original file line number Diff line number Diff line change
@@ -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 "@/app/admin/applicants/components/ApplicantStatus";
import { Applicant } from "@/lib/admin/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 (
<Container header={<Header variant="h2">Overview</Header>}>
<ColumnLayout columns={3} variant="text-grid">
<div>
<Box variant="awsui-key-label">Submitted</Box>
{submittedDate}
</div>
<div>
<Box variant="awsui-key-label">Status</Box>
<ApplicantStatus status={status} />
</div>
<div>
<Box variant="awsui-key-label">Reviews</Box>
<ApplicationReviews reviews={reviews} />
</div>
</ColumnLayout>
</Container>
);
}

export default ApplicantOverview;
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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 "@/lib/admin/useApplicant";

import ApplicationSection from "./ApplicationSection";

interface ApplicationSections {
[key: string]: ApplicationQuestion[];
}

const APPLICATION_SECTIONS: ApplicationSections = {
"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 {
applicant: Applicant;
}

function Application({ applicant }: ApplicationProps) {
const { application_data } = applicant;

return (
<Container header={<Header variant="h2">Application</Header>}>
<SpaceBetween direction="vertical" size="m">
{Object.entries(APPLICATION_SECTIONS).map(([section, questions]) => (
<ApplicationSection
key={section}
title={section}
data={application_data}
propsToShow={questions}
/>
))}
</SpaceBetween>
</Container>
);
}

export default Application;
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useContext } from "react";

import ApplicantStatus from "@/app/admin/applicants/components/ApplicantStatus";
import { Review, Uid } from "@/lib/admin/useApplicant";
import UserContext from "@/lib/admin/UserContext";

interface ApplicationReviewsProps {
reviews: Review[];
}

function ApplicationReviews({ reviews }: ApplicationReviewsProps) {
const { uid } = useContext(UserContext);

if (reviews.length === 0) {
return <p>-</p>;
}

const formatUid = (uid: Uid) => uid.split(".").at(-1);
const formatDate = (timestamp: string) =>
new Date(timestamp).toLocaleDateString();

return (
<ul>
{reviews.map(([date, reviewer, decision]) =>
reviewer === uid ? (
<li key={date}>
<>
You reviewed as <ApplicantStatus status={decision} /> on{" "}
{formatDate(date)}
</>
</li>
) : (
<li key={date}>
{formatUid(reviewer)} reviewed this application on{" "}
{formatDate(date)}
</li>
),
)}
</ul>
);
}

export default ApplicationReviews;
Loading

0 comments on commit 68bd937

Please sign in to comment.