From 21144193c779e816891449fed4ad7ecc923aa527 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Tue, 23 Jun 2026 07:12:49 -0700 Subject: [PATCH 1/8] feat(blade): customizable hackathon dashboards --- .../hackathon-dashboard/components.tsx | 169 ++++++++++++++++++ .../hackathon-dashboard/countdown.tsx | 7 +- .../hackathon-dashboard.tsx | 135 +------------- .../hackathon-dashboard/hackathon-data.tsx | 66 ++++--- .../hackathon-dashboard/issue-dialog.tsx | 4 +- .../hackathon-dashboard/point-leaderboard.tsx | 2 +- .../hackathon-dashboard/team-points.tsx | 2 +- .../hackathon-dashboard/upcoming-events.tsx | 10 +- .../hacker-dashboard/hacker-dashboard.tsx | 14 +- .../hacker-dashboard/hacker-data.tsx | 24 ++- .../current-hackathon-notice.tsx | 75 ++++++++ .../member-dashboard/member-dashboard.tsx | 23 ++- .../src/app/_components/user-interface.tsx | 104 +---------- .../components/bk-hackathon-dashboard.tsx | 27 +++ .../src/app/hackathon/bloomknights/page.tsx | 51 ++++++ apps/blade/src/app/hackathon/current/page.tsx | 27 +++ apps/blade/src/app/hackathon/page.tsx | 11 ++ packages/api/src/routers/hackathon.ts | 8 +- 18 files changed, 484 insertions(+), 275 deletions(-) create mode 100644 apps/blade/src/app/_components/dashboard/hackathon-dashboard/components.tsx create mode 100644 apps/blade/src/app/_components/dashboard/member-dashboard/current-hackathon-notice.tsx create mode 100644 apps/blade/src/app/hackathon/bloomknights/components/bk-hackathon-dashboard.tsx create mode 100644 apps/blade/src/app/hackathon/bloomknights/page.tsx create mode 100644 apps/blade/src/app/hackathon/current/page.tsx create mode 100644 apps/blade/src/app/hackathon/page.tsx diff --git a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/components.tsx b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/components.tsx new file mode 100644 index 000000000..5a7aa0098 --- /dev/null +++ b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/components.tsx @@ -0,0 +1,169 @@ +import type { SelectHackathon } from "@forge/db/schemas/knight-hacks"; +import { HACKATHONS } from "@forge/consts"; + +import type { api as serverCall } from "~/trpc/server"; +import { HackerAppCard } from "~/app/_components/option-cards"; +import { BaseHackathonCountdown } from "./countdown"; +import { BaseHackathonData } from "./hackathon-data"; +import { BaseHackathonTeamPoints } from "./team-points"; +import { BaseHackathonUpcomingEvents } from "./upcoming-events"; + +export { + BaseHackathonGuideButton, + BaseHackathonQRCodeButton, + BaseHackathonWalletButton, +} from "./hackathon-data"; +export { BaseHackathonCountdown } from "./countdown"; +export * from "./issue-dialog"; +export { BaseHackathonPointLeaderboard } from "./point-leaderboard"; +export { BaseHackathonTeamPoints } from "./team-points"; +export { BaseHackathonUpcomingEvents } from "./upcoming-events"; + +export interface BaseHackathonClassInfo { + classPfp: string; + team: string; + teamColor: string; +} + +type BaseHackathonHacker = Awaited< + ReturnType<(typeof serverCall.hackerQuery)["getHacker"]> +>; + +const DEFAULT_HACKER_GUIDE_HREF = + "https://knight-hacks.notion.site/knight-hacks-viii"; + +const DEFAULT_CLASS_INFO = HACKATHONS.KNIGHT_HACKS_8 + .HACKER_CLASS_INFO as Record; + +export function BaseHackathonClassError({ + hackerClass, +}: { + hackerClass?: string | null; +}) { + return ( +
+
+

+ Configuration Error +

+

+ Unable to load your team information. Please contact support or try + refreshing the page. +

+ {hackerClass && ( +

+ Class: {hackerClass} +

+ )} +
+
+ ); +} + +export function BaseHackathonRegistrationPrompt({ + hackathon, +}: { + hackathon: SelectHackathon; +}) { + return ( +
+

+ Register for {hackathon.displayName} today! +

+
+ +
+
+ ); +} + +export function BaseHackathonDashboard({ + classInfoByClass = DEFAULT_CLASS_INFO, + guideHref = DEFAULT_HACKER_GUIDE_HREF, + hackathon, + hacker, +}: { + classInfoByClass?: Record; + guideHref?: string; + hackathon: SelectHackathon; + hacker: BaseHackathonHacker; +}) { + if (!hacker) { + return ; + } + + if (!hacker.class || !(hacker.class in classInfoByClass)) { + return ; + } + + const classInfo = classInfoByClass[hacker.class]; + + if (!classInfo) { + return ; + } + + const { classPfp, team, teamColor } = classInfo; + + return ( + <> +
+

+ Hello, + + {hacker.class} + + {hacker.firstName}! +

+

+ {hackathon.displayName} Dashboard +

+
+ +
+ + +
+ +
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+ + ); +} diff --git a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/countdown.tsx b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/countdown.tsx index 3ec9a32e8..3bab0cd08 100644 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/countdown.tsx +++ b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/countdown.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from "react"; import { Card, CardContent } from "@forge/ui/card"; -export default function HackingCountdown() { +export function BaseHackathonCountdown({ endDate }: { endDate: Date }) { const [timeLeft, setTimeLeft] = useState({ days: 0, hours: 0, @@ -13,8 +13,7 @@ export default function HackingCountdown() { }); useEffect(() => { - // Set your target end date here - const targetDate = new Date("2025-10-26T11:00:00").getTime(); + const targetDate = endDate.getTime(); const updateCountdown = () => { const now = new Date().getTime(); @@ -39,7 +38,7 @@ export default function HackingCountdown() { const interval = setInterval(updateCountdown, 1000); return () => clearInterval(interval); - }, []); + }, [endDate]); const formatNumber = (num: number) => String(num).padStart(2, "0"); diff --git a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-dashboard.tsx b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-dashboard.tsx index d22455004..a1ee4b770 100644 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-dashboard.tsx +++ b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-dashboard.tsx @@ -1,14 +1,10 @@ import type { Metadata } from "next"; -import { HACKATHONS } from "@forge/consts"; +import type { SelectHackathon } from "@forge/db/schemas/knight-hacks"; import type { api as serverCall } from "~/trpc/server"; -import { HackerAppCard } from "~/app/_components/option-cards"; import { api } from "~/trpc/server"; -import HackingCountdown from "./countdown"; -import { HackathonData } from "./hackathon-data"; -import { TeamPoints } from "./team-points"; -import UpcomingEvents from "./upcoming-events"; +import { BaseHackathonDashboard } from "./components"; export const metadata: Metadata = { title: "Hacker Dashboard", @@ -16,137 +12,24 @@ export const metadata: Metadata = { }; export default async function HackathonDashboard({ + hackathon, hacker, }: { + hackathon?: SelectHackathon | null; hacker: Awaited>; }) { - interface HackerClassInfo { - teamColor: string; - team: string; - classPfp: string; - } - const currentHackathon = await api.hackathon.getCurrentHackathon(); + const activeHackathon = + hackathon ?? (await api.hackathon.getCurrentHackathon()); - if (!hacker) { + if (!activeHackathon) { return (

- Register for Knight Hacks today! + There is not a hackathon running right now.

-
- {currentHackathon && ( - - )} -
-
- ); - } - - if ( - !hacker.class || - !(hacker.class in HACKATHONS.KNIGHT_HACKS_8.HACKER_CLASS_INFO) - ) { - return ( -
-
-

- Configuration Error -

-

- Unable to load your team information. Please contact support or try - refreshing the page. -

- {hacker.class && ( -

- Class: {hacker.class} -

- )} -
); } - interface HackerClassInfo { - teamColor: string; - team: string; - classPfp: string; - } - - const HACKER_CLASS_INFO_TYPED: Record = HACKATHONS - .KNIGHT_HACKS_8.HACKER_CLASS_INFO as Record; - - const classInfo = HACKER_CLASS_INFO_TYPED[hacker.class] ?? { - teamColor: "#000000", - team: "Unknown Team", - classPfp: "/default.png", - }; - - const { teamColor, team, classPfp } = classInfo; - - return ( - <> -
-

- Hello, - - {hacker.class} - - {hacker.firstName}! -

-

- Hackathon Dashboard -

-
- -
- {/* Main content */} - - - {/* Transparent Triangle overlay in bottom right corner - hidden on mobile */} -
- - {/* Triangle in bottom right corner - hidden on mobile */} -
- - {/* Top rectangle - hidden on mobile */} -
-
-
- - {/* Bottom rectangle - hidden on mobile */} -
-
-
- - {/* Left side rectangle - hidden on mobile */} -
-
-
- -
-
- -
-
- -
- - ); + return ; } diff --git a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx index 1db3589fd..1b8da7f55 100644 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx +++ b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx @@ -5,6 +5,7 @@ import Image from "next/image"; import Link from "next/link"; import { BookOpen, CircleCheckBig, Trophy } from "lucide-react"; +import type { SelectHackathon } from "@forge/db/schemas/knight-hacks"; import { Dialog, DialogContent, @@ -18,18 +19,49 @@ import { HackerQRCodePopup } from "~/app/_components/dashboard/hacker-dashboard/ import { DownloadQRPass } from "~/app/_components/dashboard/member-dashboard/download-qr-pass"; import { HACKER_STATUS_MAP } from "~/consts"; import { api } from "~/trpc/react"; -import AlertButton from "./issue-dialog"; -import { PointLeaderboard } from "./point-leaderboard"; +import { BaseHackathonIssueButton } from "./issue-dialog"; +import { BaseHackathonPointLeaderboard } from "./point-leaderboard"; type StatusKey = keyof typeof HACKER_STATUS_MAP | null | undefined; +type HackerProfile = Awaited< + ReturnType<(typeof serverCall.hackerQuery)["getHacker"]> +>; -export function HackathonData({ +export function BaseHackathonQRCodeButton() { + return ; +} + +export function BaseHackathonWalletButton({ + profile, +}: { + profile: HackerProfile; +}) { + return ; +} + +export function BaseHackathonGuideButton({ href }: { href: string }) { + return ( + + + Hacker's Guide + + ); +} + +export function BaseHackathonData({ data, + guideHref, + hackathon, teamColor, team, classPfp, }: { - data: Awaited>; + data: HackerProfile; + guideHref: string; + hackathon: SelectHackathon; teamColor: string; team: string; classPfp: string; @@ -38,16 +70,12 @@ export function HackathonData({ const [hackerStatusColor, setHackerStatusColor] = useState(""); const { data: hacker, isError } = api.hackerQuery.getHacker.useQuery( - {}, + { hackathonName: hackathon.name }, { initialData: data, }, ); - const { data: hackathonData } = api.hackathon.getHackathon.useQuery({ - hackathonName: undefined, - }); - function getStatusName(status: StatusKey) { if (!status) return ""; return HACKER_STATUS_MAP[status].name; @@ -114,7 +142,7 @@ export function HackathonData({ {/* Status Badge */}

- Status for {hackathonData?.displayName} + Status for {hackathon.displayName}

- - + +
{/* Hacker Guide Link */}
- - - Hacker's Guide - - + +
@@ -246,9 +268,9 @@ export function HackathonData({ Leaderboard - diff --git a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/issue-dialog.tsx b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/issue-dialog.tsx index 486d9b51f..2dbd7d119 100644 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/issue-dialog.tsx +++ b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/issue-dialog.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useState } from "react"; import { AlertCircle } from "lucide-react"; @@ -17,7 +19,7 @@ import { toast } from "@forge/ui/toast"; import { api } from "~/trpc/react"; -export default function AlertButton() { +export function BaseHackathonIssueButton() { const [open, setOpen] = useState(false); const [issue, setIssue] = useState(""); diff --git a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/point-leaderboard.tsx b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/point-leaderboard.tsx index 0e0ef971d..ff84d7427 100644 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/point-leaderboard.tsx +++ b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/point-leaderboard.tsx @@ -18,7 +18,7 @@ interface LeaderboardEntry { id: string; } -export function PointLeaderboard({ +export function BaseHackathonPointLeaderboard({ hacker, hId, }: { diff --git a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/team-points.tsx b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/team-points.tsx index d930ab30c..cd85d5bd7 100644 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/team-points.tsx +++ b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/team-points.tsx @@ -10,7 +10,7 @@ import { hackathons } from "@forge/utils"; import { api } from "~/trpc/react"; -export function TeamPoints({ +export function BaseHackathonTeamPoints({ hId, hClass, }: { diff --git a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/upcoming-events.tsx b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/upcoming-events.tsx index 4c0c99c9f..abb189453 100644 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/upcoming-events.tsx +++ b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/upcoming-events.tsx @@ -12,7 +12,11 @@ import { time } from "@forge/utils"; import { api } from "~/trpc/server"; -export default async function UpcomingEvents() { +export async function BaseHackathonUpcomingEvents({ + hackathonId, +}: { + hackathonId: string; +}) { const events = await api.event.getEvents(); // eslint-disable-next-line react-hooks/purity @@ -24,7 +28,9 @@ export default async function UpcomingEvents() { const oneDayOffset = 24 * 60 * 60 * 1000; const start = new Date(event.start_datetime).getTime() + oneDayOffset; return ( - event.hackathonId != null && start >= now && start <= fiveHoursLater + event.hackathonId === hackathonId && + start >= now && + start <= fiveHoursLater ); }) .sort( diff --git a/apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-dashboard.tsx b/apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-dashboard.tsx index 9ccb5281b..d8dd17d97 100644 --- a/apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-dashboard.tsx +++ b/apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-dashboard.tsx @@ -1,5 +1,7 @@ import type { Metadata } from "next"; +import type { SelectHackathon } from "@forge/db/schemas/knight-hacks"; + import type { api as serverCall } from "~/trpc/server"; import { HackerAppCard } from "~/app/_components/option-cards"; import { api } from "~/trpc/server"; @@ -13,8 +15,10 @@ export const metadata: Metadata = { }; export default async function HackerDashboard({ + hackathon, hacker, }: { + hackathon?: SelectHackathon | null; hacker: Awaited>; }) { const [resume, pastHackathons] = await Promise.allSettled([ @@ -22,7 +26,9 @@ export default async function HackerDashboard({ api.hackathon.getPastHackathons(), ]); - const currentHackathon = await api.hackathon.getCurrentHackathon(); + const activeHackathon = + hackathon ?? + (await api.hackathon.getHackathon({ hackathonName: undefined })); if (!hacker) { return ( @@ -33,8 +39,8 @@ export default async function HackerDashboard({
{ //if there is no current hackathon then this page is never rendered anyway - currentHackathon && ( - + activeHackathon && ( + ) }
@@ -52,7 +58,7 @@ export default async function HackerDashboard({
{/* Main content */} - + {/* Transparent Triangle overlay in bottom right corner */}
diff --git a/apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-data.tsx b/apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-data.tsx index df8301f14..2444c743e 100644 --- a/apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-data.tsx +++ b/apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-data.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from "react"; import Image from "next/image"; import { CircleCheckBig, Loader2 } from "lucide-react"; +import type { SelectHackathon } from "@forge/db/schemas/knight-hacks"; import { CLUB } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { @@ -28,8 +29,10 @@ type StatusKey = keyof typeof HACKER_STATUS_MAP | null | undefined; export function HackerData({ data, + hackathon, }: { data: Awaited>; + hackathon?: SelectHackathon | null; }) { const [hackerStatus, setHackerStatus] = useState(""); const [hackerStatusColor, setHackerStatusColor] = useState(""); @@ -38,26 +41,29 @@ export function HackerData({ const [isOpen, setIsOpen] = useState(false); const [isConfirmOpen, setIsConfirmOpen] = useState(false); - const { data: currentHackathon } = - api.hackathon.getCurrentHackathon.useQuery(); + const { data: fallbackHackathon } = api.hackathon.getHackathon.useQuery( + { hackathonName: undefined }, + { + enabled: !hackathon, + }, + ); + + const hackathonData = hackathon ?? fallbackHackathon ?? null; const { data: hacker, isError } = api.hackerQuery.getHacker.useQuery( - {}, + { hackathonName: hackathonData?.name }, { + enabled: Boolean(hackathonData?.name), initialData: data, }, ); - const { data: hackathonData } = api.hackathon.getHackathon.useQuery({ - hackathonName: undefined, - }); - const { data: numConfirmed } = api.hackathon.getNumConfirmed.useQuery( { - hackathonId: currentHackathon?.id ?? "", + hackathonId: hackathonData?.id ?? "", }, { - enabled: Boolean(currentHackathon?.id), + enabled: Boolean(hackathonData?.id), retry: false, }, ); diff --git a/apps/blade/src/app/_components/dashboard/member-dashboard/current-hackathon-notice.tsx b/apps/blade/src/app/_components/dashboard/member-dashboard/current-hackathon-notice.tsx new file mode 100644 index 000000000..82c251b1f --- /dev/null +++ b/apps/blade/src/app/_components/dashboard/member-dashboard/current-hackathon-notice.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { Trophy } from "lucide-react"; + +import { cn } from "@forge/ui"; +import { Alert, AlertDescription, AlertTitle } from "@forge/ui/alert"; +import { buttonVariants } from "@forge/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@forge/ui/dialog"; + +const CURRENT_HACKATHON_HREF = "/hackathon/current"; + +export function CurrentHackathonNotice({ + hackathonDisplayName, +}: { + hackathonDisplayName: string; +}) { + const [open, setOpen] = useState(true); + + return ( + <> + + + {hackathonDisplayName} is happening now + + + Head to the hackathon dashboard for your check-in tools, points, + live events, and event-specific info. + + + Open Hackathon Dashboard + + + + + + + + {hackathonDisplayName} is live + + The hackathon dashboard has check-in tools, points, live events, + and event-specific info. + + + + + Stay Here + + + Open Hackathon Dashboard + + + + + + ); +} diff --git a/apps/blade/src/app/_components/dashboard/member-dashboard/member-dashboard.tsx b/apps/blade/src/app/_components/dashboard/member-dashboard/member-dashboard.tsx index b29f0c933..85a0cc1e7 100644 --- a/apps/blade/src/app/_components/dashboard/member-dashboard/member-dashboard.tsx +++ b/apps/blade/src/app/_components/dashboard/member-dashboard/member-dashboard.tsx @@ -5,6 +5,7 @@ import { MemberAppCard } from "~/app/_components/option-cards"; import { api } from "~/trpc/server"; import { AlumniDiscord } from "./AlumniDiscord"; import { AlumniRecap } from "./AlumniRecap"; +import { CurrentHackathonNotice } from "./current-hackathon-notice"; import DayInHistory from "./day-in-history"; import EarlyAccessVolunteer from "./early-access-volunteer"; import { EventNumber } from "./event/event-number"; @@ -83,11 +84,13 @@ export default async function MemberDashboard({ const isAlumni = calcAlumniStatus(member.gradDate, member); - const [eventsValue, dues, hackathonsValue] = await Promise.all([ - api.member.getEvents(), - api.duesPayment.validatePaidDues(), - isAlumni ? api.hackathon.getPastHackathons() : Promise.resolve([]), - ]); + const [eventsValue, dues, hackathonsValue, currentHackathon] = + await Promise.all([ + api.member.getEvents(), + api.duesPayment.validatePaidDues(), + isAlumni ? api.hackathon.getPastHackathons() : Promise.resolve([]), + api.hackathon.getCurrentHackathon(), + ]); const duesPaid = dues.duesPaid; @@ -108,6 +111,11 @@ export default async function MemberDashboard({

Alumni Dashboard

+ {currentHackathon && ( + + )} {/* Unified View */}
@@ -147,6 +155,11 @@ export default async function MemberDashboard({

Member Dashboard

+ {currentHackathon && ( + + )} {/* Unified View */}
diff --git a/apps/blade/src/app/_components/user-interface.tsx b/apps/blade/src/app/_components/user-interface.tsx index d12417cd9..c8618ccdf 100644 --- a/apps/blade/src/app/_components/user-interface.tsx +++ b/apps/blade/src/app/_components/user-interface.tsx @@ -1,59 +1,19 @@ -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@forge/ui/tabs"; - -import HackathonDashboard from "~/app/_components/dashboard/hackathon-dashboard/hackathon-dashboard"; -import HackerDashboard from "~/app/_components/dashboard/hacker-dashboard/hacker-dashboard"; import MemberDashboard from "~/app/_components/dashboard/member-dashboard/member-dashboard"; -import { HackerAppCard, MemberAppCard } from "~/app/_components/option-cards"; +import { MemberAppCard } from "~/app/_components/option-cards"; import { api } from "~/trpc/server"; export async function UserInterface() { - const [member, hacker] = await Promise.allSettled([ - api.member.getMember(), - api.hackerQuery.getHacker({}), - ]); - - const currentHackathonResult = await Promise.allSettled([ - api.hackathon.getCurrentHackathon(), - ]); - if ( - member.status === "rejected" || - hacker.status === "rejected" || - currentHackathonResult[0].status === "rejected" - ) { - return ( -
- Something went wrong. Please try again later. -
- ); - } + const member = await api.member.getMember(); - const memberValue = member.value; - const hackerValue = hacker.value; - const currentHackathon = currentHackathonResult[0].value; - - if (!memberValue && !hackerValue) { + if (!member) { return (

- You have not applied to be a Knight Hacks member or hacker for an - upcoming Hackathon yet. Please fill out an application below to get - started! + You have not applied to be a Knight Hacks member yet. Please fill out + an application below to get started!

- {currentHackathon && ( - - )} -
-
- ); - } - - if (memberValue && !currentHackathon) { - return ( -
-
-
); @@ -61,57 +21,9 @@ export async function UserInterface() { return (
- -
-
-

- Select Your Dashboard -

- - - {!memberValue ? ( - "Become a Member" - ) : ( - <> - Member - Member - - )} - - - - {currentHackathon ? currentHackathon.displayName : "Hacker"} - - - {currentHackathon - ? `${currentHackathon.displayName}` - : "Hacker Dashboard"} - - - -
-
- -
- -
-
- -
- {hackerValue && (hackerValue.status as string) === "checkedin" ? ( - - ) : ( - - )} -
-
-
+
+ +
); } diff --git a/apps/blade/src/app/hackathon/bloomknights/components/bk-hackathon-dashboard.tsx b/apps/blade/src/app/hackathon/bloomknights/components/bk-hackathon-dashboard.tsx new file mode 100644 index 000000000..75aa12d4e --- /dev/null +++ b/apps/blade/src/app/hackathon/bloomknights/components/bk-hackathon-dashboard.tsx @@ -0,0 +1,27 @@ +import type { SelectHackathon } from "@forge/db/schemas/knight-hacks"; + +import type { api as serverCall } from "~/trpc/server"; +import { BaseHackathonDashboard } from "~/app/_components/dashboard/hackathon-dashboard/components"; + +type BKHackathonHacker = Awaited< + ReturnType<(typeof serverCall.hackerQuery)["getHacker"]> +>; + +const BK_HACKER_GUIDE_HREF = + "https://knight-hacks.notion.site/knight-hacks-viii"; + +export function BKHackathonDashboard({ + hackathon, + hacker, +}: { + hackathon: SelectHackathon; + hacker: BKHackathonHacker; +}) { + return ( + + ); +} diff --git a/apps/blade/src/app/hackathon/bloomknights/page.tsx b/apps/blade/src/app/hackathon/bloomknights/page.tsx new file mode 100644 index 000000000..520a9885e --- /dev/null +++ b/apps/blade/src/app/hackathon/bloomknights/page.tsx @@ -0,0 +1,51 @@ +import type { Metadata } from "next"; +import { notFound, redirect } from "next/navigation"; + +import { auth } from "@forge/auth"; + +import HackerDashboard from "~/app/_components/dashboard/hacker-dashboard/hacker-dashboard"; +import { SessionNavbar } from "~/app/_components/navigation/session-navbar"; +import { api, HydrateClient } from "~/trpc/server"; +import { BKHackathonDashboard } from "./components/bk-hackathon-dashboard"; + +export const metadata: Metadata = { + title: "Blade | BloomKnights Dashboard", + description: "The official BloomKnights hackathon dashboard.", +}; + +export default async function BloomKnightsHackathonPage() { + const session = await auth(); + + if (!session) { + redirect("/"); + } + + const hackathon = await api.hackathon.getHackathon({ + hackathonName: "bloomknights", + }); + + if (!hackathon) { + notFound(); + } + + const hacker = await api.hackerQuery.getHacker({ + hackathonName: hackathon.name, + }); + + return ( + + +
+
+
+ {hacker && (hacker.status as string) === "checkedin" ? ( + + ) : ( + + )} +
+
+
+
+ ); +} diff --git a/apps/blade/src/app/hackathon/current/page.tsx b/apps/blade/src/app/hackathon/current/page.tsx new file mode 100644 index 000000000..96d1ac4d0 --- /dev/null +++ b/apps/blade/src/app/hackathon/current/page.tsx @@ -0,0 +1,27 @@ +import type { Metadata } from "next"; +import { redirect } from "next/navigation"; + +import { auth } from "@forge/auth"; + +import { api } from "~/trpc/server"; + +export const metadata: Metadata = { + title: "Blade | Current Hackathon", + description: "Open the currently running Knight Hacks hackathon dashboard.", +}; + +export default async function CurrentHackathonPage() { + const session = await auth(); + + if (!session) { + redirect("/"); + } + + const currentHackathon = await api.hackathon.getCurrentHackathon(); + + if (!currentHackathon) { + redirect("/dashboard"); + } + + redirect(`/hackathon/${currentHackathon.name}`); +} diff --git a/apps/blade/src/app/hackathon/page.tsx b/apps/blade/src/app/hackathon/page.tsx new file mode 100644 index 000000000..660cc42f3 --- /dev/null +++ b/apps/blade/src/app/hackathon/page.tsx @@ -0,0 +1,11 @@ +import type { Metadata } from "next"; +import { redirect } from "next/navigation"; + +export const metadata: Metadata = { + title: "Blade | Hackathon", + description: "Open the running Knight Hacks hackathon dashboard.", +}; + +export default function HackathonIndexPage() { + redirect("/hackathon/current"); +} diff --git a/packages/api/src/routers/hackathon.ts b/packages/api/src/routers/hackathon.ts index 321baa31b..d83d80a8a 100644 --- a/packages/api/src/routers/hackathon.ts +++ b/packages/api/src/routers/hackathon.ts @@ -3,7 +3,7 @@ import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { HACKATHONS } from "@forge/consts"; -import { and, count, desc, eq, getTableColumns, lt } from "@forge/db"; +import { and, count, desc, eq, getTableColumns, gte, lt, lte } from "@forge/db"; import { db } from "@forge/db/client"; import { Hackathon, @@ -137,12 +137,12 @@ export const hackathonRouter = { }), getCurrentHackathon: publicProcedure.query(async () => { - // Find first hackathon that hasnt ended yet + const now = new Date(); const hackathon = await db.query.Hackathon.findFirst({ orderBy: (t, { asc }) => asc(t.endDate), - where: (t, { and, gte, lte }) => - and(gte(t.endDate, new Date()), lte(t.applicationOpen, new Date())), + where: and(lte(Hackathon.startDate, now), gte(Hackathon.endDate, now)), }); + return hackathon ?? null; }), From a2c28bbcb48d6a1c6b294aa8ece4af1c5c2ad3b8 Mon Sep 17 00:00:00 2001 From: DGoel1602 Date: Tue, 23 Jun 2026 11:37:10 -0400 Subject: [PATCH 2/8] chore: fix class validation bug in hackathon check in --- packages/api/src/routers/hackers/mutations.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/api/src/routers/hackers/mutations.ts b/packages/api/src/routers/hackers/mutations.ts index 4263b77c0..47bb1acdd 100644 --- a/packages/api/src/routers/hackers/mutations.ts +++ b/packages/api/src/routers/hackers/mutations.ts @@ -529,11 +529,7 @@ export const hackerMutationRouter = { hackathonId: z.string().uuid(), assignedClassCheckin: z.string().superRefine((v, ctx) => { //Idk man leave me alone - if ( - !( - AssignedClassCheckinSchema.options as unknown as string[] - ).includes(v) - ) { + if (!AssignedClassCheckinSchema.safeParse(v).success) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Invalid assignedClassCheckin", From 04a26a150e5766a30edc3f11f88cafc7d716f260 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Tue, 23 Jun 2026 19:14:46 -0700 Subject: [PATCH 3/8] chore: fix some mistakes ig --- .../hackathon-dashboard/components.tsx | 58 +++++-------------- .../member-dashboard/member-dashboard.tsx | 16 ----- .../src/app/hackathon/bloomknights/page.tsx | 2 +- 3 files changed, 14 insertions(+), 62 deletions(-) diff --git a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/components.tsx b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/components.tsx index 5a7aa0098..c71e37ede 100644 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/components.tsx +++ b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/components.tsx @@ -19,27 +19,12 @@ export { BaseHackathonPointLeaderboard } from "./point-leaderboard"; export { BaseHackathonTeamPoints } from "./team-points"; export { BaseHackathonUpcomingEvents } from "./upcoming-events"; -export interface BaseHackathonClassInfo { - classPfp: string; - team: string; - teamColor: string; -} - -type BaseHackathonHacker = Awaited< - ReturnType<(typeof serverCall.hackerQuery)["getHacker"]> ->; - const DEFAULT_HACKER_GUIDE_HREF = "https://knight-hacks.notion.site/knight-hacks-viii"; -const DEFAULT_CLASS_INFO = HACKATHONS.KNIGHT_HACKS_8 - .HACKER_CLASS_INFO as Record; +const DEFAULT_CLASS_INFO = HACKATHONS.KNIGHT_HACKS_8.HACKER_CLASS_INFO; -export function BaseHackathonClassError({ - hackerClass, -}: { - hackerClass?: string | null; -}) { +export function BaseHackathonClassError() { return (
@@ -50,11 +35,6 @@ export function BaseHackathonClassError({ Unable to load your team information. Please contact support or try refreshing the page.

- {hackerClass && ( -

- Class: {hackerClass} -

- )}
); @@ -83,48 +63,36 @@ export function BaseHackathonDashboard({ hackathon, hacker, }: { - classInfoByClass?: Record; + classInfoByClass?: Record< + string, + { + classPfp: string; + team: string; + teamColor: string; + } + >; guideHref?: string; hackathon: SelectHackathon; - hacker: BaseHackathonHacker; + hacker: Awaited>; }) { if (!hacker) { return ; } if (!hacker.class || !(hacker.class in classInfoByClass)) { - return ; + return ; } const classInfo = classInfoByClass[hacker.class]; if (!classInfo) { - return ; + return ; } const { classPfp, team, teamColor } = classInfo; return ( <> -
-

- Hello, - - {hacker.class} - - {hacker.firstName}! -

-

- {hackathon.displayName} Dashboard -

-
-
-
-
-

- Hello, {member.firstName}! -

-

Alumni Dashboard

-
-
{currentHackathon && (
-
-
-

- Hello, {member.firstName}! -

-

Member Dashboard

-
-
{currentHackathon && (
- {hacker && (hacker.status as string) === "checkedin" ? ( + {hacker?.status === "checkedin" ? ( ) : ( From 8f7eb2c58a471e9225c84283d4d9eab590e824fc Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Wed, 24 Jun 2026 22:07:44 -0700 Subject: [PATCH 4/8] temp for dhruv --- .../hackathon-dashboard/components.tsx | 41 ---- .../hackathon-dashboard.tsx | 35 --- .../hackathon-dashboard/hackathon-data.tsx | 83 ++----- .../hackathon-dashboard/point-leaderboard.tsx | 214 ------------------ .../hackathon-dashboard/team-points.tsx | 155 ------------- .../hacker-dashboard/hacker-dashboard.tsx | 20 +- .../hacker-dashboard/hacker-data.tsx | 2 +- .../current-hackathon-notice.tsx | 6 +- .../components/bk-hackathon-dashboard.tsx | 27 --- .../src/app/hackathon/bloomknights/page.tsx | 9 +- apps/blade/src/app/hackathon/current/page.tsx | 27 --- apps/blade/src/app/hackathon/page.tsx | 43 +++- 12 files changed, 65 insertions(+), 597 deletions(-) delete mode 100644 apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-dashboard.tsx delete mode 100644 apps/blade/src/app/_components/dashboard/hackathon-dashboard/point-leaderboard.tsx delete mode 100644 apps/blade/src/app/_components/dashboard/hackathon-dashboard/team-points.tsx delete mode 100644 apps/blade/src/app/hackathon/bloomknights/components/bk-hackathon-dashboard.tsx delete mode 100644 apps/blade/src/app/hackathon/current/page.tsx diff --git a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/components.tsx b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/components.tsx index c71e37ede..a45b046b1 100644 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/components.tsx +++ b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/components.tsx @@ -1,11 +1,9 @@ import type { SelectHackathon } from "@forge/db/schemas/knight-hacks"; -import { HACKATHONS } from "@forge/consts"; import type { api as serverCall } from "~/trpc/server"; import { HackerAppCard } from "~/app/_components/option-cards"; import { BaseHackathonCountdown } from "./countdown"; import { BaseHackathonData } from "./hackathon-data"; -import { BaseHackathonTeamPoints } from "./team-points"; import { BaseHackathonUpcomingEvents } from "./upcoming-events"; export { @@ -15,31 +13,11 @@ export { } from "./hackathon-data"; export { BaseHackathonCountdown } from "./countdown"; export * from "./issue-dialog"; -export { BaseHackathonPointLeaderboard } from "./point-leaderboard"; -export { BaseHackathonTeamPoints } from "./team-points"; export { BaseHackathonUpcomingEvents } from "./upcoming-events"; const DEFAULT_HACKER_GUIDE_HREF = "https://knight-hacks.notion.site/knight-hacks-viii"; -const DEFAULT_CLASS_INFO = HACKATHONS.KNIGHT_HACKS_8.HACKER_CLASS_INFO; - -export function BaseHackathonClassError() { - return ( -
-
-

- Configuration Error -

-

- Unable to load your team information. Please contact support or try - refreshing the page. -

-
-
- ); -} - export function BaseHackathonRegistrationPrompt({ hackathon, }: { @@ -58,7 +36,6 @@ export function BaseHackathonRegistrationPrompt({ } export function BaseHackathonDashboard({ - classInfoByClass = DEFAULT_CLASS_INFO, guideHref = DEFAULT_HACKER_GUIDE_HREF, hackathon, hacker, @@ -79,28 +56,13 @@ export function BaseHackathonDashboard({ return ; } - if (!hacker.class || !(hacker.class in classInfoByClass)) { - return ; - } - - const classInfo = classInfoByClass[hacker.class]; - - if (!classInfo) { - return ; - } - - const { classPfp, team, teamColor } = classInfo; - return ( <>
@@ -123,9 +85,6 @@ export function BaseHackathonDashboard({
-
- -
diff --git a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-dashboard.tsx b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-dashboard.tsx deleted file mode 100644 index a1ee4b770..000000000 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-dashboard.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import type { Metadata } from "next"; - -import type { SelectHackathon } from "@forge/db/schemas/knight-hacks"; - -import type { api as serverCall } from "~/trpc/server"; -import { api } from "~/trpc/server"; -import { BaseHackathonDashboard } from "./components"; - -export const metadata: Metadata = { - title: "Hacker Dashboard", - description: "The official Knight Hacks Hacker Dashboard", -}; - -export default async function HackathonDashboard({ - hackathon, - hacker, -}: { - hackathon?: SelectHackathon | null; - hacker: Awaited>; -}) { - const activeHackathon = - hackathon ?? (await api.hackathon.getCurrentHackathon()); - - if (!activeHackathon) { - return ( -
-

- There is not a hackathon running right now. -

-
- ); - } - - return ; -} diff --git a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx index 1b8da7f55..67e4e4ed9 100644 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx +++ b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx @@ -1,18 +1,11 @@ "use client"; import { useEffect, useState } from "react"; -import Image from "next/image"; import Link from "next/link"; import { BookOpen, CircleCheckBig, Trophy } from "lucide-react"; import type { SelectHackathon } from "@forge/db/schemas/knight-hacks"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@forge/ui/dialog"; +import { Dialog, DialogTrigger } from "@forge/ui/dialog"; import type { api as serverCall } from "~/trpc/server"; import { HackerQRCodePopup } from "~/app/_components/dashboard/hacker-dashboard/hacker-qr-button"; @@ -20,7 +13,6 @@ import { DownloadQRPass } from "~/app/_components/dashboard/member-dashboard/dow import { HACKER_STATUS_MAP } from "~/consts"; import { api } from "~/trpc/react"; import { BaseHackathonIssueButton } from "./issue-dialog"; -import { BaseHackathonPointLeaderboard } from "./point-leaderboard"; type StatusKey = keyof typeof HACKER_STATUS_MAP | null | undefined; type HackerProfile = Awaited< @@ -55,16 +47,10 @@ export function BaseHackathonData({ data, guideHref, hackathon, - teamColor, - team, - classPfp, }: { data: HackerProfile; guideHref: string; hackathon: SelectHackathon; - teamColor: string; - team: string; - classPfp: string; }) { const [hackerStatus, setHackerStatus] = useState(""); const [hackerStatusColor, setHackerStatusColor] = useState(""); @@ -117,32 +103,10 @@ export function BaseHackathonData({ )} -
- - {hacker?.class} - - - - {"Team " + team} - -
- {/* Status Badge */}

- Status for {hackathon.displayName} + Application Status

- - {/* TK Image */} -
- Team Mascot Image -
@@ -200,7 +152,7 @@ export function BaseHackathonData({ {/* Decorative gradient overlay */}
@@ -208,11 +160,11 @@ export function BaseHackathonData({
@@ -231,15 +183,15 @@ export function BaseHackathonData({
{hacker?.points || 0} @@ -251,28 +203,19 @@ export function BaseHackathonData({ - - - Leaderboard - - -
diff --git a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/point-leaderboard.tsx b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/point-leaderboard.tsx deleted file mode 100644 index ff84d7427..000000000 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/point-leaderboard.tsx +++ /dev/null @@ -1,214 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { Dot, Loader } from "lucide-react"; - -import type { HackerClass } from "@forge/db/schemas/knight-hacks"; -import { HACKER_TEAMS } from "@forge/db/schemas/knight-hacks"; -import { hackathons } from "@forge/utils"; - -import type { api as serverCall } from "~/trpc/server"; -import { api } from "~/trpc/react"; - -interface LeaderboardEntry { - firstName: string; - lastName: string; - points: number; - class: HackerClass | null; - id: string; -} - -export function BaseHackathonPointLeaderboard({ - hacker, - hId, -}: { - hacker: Awaited>; - hId: string; -}) { - const dummy: HackerClass[] = [ - "Operator", - "Harbinger", - "Mechanist", - "Sentinel", - "Monstologist", - ]; - - const { data: data } = api.hackerQuery.getTopHackers.useQuery({ - hackathonName: hId, - hPoints: hacker?.points || 0, - hClass: hacker?.class || "Alchemist", - }); - const team = hackathons.getClassTeam(hacker?.class || "Alchemist"); - - const [overall, setOverall] = useState(); - const [showYours, setShowYours] = useState(false); - - const [activeInd, setInd] = useState(-1); - const [activeTop, setTop] = useState(overall); - - const { data: isAdmin } = api.roles.hasPermission.useQuery({ - or: ["READ_CLUB_DATA"], - }); - - const targetDate = new Date("2025-10-25T23:00:00").getTime(); - - useEffect(() => { - if (data) { - setOverall( - data.topA - .concat(data.topB) - .sort((a, b) => b.points - a.points) - .splice(0, 5), - ); - setInd(1); - } - }, [data]); - - useEffect(() => { - switch (activeInd) { - case 0: - setTop(data?.topA ?? []); - break; - case 1: - setTop(overall ?? []); - break; - case 2: - setTop(data?.topB ?? []); - break; - } - }, [activeInd, data, overall]); - - useEffect(() => { - if (activeTop) - setShowYours( - !activeTop.find((v) => v.id == hacker?.id) && - (data?.place[activeInd] ?? -1) != -1, - ); - }, [activeTop, hacker?.id, data?.place, activeInd]); - - // eslint-disable-next-line react-hooks/purity - return targetDate <= Date.now() && !isAdmin ? ( - <> -

- The leaderboard has been hidden while we decide who gets to be crowned{" "} - Most Involved Hacker. -

-

- However, the fight's not over yet. Keep on earning points! Everything - will still be counted until the end of the event. -

-

- Make sure to come to the Closing Ceremony to see the winners! -

-
- You have{" "} - - {hacker?.points} - {" "} - points. -
- - ) : ( - <> -
- - - -
-
- {!activeTop ? ( - dummy.map((v, i) => { - const t = hackathons.getClassTeam(v); - return ( -
- - -
- ); - }) - ) : ( - <> - {activeTop.map((v, i) => { - const t = hackathons.getClassTeam(v.class || "Alchemist"); - return ( -
-
- {`${i + 1}. ${v.firstName} ${v.lastName}`} -
- {v.class} -
-
-
{`${v.points} pts.`}
-
- ); - })} - {showYours && ( - <> -
- - - -
-
-
{`${(data?.place[activeInd] ?? 0) + 1}. ${hacker?.firstName} ${hacker?.lastName}`}
-
{`${hacker?.points} pts.`}
-
- - )} - - )} -
- - ); -} diff --git a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/team-points.tsx b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/team-points.tsx deleted file mode 100644 index cd85d5bd7..000000000 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/team-points.tsx +++ /dev/null @@ -1,155 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { Crown, Dot, Loader } from "lucide-react"; - -import type { HackerClass } from "@forge/db/schemas/knight-hacks"; -import { HACKER_TEAMS } from "@forge/db/schemas/knight-hacks"; -import { Card, CardContent, CardHeader } from "@forge/ui/card"; -import { hackathons } from "@forge/utils"; - -import { api } from "~/trpc/react"; - -export function BaseHackathonTeamPoints({ - hId, - hClass, -}: { - hId: string; - hClass: HackerClass; -}) { - const { data: classPoints } = api.hackerQuery.getPointsByClass.useQuery({ - hackathonName: hId, - }); - const [byTeam, setByTeam] = useState([0, 0]); - const team = hackathons.getClassTeam(hClass); - - function formatPts(pt: number) { - const fmt = new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 }); - if (pt >= 1000) return `${fmt.format(pt / 1000)}k`; - - return `${pt}`; - } - - function clamp(value: number, min: number, max: number) { - return Math.max(min, Math.min(value, max)); - } - - useEffect(() => { - function updateByTeam() { - if (!classPoints) return; - let a = 0; - let b = 0; - for (let i = 0; i < classPoints.length; i++) { - if (i < classPoints.length / 2) a += classPoints.at(i) || 0; - else b += classPoints.at(i) || 0; - } - - setByTeam([a, b]); - } - - if (classPoints) updateByTeam(); - }, [classPoints]); - - const humanityHex = "#4075b7"; - const monstrosityHex = "#c04b3d"; - - return ( - - -
-
{`${team.team == HACKER_TEAMS[0] ? "> " : ""}${HACKER_TEAMS[0].toUpperCase()}`}
-
{`${HACKER_TEAMS[1].toUpperCase()}${team.team == HACKER_TEAMS[1] ? " <" : ""}`}
-
-
- -
-
p + c)) * 100), 15, 85)}%`, - backgroundColor: - team.team == HACKER_TEAMS[0] ? "#223e61" : "#4075b7", - }} - > - {!classPoints ? ( - - ) : ( -
-
- {team.team != HACKER_TEAMS[0] - ? "" - : (byTeam.at(0) || 0) >= (byTeam.at(1) || 0) - ? "You're in the lead!" - : "You're falling behind!"} -
- {(byTeam.at(0) || 0) >= (byTeam.at(1) || 0) && ( - - )} -
{formatPts(byTeam.at(0) || 0)}
-
- )} -
-
p + c)) * 100), 15, 85)}%`, - backgroundColor: - team.team == HACKER_TEAMS[1] ? "#451c17" : "#c04b3d", - }} - > - {!classPoints ? ( - - ) : ( -
- {(byTeam.at(1) || 0) >= (byTeam.at(0) || 0) && ( - - )} -
{formatPts(byTeam.at(1) || 0)}
-
- {team.team != HACKER_TEAMS[1] - ? "" - : (byTeam.at(1) || 0) >= (byTeam.at(0) || 0) - ? "You're in the lead!" - : "You're falling behind!"} -
-
- )} -
-
-
- - - - - - - - - -
-
-
- ); -} diff --git a/apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-dashboard.tsx b/apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-dashboard.tsx index d8dd17d97..80e234c55 100644 --- a/apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-dashboard.tsx +++ b/apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-dashboard.tsx @@ -18,7 +18,7 @@ export default async function HackerDashboard({ hackathon, hacker, }: { - hackathon?: SelectHackathon | null; + hackathon: SelectHackathon; hacker: Awaited>; }) { const [resume, pastHackathons] = await Promise.allSettled([ @@ -26,22 +26,16 @@ export default async function HackerDashboard({ api.hackathon.getPastHackathons(), ]); - const activeHackathon = - hackathon ?? - (await api.hackathon.getHackathon({ hackathonName: undefined })); - if (!hacker) { return (

- Register for Knight Hacks today! + Register for {hackathon.displayName} today!

{ //if there is no current hackathon then this page is never rendered anyway - activeHackathon && ( - - ) + }
@@ -50,15 +44,9 @@ export default async function HackerDashboard({ return ( <> -
-

- Hello, {hacker.firstName}! -

-

Hackathon Dashboard

-
{/* Main content */} - + {/* Transparent Triangle overlay in bottom right corner */}
diff --git a/apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-data.tsx b/apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-data.tsx index 2444c743e..46e325bc3 100644 --- a/apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-data.tsx +++ b/apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-data.tsx @@ -159,7 +159,7 @@ export function HackerData({
)}
- Status for {hackathonData?.displayName} + Application Status
Open Hackathon Dashboard diff --git a/apps/blade/src/app/hackathon/bloomknights/components/bk-hackathon-dashboard.tsx b/apps/blade/src/app/hackathon/bloomknights/components/bk-hackathon-dashboard.tsx deleted file mode 100644 index 75aa12d4e..000000000 --- a/apps/blade/src/app/hackathon/bloomknights/components/bk-hackathon-dashboard.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import type { SelectHackathon } from "@forge/db/schemas/knight-hacks"; - -import type { api as serverCall } from "~/trpc/server"; -import { BaseHackathonDashboard } from "~/app/_components/dashboard/hackathon-dashboard/components"; - -type BKHackathonHacker = Awaited< - ReturnType<(typeof serverCall.hackerQuery)["getHacker"]> ->; - -const BK_HACKER_GUIDE_HREF = - "https://knight-hacks.notion.site/knight-hacks-viii"; - -export function BKHackathonDashboard({ - hackathon, - hacker, -}: { - hackathon: SelectHackathon; - hacker: BKHackathonHacker; -}) { - return ( - - ); -} diff --git a/apps/blade/src/app/hackathon/bloomknights/page.tsx b/apps/blade/src/app/hackathon/bloomknights/page.tsx index c40d38026..0cf30f84d 100644 --- a/apps/blade/src/app/hackathon/bloomknights/page.tsx +++ b/apps/blade/src/app/hackathon/bloomknights/page.tsx @@ -1,12 +1,13 @@ import type { Metadata } from "next"; -import { notFound, redirect } from "next/navigation"; +import { redirect } from "next/navigation"; import { auth } from "@forge/auth"; +import { BaseHackathonDashboard } from "~/app/_components/dashboard/hackathon-dashboard/components"; import HackerDashboard from "~/app/_components/dashboard/hacker-dashboard/hacker-dashboard"; import { SessionNavbar } from "~/app/_components/navigation/session-navbar"; +import NotFoundPage from "~/app/[...not-found]/page"; import { api, HydrateClient } from "~/trpc/server"; -import { BKHackathonDashboard } from "./components/bk-hackathon-dashboard"; export const metadata: Metadata = { title: "Blade | BloomKnights Dashboard", @@ -25,7 +26,7 @@ export default async function BloomKnightsHackathonPage() { }); if (!hackathon) { - notFound(); + return ; } const hacker = await api.hackerQuery.getHacker({ @@ -39,7 +40,7 @@ export default async function BloomKnightsHackathonPage() {
{hacker?.status === "checkedin" ? ( - + ) : ( )} diff --git a/apps/blade/src/app/hackathon/current/page.tsx b/apps/blade/src/app/hackathon/current/page.tsx deleted file mode 100644 index 96d1ac4d0..000000000 --- a/apps/blade/src/app/hackathon/current/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import type { Metadata } from "next"; -import { redirect } from "next/navigation"; - -import { auth } from "@forge/auth"; - -import { api } from "~/trpc/server"; - -export const metadata: Metadata = { - title: "Blade | Current Hackathon", - description: "Open the currently running Knight Hacks hackathon dashboard.", -}; - -export default async function CurrentHackathonPage() { - const session = await auth(); - - if (!session) { - redirect("/"); - } - - const currentHackathon = await api.hackathon.getCurrentHackathon(); - - if (!currentHackathon) { - redirect("/dashboard"); - } - - redirect(`/hackathon/${currentHackathon.name}`); -} diff --git a/apps/blade/src/app/hackathon/page.tsx b/apps/blade/src/app/hackathon/page.tsx index 660cc42f3..ef7d6ca68 100644 --- a/apps/blade/src/app/hackathon/page.tsx +++ b/apps/blade/src/app/hackathon/page.tsx @@ -1,11 +1,48 @@ import type { Metadata } from "next"; +import Link from "next/link"; import { redirect } from "next/navigation"; +import { auth } from "@forge/auth"; + +import { api } from "~/trpc/server"; + export const metadata: Metadata = { title: "Blade | Hackathon", - description: "Open the running Knight Hacks hackathon dashboard.", + description: "Open the currently running Knight Hacks hackathon dashboard.", }; -export default function HackathonIndexPage() { - redirect("/hackathon/current"); +export default async function CurrentHackathonPage() { + const session = await auth(); + + if (!session) { + redirect("/"); + } + + const currentHackathon = await api.hackathon.getCurrentHackathon(); + + if (!currentHackathon) { + return ( +
+
+

+ There's no Hackathon running right now. +

+

+ Stay on the lookout for the next one by joining our{" "} + + Discord + + . +

+
+
+ ); + } + + redirect(`/hackathon/${currentHackathon.name}`); } From 64f5d90e35abe79f54288be1a3f6d8abc85eef78 Mon Sep 17 00:00:00 2001 From: Lenny Date: Fri, 26 Jun 2026 02:57:04 +0000 Subject: [PATCH 5/8] Address hackathon dashboard review comments --- .../hackathon-dashboard/components.tsx | 8 -- .../hackathon-dashboard/hackathon-data.tsx | 86 +------------------ .../src/app/hackathon/bloomknights/page.tsx | 5 +- 3 files changed, 6 insertions(+), 93 deletions(-) diff --git a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/components.tsx b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/components.tsx index a45b046b1..c4f4e8c36 100644 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/components.tsx +++ b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/components.tsx @@ -40,14 +40,6 @@ export function BaseHackathonDashboard({ hackathon, hacker, }: { - classInfoByClass?: Record< - string, - { - classPfp: string; - team: string; - teamColor: string; - } - >; guideHref?: string; hackathon: SelectHackathon; hacker: Awaited>; diff --git a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx index 67e4e4ed9..a0b054952 100644 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx +++ b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx @@ -2,10 +2,9 @@ import { useEffect, useState } from "react"; import Link from "next/link"; -import { BookOpen, CircleCheckBig, Trophy } from "lucide-react"; +import { BookOpen, CircleCheckBig } from "lucide-react"; import type { SelectHackathon } from "@forge/db/schemas/knight-hacks"; -import { Dialog, DialogTrigger } from "@forge/ui/dialog"; import type { api as serverCall } from "~/trpc/server"; import { HackerQRCodePopup } from "~/app/_components/dashboard/hacker-dashboard/hacker-qr-button"; @@ -89,9 +88,9 @@ export function BaseHackathonData({ } return ( -
- {/* Left Section - Name, Status, Image, and Actions */} -
+
+ {/* Name, Status, and Actions */} +
{/* Profile Card */}
@@ -144,83 +143,6 @@ export function BaseHackathonData({
- - {/* Right Section - Hack Points */} -
-
-
- {/* Decorative gradient overlay */} -
- -
- {/* Icon */} -
-
- -
-
- -

- Hack Points -

- -

- Accumulate by attending workshops, socials, meals, sponsor fair, - and more! -

- - {/* Points Display */} -
-
-
- {hacker?.points || 0} -
-
-
- - - - - -
-
-
-
); } diff --git a/apps/blade/src/app/hackathon/bloomknights/page.tsx b/apps/blade/src/app/hackathon/bloomknights/page.tsx index 0cf30f84d..5a8fb4b59 100644 --- a/apps/blade/src/app/hackathon/bloomknights/page.tsx +++ b/apps/blade/src/app/hackathon/bloomknights/page.tsx @@ -1,12 +1,11 @@ import type { Metadata } from "next"; -import { redirect } from "next/navigation"; +import { notFound, redirect } from "next/navigation"; import { auth } from "@forge/auth"; import { BaseHackathonDashboard } from "~/app/_components/dashboard/hackathon-dashboard/components"; import HackerDashboard from "~/app/_components/dashboard/hacker-dashboard/hacker-dashboard"; import { SessionNavbar } from "~/app/_components/navigation/session-navbar"; -import NotFoundPage from "~/app/[...not-found]/page"; import { api, HydrateClient } from "~/trpc/server"; export const metadata: Metadata = { @@ -26,7 +25,7 @@ export default async function BloomKnightsHackathonPage() { }); if (!hackathon) { - return ; + notFound(); } const hacker = await api.hackerQuery.getHacker({ From b6aa22cdc09f933ae67a3dcab24f217992f065d7 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Fri, 26 Jun 2026 19:25:03 -0700 Subject: [PATCH 6/8] chore: cleanup --- .../hackathon-dashboard/hackathon-data.tsx | 46 +++++-------------- 1 file changed, 11 insertions(+), 35 deletions(-) diff --git a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx index a0b054952..7db6e1fb1 100644 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx +++ b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx @@ -1,6 +1,5 @@ "use client"; -import { useEffect, useState } from "react"; import Link from "next/link"; import { BookOpen, CircleCheckBig } from "lucide-react"; @@ -13,7 +12,6 @@ import { HACKER_STATUS_MAP } from "~/consts"; import { api } from "~/trpc/react"; import { BaseHackathonIssueButton } from "./issue-dialog"; -type StatusKey = keyof typeof HACKER_STATUS_MAP | null | undefined; type HackerProfile = Awaited< ReturnType<(typeof serverCall.hackerQuery)["getHacker"]> >; @@ -51,9 +49,6 @@ export function BaseHackathonData({ guideHref: string; hackathon: SelectHackathon; }) { - const [hackerStatus, setHackerStatus] = useState(""); - const [hackerStatusColor, setHackerStatusColor] = useState(""); - const { data: hacker, isError } = api.hackerQuery.getHacker.useQuery( { hackathonName: hackathon.name }, { @@ -61,23 +56,7 @@ export function BaseHackathonData({ }, ); - function getStatusName(status: StatusKey) { - if (!status) return ""; - return HACKER_STATUS_MAP[status].name; - } - - function getStatusColor(status: StatusKey) { - if (!status) return ""; - return HACKER_STATUS_MAP[status].color; - } - - useEffect(() => { - // eslint-disable-next-line react-hooks/set-state-in-effect - setHackerStatus(getStatusName(hacker?.status)); - setHackerStatusColor(getStatusColor(hacker?.status)); - }, [hacker]); - - if (isError) { + if (isError || !hacker) { return (

@@ -87,6 +66,9 @@ export function BaseHackathonData({ ); } + const hackerStatus = HACKER_STATUS_MAP.checkedin.name; + const hackerStatusColor = HACKER_STATUS_MAP.checkedin.color; + return (

{/* Name, Status, and Actions */} @@ -96,28 +78,22 @@ export function BaseHackathonData({
{/* Name and Info Column */}
- {hacker?.firstName && hacker.lastName && ( -

- {hacker.firstName} {hacker.lastName} -

- )} +

+ {hacker.firstName} {hacker.lastName} +

{/* Status Badge */}
-

- Application Status -

{hackerStatus} - {hackerStatus === "Checked-in" && ( - - )} + +
From 22421f7c9345a14429ebd80889620c64889d2550 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Fri, 26 Jun 2026 19:56:32 -0700 Subject: [PATCH 7/8] feat: add hackathon/[slug] backup --- .../hackathon-dashboard/upcoming-events.tsx | 70 ++++++++++--------- apps/blade/src/app/hackathon/[slug]/page.tsx | 55 +++++++++++++++ 2 files changed, 93 insertions(+), 32 deletions(-) create mode 100644 apps/blade/src/app/hackathon/[slug]/page.tsx diff --git a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/upcoming-events.tsx b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/upcoming-events.tsx index abb189453..0384cb20c 100644 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/upcoming-events.tsx +++ b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/upcoming-events.tsx @@ -48,40 +48,46 @@ export async function BaseHackathonUpcomingEvents({
- {upcomingEvents.map((event) => ( - - - - {event.name} - - - {time.formatDateTime(event.start_datetime)} @{" "} - {event.location} - - - -

- {event.description} -

+ {upcomingEvents.length === 0 ? ( +
+ No events coming up in the next few hours. +
+ ) : ( + upcomingEvents.map((event) => ( + + + + {event.name} + + + {time.formatDateTime(event.start_datetime)} @{" "} + {event.location} + + + +

+ {event.description} +

-
- - {event.tag} - -
- -
{event.points} Points
+
+ + {event.tag} + +
+ +
{event.points} Points
+
-
- - - ))} + + + )) + )}
diff --git a/apps/blade/src/app/hackathon/[slug]/page.tsx b/apps/blade/src/app/hackathon/[slug]/page.tsx new file mode 100644 index 000000000..d7c71d1e2 --- /dev/null +++ b/apps/blade/src/app/hackathon/[slug]/page.tsx @@ -0,0 +1,55 @@ +import type { Metadata } from "next"; +import { notFound, redirect } from "next/navigation"; + +import { auth } from "@forge/auth"; + +import { BaseHackathonDashboard } from "~/app/_components/dashboard/hackathon-dashboard/components"; +import HackerDashboard from "~/app/_components/dashboard/hacker-dashboard/hacker-dashboard"; +import { SessionNavbar } from "~/app/_components/navigation/session-navbar"; +import { api, HydrateClient } from "~/trpc/server"; + +export const metadata: Metadata = { + title: "Blade | Hackathon Dashboard", + description: "The official hackathon dashboard.", +}; + +export default async function HackathonSlugPage(props: { + params: Promise<{ slug: string }>; +}) { + const { slug } = await props.params; + + const session = await auth(); + + if (!session) { + redirect("/"); + } + + const hackathon = await api.hackathon.getHackathon({ + hackathonName: slug, + }); + + if (!hackathon) { + notFound(); + } + + const hacker = await api.hackerQuery.getHacker({ + hackathonName: hackathon.name, + }); + + return ( + + +
+
+
+ {hacker?.status === "checkedin" ? ( + + ) : ( + + )} +
+
+
+
+ ); +} From 0232dc8415e88031211008d5d63594af10a2f684 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Fri, 26 Jun 2026 20:06:34 -0700 Subject: [PATCH 8/8] style changes --- .../hackathon-dashboard/hackathon-data.tsx | 35 +++++++++++++------ .../member-dashboard/download-qr-pass.tsx | 24 ++++++++++--- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx index 7db6e1fb1..5b2f6f9de 100644 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx +++ b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx @@ -4,6 +4,7 @@ import Link from "next/link"; import { BookOpen, CircleCheckBig } from "lucide-react"; import type { SelectHackathon } from "@forge/db/schemas/knight-hacks"; +import { Button } from "@forge/ui/button"; import type { api as serverCall } from "~/trpc/server"; import { HackerQRCodePopup } from "~/app/_components/dashboard/hacker-dashboard/hacker-qr-button"; @@ -25,18 +26,30 @@ export function BaseHackathonWalletButton({ }: { profile: HackerProfile; }) { - return ; + return ( + + ); } export function BaseHackathonGuideButton({ href }: { href: string }) { return ( - - - Hacker's Guide - + + + + Hacker's Guide + + + ); } @@ -106,15 +119,15 @@ export function BaseHackathonData({ Quick Actions - {/* QR Code and Apple Wallet */} + {/* Primary actions */}
- +
- {/* Hacker Guide Link */} + {/* Secondary actions */}
- +
diff --git a/apps/blade/src/app/_components/dashboard/member-dashboard/download-qr-pass.tsx b/apps/blade/src/app/_components/dashboard/member-dashboard/download-qr-pass.tsx index 26abf133a..d80719a12 100644 --- a/apps/blade/src/app/_components/dashboard/member-dashboard/download-qr-pass.tsx +++ b/apps/blade/src/app/_components/dashboard/member-dashboard/download-qr-pass.tsx @@ -3,6 +3,7 @@ import { useState } from "react"; import { Loader2, WalletCards } from "lucide-react"; +import type { ButtonProps } from "@forge/ui/button"; import { Button } from "@forge/ui/button"; import { toast } from "@forge/ui/toast"; @@ -11,11 +12,17 @@ import { api } from "~/trpc/react"; type PassProfileKind = "member" | "hacker"; export function DownloadQRPass({ + buttonClassName, + iconClassName, profile, profileKind = "member", + size = "sm", }: { + buttonClassName?: string; + iconClassName?: string; profile?: { firstName?: string | null; lastName?: string | null } | null; profileKind?: PassProfileKind; + size?: ButtonProps["size"]; }) { const [isDownloading, setIsDownloading] = useState(false); @@ -82,19 +89,28 @@ export function DownloadQRPass({ return (