From c2f9fc534ce056e73abe69fea41dcf34b0a21fe5 Mon Sep 17 00:00:00 2001 From: Dylan Vidal <111909137+DVidal1205@users.noreply.github.com> Date: Thu, 25 Jun 2026 18:16:17 -0700 Subject: [PATCH 01/17] Match club navbar buttons to hero style (#476) Co-authored-by: Lenny --- apps/club/src/app/globals.css | 61 ++++++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/apps/club/src/app/globals.css b/apps/club/src/app/globals.css index bd76f8e2a..d399e92b7 100644 --- a/apps/club/src/app/globals.css +++ b/apps/club/src/app/globals.css @@ -2512,47 +2512,76 @@ html[data-motion="ready"] .club-history-card.is-visible:hover { } .club-nav-action { + position: relative; + isolation: isolate; min-height: 2.75rem; gap: 0.45rem; - border: 1px solid rgba(255, 255, 255, 0.18); - border-radius: 0.35rem; - padding: 0.8rem 1.05rem; - box-shadow: none; + overflow: hidden; + border: 3px solid #000; + border-radius: 0; + padding: 0.72rem 1.05rem; font-family: var(--font-montserrat), Arial, Helvetica, sans-serif; font-size: 12px; font-weight: 800; - letter-spacing: 0; + letter-spacing: 0.04em; line-height: 1; text-transform: uppercase; transition: - background-color 160ms ease, - border-color 160ms ease, - color 160ms ease; + transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1), + box-shadow 180ms cubic-bezier(0.2, 0.8, 0.2, 1), + opacity 150ms ease, + filter 180ms ease; +} + +.club-nav-action::before { + position: absolute; + inset: -45% 115% -45% -55%; + z-index: -1; + background: linear-gradient( + 110deg, + transparent 10%, + rgba(255, 255, 255, 0.56) 48%, + transparent 86% + ); + content: ""; + transform: skewX(-20deg); + transition: transform 420ms cubic-bezier(0.2, 0.8, 0.2, 1); } .club-nav-action:hover { - box-shadow: none; + filter: saturate(1.08); + transform: translate3d(-1px, -3px, 0); +} + +.club-nav-action:hover::before { + transform: translateX(175%) skewX(-20deg); +} + +.club-nav-action:active { + transform: translate3d(2px, 2px, 0); } .club-nav-action-primary { background: var(--club-gold); - border-color: var(--club-gold); - color: #120616; + box-shadow: 4px 4px 0 #ffffff; + color: #000; } .club-nav-action-primary:hover { - background: #ffc85f; - color: #120616; + background: var(--club-gold); + box-shadow: 4px 4px 0 #ffffff; + color: #000; } .club-nav-action-secondary { - background: transparent; + background: #170d1c; + box-shadow: 4px 4px 0 rgba(255, 255, 255, 0.35); color: #fff; } .club-nav-action-secondary:hover { - background: rgba(255, 255, 255, 0.08); - border-color: rgba(255, 255, 255, 0.28); + background: #170d1c; + box-shadow: 4px 4px 0 rgba(255, 255, 255, 0.35); color: #fff; } From 53d49b01c7eb26e1a57c593e34290801cc18c3f8 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Fri, 26 Jun 2026 21:00:04 -0700 Subject: [PATCH 02/17] [0] Custom Hackathon Dashboards (#475) Co-authored-by: DGoel1602 Co-authored-by: Lenny --- .../hackathon-dashboard/components.tsx | 88 ++++++ .../hackathon-dashboard/countdown.tsx | 7 +- .../hackathon-dashboard.tsx | 152 ---------- .../hackathon-dashboard/hackathon-data.tsx | 264 +++++------------- .../hackathon-dashboard/issue-dialog.tsx | 4 +- .../hackathon-dashboard/point-leaderboard.tsx | 214 -------------- .../hackathon-dashboard/team-points.tsx | 155 ---------- .../hackathon-dashboard/upcoming-events.tsx | 80 +++--- .../hacker-dashboard/hacker-dashboard.tsx | 20 +- .../hacker-dashboard/hacker-data.tsx | 26 +- .../current-hackathon-notice.tsx | 73 +++++ .../member-dashboard/download-qr-pass.tsx | 24 +- .../member-dashboard/member-dashboard.tsx | 39 ++- .../src/app/_components/user-interface.tsx | 104 +------ apps/blade/src/app/hackathon/[slug]/page.tsx | 55 ++++ .../src/app/hackathon/bloomknights/page.tsx | 51 ++++ apps/blade/src/app/hackathon/page.tsx | 48 ++++ packages/api/src/routers/hackathon.ts | 8 +- packages/api/src/routers/hackers/mutations.ts | 6 +- 19 files changed, 511 insertions(+), 907 deletions(-) create mode 100644 apps/blade/src/app/_components/dashboard/hackathon-dashboard/components.tsx 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 create mode 100644 apps/blade/src/app/_components/dashboard/member-dashboard/current-hackathon-notice.tsx create mode 100644 apps/blade/src/app/hackathon/[slug]/page.tsx create mode 100644 apps/blade/src/app/hackathon/bloomknights/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..c4f4e8c36 --- /dev/null +++ b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/components.tsx @@ -0,0 +1,88 @@ +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 { BaseHackathonCountdown } from "./countdown"; +import { BaseHackathonData } from "./hackathon-data"; +import { BaseHackathonUpcomingEvents } from "./upcoming-events"; + +export { + BaseHackathonGuideButton, + BaseHackathonQRCodeButton, + BaseHackathonWalletButton, +} from "./hackathon-data"; +export { BaseHackathonCountdown } from "./countdown"; +export * from "./issue-dialog"; +export { BaseHackathonUpcomingEvents } from "./upcoming-events"; + +const DEFAULT_HACKER_GUIDE_HREF = + "https://knight-hacks.notion.site/knight-hacks-viii"; + +export function BaseHackathonRegistrationPrompt({ + hackathon, +}: { + hackathon: SelectHackathon; +}) { + return ( +
+

+ Register for {hackathon.displayName} today! +

+
+ +
+
+ ); +} + +export function BaseHackathonDashboard({ + guideHref = DEFAULT_HACKER_GUIDE_HREF, + hackathon, + hacker, +}: { + guideHref?: string; + hackathon: SelectHackathon; + hacker: Awaited>; +}) { + if (!hacker) { + return ; + } + + return ( + <> +
+ + +
+ +
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+ +
+ + ); +} 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 deleted file mode 100644 index d22455004..000000000 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-dashboard.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import type { Metadata } from "next"; - -import { HACKATHONS } from "@forge/consts"; - -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"; - -export const metadata: Metadata = { - title: "Hacker Dashboard", - description: "The official Knight Hacks Hacker Dashboard", -}; - -export default async function HackathonDashboard({ - hacker, -}: { - hacker: Awaited>; -}) { - interface HackerClassInfo { - teamColor: string; - team: string; - classPfp: string; - } - const currentHackathon = await api.hackathon.getCurrentHackathon(); - - if (!hacker) { - return ( -
-

- Register for Knight Hacks today! -

-
- {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 */} -
-
-
- -
-
- -
-
- -
- - ); -} 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..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 @@ -1,70 +1,75 @@ "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 { BookOpen, CircleCheckBig } from "lucide-react"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@forge/ui/dialog"; +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"; 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"; -type StatusKey = keyof typeof HACKER_STATUS_MAP | null | undefined; +type HackerProfile = Awaited< + ReturnType<(typeof serverCall.hackerQuery)["getHacker"]> +>; -export function HackathonData({ - data, - teamColor, - team, - classPfp, +export function BaseHackathonQRCodeButton() { + return ; +} + +export function BaseHackathonWalletButton({ + profile, }: { - data: Awaited>; - teamColor: string; - team: string; - classPfp: string; + profile: HackerProfile; }) { - const [hackerStatus, setHackerStatus] = useState(""); - const [hackerStatusColor, setHackerStatusColor] = useState(""); + return ( + + ); +} +export function BaseHackathonGuideButton({ href }: { href: string }) { + return ( + + ); +} + +export function BaseHackathonData({ + data, + guideHref, + hackathon, +}: { + data: HackerProfile; + guideHref: string; + hackathon: SelectHackathon; +}) { 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; - } - - 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 (

@@ -74,74 +79,37 @@ export function HackathonData({ ); } + const hackerStatus = HACKER_STATUS_MAP.checkedin.name; + const hackerStatusColor = HACKER_STATUS_MAP.checkedin.color; + return ( -

- {/* Left Section - Name, Status, Image, and Actions */} -
+
+ {/* Name, Status, and Actions */} +
{/* Profile Card */}
{/* Name and Info Column */}
- {hacker?.firstName && hacker.lastName && ( -

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

- )} - -
- - {hacker?.class} - - - - {"Team " + team} - -
+

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

{/* Status Badge */}
-

- Status for {hackathonData?.displayName} -

{hackerStatus} - {hackerStatus === "Checked-in" && ( - - )} + +
- - {/* TK Image */} -
- Team Mascot Image -
@@ -151,108 +119,16 @@ export function HackathonData({ Quick Actions - {/* QR Code and Apple Wallet */} + {/* Primary actions */}
- - + +
- {/* Hacker Guide Link */} + {/* Secondary actions */}
- - - Hacker's Guide - - -
-
-
- - {/* 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} -
-
-
- - - - - - - 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 deleted file mode 100644 index 0e0ef971d..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 PointLeaderboard({ - 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 d930ab30c..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 TeamPoints({ - 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/hackathon-dashboard/upcoming-events.tsx b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/upcoming-events.tsx index 4c0c99c9f..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 @@ -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( @@ -42,40 +48,46 @@ export default async function UpcomingEvents() {
- {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/_components/dashboard/hacker-dashboard/hacker-dashboard.tsx b/apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-dashboard.tsx index 9ccb5281b..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 @@ -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; hacker: Awaited>; }) { const [resume, pastHackathons] = await Promise.allSettled([ @@ -22,20 +26,16 @@ export default async function HackerDashboard({ api.hackathon.getPastHackathons(), ]); - const currentHackathon = await api.hackathon.getCurrentHackathon(); - 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 - currentHackathon && ( - - ) + }
@@ -44,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 df8301f14..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 @@ -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, }, ); @@ -153,7 +159,7 @@ export function HackerData({
)}
- Status for {hackathonData?.displayName} + Application Status
+ + + {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/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 ( - - - - Check-in {eventType} - -
- { - const result = (detectedCodes as { rawValue: string }[])[0]; - if (!result) return; - if (scanningRef.current) return; - scanningRef.current = true; - try { - const userId = result.rawValue.substring(5); - form.setValue("userId", userId); - const eventId = form.getValues("eventId"); - if (eventId) { - if (eventType === "Hacker") { - await form.handleSubmit((data) => - hackerEventCheckIn.mutate(data), - )(); - } else { - await form.handleSubmit((data) => - memberCheckIn.mutate(data), - )(); - } - } else { - toast.error("Please select an event first!"); - } - } finally { - setTimeout(() => { - scanningRef.current = false; - }, 3000); - } - }} - /> -
-
- { - if (eventType == "Hacker") hackerEventCheckIn.mutate(data); - else memberCheckIn.mutate(data); - })} - className="space-y-4" - > - - - Upcoming Events - Previous Events - - - {renderEventSelect(currentEvents)} - {eventType === "Hacker" && renderClassCheckinSelect()} - {eventType === "Hacker" && ( - { - setToggleRepeatedCheckIn(value); - form.setValue("repeatedCheckin", value); - }} - /> - )} - - - {renderEventSelect(previousEvents)} - {eventType === "Hacker" && renderClassCheckinSelect()} - {eventType === "Hacker" && ( - { - setToggleRepeatedCheckIn(value); - form.setValue("repeatedCheckin", value); - }} - /> - )} - - -
- -
- -
- {openPersistentDialog && ( -
- -
- CHECKED IN -
-
-
FIRST
-
{firstName}
-
-
-
LAST
-
{lastName}
-
-
-
CLASS
-
- {assignedClass} -
-
-
- {errorColor != "" ? ( -
- {checkInMessage} -
- ) : ( -
- {checkInMessage} -
- )} -
- )} - - - ); -}; - -export default ScannerPopUp; diff --git a/apps/blade/src/app/_components/admin/hackathon/check-in/manual-entry-form.tsx b/apps/blade/src/app/_components/admin/hackathon/check-in/manual-entry-form.tsx index 0ee7853a8..8b92792bc 100644 --- a/apps/blade/src/app/_components/admin/hackathon/check-in/manual-entry-form.tsx +++ b/apps/blade/src/app/_components/admin/hackathon/check-in/manual-entry-form.tsx @@ -4,7 +4,6 @@ import { useEffect, useRef, useState } from "react"; import { Check, ChevronsUpDown, WrenchIcon } from "lucide-react"; import { z } from "zod"; -import { HACKER_CLASSES } from "@forge/db/schemas/knight-hacks"; import { Button } from "@forge/ui/button"; import { Command, @@ -102,18 +101,12 @@ export function ManualEntryForm() { }, }); - const AssignedClassCheckinSchema = z.union([ - z.literal("All"), - z.enum(HACKER_CLASSES), - ]); - const form = useForm({ schema: z.object({ userId: z.string().min(1, "Please select a hacker"), eventId: z.string().min(1, "Event is required"), eventPoints: z.number(), hackathonId: z.string(), - assignedClassCheckin: AssignedClassCheckinSchema, repeatedCheckin: z.boolean(), }), defaultValues: { @@ -121,7 +114,6 @@ export function ManualEntryForm() { userId: "", eventPoints: 0, hackathonId: "", - assignedClassCheckin: "All", repeatedCheckin: false, }, }); @@ -131,7 +123,6 @@ export function ManualEntryForm() { eventId: string; eventPoints: number; hackathonId: string; - assignedClassCheckin: string; repeatedCheckin: boolean; }) => { hackerEventCheckIn.mutate(data); @@ -188,42 +179,6 @@ export function ManualEntryForm() { /> ); - const renderClassCheckinSelect = () => ( - ( - - Check-in class - - - - - - )} - /> - ); - return (
{/* Hackathon Events Header */} @@ -343,7 +298,6 @@ export function ManualEntryForm() { {renderEventSelect(currentEvents)} - {renderClassCheckinSelect()} { @@ -354,7 +308,6 @@ export function ManualEntryForm() { {renderEventSelect(previousEvents)} - {renderClassCheckinSelect()} { diff --git a/apps/blade/src/app/_components/shared/scanner.tsx b/apps/blade/src/app/_components/shared/scanner.tsx index a0240808b..332bbc6ff 100644 --- a/apps/blade/src/app/_components/shared/scanner.tsx +++ b/apps/blade/src/app/_components/shared/scanner.tsx @@ -5,8 +5,6 @@ import { Scanner } from "@yudiel/react-qr-scanner"; import { AwardIcon, WrenchIcon } from "lucide-react"; import { z } from "zod"; -import type { HackerClass } from "@forge/db/schemas/knight-hacks"; -import { HACKER_CLASSES } from "@forge/db/schemas/knight-hacks"; import { Button } from "@forge/ui/button"; import { Dialog, @@ -26,7 +24,6 @@ import { } from "@forge/ui/form"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@forge/ui/tabs"; import { toast } from "@forge/ui/toast"; -import { hackathons } from "@forge/utils"; import ToggleButton from "~/app/_components/admin/hackathon/hackers/toggle-button"; import { api } from "~/trpc/react"; @@ -43,7 +40,6 @@ const ScannerPopUp = ({ eventType }: { eventType: "Member" | "Hacker" }) => { const [openPersistentDialog, setOpenPersistentDialog] = useState(false); const [firstName, setFirstName] = useState(""); const [lastName, setLastName] = useState(""); - const [assignedClass, setAssignedClass] = useState(""); const [checkInMessage, setCheckInMessage] = useState(""); const [toggleRepeatedCheckIn, setToggleRepeatedCheckIn] = useState(false); const [errorColor, setErrorColor] = useState("text-red-500"); @@ -83,7 +79,6 @@ const ScannerPopUp = ({ eventType }: { eventType: "Member" | "Hacker" }) => { ); setFirstName(opts.firstName); setLastName(opts.lastName); - setAssignedClass(opts.class ?? "No class assigned"); setCheckInMessage(opts.messageforHackers); setOpenPersistentDialog(true); @@ -98,7 +93,6 @@ const ScannerPopUp = ({ eventType }: { eventType: "Member" | "Hacker" }) => { setCheckInMessage(opts.message); setFirstName("Error"); setLastName("Error"); - setAssignedClass(""); setOpenPersistentDialog(true); } toast.error(opts.message, { @@ -106,17 +100,12 @@ const ScannerPopUp = ({ eventType }: { eventType: "Member" | "Hacker" }) => { }); }, }); - const AssignedClassCheckinSchema = z.union([ - z.literal("All"), - z.enum(HACKER_CLASSES), - ]); const form = useForm({ schema: z.object({ userId: z.string(), eventId: z.string(), eventPoints: z.number(), hackathonId: z.string(), - assignedClassCheckin: AssignedClassCheckinSchema, repeatedCheckin: z.boolean(), }), defaultValues: { @@ -124,7 +113,6 @@ const ScannerPopUp = ({ eventType }: { eventType: "Member" | "Hacker" }) => { userId: "", eventPoints: 0, hackathonId: "", - assignedClassCheckin: "All", repeatedCheckin: false, }, }); @@ -182,43 +170,6 @@ const ScannerPopUp = ({ eventType }: { eventType: "Member" | "Hacker" }) => { )} /> ); - const renderClassCheckinSelect = () => ( - ( - - Check-in class - - - - - - )} - /> - ); - return ( @@ -283,7 +234,6 @@ const ScannerPopUp = ({ eventType }: { eventType: "Member" | "Hacker" }) => { {renderEventSelect(currentEvents)} - {eventType === "Hacker" && renderClassCheckinSelect()} {eventType === "Hacker" && ( { {renderEventSelect(previousEvents)} - {eventType === "Hacker" && renderClassCheckinSelect()} {eventType === "Hacker" && ( {
LAST
{lastName}
-
-
CLASS
-
- {assignedClass} -
-
{errorColor != "" ? (
{ - //Idk man leave me alone - if (!AssignedClassCheckinSchema.safeParse(v).success) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Invalid assignedClassCheckin", - }); - } - }), repeatedCheckin: z.boolean(), }), ) @@ -550,7 +538,8 @@ export const hackerMutationRouter = { code: "NOT_FOUND", message: `Event with ID ${input.eventId} not found.`, }); - if (!event.hackathonId) { + const hackathonId = event.hackathonId; + if (!hackathonId) { throw new TRPCError({ code: "UNAUTHORIZED", message: `Event with ID ${input.eventId} is not a hackathon event.`, @@ -566,7 +555,6 @@ export const hackerMutationRouter = { lastName: Hacker.lastName, discordUser: Hacker.discordUser, status: HackerAttendee.status, - class: HackerAttendee.class, points: HackerAttendee.points, hackerAttendeeId: HackerAttendee.id, }) @@ -575,7 +563,7 @@ export const hackerMutationRouter = { .where( and( eq(Hacker.userId, input.userId), - eq(HackerAttendee.hackathonId, event.hackathonId), + eq(HackerAttendee.hackathonId, hackathonId), ), ) .limit(1); @@ -593,17 +581,12 @@ export const hackerMutationRouter = { const hackerAttendee = { id: result.hackerAttendeeId, status: result.status, - class: result.class, points: result.points, - hackathonId: event.hackathonId, + hackathonId, hackerId: hacker.id, }; const eventTag = event.tag; - const discordId = await discord.resolveDiscordUserId(hacker.discordUser); - const isVIP = discordId ? await discord.isDiscordVIP(discordId) : false; - - let assignedClass: HackerClass | null = hackerAttendee.class ?? null; if ( hackerAttendee.status !== "confirmed" && @@ -622,106 +605,75 @@ export const hackerMutationRouter = { } if (hackerAttendee.status === "confirmed" && eventTag === "Check-in") { - await db.transaction(async (tx) => { - // Use the already fetched hackerAttendee data instead of querying again - if (hackerAttendee.class && hackerAttendee.class in HACKER_CLASSES) { - assignedClass = hackerAttendee.class; - return; - } - - const totalHackerinClass = await Promise.all( - HACKER_CLASSES.map(async (cls) => { - const rows = await tx - .select({ c: count() }) - .from(HackerAttendee) - .where( - and( - eq(HackerAttendee.hackathonId, input.hackathonId), - eq(HackerAttendee.class, cls), - ), - ); - return { cls, count: Number(rows[0]?.c ?? 0) } as const; - }), - ); - - const leastPopulatedClass = Math.min( - ...totalHackerinClass.map((c) => c.count), - ); - const candidates = totalHackerinClass - .filter((c) => c.count === leastPopulatedClass) - .map((c) => c.cls); - - const pick: HackerClass = - candidates[Math.floor(Math.random() * candidates.length)] ?? - HACKER_CLASSES[0]; - - await tx - .update(HackerAttendee) - .set({ class: pick, status: "checkedin" }) - .where( - and( - eq(HackerAttendee.hackerId, hacker.id), - eq(HackerAttendee.hackathonId, input.hackathonId), - ), - ); - - assignedClass = pick; + const hackathon = await db.query.Hackathon.findFirst({ + columns: { name: true }, + where: (t, { eq }) => eq(t.id, hackathonId), }); - if (!discordId) { - await discord.log({ - title: "Discord role assign skipped", - message: `Could not resolve Discord ID for "${hacker.discordUser}".`, - color: "uhoh_red", - userId: ctx.session.user.discordUserId, + if (!hackathon) { + throw new TRPCError({ + code: "NOT_FOUND", + message: `Hackathon with ID ${hackathonId} not found.`, }); - } else { + } + + await db + .update(HackerAttendee) + .set({ status: "checkedin" }) + .where(eq(HackerAttendee.id, hackerAttendee.id)); + + if (hackathon.name === HACKATHONS.BLOOMKNIGHTS.HACKATHON_NAME) { try { - await discord.addRoleToMember( - discordId, - HACKATHONS.KNIGHT_HACKS_8.KH_EVENT_ROLE_ID, - ); - logger.log( - `Assigned role ${HACKATHONS.KNIGHT_HACKS_8.KH_EVENT_ROLE_ID} to user ${discordId}`, + const discordId = await discord.resolveDiscordUserId( + hacker.discordUser, ); - // VIP will already be given the discord role ahead of time, so no need to assign again - if (assignedClass) { + + if (!discordId) { + try { + await discord.log({ + title: "Discord role assign skipped", + message: `Could not resolve Discord ID for "${hacker.discordUser}".`, + color: "uhoh_red", + userId: ctx.session.user.discordUserId, + }); + } catch (logError) { + logger.error( + "Failed to log skipped BloomKnights role assignment:", + logError instanceof Error + ? logError.message + : "Unknown error", + ); + } + } else { await discord.addRoleToMember( discordId, - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - HACKATHONS.KNIGHT_HACKS_8.CLASS_ROLE_ID[ - assignedClass as HACKATHONS.KNIGHT_HACKS_8.AssignableHackerClass - ] ?? "", + HACKATHONS.BLOOMKNIGHTS.EVENT_ROLE_ID, + ); + logger.log( + `Assigned role ${HACKATHONS.BLOOMKNIGHTS.EVENT_ROLE_ID} to user ${discordId}`, ); } } catch (e) { - await discord.log({ - title: "Discord role assign failed", - message: `Failed to assign Discord roles for "${hacker.discordUser}".`, - color: "uhoh_red", - userId: ctx.session.user.discordUserId, - }); logger.error( - "Failed to assign Discord roles:", - (e as Error).message, + "Failed to assign the BloomKnights role:", + e instanceof Error ? e.message : "Unknown error", ); + try { + await discord.log({ + title: "Discord role assign failed", + message: `Failed to assign the BloomKnights role for "${hacker.discordUser}".`, + color: "uhoh_red", + userId: ctx.session.user.discordUserId, + }); + } catch (logError) { + logger.error( + "Failed to log BloomKnights role assignment failure:", + logError instanceof Error ? logError.message : "Unknown error", + ); + } } } } - if ( - input.assignedClassCheckin !== "All" && - hackerAttendee.class !== input.assignedClassCheckin && - !isVIP - ) { - return { - message: `Hacker ${hacker.firstName} ${hacker.lastName} has already checked into this event!`, - firstName: hacker.firstName, - lastName: hacker.lastName, - class: assignedClass, - messageforHackers: `[ERROR]\nOnly ${input.assignedClassCheckin} hackers can check in. Hacker has class ${hackerAttendee.class}`, - eventName: eventTag, - }; - } const duplicates = await db .select({ id: HackerEventAttendee.id }) @@ -738,7 +690,6 @@ export const hackerMutationRouter = { message: `Hacker ${hacker.firstName} ${hacker.lastName} has already checked into this event!`, firstName: hacker.firstName, lastName: hacker.lastName, - class: assignedClass, messageforHackers: "[ERROR]\nThe hacker has already checked into this event.", eventName: eventTag, @@ -746,7 +697,7 @@ export const hackerMutationRouter = { await db.insert(HackerEventAttendee).values({ hackerAttId: hackerAttendee.id, eventId: input.eventId, - hackathonId: event.hackathonId, + hackathonId, }); await db .update(HackerAttendee) @@ -756,20 +707,15 @@ export const hackerMutationRouter = { if (eventTag === "Check-in") { await discord.log({ title: `Hacker Checked-In`, - message: `${hacker.firstName} ${hacker.lastName} has been checked in to Hackathon ${ - assignedClass ? ` (Class: ${assignedClass}).` : "" - }`, + message: `${hacker.firstName} ${hacker.lastName} has been checked in to the hackathon.`, color: "success_green", userId: ctx.session.user.discordUserId, }); return { - message: `${hacker.firstName} ${hacker.lastName} has been checked in to this Hackathon!${ - assignedClass ? ` Assigned class: ${assignedClass}.` : "" - }`, + message: `${hacker.firstName} ${hacker.lastName} has been checked in to this hackathon!`, firstName: hacker.firstName, lastName: hacker.lastName, - class: assignedClass, - messageforHackers: "Check ID, and send them to correct lanyard area", + messageforHackers: "Check their ID and give them their badge.", eventName: eventTag, }; } @@ -783,7 +729,6 @@ export const hackerMutationRouter = { message: `Hacker ${hacker.firstName} ${hacker.lastName} has been checked in to this event!`, firstName: hacker.firstName, lastName: hacker.lastName, - class: assignedClass, messageforHackers: "Check their badge and send them to event area", eventName: eventTag, }; diff --git a/packages/api/src/routers/hackers/queries.ts b/packages/api/src/routers/hackers/queries.ts index 13bdc164f..ffca18b8a 100644 --- a/packages/api/src/routers/hackers/queries.ts +++ b/packages/api/src/routers/hackers/queries.ts @@ -2,13 +2,9 @@ import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { FORMS } from "@forge/consts"; -import { and, count, desc, eq, gt, or, sum } from "@forge/db"; +import { and, count, eq } from "@forge/db"; import { db } from "@forge/db/client"; -import { - Hacker, - HACKER_CLASSES, - HackerAttendee, -} from "@forge/db/schemas/knight-hacks"; +import { Hacker, HackerAttendee } from "@forge/db/schemas/knight-hacks"; import { permissions } from "@forge/utils"; import { permProcedure, protectedProcedure } from "../../trpc"; @@ -80,7 +76,6 @@ export const hackerQueryRouter = { dateCreated: Hacker.dateCreated, timeCreated: Hacker.timeCreated, status: HackerAttendee.status, - class: HackerAttendee.class, points: HackerAttendee.points, }) .from(Hacker) @@ -229,194 +224,6 @@ export const hackerQueryRouter = { return hackers; }), - getPointsByClass: protectedProcedure - .input(z.object({ hackathonName: z.string().optional() })) - .query(async ({ input }) => { - let hackathon; - const points: number[] = []; - - HACKER_CLASSES.forEach(() => { - points.push(0); - }); - - if (input.hackathonName) { - // If a hackathon name is provided, grab that hackathon - hackathon = await db.query.Hackathon.findFirst({ - where: (t, { eq }) => eq(t.name, input.hackathonName ?? ""), - }); - - if (!hackathon) { - return points; - } - } else { - // If not provided, grab a FUTURE hackathon with a start date CLOSEST to now - const now = new Date(); - const futureHackathons = await db.query.Hackathon.findMany({ - where: (t, { gt }) => gt(t.startDate, now), - orderBy: (t, { asc }) => [asc(t.startDate)], - limit: 1, - }); - hackathon = futureHackathons[0]; - - if (!hackathon) { - return points; - } - } - - for (let i = 0; i < HACKER_CLASSES.length; i++) { - const c = HACKER_CLASSES[i]; - const s = await db - .select({ - sum: sum(HackerAttendee.points).mapWith(Number), - }) - .from(HackerAttendee) - .where( - and( - eq(HackerAttendee.hackathonId, hackathon.id), - eq(HackerAttendee.class, c ?? "Alchemist"), - ), - ); - - points[i] = s.at(0)?.sum ?? 0; - } - - return points; - }), - - getTopHackers: protectedProcedure - .input( - z.object({ - hackathonName: z.string().optional(), - hPoints: z.number(), - hClass: z.string(), - }), - ) - .query(async ({ input }) => { - let hackathon; - - if (input.hackathonName) { - // If a hackathon name is provided, grab that hackathon - hackathon = await db.query.Hackathon.findFirst({ - where: (t, { eq }) => eq(t.name, input.hackathonName ?? ""), - }); - - if (!hackathon) { - return { topA: [], topB: [], place: [0, 0, 0] }; - } - } else { - // If not provided, grab a FUTURE hackathon with a start date CLOSEST to now - const now = new Date(); - const futureHackathons = await db.query.Hackathon.findMany({ - where: (t, { gt }) => gt(t.startDate, now), - orderBy: (t, { asc }) => [asc(t.startDate)], - limit: 1, - }); - hackathon = futureHackathons[0]; - - if (!hackathon) { - return { topA: [], topB: [], place: [0, 0, 0] }; - } - } - - // this code is going to start looking really stupid - // but its all so that we dont have to send like half the DB of hackers to the client - // and hopefully save performance - - const topA = await db - .select({ - firstName: Hacker.firstName, - lastName: Hacker.lastName, - points: HackerAttendee.points, - class: HackerAttendee.class, - id: Hacker.id, - }) - .from(HackerAttendee) - .innerJoin(Hacker, eq(Hacker.id, HackerAttendee.hackerId)) - .where( - and( - eq(HackerAttendee.hackathonId, hackathon.id), - or( - eq(HackerAttendee.class, HACKER_CLASSES[0]), - eq(HackerAttendee.class, HACKER_CLASSES[1]), - eq(HackerAttendee.class, HACKER_CLASSES[2]), - ), - ), - ) - .orderBy(desc(HackerAttendee.points)) - .limit(5); - - const topB = await db - .select({ - firstName: Hacker.firstName, - lastName: Hacker.lastName, - points: HackerAttendee.points, - class: HackerAttendee.class, - id: Hacker.id, - }) - .from(HackerAttendee) - .innerJoin(Hacker, eq(Hacker.id, HackerAttendee.hackerId)) - .where( - and( - eq(HackerAttendee.hackathonId, hackathon.id), - or( - eq(HackerAttendee.class, HACKER_CLASSES[3]), - eq(HackerAttendee.class, HACKER_CLASSES[4]), - eq(HackerAttendee.class, HACKER_CLASSES[5]), - ), - ), - ) - .orderBy(desc(HackerAttendee.points)) - .limit(5); - - // stores your place in each sorted leaderboard - // 0: team A, 2: overall, 3: team B - - let ind = 0; - HACKER_CLASSES.forEach((v, i) => { - if (v == input.hClass) ind = i; - }); - - const place = [ - ind >= 3 - ? -1 - : await db.$count( - HackerAttendee, - and( - eq(HackerAttendee.hackathonId, hackathon.id), - gt(HackerAttendee.points, input.hPoints), - or( - eq(HackerAttendee.class, HACKER_CLASSES[0]), - eq(HackerAttendee.class, HACKER_CLASSES[1]), - eq(HackerAttendee.class, HACKER_CLASSES[2]), - ), - ), - ), - await db.$count( - HackerAttendee, - and( - eq(HackerAttendee.hackathonId, hackathon.id), - gt(HackerAttendee.points, input.hPoints), - ), - ), - ind < 3 - ? -1 - : await db.$count( - HackerAttendee, - and( - eq(HackerAttendee.hackathonId, hackathon.id), - gt(HackerAttendee.points, input.hPoints), - or( - eq(HackerAttendee.class, HACKER_CLASSES[3]), - eq(HackerAttendee.class, HACKER_CLASSES[4]), - eq(HackerAttendee.class, HACKER_CLASSES[5]), - ), - ), - ), - ]; - - return { topA: topA, topB: topB, place: place }; - }), - statusCountByHackathonId: permProcedure .input(z.string()) .query(async ({ ctx, input: hackathonId }) => { diff --git a/packages/consts/src/hackathons/bloomknights.ts b/packages/consts/src/hackathons/bloomknights.ts new file mode 100644 index 000000000..0232a0b25 --- /dev/null +++ b/packages/consts/src/hackathons/bloomknights.ts @@ -0,0 +1,6 @@ +import { IS_PROD } from "../util"; + +export const HACKATHON_NAME = "bloomknights"; +export const PROD_DISCORD_ROLE = "1510751065786683412"; +export const DEV_DISCORD_ROLE = "1520492619921227836"; +export const EVENT_ROLE_ID = IS_PROD ? PROD_DISCORD_ROLE : DEV_DISCORD_ROLE; diff --git a/packages/consts/src/hackathons/index.ts b/packages/consts/src/hackathons/index.ts index 34c4df60e..cb4daeacc 100644 --- a/packages/consts/src/hackathons/index.ts +++ b/packages/consts/src/hackathons/index.ts @@ -1,4 +1,4 @@ -export * as KNIGHT_HACKS_8 from "./kh8"; +export * as BLOOMKNIGHTS from "./bloomknights"; export const APPLICATION_BACKGROUND_OPTIONS = [ { diff --git a/packages/consts/src/hackathons/kh8.ts b/packages/consts/src/hackathons/kh8.ts deleted file mode 100644 index 8d232fdef..000000000 --- a/packages/consts/src/hackathons/kh8.ts +++ /dev/null @@ -1,84 +0,0 @@ -// Because @forge/db requires @forge/consts we can't import @forge/db right -// here. Ideally there is another way but im not too sure. -// TODO: look into not doing this -import type { HackerClass } from "../../../db/src/schemas/knight-hacks"; -import { IS_PROD } from "../util"; - -export const PROD_DISCORD_ROLE_KNIGHT_HACKS_8 = "1408025502119231498"; -export const DEV_DISCORD_ROLE_KNIGHT_HACKS_8 = "1420819573816692857"; -export const KH_EVENT_ROLE_ID = IS_PROD - ? PROD_DISCORD_ROLE_KNIGHT_HACKS_8 - : DEV_DISCORD_ROLE_KNIGHT_HACKS_8; - -export const PROD_DISCORD_ROLE_OPERATORS = "1415702220825038879"; -export const DEV_DISCORD_ROLE_OPERATORS = "1420819261223600239"; - -export const PROD_DISCORD_ROLE_MACHINIST = "1415702276433248406"; -export const DEV_DISCORD_ROLE_MACHINIST = "1420819223797698683"; - -export const PROD_DISCORD_ROLE_SENTINELS = "1415702308494250136"; -export const DEV_DISCORD_ROLE_SENTINELS = "1420819277279137892"; - -export const PROD_DISCORD_ROLE_HARBINGER = "1415702341214011392"; -export const DEV_DISCORD_ROLE_HARBINGER = "1420819326075801670"; - -export const PROD_DISCORD_ROLE_MONSTOLOGIST = "1415702361653121044"; -export const DEV_DISCORD_ROLE_MONSTOLOGIST = "1420819295759237222"; - -export const PROD_DISCORD_ROLE_ALCHEMIST = "1415702383274491934"; -export const DEV_DISCORD_ROLE_ALCHEMIST = "1420819309965611140"; - -export const PROD_DISCORD_SUPERADMIN = "486629374758748180"; -export const DEV_DISCORD_SUPERADMIN = "1246637685011906560"; - -export type AssignableHackerClass = Exclude; - -export const CLASS_ROLE_ID: Record = { - Operator: IS_PROD ? PROD_DISCORD_ROLE_OPERATORS : DEV_DISCORD_ROLE_OPERATORS, - Mechanist: IS_PROD ? PROD_DISCORD_ROLE_MACHINIST : DEV_DISCORD_ROLE_MACHINIST, - Sentinel: IS_PROD ? PROD_DISCORD_ROLE_SENTINELS : DEV_DISCORD_ROLE_SENTINELS, - Harbinger: IS_PROD ? PROD_DISCORD_ROLE_HARBINGER : DEV_DISCORD_ROLE_HARBINGER, - Monstologist: IS_PROD - ? PROD_DISCORD_ROLE_MONSTOLOGIST - : DEV_DISCORD_ROLE_MONSTOLOGIST, - Alchemist: IS_PROD ? PROD_DISCORD_ROLE_ALCHEMIST : DEV_DISCORD_ROLE_ALCHEMIST, -} as const satisfies Record; - -export interface ClassInfo { - team: string; - teamColor: string; - classPfp: string; -} - -export const HACKER_CLASS_INFO: Record = { - Mechanist: { - team: "Humanity", - teamColor: "#228be6", - classPfp: "/khviii/mechanist.jpg", - }, - Operator: { - team: "Humanity", - teamColor: "#228be6", - classPfp: "/khviii/operator.jpg", - }, - Sentinel: { - team: "Humanity", - teamColor: "#228be6", - classPfp: "/khviii/sentinel.jpg", - }, - Monstologist: { - team: "Monstrosity", - teamColor: "#e03131", - classPfp: "/khviii/monstologist.jpg", - }, - Harbinger: { - team: "Monstrosity", - teamColor: "#e03131", - classPfp: "/khviii/harbinger.jpg", - }, - Alchemist: { - team: "Monstrosity", - teamColor: "#e03131", - classPfp: "/khviii/alchemist.jpg", - }, -}; diff --git a/packages/db/src/schemas/knight-hacks.ts b/packages/db/src/schemas/knight-hacks.ts index 07d875614..2133ddd70 100644 --- a/packages/db/src/schemas/knight-hacks.ts +++ b/packages/db/src/schemas/knight-hacks.ts @@ -225,27 +225,6 @@ export const EventAttendee = createTable("event_attendee", (t) => ({ }), })); -export const HACKER_TEAMS = ["Humanity", "Monstrosity"] as const; -export const HACKER_CLASSES = [ - "Operator", - "Mechanist", - "Sentinel", - "Harbinger", - "Monstologist", - "Alchemist", -] as const; -export const SPECIAL_HACKER_CLASSES = ["VIP"] as const; -export const HACKER_CLASSES_ALL = [ - ...HACKER_CLASSES, - ...SPECIAL_HACKER_CLASSES, -] as const; -export type HackerClass = (typeof HACKER_CLASSES_ALL)[number]; -export type RepeatPolicy = "none" | "all" | "class"; -export const AssignedClassCheckinSchema = z.union([ - z.literal("All"), - z.enum(HACKER_CLASSES), -]); - export const HackerAttendee = createTable("hacker_attendee", (t) => ({ id: t.uuid().notNull().primaryKey().defaultRandom(), hackerId: t @@ -269,7 +248,7 @@ export const HackerAttendee = createTable("hacker_attendee", (t) => ({ timeApplied: t.timestamp().notNull().defaultNow(), timeConfirmed: t.timestamp(), points: t.integer().notNull().default(0), - class: t.varchar({ length: 20 }).$type().default(null), + class: t.varchar({ length: 20 }).$type().default(null), })); export const HackerEventAttendee = createTable( diff --git a/packages/utils/src/hackathons.ts b/packages/utils/src/hackathons.ts deleted file mode 100644 index 180e9e54e..000000000 --- a/packages/utils/src/hackathons.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { HackerClass } from "@forge/db/schemas/knight-hacks"; - -/** - * Gets the team information for a hackathon class. - * - * @param {HackerClass} tag - The hacker class. - * @returns {object} Team information including name, color, and image URL. - * - * @example - * getClassTeam("Harbinger") // { team: "Monstrosity", teamColor: "#e03131", imgUrl: "/khviii/lenneth.jpg" } - */ -export const getClassTeam = (tag: HackerClass) => { - if (["Harbinger", "Alchemist", "Monstologist"].includes(tag)) { - return { - team: "Monstrosity", - teamColor: "#e03131", - imgUrl: "/khviii/lenneth.jpg", - }; - } - return { - team: "Humanity", - teamColor: "#228be6", - imgUrl: "/khviii/tkhero.jpg", - }; -}; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index c805bd0e0..e56d8ee1a 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,5 +1,4 @@ export * as events from "./events"; -export * as hackathons from "./hackathons"; export { logger } from "./logger"; export * as permissions from "./permissions"; export * as time from "./time"; From a5218cf3af2e778c01f0d133acfb47fb7c242e1a Mon Sep 17 00:00:00 2001 From: Dylan Vidal <111909137+DVidal1205@users.noreply.github.com> Date: Sat, 27 Jun 2026 12:28:20 -0700 Subject: [PATCH 04/17] [#479] Fix member and hacker dashboard routing (#480) Co-authored-by: Codex --- .../dashboard/dashboard-entry-dialogs.tsx | 89 +++++++++++++++++++ .../hacker-dashboard/hacker-dashboard.tsx | 48 ++++++++-- .../hacker/hacker-application-form.tsx | 13 +-- .../current-hackathon-notice.tsx | 73 --------------- .../member-dashboard/member-dashboard.tsx | 23 ++--- .../src/app/_components/user-interface.tsx | 10 ++- apps/blade/src/app/dashboard/page.tsx | 33 ++++++- apps/blade/src/app/hackathon/page.tsx | 4 +- .../application/[hackathon-id]/page.tsx | 2 +- packages/api/src/routers/hackathon.ts | 11 ++- packages/api/src/routers/hackers/queries.ts | 15 ++-- 11 files changed, 197 insertions(+), 124 deletions(-) create mode 100644 apps/blade/src/app/_components/dashboard/dashboard-entry-dialogs.tsx delete mode 100644 apps/blade/src/app/_components/dashboard/member-dashboard/current-hackathon-notice.tsx diff --git a/apps/blade/src/app/_components/dashboard/dashboard-entry-dialogs.tsx b/apps/blade/src/app/_components/dashboard/dashboard-entry-dialogs.tsx new file mode 100644 index 000000000..b6f8e7509 --- /dev/null +++ b/apps/blade/src/app/_components/dashboard/dashboard-entry-dialogs.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; + +import { Button } from "@forge/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@forge/ui/dialog"; + +import { TacoTuesday } from "../discord-modal"; + +interface DashboardHackathon { + applicationsOpen: boolean; + displayName: string; + isLive: boolean; + name: string; +} + +export function DashboardEntryDialogs({ + currentHackathon, + hasHacker, + hasMember, + showDiscordPrompt, +}: { + currentHackathon: DashboardHackathon | null; + hasHacker: boolean; + hasMember: boolean; + showDiscordPrompt: boolean; +}) { + const [routingOpen, setRoutingOpen] = useState(currentHackathon != null); + + const hackathonAction = hasHacker + ? `Open ${currentHackathon?.displayName ?? "Hackathon"} Dashboard` + : currentHackathon?.applicationsOpen + ? `Register for ${currentHackathon.displayName}` + : `View ${currentHackathon?.displayName ?? "Hackathon"}`; + const memberAction = hasMember + ? "Continue to Member Dashboard" + : "Explore Knight Hacks Membership"; + + return ( + <> + {currentHackathon && ( + + + + + {currentHackathon.displayName} is{" "} + {currentHackathon.isLive ? "live" : "coming up"} + + + {hasHacker + ? `You have a ${currentHackathon.displayName} application. Choose which dashboard you want to open.` + : `Choose whether you are here for ${currentHackathon.displayName} or year-round Knight Hacks membership.`} + + + + + + + + + + + )} + + {!routingOpen && showDiscordPrompt && } + + ); +} 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 80e234c55..40e7ea50d 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 @@ -21,27 +21,57 @@ export default async function HackerDashboard({ hackathon: SelectHackathon; hacker: Awaited>; }) { - const [resume, pastHackathons] = await Promise.allSettled([ - api.resume.getResume(), - api.hackathon.getPastHackathons(), - ]); - if (!hacker) { + const now = new Date(); + + if (now < hackathon.applicationOpen) { + const applicationOpen = new Intl.DateTimeFormat("en-US", { + dateStyle: "long", + timeZone: "America/New_York", + }).format(hackathon.applicationOpen); + + return ( +
+

+ Applications for {hackathon.displayName} open on {applicationOpen}. +

+

+ Check back then to submit your hacker application. +

+
+ ); + } + + if (now > hackathon.applicationDeadline) { + return ( +
+

+ Applications for {hackathon.displayName} are closed. +

+

+ Existing applicants can still view their application status here. +

+
+ ); + } + return (

Register for {hackathon.displayName} today!

- { - //if there is no current hackathon then this page is never rendered anyway - - } +
); } + const [resume, pastHackathons] = await Promise.allSettled([ + api.resume.getResume(), + api.hackathon.getPastHackathons(), + ]); + return ( <>
diff --git a/apps/blade/src/app/_components/dashboard/hacker/hacker-application-form.tsx b/apps/blade/src/app/_components/dashboard/hacker/hacker-application-form.tsx index d14275e79..6c4689526 100644 --- a/apps/blade/src/app/_components/dashboard/hacker/hacker-application-form.tsx +++ b/apps/blade/src/app/_components/dashboard/hacker/hacker-application-form.tsx @@ -920,9 +920,9 @@ export function HackerFormPage({ existingApplicationRedirectedRef.current = true; toast.info("You already submitted an application for this hackathon."); - router.replace("/dashboard"); + router.replace(`/hackathon/${hackathonId}`); router.refresh(); - }, [applicationSubmitted, existingApplication, router]); + }, [applicationSubmitted, existingApplication, hackathonId, router]); useEffect(() => { if (prefillAppliedRef.current) return; @@ -1200,7 +1200,7 @@ export function HackerFormPage({ toast.info( "You already submitted an application for this hackathon.", ); - router.push("/dashboard"); + router.push(`/hackathon/${hackathonId}`); router.refresh(); setLoading(false); return; @@ -1298,13 +1298,14 @@ export function HackerFormPage({ asChild className="h-12 rounded-full bg-white px-6 text-base font-black text-[#21103d] shadow-[0_18px_46px_rgba(0,0,0,0.42)] hover:bg-white/90" > - - Go to dashboard + + View {hackathonName} dashboard

- Your dashboard will show your current application status. + Your hackathon dashboard will show your current + application status.

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 deleted file mode 100644 index 7ced0c995..000000000 --- a/apps/blade/src/app/_components/dashboard/member-dashboard/current-hackathon-notice.tsx +++ /dev/null @@ -1,73 +0,0 @@ -"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"; - -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 44ee66ad9..323c36953 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,7 +5,6 @@ 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"; @@ -84,13 +83,11 @@ export default async function MemberDashboard({ const isAlumni = calcAlumniStatus(member.gradDate, member); - const [eventsValue, dues, hackathonsValue, currentHackathon] = - await Promise.all([ - api.member.getEvents(), - api.duesPayment.validatePaidDues(), - isAlumni ? api.hackathon.getPastHackathons() : Promise.resolve([]), - api.hackathon.getCurrentHackathon(), - ]); + const [eventsValue, dues, hackathonsValue] = await Promise.all([ + api.member.getEvents(), + api.duesPayment.validatePaidDues(), + isAlumni ? api.hackathon.getPastHackathons() : Promise.resolve([]), + ]); const duesPaid = dues.duesPaid; @@ -103,11 +100,6 @@ export default async function MemberDashboard({ return (
- {currentHackathon && ( - - )} {/* Unified View */}
@@ -139,11 +131,6 @@ export default async function MemberDashboard({ return (
- {currentHackathon && ( - - )} {/* Unified View */}
diff --git a/apps/blade/src/app/_components/user-interface.tsx b/apps/blade/src/app/_components/user-interface.tsx index c8618ccdf..86b42a2ac 100644 --- a/apps/blade/src/app/_components/user-interface.tsx +++ b/apps/blade/src/app/_components/user-interface.tsx @@ -1,10 +1,12 @@ +import type { api as serverCaller } from "~/trpc/server"; import MemberDashboard from "~/app/_components/dashboard/member-dashboard/member-dashboard"; import { MemberAppCard } from "~/app/_components/option-cards"; -import { api } from "~/trpc/server"; - -export async function UserInterface() { - const member = await api.member.getMember(); +export function UserInterface({ + member, +}: { + member: Awaited>; +}) { if (!member) { return (
diff --git a/apps/blade/src/app/dashboard/page.tsx b/apps/blade/src/app/dashboard/page.tsx index 31e14c419..5a79ff79d 100644 --- a/apps/blade/src/app/dashboard/page.tsx +++ b/apps/blade/src/app/dashboard/page.tsx @@ -3,7 +3,7 @@ import { redirect } from "next/navigation"; import { auth } from "@forge/auth"; -import { TacoTuesday } from "~/app/_components/discord-modal"; +import { DashboardEntryDialogs } from "~/app/_components/dashboard/dashboard-entry-dialogs"; import { SessionNavbar } from "~/app/_components/navigation/session-navbar"; import { UserInterface } from "~/app/_components/user-interface"; import { api, HydrateClient } from "~/trpc/server"; @@ -15,18 +15,43 @@ export const metadata: Metadata = { export default async function Dashboard() { const session = await auth(); - const isMember = await api.auth.getDiscordMemberStatus(); if (!session) { redirect("/"); } + const [isDiscordMember, member, currentHackathon] = await Promise.all([ + api.auth.getDiscordMemberStatus(), + api.member.getMember(), + api.hackathon.getCurrentHackathon(), + ]); + const hacker = currentHackathon + ? await api.hackerQuery.getHacker({ + hackathonName: currentHackathon.name, + }) + : null; + const now = new Date(); + return (
- - + = now, + displayName: currentHackathon.displayName, + isLive: currentHackathon.startDate <= now, + name: currentHackathon.name, + } + : null + } + hasHacker={hacker != null} + hasMember={member != null} + showDiscordPrompt={!isDiscordMember} + /> +
); diff --git a/apps/blade/src/app/hackathon/page.tsx b/apps/blade/src/app/hackathon/page.tsx index ef7d6ca68..6589c7f07 100644 --- a/apps/blade/src/app/hackathon/page.tsx +++ b/apps/blade/src/app/hackathon/page.tsx @@ -8,7 +8,7 @@ import { api } from "~/trpc/server"; export const metadata: Metadata = { title: "Blade | Hackathon", - description: "Open the currently running Knight Hacks hackathon dashboard.", + description: "Open the current Knight Hacks hackathon dashboard.", }; export default async function CurrentHackathonPage() { @@ -25,7 +25,7 @@ export default async function CurrentHackathonPage() {

- There's no Hackathon running right now. + There's no upcoming or active hackathon right now.

Stay on the lookout for the next one by joining our{" "} diff --git a/apps/blade/src/app/hacker/application/[hackathon-id]/page.tsx b/apps/blade/src/app/hacker/application/[hackathon-id]/page.tsx index bbba1cc54..bdb42dfe7 100644 --- a/apps/blade/src/app/hacker/application/[hackathon-id]/page.tsx +++ b/apps/blade/src/app/hacker/application/[hackathon-id]/page.tsx @@ -51,7 +51,7 @@ export default async function HackerApplicationPage(props: { }); if (isHacker != null) { - return redirect("/dashboard"); + return redirect(`/hackathon/${params["hackathon-id"]}`); } } catch { return redirect("/dashboard"); diff --git a/packages/api/src/routers/hackathon.ts b/packages/api/src/routers/hackathon.ts index d83d80a8a..08eb58ccb 100644 --- a/packages/api/src/routers/hackathon.ts +++ b/packages/api/src/routers/hackathon.ts @@ -139,8 +139,15 @@ export const hackathonRouter = { getCurrentHackathon: publicProcedure.query(async () => { const now = new Date(); const hackathon = await db.query.Hackathon.findFirst({ - orderBy: (t, { asc }) => asc(t.endDate), - where: and(lte(Hackathon.startDate, now), gte(Hackathon.endDate, now)), + orderBy: (t, { asc }) => [ + asc(t.applicationOpen), + asc(t.startDate), + asc(t.endDate), + ], + where: and( + lte(Hackathon.applicationOpen, now), + gte(Hackathon.endDate, now), + ), }); return hackathon ?? null; diff --git a/packages/api/src/routers/hackers/queries.ts b/packages/api/src/routers/hackers/queries.ts index ffca18b8a..bf99ba031 100644 --- a/packages/api/src/routers/hackers/queries.ts +++ b/packages/api/src/routers/hackers/queries.ts @@ -28,14 +28,19 @@ export const hackerQueryRouter = { }); } } else { - // If not provided, grab a FUTURE hackathon with a start date CLOSEST to now + // Match the participant lifecycle used by getCurrentHackathon. const now = new Date(); - const futureHackathons = await db.query.Hackathon.findMany({ - where: (t, { gt }) => gt(t.endDate, now), - orderBy: (t, { asc }) => [asc(t.startDate)], + const currentHackathons = await db.query.Hackathon.findMany({ + where: (t, { and, gte, lte }) => + and(lte(t.applicationOpen, now), gte(t.endDate, now)), + orderBy: (t, { asc }) => [ + asc(t.applicationOpen), + asc(t.startDate), + asc(t.endDate), + ], limit: 1, }); - hackathon = futureHackathons[0]; + hackathon = currentHackathons[0]; if (!hackathon) { return null; From d9803f4fe0538dd8f14ea3435b5cebfd7ee56ece Mon Sep 17 00:00:00 2001 From: DVidal1205 Date: Sat, 27 Jun 2026 12:42:29 -0700 Subject: [PATCH 05/17] dashboard readme --- apps/blade/src/app/hackathon/README.md | 142 +++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 apps/blade/src/app/hackathon/README.md diff --git a/apps/blade/src/app/hackathon/README.md b/apps/blade/src/app/hackathon/README.md new file mode 100644 index 000000000..f23c4c4a8 --- /dev/null +++ b/apps/blade/src/app/hackathon/README.md @@ -0,0 +1,142 @@ +# Hackathon dashboards in Blade + +Blade includes a generic dashboard for every hackathon. Use it unless an event +needs a meaningfully different layout or checked-in experience. A custom route +is an escape hatch, not a requirement for each event. + +## How routing works + +| Route | Behavior | +| ------------------- | ---------------------------------------------------------------------------- | +| `/hackathon` | Finds the current participant-lifecycle hackathon and redirects to its slug. | +| `/hackathon/[slug]` | Provides the generic dashboard for any hackathon in the database. | +| `/hackathon/` | A static event folder that overrides the generic route for that slug. | + +A hackathon is considered current from `applicationOpen` through `endDate`. +When multiple hackathons overlap, Blade selects the one with the earliest +`applicationOpen`, then the earliest `startDate`, then the earliest `endDate`. +An explicit slug route remains available outside this window. + +The generic route loads the user's attendee record for that specific hackathon. +It renders `HackerDashboard` before check-in and `BaseHackathonDashboard` after +the attendee reaches `checkedin` status. + +## Set up a hackathon + +1. Create the hackathon in `/admin/hackathon/manage`. + - `name` is the URL slug and database identifier. Use 2 to 64 lowercase + letters, numbers, and hyphens, with a letter or number at each end. + - `displayName` is the event name shown to users. + - Dates must follow this order: + `applicationOpen < applicationDeadline <= confirmationDeadline <= startDate < endDate`. +2. Add optional application and email presets only when the event needs them. + - Register application presets in + `packages/consts/src/hackathons/index.ts` and + `apps/blade/src/app/_components/dashboard/hacker/hackbackgrounds/index.ts`. + - Register email presets in + `packages/email/src/hackathons/templates.ts`. +3. Create the event schedule in `/admin/hackathon/events` and associate every + event with the correct hackathon. +4. Use the exact event tag `Check-in` for the event that admits confirmed + hackers. The tag controls attendee status changes and later event access. + +At this point `/hackathon/` works through the generic route. No dashboard +code is required. + +## Add a custom dashboard + +Create `apps/blade/src/app/hackathon//page.tsx`, where `` exactly +matches the hackathon's database `name`. Next.js gives this static route +priority over `[slug]`. + +Start from `apps/blade/src/app/hackathon/[slug]/page.tsx` and preserve its core +responsibilities: + +- Require an authenticated session. +- Fetch the hackathon by its explicit slug and call `notFound()` when missing. +- Fetch the attendee record with the same hackathon slug. +- Render the application and status experience before check-in. +- Render the event dashboard only for a `checkedin` attendee. +- Keep the shared session navigation and tRPC hydration boundary. + +Compose the exports from +`apps/blade/src/app/_components/dashboard/hackathon-dashboard/components.tsx` +where possible. Shared components already cover the QR code, wallet pass, +hacker guide, issue reporting, countdown, and upcoming events. Keep mutations +and status rules in shared code instead of copying them into an event route. + +Always pass an event-specific `guideHref` to `BaseHackathonDashboard`. Its +current fallback points to the old Knight Hacks VIII guide. + +## Required behavior checklist + +- [ ] Unauthenticated users cannot access the dashboard. +- [ ] A missing or misspelled database slug returns a not-found page. +- [ ] A user without an application sees the correct state before applications + open, while they are open, and after they close. +- [ ] Existing applicants can see an accurate status for `pending`, `accepted`, + `waitlisted`, `confirmed`, `withdrawn`, `denied`, and `checkedin`. +- [ ] Accepted hackers can confirm before the confirmation deadline. +- [ ] Confirmed hackers can withdraw through a destructive confirmation dialog. +- [ ] Checked-in hackers can open their QR code and wallet pass. +- [ ] The hacker guide, Discord, support, and event links point to current + resources. +- [ ] The countdown uses the current hackathon's `endDate`. +- [ ] Upcoming events are scoped to the current hackathon ID. +- [ ] Points are shown only if the event uses them and the number helps hackers. +- [ ] The page works on mobile, in dark mode, and with keyboard navigation. +- [ ] Loading, empty, and error states remain usable. +- [ ] Only intentionally selected profile fields are rendered. + +Do not dump the full hacker object into the dashboard. It contains personal +application data that most attendees do not need to see during the event. Add a +field such as shirt size or allergies only when the user has a clear reason to +review or update it. + +Classes are no longer part of active check-in behavior. Do not add class UI for +a new hackathon. A leaderboard is also not built into the base dashboard; add +one only with an event-specific product requirement and a supported API. + +## Status expectations + +| Status | Expected experience | +| ------------------ | --------------------------------------------------------------- | +| No attendee record | Date-aware application prompt or closed state. | +| `pending` | Application received, with no confirmation action. | +| `accepted` | Status plus confirmation action until the deadline. | +| `waitlisted` | Clear waitlist status and event guidance. | +| `confirmed` | Confirmed status, QR access, and destructive withdrawal action. | +| `withdrawn` | Clear final status with no confirmation action. | +| `denied` | Clear final status with no confirmation action. | +| `checkedin` | Full event dashboard and on-site tools. | + +## Operations before launch + +- Confirm the hackathon record, slug, dates, theme, and optional presets in + `/admin/hackathon/manage`. +- Confirm every schedule item is attached to the right hackathon in + `/admin/hackathon/events`. +- Test both QR and manual check-in in `/admin/hackathon/check-in` with an account + that has `CHECKIN_HACK_EVENT`. +- Use `/admin/hackathon/hackers` to verify attendee statuses and confirmation + behavior. +- Check in a confirmed hacker through the exact `Check-in` event, then verify + their status becomes `checkedin` and their attendance and points are recorded. +- Check the same hacker into a normal event and verify duplicate-check-in rules. + +Discord event roles are not configurable per hackathon yet. Check-in currently +grants an event role only for BloomKnights. Do not copy that hardcoded role logic +for another event; future role support should come from database-backed +hackathon configuration. + +## Known debt and gotchas + +- The base hacker-guide fallback still points to Knight Hacks VIII. Pass an + explicit `guideHref` until the guide becomes database-configurable. +- Static event routes can drift from `[slug]`. Recheck them whenever shared + routing, status, or application behavior changes. +- Upcoming-event formatting contains legacy date adjustments. Verify schedule + times in a production-like environment before launch. +- The nullable attendee `class` column is legacy data and is intentionally + inactive. +- The base dashboard has no general leaderboard implementation. \ No newline at end of file From ace648901ed4e4ba3b081b1705f68c1f9e2cb5e0 Mon Sep 17 00:00:00 2001 From: Dylan Vidal <111909137+DVidal1205@users.noreply.github.com> Date: Wed, 1 Jul 2026 23:14:51 -0700 Subject: [PATCH 06/17] Fix mobile hacker dashboard actions and add reminders (#483) Co-authored-by: Lenny Co-authored-by: Adrian Osorio Blanchard --- .../hackers/acceptance-reminder-button.tsx | 102 +++++++ .../hackers/hacker-status-toggle.tsx | 5 + .../admin/hackathon/hackers/hackers-table.tsx | 272 +++++++++--------- apps/blade/src/app/hackathon/README.md | 2 +- packages/email/src/hackathons/index.ts | 2 + packages/email/src/hackathons/templates.ts | 4 + 6 files changed, 253 insertions(+), 134 deletions(-) create mode 100644 apps/blade/src/app/_components/admin/hackathon/hackers/acceptance-reminder-button.tsx diff --git a/apps/blade/src/app/_components/admin/hackathon/hackers/acceptance-reminder-button.tsx b/apps/blade/src/app/_components/admin/hackathon/hackers/acceptance-reminder-button.tsx new file mode 100644 index 000000000..49437db93 --- /dev/null +++ b/apps/blade/src/app/_components/admin/hackathon/hackers/acceptance-reminder-button.tsx @@ -0,0 +1,102 @@ +import { useState } from "react"; +import { Loader2, MailCheck } from "lucide-react"; + +import type { InsertHacker } from "@forge/db/schemas/knight-hacks"; +import { Button } from "@forge/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@forge/ui/dialog"; +import { toast } from "@forge/ui/toast"; + +import { api } from "~/trpc/react"; + +export default function AcceptanceReminderButton({ + hacker, + hackathonRouteName, +}: { + hacker: InsertHacker & { status: string }; + hackathonRouteName: string; +}) { + const [isOpen, setIsOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const sendHackathonEmail = api.email.sendHackathonEmail.useMutation({ + onSuccess: () => { + toast.success( + `Acceptance reminder sent to ${hacker.firstName} ${hacker.lastName}!`, + ); + setIsOpen(false); + }, + onError: (opts) => { + toast.error(opts.message); + }, + onSettled: () => { + setIsLoading(false); + }, + }); + + const handleSendReminder = () => { + setIsLoading(true); + sendHackathonEmail.mutate({ + from: "donotreply@knighthacks.org", + hackathonName: hackathonRouteName, + kind: "AcceptedReminder", + recipientName: hacker.firstName, + to: hacker.email, + }); + }; + + const disabled = hacker.status !== "accepted" || !hackathonRouteName; + + return ( +

+ + + + + + + + Send Acceptance Reminder + + + + + Send an acceptance reminder email to {hacker.firstName}{" "} + {hacker.lastName} at {hacker.email}. This uses a separate reminder + email kind from the original acceptance email. + + + + + {isLoading ? ( + + ) : ( + + )} + + + + ); +} diff --git a/apps/blade/src/app/_components/admin/hackathon/hackers/hacker-status-toggle.tsx b/apps/blade/src/app/_components/admin/hackathon/hackers/hacker-status-toggle.tsx index df1203dd8..78b30e7a4 100644 --- a/apps/blade/src/app/_components/admin/hackathon/hackers/hacker-status-toggle.tsx +++ b/apps/blade/src/app/_components/admin/hackathon/hackers/hacker-status-toggle.tsx @@ -1,6 +1,7 @@ import type { InsertHacker } from "@forge/db/schemas/knight-hacks"; import AcceptButton from "./accept-button"; +import AcceptanceReminderButton from "./acceptance-reminder-button"; import BlacklistButton from "./blacklist-button"; import DenyButton from "./deny-button"; import WaitlistButton from "./waitlist-button"; @@ -15,6 +16,10 @@ export default function HackerStatusToggle({ return (
+
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {isTableLoading ? ( +
+

+ Swipe left or right on the table to review Discord, email, status, + reminder, and admit/waitlist actions. +

+
+
+ - - Loading Hackers ... - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - ) : tableError ? ( - - - Failed to load hackers! - - - ) : ( - hackers.map((hacker) => { - const statusMeta = - hacker.status in HACKER_STATUS_MAP - ? HACKER_STATUS_MAP[ - hacker.status as keyof typeof HACKER_STATUS_MAP - ] - : DEFAULT_STATUS_META; - return ( - - - {hacker.firstName} - - - {hacker.lastName} - - - {hacker.discordUser} - - - {hacker.email} - - - {statusMeta.name} - - - - - - - - - - - - - - - - - - ); - }) - )} - -
+ + + {isTableLoading ? ( + + + Loading Hackers ... + + + ) : tableError ? ( + + + Failed to load hackers! + + + ) : ( + hackers.map((hacker) => { + const statusMeta = + hacker.status in HACKER_STATUS_MAP + ? HACKER_STATUS_MAP[ + hacker.status as keyof typeof HACKER_STATUS_MAP + ] + : DEFAULT_STATUS_META; + return ( + + + {hacker.firstName} + + + {hacker.lastName} + + + {hacker.discordUser} + + + {hacker.email} + + + {statusMeta.name} + + + + + + + + + + + + + + + + + + ); + }) + )} + + +
Date: Thu, 2 Jul 2026 00:43:45 -0600 Subject: [PATCH 07/17] replace unavailable listmonk package with direct api client --- packages/email/package.json | 1 - packages/email/src/index.ts | 90 ++++++++++++++++++++++++++++++++----- pnpm-lock.yaml | 12 ----- 3 files changed, 79 insertions(+), 24 deletions(-) diff --git a/packages/email/package.json b/packages/email/package.json index 81cd2a4e3..6897a493b 100644 --- a/packages/email/package.json +++ b/packages/email/package.json @@ -30,7 +30,6 @@ }, "dependencies": { "@forge/utils": "workspace:*", - "@maloma/listmonk": "^1.0.1", "@t3-oss/env-core": "^0.13.10", "minimatch": "^10.2.4" }, diff --git a/packages/email/src/index.ts b/packages/email/src/index.ts index f3878d457..4861ebe3b 100644 --- a/packages/email/src/index.ts +++ b/packages/email/src/index.ts @@ -1,18 +1,86 @@ -import { Listmonk } from "@maloma/listmonk"; - import { logger } from "@forge/utils"; import type { BuildHackathonEmailInput } from "./hackathons"; import { env } from "./env"; import { buildHackathonEmail } from "./hackathons"; -export const client = new Listmonk({ - url: env.LISTMONK_URL, - auth: { - username: env.LISTMONK_USER, - password: env.LISTMONK_TOKEN, +interface TransactionalEmailPayload { + template_id: number; + subscriber_mode: "external"; + subscriber_emails: string[]; + from_email: string; + subject: string; + data: Record; +} + +const createBasicAuthHeader = ({ + username, + password, +}: { + username: string; + password: string; +}) => { + return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`; +}; + +const createListmonkUrl = (path: string) => { + const baseUrl = new URL(env.LISTMONK_URL); + baseUrl.pathname = `${baseUrl.pathname.replace(/\/$/, "")}${path}`; + return baseUrl; +}; + +const getListmonkResponseData = async (response: Response) => { + const text = await response.text(); + + if (!response.ok) { + throw new Error( + `Listmonk transactional email failed with status ${response.status}: ${text}`, + ); + } + + if (text.length === 0) { + throw new Error("Listmonk transactional email returned an empty response"); + } + + const body: unknown = JSON.parse(text); + + if (typeof body === "object" && body !== null && "data" in body) { + return body.data; + } + + return body; +}; + +const sendTransactionalEmail = async (payload: TransactionalEmailPayload) => { + const response = await fetch(createListmonkUrl("/api/tx"), { + method: "POST", + headers: { + Accept: "application/json", + Authorization: createBasicAuthHeader({ + username: env.LISTMONK_USER, + password: env.LISTMONK_TOKEN, + }), + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + + const data = await getListmonkResponseData(response); + + if (data !== true) { + throw new Error( + "Listmonk transactional email returned an invalid response", + ); + } + + return true; +}; + +export const client = { + tx: { + send: sendTransactionalEmail, }, -}); +}; export const sendEmail = async ({ to, @@ -29,12 +97,12 @@ export const sendEmail = async ({ }): Promise<{ success: true }> => { try { await client.tx.send({ - template_id: template_id, + template_id, from_email: from ?? env.LISTMONK_FROM_EMAIL, subscriber_mode: "external", subscriber_emails: typeof to === "string" ? [to] : to, - subject: subject, - data: data, + subject, + data, }); return { success: true }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25879b92f..4ec326310 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1183,9 +1183,6 @@ importers: '@forge/utils': specifier: workspace:* version: link:../utils - '@maloma/listmonk': - specifier: ^1.0.1 - version: 1.0.1 '@t3-oss/env-core': specifier: ^0.13.10 version: 0.13.10(typescript@5.9.3)(zod@4.3.6) @@ -3384,11 +3381,6 @@ packages: '@libsql/core@0.17.0': resolution: {integrity: sha512-hnZRnJHiS+nrhHKLGYPoJbc78FE903MSDrFJTbftxo+e52X+E0Y0fHOCVYsKWcg6XgB7BbJYUrz/xEkVTSaipw==} - '@maloma/listmonk@1.0.1': - resolution: {integrity: sha512-oE3IZLZZEonKTUvhuZ0DTy6gOCwSqlay8vLm+wiBMjmXHnE/RrPkszvdVtksB/IGRTxnUJuXbH4X+HaJKh2BeQ==} - engines: {bun: '>=1.3.0', node: '>=22.0.0'} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - '@mongodb-js/saslprep@1.4.6': resolution: {integrity: sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==} @@ -10714,10 +10706,6 @@ snapshots: js-base64: 3.7.8 optional: true - '@maloma/listmonk@1.0.1': - dependencies: - zod: 4.3.6 - '@mongodb-js/saslprep@1.4.6': dependencies: sparse-bitfield: 3.0.3 From afd2f8946f5355fc7679b451c1cdf5ff68cd238b Mon Sep 17 00:00:00 2001 From: Dylan Vidal <111909137+DVidal1205@users.noreply.github.com> Date: Fri, 3 Jul 2026 18:32:52 -0700 Subject: [PATCH 08/17] [#485] Move Bloom participant flows out of Blade (#486) Co-authored-by: Madu Co-authored-by: Adrian Osorio Blanchard --- .env.example | 4 +- .../hackathon/manage/hackathon-manager.tsx | 114 +- .../dashboard/dashboard-entry-dialogs.tsx | 89 - .../hackathon-dashboard/components.tsx | 88 - .../hackathon-dashboard/countdown.tsx | 127 - .../hackathon-dashboard/hackathon-data.tsx | 137 - .../hackathon-dashboard/issue-dialog.tsx | 96 - .../hackathon-dashboard/upcoming-events.tsx | 96 - .../hacker-dashboard/confirm-button.tsx | 112 - .../hacker-dashboard/hacker-dashboard.tsx | 121 - .../hacker-dashboard/hacker-data.tsx | 307 -- .../hacker-dashboard/hacker-qr-button.tsx | 108 - .../hacker-dashboard/hacker-resume-button.tsx | 30 - .../hacker-dashboard/past-hackathons.tsx | 114 - .../dashboard/hacker/hackbackgrounds/khix.ts | 456 --- .../dashboard/member/checkout-form.tsx | 4 +- .../hackathon/portal-unavailable.tsx | 24 + .../src/app/_components/option-cards.tsx | 42 +- apps/blade/src/app/_components/providers.tsx | 7 +- .../settings/delete-hacker-button.tsx | 124 - .../app/_components/settings/sidebar-nav.tsx | 22 +- .../_components/theme-toggle-route-guard.tsx | 20 - apps/blade/src/app/auth/bloom-return/route.ts | 57 + apps/blade/src/app/dashboard/page.tsx | 28 +- apps/blade/src/app/hackathon/README.md | 156 +- apps/blade/src/app/hackathon/[slug]/page.tsx | 61 +- .../src/app/hackathon/bloomknights/page.tsx | 51 - apps/blade/src/app/hackathon/page.tsx | 47 +- .../application/[hackathon-id]/page.tsx | 106 +- .../src/app/settings/hacker-profile/page.tsx | 79 +- apps/blade/src/app/settings/page.tsx | 5 - apps/blade/src/consts/index.ts | 4 - apps/blade/src/env.client.ts | 22 + apps/blade/src/env.ts | 9 +- apps/blade/src/lib/hackathon-portal.ts | 30 + apps/blade/src/trpc/react.tsx | 4 +- apps/bloomknights/next.config.js | 14 +- apps/bloomknights/package.json | 5 + apps/bloomknights/public/knighthacks.svg | 52 + .../home-page-client.tsx} | 50 +- .../src/app/(marketing)/layout.tsx | 41 + .../bloomknights/src/app/(marketing)/page.tsx | 14 + .../hackbackgrounds/bloomknights.ts | 0 .../application}/hackbackgrounds/index.ts | 30 +- .../application}/hackbackgrounds/types.ts | 0 .../hacker-application-background.tsx | 0 .../application}/hacker-application-form.tsx | 562 +--- .../app/(portal)/_components/auth-retry.tsx | 41 + .../(portal)/_components/bloom-dashboard.tsx | 637 ++++ .../bloomknights-action-blooms.tsx | 367 +++ .../bloomknights-ambient-background.tsx | 102 + .../bloomknights-dashboard-logo.tsx | 23 + .../bloomknights-dashboard-shell.tsx | 165 + .../bloomknights-flower-cursor.tsx | 204 ++ .../_components}/hacker-profile-form.tsx | 330 +- .../(portal)/_components/portal-header.tsx | 75 + .../src/app/(portal)/apply/page.tsx | 83 + .../src/app/(portal)/dashboard/layout.tsx | 15 + .../src/app/(portal)/dashboard/page.tsx | 41 + .../app/(portal)/dashboard/profile/page.tsx | 42 + .../hacker/application/bloomknights/page.tsx | 5 + apps/bloomknights/src/app/(portal)/layout.tsx | 15 + .../src/app/_components/navbar/NavContent.tsx | 34 +- .../src/app/_components/navbar/Navbar.tsx | 14 +- .../_components/register/registerButton.tsx | 17 +- .../src/app/api/auth/[...all]/route.ts | 3 + .../src/app/api/auth/signin/route.ts | 37 + .../src/app/api/trpc/[trpc]/route.ts | 80 + apps/bloomknights/src/app/globals.css | 562 ++++ apps/bloomknights/src/app/layout.tsx | 40 +- apps/bloomknights/src/auth/client.ts | 9 + apps/bloomknights/src/auth/server.ts | 10 + apps/bloomknights/src/env.ts | 35 + apps/bloomknights/src/lib/portal-config.ts | 17 + docs/API-AND-PERMISSIONS.md | 12 + docs/ARCHITECTURE.md | 49 +- packages/api/package.json | 4 + packages/api/src/minio/minio-client.ts | 8 +- packages/api/src/participant-contract.ts | 107 + packages/api/src/participant.ts | 41 + packages/api/src/resume-storage.ts | 8 +- packages/api/src/routers/hackathon.ts | 6 + packages/api/src/routers/hackers/mutations.ts | 31 +- .../api/src/routers/participant-portal.ts | 579 ++++ packages/api/src/storage-env.ts | 14 + packages/api/src/trpc.ts | 12 +- packages/auth/package.json | 5 + packages/auth/src/callback-url.ts | 20 +- packages/auth/src/client-factory.ts | 69 + packages/auth/src/config.ts | 123 - packages/auth/src/env.ts | 21 +- packages/auth/src/factory.ts | 113 + packages/auth/src/index.rsc.ts | 100 +- packages/auth/src/index.ts | 42 +- packages/auth/src/server-factory.ts | 115 + packages/auth/src/shared-env.ts | 20 + .../0010_wooden_supreme_intelligence.sql | 7 + packages/db/drizzle/meta/0010_snapshot.json | 2786 +++++++++++++++++ packages/db/drizzle/meta/_journal.json | 7 + packages/db/src/schemas/knight-hacks.ts | 2 + packages/hackathon/README.md | 58 + packages/hackathon/eslint.config.js | 4 + packages/hackathon/package.json | 43 + packages/hackathon/src/application-schema.ts | 344 ++ packages/hackathon/src/client.tsx | 328 ++ packages/hackathon/src/config.ts | 78 + packages/hackathon/src/index.ts | 3 + packages/hackathon/src/lifecycle.ts | 60 + packages/hackathon/src/query-client.ts | 20 + packages/hackathon/src/server.ts | 15 + packages/hackathon/src/types.ts | 18 + packages/hackathon/tsconfig.json | 9 + packages/utils/package.json | 1 - packages/utils/src/discord-env.ts | 9 + packages/utils/src/discord.ts | 16 +- packages/utils/src/google-env.ts | 12 + packages/utils/src/google.ts | 9 +- packages/utils/src/stripe-env.ts | 9 + packages/utils/src/stripe.ts | 6 +- packages/validators/package.json | 1 + packages/validators/src/hackathons.ts | 45 + packages/validators/src/hacker.ts | 44 + packages/validators/src/index.ts | 1 + pnpm-lock.yaml | 76 +- turbo.json | 4 +- 125 files changed, 8527 insertions(+), 3659 deletions(-) delete mode 100644 apps/blade/src/app/_components/dashboard/dashboard-entry-dialogs.tsx delete mode 100644 apps/blade/src/app/_components/dashboard/hackathon-dashboard/components.tsx delete mode 100644 apps/blade/src/app/_components/dashboard/hackathon-dashboard/countdown.tsx delete mode 100644 apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx delete mode 100644 apps/blade/src/app/_components/dashboard/hackathon-dashboard/issue-dialog.tsx delete mode 100644 apps/blade/src/app/_components/dashboard/hackathon-dashboard/upcoming-events.tsx delete mode 100644 apps/blade/src/app/_components/dashboard/hacker-dashboard/confirm-button.tsx delete mode 100644 apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-dashboard.tsx delete mode 100644 apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-data.tsx delete mode 100644 apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-qr-button.tsx delete mode 100644 apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-resume-button.tsx delete mode 100644 apps/blade/src/app/_components/dashboard/hacker-dashboard/past-hackathons.tsx delete mode 100644 apps/blade/src/app/_components/dashboard/hacker/hackbackgrounds/khix.ts create mode 100644 apps/blade/src/app/_components/hackathon/portal-unavailable.tsx delete mode 100644 apps/blade/src/app/_components/settings/delete-hacker-button.tsx delete mode 100644 apps/blade/src/app/_components/theme-toggle-route-guard.tsx create mode 100644 apps/blade/src/app/auth/bloom-return/route.ts delete mode 100644 apps/blade/src/app/hackathon/bloomknights/page.tsx create mode 100644 apps/blade/src/env.client.ts create mode 100644 apps/blade/src/lib/hackathon-portal.ts create mode 100644 apps/bloomknights/public/knighthacks.svg rename apps/bloomknights/src/app/{page.tsx => (marketing)/home-page-client.tsx} (77%) create mode 100644 apps/bloomknights/src/app/(marketing)/layout.tsx create mode 100644 apps/bloomknights/src/app/(marketing)/page.tsx rename apps/{blade/src/app/_components/dashboard/hacker => bloomknights/src/app/(portal)/_components/application}/hackbackgrounds/bloomknights.ts (100%) rename apps/{blade/src/app/_components/dashboard/hacker => bloomknights/src/app/(portal)/_components/application}/hackbackgrounds/index.ts (59%) rename apps/{blade/src/app/_components/dashboard/hacker => bloomknights/src/app/(portal)/_components/application}/hackbackgrounds/types.ts (100%) rename apps/{blade/src/app/_components/dashboard/hacker => bloomknights/src/app/(portal)/_components/application}/hacker-application-background.tsx (100%) rename apps/{blade/src/app/_components/dashboard/hacker => bloomknights/src/app/(portal)/_components/application}/hacker-application-form.tsx (82%) create mode 100644 apps/bloomknights/src/app/(portal)/_components/auth-retry.tsx create mode 100644 apps/bloomknights/src/app/(portal)/_components/bloom-dashboard.tsx create mode 100644 apps/bloomknights/src/app/(portal)/_components/bloomknights-action-blooms.tsx create mode 100644 apps/bloomknights/src/app/(portal)/_components/bloomknights-ambient-background.tsx create mode 100644 apps/bloomknights/src/app/(portal)/_components/bloomknights-dashboard-logo.tsx create mode 100644 apps/bloomknights/src/app/(portal)/_components/bloomknights-dashboard-shell.tsx create mode 100644 apps/bloomknights/src/app/(portal)/_components/bloomknights-flower-cursor.tsx rename apps/{blade/src/app/_components/settings => bloomknights/src/app/(portal)/_components}/hacker-profile-form.tsx (75%) create mode 100644 apps/bloomknights/src/app/(portal)/_components/portal-header.tsx create mode 100644 apps/bloomknights/src/app/(portal)/apply/page.tsx create mode 100644 apps/bloomknights/src/app/(portal)/dashboard/layout.tsx create mode 100644 apps/bloomknights/src/app/(portal)/dashboard/page.tsx create mode 100644 apps/bloomknights/src/app/(portal)/dashboard/profile/page.tsx create mode 100644 apps/bloomknights/src/app/(portal)/hacker/application/bloomknights/page.tsx create mode 100644 apps/bloomknights/src/app/(portal)/layout.tsx create mode 100644 apps/bloomknights/src/app/api/auth/[...all]/route.ts create mode 100644 apps/bloomknights/src/app/api/auth/signin/route.ts create mode 100644 apps/bloomknights/src/app/api/trpc/[trpc]/route.ts create mode 100644 apps/bloomknights/src/auth/client.ts create mode 100644 apps/bloomknights/src/auth/server.ts create mode 100644 apps/bloomknights/src/env.ts create mode 100644 apps/bloomknights/src/lib/portal-config.ts create mode 100644 packages/api/src/participant-contract.ts create mode 100644 packages/api/src/participant.ts create mode 100644 packages/api/src/routers/participant-portal.ts create mode 100644 packages/api/src/storage-env.ts create mode 100644 packages/auth/src/client-factory.ts delete mode 100644 packages/auth/src/config.ts create mode 100644 packages/auth/src/factory.ts create mode 100644 packages/auth/src/server-factory.ts create mode 100644 packages/auth/src/shared-env.ts create mode 100644 packages/db/drizzle/0010_wooden_supreme_intelligence.sql create mode 100644 packages/db/drizzle/meta/0010_snapshot.json create mode 100644 packages/hackathon/README.md create mode 100644 packages/hackathon/eslint.config.js create mode 100644 packages/hackathon/package.json create mode 100644 packages/hackathon/src/application-schema.ts create mode 100644 packages/hackathon/src/client.tsx create mode 100644 packages/hackathon/src/config.ts create mode 100644 packages/hackathon/src/index.ts create mode 100644 packages/hackathon/src/lifecycle.ts create mode 100644 packages/hackathon/src/query-client.ts create mode 100644 packages/hackathon/src/server.ts create mode 100644 packages/hackathon/src/types.ts create mode 100644 packages/hackathon/tsconfig.json create mode 100644 packages/utils/src/discord-env.ts create mode 100644 packages/utils/src/google-env.ts create mode 100644 packages/utils/src/stripe-env.ts create mode 100644 packages/validators/src/hacker.ts diff --git a/.env.example b/.env.example index bb272bc96..e07a867d1 100644 --- a/.env.example +++ b/.env.example @@ -35,6 +35,8 @@ MINIO_SECRET_KEY="minioadmin" # Blade URL BLADE_URL="http://localhost:3000" +NEXT_PUBLIC_BLADE_URL="http://localhost:3000" +BLOOMKNIGHTS_URL="http://localhost:3006" # Passkit WWDR_CERT_BASE64="wwdr-cert-b64" @@ -48,4 +50,4 @@ TEAM_IDENTIFIER="team-identifier" LISTMONK_URL="https://localhost/" LISTMONK_USER="ci" LISTMONK_TOKEN="ci" -LISTMONK_FROM_EMAIL="ci@localhost" \ No newline at end of file +LISTMONK_FROM_EMAIL="ci@localhost" diff --git a/apps/blade/src/app/_components/admin/hackathon/manage/hackathon-manager.tsx b/apps/blade/src/app/_components/admin/hackathon/manage/hackathon-manager.tsx index 3b5c8573f..368e14dd1 100644 --- a/apps/blade/src/app/_components/admin/hackathon/manage/hackathon-manager.tsx +++ b/apps/blade/src/app/_components/admin/hackathon/manage/hackathon-manager.tsx @@ -1,7 +1,6 @@ "use client"; import { useState } from "react"; -import Link from "next/link"; import { ExternalLink, Loader2, Pencil, Plus, ShieldCheck } from "lucide-react"; import { z } from "zod"; @@ -55,7 +54,9 @@ import { getHackathonBackgroundIssues, getHackathonDateWindowIssues, getHackathonEmailTemplateIssues, + hackathonConfirmationCapacitySchema, hackathonDisplayNameSchema, + hackathonPortalBaseUrlSchema, hackathonRouteNameSchema, hackathonThemeSchema, } from "@forge/validators"; @@ -105,6 +106,8 @@ const formSchema = z applicationBackgroundKey: hackathonApplicationBackgroundKeySchema, emailTemplateEnabled: z.boolean(), emailTemplateKey: hackathonEmailTemplateKeySchema, + portalBaseUrl: z.string(), + confirmationCapacity: z.string(), applicationOpen: z.string().min(1, "Application open is required."), applicationDeadline: z.string().min(1, "Application deadline is required."), confirmationDeadline: z @@ -119,6 +122,28 @@ const formSchema = z const confirmationDeadline = new Date(values.confirmationDeadline); const startDate = new Date(values.startDate); const endDate = new Date(values.endDate); + const portalResult = hackathonPortalBaseUrlSchema.safeParse( + values.portalBaseUrl, + ); + const capacityResult = hackathonConfirmationCapacitySchema.safeParse( + values.confirmationCapacity, + ); + + if (!portalResult.success) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: portalResult.error.issues[0]?.message ?? "Invalid portal URL.", + path: ["portalBaseUrl"], + }); + } + + if (!capacityResult.success) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: capacityResult.error.issues[0]?.message ?? "Invalid capacity.", + path: ["confirmationCapacity"], + }); + } for (const issue of getHackathonBackgroundIssues(values)) { ctx.addIssue({ @@ -197,6 +222,8 @@ function getDefaultValues( ), emailTemplateEnabled: hackathon.emailTemplateEnabled, emailTemplateKey: getSafeEmailTemplateKey(hackathon.emailTemplateKey), + portalBaseUrl: hackathon.portalBaseUrl ?? "", + confirmationCapacity: hackathon.confirmationCapacity?.toString() ?? "", applicationOpen: toDateTimeLocalValue(hackathon.applicationOpen), applicationDeadline: toDateTimeLocalValue(hackathon.applicationDeadline), confirmationDeadline: toDateTimeLocalValue( @@ -223,6 +250,8 @@ function getDefaultValues( applicationBackgroundKey: DEFAULT_BACKGROUND_KEY, emailTemplateEnabled: false, emailTemplateKey: DEFAULT_EMAIL_TEMPLATE_KEY, + portalBaseUrl: "", + confirmationCapacity: "", applicationOpen: toDateTimeLocalValue(applicationOpen), applicationDeadline: toDateTimeLocalValue(applicationDeadline), confirmationDeadline: toDateTimeLocalValue(confirmationDeadline), @@ -246,6 +275,10 @@ function toMutationPayload(values: HackathonFormValues) { emailTemplateKey: values.emailTemplateEnabled ? (values.emailTemplateKey as EmailTemplateKey | undefined) : null, + portalBaseUrl: hackathonPortalBaseUrlSchema.parse(values.portalBaseUrl), + confirmationCapacity: hackathonConfirmationCapacitySchema.parse( + values.confirmationCapacity, + ), applicationOpen: new Date(values.applicationOpen), applicationDeadline: new Date(values.applicationDeadline), confirmationDeadline: new Date(values.confirmationDeadline), @@ -401,8 +434,15 @@ export function HackathonManager() {
{hackathon.displayName}
- /hacker/application/{hackathon.name} + {hackathon.portalBaseUrl ?? + "No participant portal configured"}
+ {hackathon.confirmationCapacity != null && ( +
+ Capacity:{" "} + {hackathon.confirmationCapacity.toLocaleString()} +
+ )}
@@ -437,12 +477,20 @@ export function HackathonManager() {
- + {hackathon.portalBaseUrl ? ( + + ) : ( + Portal missing + )}
diff --git a/apps/blade/src/app/_components/dashboard/dashboard-entry-dialogs.tsx b/apps/blade/src/app/_components/dashboard/dashboard-entry-dialogs.tsx deleted file mode 100644 index b6f8e7509..000000000 --- a/apps/blade/src/app/_components/dashboard/dashboard-entry-dialogs.tsx +++ /dev/null @@ -1,89 +0,0 @@ -"use client"; - -import { useState } from "react"; -import Link from "next/link"; - -import { Button } from "@forge/ui/button"; -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@forge/ui/dialog"; - -import { TacoTuesday } from "../discord-modal"; - -interface DashboardHackathon { - applicationsOpen: boolean; - displayName: string; - isLive: boolean; - name: string; -} - -export function DashboardEntryDialogs({ - currentHackathon, - hasHacker, - hasMember, - showDiscordPrompt, -}: { - currentHackathon: DashboardHackathon | null; - hasHacker: boolean; - hasMember: boolean; - showDiscordPrompt: boolean; -}) { - const [routingOpen, setRoutingOpen] = useState(currentHackathon != null); - - const hackathonAction = hasHacker - ? `Open ${currentHackathon?.displayName ?? "Hackathon"} Dashboard` - : currentHackathon?.applicationsOpen - ? `Register for ${currentHackathon.displayName}` - : `View ${currentHackathon?.displayName ?? "Hackathon"}`; - const memberAction = hasMember - ? "Continue to Member Dashboard" - : "Explore Knight Hacks Membership"; - - return ( - <> - {currentHackathon && ( - - - - - {currentHackathon.displayName} is{" "} - {currentHackathon.isLive ? "live" : "coming up"} - - - {hasHacker - ? `You have a ${currentHackathon.displayName} application. Choose which dashboard you want to open.` - : `Choose whether you are here for ${currentHackathon.displayName} or year-round Knight Hacks membership.`} - - - - - - - - - - - )} - - {!routingOpen && showDiscordPrompt && } - - ); -} diff --git a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/components.tsx b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/components.tsx deleted file mode 100644 index c4f4e8c36..000000000 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/components.tsx +++ /dev/null @@ -1,88 +0,0 @@ -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 { BaseHackathonCountdown } from "./countdown"; -import { BaseHackathonData } from "./hackathon-data"; -import { BaseHackathonUpcomingEvents } from "./upcoming-events"; - -export { - BaseHackathonGuideButton, - BaseHackathonQRCodeButton, - BaseHackathonWalletButton, -} from "./hackathon-data"; -export { BaseHackathonCountdown } from "./countdown"; -export * from "./issue-dialog"; -export { BaseHackathonUpcomingEvents } from "./upcoming-events"; - -const DEFAULT_HACKER_GUIDE_HREF = - "https://knight-hacks.notion.site/knight-hacks-viii"; - -export function BaseHackathonRegistrationPrompt({ - hackathon, -}: { - hackathon: SelectHackathon; -}) { - return ( -
-

- Register for {hackathon.displayName} today! -

-
- -
-
- ); -} - -export function BaseHackathonDashboard({ - guideHref = DEFAULT_HACKER_GUIDE_HREF, - hackathon, - hacker, -}: { - guideHref?: string; - hackathon: SelectHackathon; - hacker: Awaited>; -}) { - if (!hacker) { - return ; - } - - return ( - <> -
- - -
- -
- -
-
-
- -
-
-
- -
-
-
- -
-
- -
- - ); -} diff --git a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/countdown.tsx b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/countdown.tsx deleted file mode 100644 index 3bab0cd08..000000000 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/countdown.tsx +++ /dev/null @@ -1,127 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; - -import { Card, CardContent } from "@forge/ui/card"; - -export function BaseHackathonCountdown({ endDate }: { endDate: Date }) { - const [timeLeft, setTimeLeft] = useState({ - days: 0, - hours: 0, - minutes: 0, - seconds: 0, - }); - - useEffect(() => { - const targetDate = endDate.getTime(); - - const updateCountdown = () => { - const now = new Date().getTime(); - const distance = targetDate - now; - - if (distance < 0) { - setTimeLeft({ days: 0, hours: 0, minutes: 0, seconds: 0 }); - return; - } - - const days = Math.floor(distance / (1000 * 60 * 60 * 24)); - const hours = Math.floor( - (distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60), - ); - const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)); - const seconds = Math.floor((distance % (1000 * 60)) / 1000); - - setTimeLeft({ days, hours, minutes, seconds }); - }; - - updateCountdown(); - const interval = setInterval(updateCountdown, 1000); - - return () => clearInterval(interval); - }, [endDate]); - - const formatNumber = (num: number) => String(num).padStart(2, "0"); - - return ( -
- - -

- HACKING ENDS IN -

- -
- {/* Days */} -
- - -
- {formatNumber(timeLeft.days)} -
-
- Days -
-
- D -
-
-
-
:
-
- - {/* Hours */} -
- - -
- {formatNumber(timeLeft.hours)} -
-
- Hours -
-
- H -
-
-
-
:
-
- - {/* Minutes */} -
- - -
- {formatNumber(timeLeft.minutes)} -
-
- Minutes -
-
- M -
-
-
-
:
-
- - {/* Seconds */} - - -
- {formatNumber(timeLeft.seconds)} -
-
- Seconds -
-
- S -
-
-
-
-
-
-
- ); -} 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 deleted file mode 100644 index 5b2f6f9de..000000000 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx +++ /dev/null @@ -1,137 +0,0 @@ -"use client"; - -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"; -import { DownloadQRPass } from "~/app/_components/dashboard/member-dashboard/download-qr-pass"; -import { HACKER_STATUS_MAP } from "~/consts"; -import { api } from "~/trpc/react"; -import { BaseHackathonIssueButton } from "./issue-dialog"; - -type HackerProfile = Awaited< - ReturnType<(typeof serverCall.hackerQuery)["getHacker"]> ->; - -export function BaseHackathonQRCodeButton() { - return ; -} - -export function BaseHackathonWalletButton({ - profile, -}: { - profile: HackerProfile; -}) { - return ( - - ); -} - -export function BaseHackathonGuideButton({ href }: { href: string }) { - return ( - - ); -} - -export function BaseHackathonData({ - data, - guideHref, - hackathon, -}: { - data: HackerProfile; - guideHref: string; - hackathon: SelectHackathon; -}) { - const { data: hacker, isError } = api.hackerQuery.getHacker.useQuery( - { hackathonName: hackathon.name }, - { - initialData: data, - }, - ); - - if (isError || !hacker) { - return ( -
-

- Something went wrong. Please refresh and try again. -

-
- ); - } - - const hackerStatus = HACKER_STATUS_MAP.checkedin.name; - const hackerStatusColor = HACKER_STATUS_MAP.checkedin.color; - - return ( -
- {/* Name, Status, and Actions */} -
- {/* Profile Card */} -
-
- {/* Name and Info Column */} -
-

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

- - {/* Status Badge */} -
-
- - {hackerStatus} - - - -
-
-
-
-
- - {/* Action Buttons */} -
-

- Quick Actions -

- - {/* Primary actions */} -
- - -
- - {/* Secondary actions */} -
- - -
-
-
-
- ); -} 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 deleted file mode 100644 index 2dbd7d119..000000000 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/issue-dialog.tsx +++ /dev/null @@ -1,96 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { AlertCircle } from "lucide-react"; - -import { Button } from "@forge/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@forge/ui/dialog"; -import { Label } from "@forge/ui/label"; -import { Textarea } from "@forge/ui/textarea"; -import { toast } from "@forge/ui/toast"; - -import { api } from "~/trpc/react"; - -export function BaseHackathonIssueButton() { - const [open, setOpen] = useState(false); - const [issue, setIssue] = useState(""); - - const reportIssueMutation = - api.eventFeedback.logHackathonFeedback.useMutation({ - onSuccess: () => { - setOpen(false); - setIssue(""); - toast.success("Issue reported successfully!"); - }, - onError: (error) => { - const errorMessage = - error instanceof Error ? error.message : "An error occurred"; - toast.error(`Error: ${errorMessage}`); - }, - }); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (issue.trim()) { - reportIssueMutation.mutate({ description: issue }); - } - }; - - return ( - - - - - -
- - Report an Issue - - Describe the issue you're experiencing. We'll look into it as soon - as possible. - - -
-
- -