Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,7 @@ dist/
# ide
.idea*

*/**/*.env
*/**/*.env

# ignore TypeScript build info files
**/tsconfig.tsbuildinfo
27 changes: 27 additions & 0 deletions apps/app-portal/src/app/(applicant)/dashboard/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from "react";

export default function Loading(): JSX.Element {
return (
<section className="min-h-screen px-4 py-8 sm:px-6 lg:px-10">
<div className="mx-auto flex w-full max-w-6xl flex-col gap-8">
<div className="space-y-4">
<div className="h-6 w-36 rounded-full bg-slate-200/80" />
<div className="h-12 w-3/4 rounded-3xl bg-slate-200/80 sm:h-16" />
<div className="h-5 w-full max-w-2xl rounded-full bg-slate-200/70" />
</div>
<div className="grid gap-6 lg:grid-cols-[minmax(0,1.15fr)_minmax(280px,0.85fr)]">
<div className="space-y-4 rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm sm:p-8">
<div className="h-8 w-2/3 rounded-2xl bg-slate-100" />
<div className="h-5 w-full rounded-full bg-slate-100" />
<div className="h-5 w-5/6 rounded-full bg-slate-100" />
<div className="h-36 rounded-3xl bg-slate-100" />
</div>
<div className="rounded-[2rem] border border-dashed border-slate-200 bg-white/60 p-6 shadow-sm sm:p-8">
<div className="h-6 w-1/2 rounded-full bg-slate-200" />
<div className="mt-4 h-28 rounded-3xl bg-slate-100" />
</div>
</div>
</div>
</section>
);
}
59 changes: 47 additions & 12 deletions apps/app-portal/src/app/(applicant)/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import React from "react";
import { returnDashboardBranch } from "../../../lib/status/machine";
import { decisionDates } from "../../../lib/status/mock-singletons";
import { getApplicantStatus } from "../../../lib/status/service";
import { fetchPortalStatus } from "../../../lib/status/fetchPortalStatus";
import type {
ApplicantStatus,
DashboardBranch,
SerializedDecisionDates,
} from "../../../lib/status/types";
import PreRegistrationView from "../../../components/dashboard/PreRegistrationView";
import InProgressView from "../../../components/dashboard/InProgressView";
import SubmittedView from "../../../components/dashboard/SubmittedView";
Expand All @@ -10,24 +13,56 @@ import WaitlistedView from "../../../components/dashboard/WaitlistedView";
import DeclinedView from "../../../components/dashboard/DeclinedView";

export default async function DashboardPage(): Promise<JSX.Element> {
const status = await getApplicantStatus("mock-user");
const showDecision = new Date() >= decisionDates.showDecision;
const branch = returnDashboardBranch(status, decisionDates, showDecision);
let branch: DashboardBranch = "submitted";
let status: ApplicantStatus | null = null;
let decisionDates: SerializedDecisionDates = {
registrationOpen: new Date().toISOString(),
showDecision: new Date().toISOString(),
confirmBy: new Date().toISOString(),
};

try {
const res = await fetchPortalStatus();
branch = res.branch;
status = res.status;
decisionDates = res.decisionDates;
} catch (err) {
// If fetch fails, render a simple error view instead of crashing the page.
return (
<div className="p-8">
<h2 className="text-xl font-semibold">Unable to load dashboard</h2>
<p className="mt-2 text-sm text-slate-600">{String(err)}</p>
</div>
);
}
// Ensure `status` is present before rendering views that require it.
if (!status) {
return (
<div className="p-8">
<h2 className="text-xl font-semibold">Loading dashboard…</h2>
</div>
);
}
const resolvedDates = {
registrationOpen: new Date(decisionDates.registrationOpen),
showDecision: new Date(decisionDates.showDecision),
confirmBy: new Date(decisionDates.confirmBy),
};

switch (branch) {
case "pre-registration":
return <PreRegistrationView />;
return <PreRegistrationView decisionDates={resolvedDates} />;
case "in-progress":
return <InProgressView />;
return <InProgressView status={status} />;
case "submitted":
return <SubmittedView />;
return <SubmittedView decisionDates={resolvedDates} status={status} />;
case "admitted":
return <AdmittedView />;
return <AdmittedView decisionDates={resolvedDates} status={status} />;
case "waitlisted":
return <WaitlistedView />;
return <WaitlistedView status={status} />;
case "declined":
return <DeclinedView />;
default:
return <SubmittedView />;
return <SubmittedView decisionDates={resolvedDates} status={status} />;
}
}
33 changes: 20 additions & 13 deletions apps/app-portal/src/app/(applicant)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,36 @@
import React from "react";
import Sidebar from "@/components/Sidebar";
import UserMenu from "@/components/auth/UserMenu";
import Image from "next/image";
import icon from "@/app/icon.ico";

// Applicant shell (header, user menu); placeholder session check
export const metadata = {
title: "Applicant Portal",
};
export default function ApplicantLayout({
children,
}: {
children: React.ReactNode;
}): JSX.Element {
// TODO: pull the real email lator
const email = "applicant@example.com";

return (
<div className="min-h-screen">
<header className="flex items-center justify-between border-b p-4">
<div className="flex flex-row items-start">
<Image src={icon.src} alt={"HBP Logo"} width={25} height={25} />
<span className="font-semibold pl-2">
HackBeanpot Applicant Portal
</span>
</div>
<UserMenu email={email} />
</header>
<main>{children}</main>
<div className="flex min-h-screen">
<Sidebar />

<div className="flex flex-1 flex-col desktop:ml-64">
<header className="flex h-16 items-center justify-between border-b bg-white px-6">
<div className="flex flex-row items-start">
<Image src={icon.src} alt={"HBP Logo"} width={25} height={25} />
<span className="font-semibold pl-2">
HackBeanpot Applicant Portal
</span>
</div>
<UserMenu email={email} />
</header>

<main className="flex-1 p-6">{children}</main>
</div>
</div>
);
}
18 changes: 18 additions & 0 deletions apps/app-portal/src/app/(applicant)/rsvp/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from "react";

export default function Loading(): JSX.Element {
return (
<section className="min-h-screen px-4 py-8 sm:px-6 lg:px-10">
<div className="mx-auto flex w-full max-w-5xl flex-col gap-6 rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm sm:p-8">
<div className="h-5 w-40 rounded-full bg-slate-200" />
<div className="h-11 w-3/4 rounded-3xl bg-slate-100" />
<div className="h-24 rounded-3xl bg-slate-100" />
<div className="grid gap-4 sm:grid-cols-2">
<div className="h-16 rounded-3xl bg-slate-100" />
<div className="h-16 rounded-3xl bg-slate-100" />
</div>
<div className="h-44 rounded-3xl bg-slate-100" />
</div>
</section>
);
}
35 changes: 14 additions & 21 deletions apps/app-portal/src/app/(applicant)/rsvp/page.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
import React from "react";
import { decisionDates } from "../../../lib/status/mock-singletons";
import { getApplicantStatus } from "../../../lib/status/service";
import ConfirmByCountdown from "../../../components/dashboard/ConfirmByCountdown";
import RsvpForm from "../../../components/dashboard/RsvpForm";
import { redirect } from "next/navigation";
import { fetchPortalStatus } from "../../../lib/status/fetchPortalStatus";
import RsvpExperience from "../../../components/dashboard/RsvpExperience";

export default async function RsvpPage(): Promise<JSX.Element> {
const status = await getApplicantStatus("mock-user");
const isAdmitted = status.decisionStatus === "admitted";
const isAfterConfirmBy = new Date() > decisionDates.confirmBy;
const canRsvp = isAdmitted && !isAfterConfirmBy;
const { branch, status, decisionDates } = await fetchPortalStatus();
const confirmBy = new Date(decisionDates.confirmBy);
const isAfterConfirmBy = Date.now() > confirmBy.getTime();

return (
<section className="p-8">
<h1 className="text-2xl font-semibold">Post-acceptance RSVP</h1>
<p className="mt-2">confirm your attendance.</p>
<div className="mt-4">
<ConfirmByCountdown confirmBy={decisionDates.confirmBy} />
</div>
if (branch !== "admitted" || isAfterConfirmBy) {
redirect("/dashboard");
}

{canRsvp ? (
<RsvpForm />
) : (
<p className="mt-6 text-gray-600">no RSVP available</p>
)}
</section>
return (
<RsvpExperience
alreadySubmitted={status.rsvpStatus === "submitted"}
confirmBy={confirmBy.toISOString()}
/>
);
}
26 changes: 24 additions & 2 deletions apps/app-portal/src/app/api/v1/post-acceptance/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
import { NextResponse } from "next/server";
import { z } from "zod";
import { saveRsvp } from "../../../../lib/status/service";

export async function POST() {
return NextResponse.json({ error: "error 501" }, { status: 501 });
const rsvpSchema = z.object({
attending: z.enum(["yes", "no"]),
dietaryRestrictions: z.string().max(240),
tshirtSize: z.enum(["xs", "s", "m", "l", "xl"]),
accessibilityNeeds: z.string().max(240),
additionalNotes: z.string().max(400),
});

export async function POST(request: Request) {
try {
const body = await request.json();
const parsed = rsvpSchema.parse(body);

await saveRsvp("mock-user", parsed);

return NextResponse.json({ success: true });
} catch {
return NextResponse.json(
{ error: "Unable to save RSVP right now" },
{ status: 400 },
);
}
}
15 changes: 14 additions & 1 deletion apps/app-portal/src/app/api/v1/status/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
import { NextResponse } from "next/server";
import { decisionDates } from "../../../../lib/status/mock-singletons";
import { returnDashboardBranch } from "../../../../lib/status/machine";
import { getApplicantStatus } from "../../../../lib/status/service";

export async function GET() {
const status = await getApplicantStatus("mock-user");
return NextResponse.json(status);
const showDecision = new Date() >= decisionDates.showDecision;
const branch = returnDashboardBranch(status, decisionDates, showDecision);

return NextResponse.json({
branch,
status,
decisionDates: {
registrationOpen: decisionDates.registrationOpen.toISOString(),
showDecision: decisionDates.showDecision.toISOString(),
confirmBy: decisionDates.confirmBy.toISOString(),
},
});
}

export async function POST() {
Expand Down
34 changes: 34 additions & 0 deletions apps/app-portal/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"use client";

import React from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";

export default function Sidebar(): JSX.Element {
const pathname = usePathname();

const items = [
{ href: "/dashboard", label: "Dashboard" },
{ href: "/rsvp", label: "RSVP" },
];

return (
<nav className="w-56 shrink-0 min-h-screen border-r border-slate-200 bg-white px-4 py-6">
<ul className="space-y-1">
{items.map((it) => {
const active = pathname === it.href;
return (
<li key={it.href}>
<Link
href={it.href}
className={`block rounded-xl px-3 py-2 text-sm font-medium transition ${active ? "bg-slate-100 text-slate-900" : "text-slate-600 hover:bg-slate-50"}`}
>
{it.label}
</Link>
</li>
);
})}
</ul>
</nav>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,7 @@ export function DemographicsChart({
{entries.length === 0 ? (
<ChartEmpty message={EMPTY_MESSAGE} />
) : (
<ChartContainer
config={config}
className="aspect-[2] w-full"
>
<ChartContainer config={config} className="aspect-[2] w-full">
<BarChart
data={entries}
barCategoryGap={8}
Expand Down
Loading