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..a45b046b1 --- /dev/null +++ b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/components.tsx @@ -0,0 +1,96 @@ +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, +}: { + classInfoByClass?: Record< + string, + { + classPfp: string; + team: string; + teamColor: string; + } + >; + 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..67e4e4ed9 100644 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx +++ b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx @@ -1,53 +1,67 @@ "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 { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@forge/ui/dialog"; +import type { SelectHackathon } from "@forge/db/schemas/knight-hacks"; +import { Dialog, DialogTrigger } from "@forge/ui/dialog"; import type { api as serverCall } from "~/trpc/server"; import { HackerQRCodePopup } from "~/app/_components/dashboard/hacker-dashboard/hacker-qr-button"; 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({ +export function BaseHackathonQRCodeButton() { + return ; +} + +export function BaseHackathonWalletButton({ + profile, +}: { + profile: HackerProfile; +}) { + return ; +} + +export function BaseHackathonGuideButton({ href }: { href: string }) { + return ( + + + Hacker's Guide + + ); +} + +export function BaseHackathonData({ data, - teamColor, - team, - classPfp, + guideHref, + hackathon, }: { - data: Awaited>; - teamColor: string; - team: string; - classPfp: string; + data: HackerProfile; + guideHref: string; + hackathon: SelectHackathon; }) { const [hackerStatus, setHackerStatus] = useState(""); const [hackerStatusColor, setHackerStatusColor] = useState(""); const { data: hacker, isError } = api.hackerQuery.getHacker.useQuery( - {}, + { hackathonName: hackathon.name }, { initialData: data, }, ); - const { data: hackathonData } = api.hackathon.getHackathon.useQuery({ - hackathonName: undefined, - }); - function getStatusName(status: StatusKey) { if (!status) return ""; return HACKER_STATUS_MAP[status].name; @@ -89,32 +103,10 @@ export function HackathonData({ )} -
- - {hacker?.class} - - - - {"Team " + team} - -
- {/* Status Badge */}

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

- - {/* TK Image */} -
- Team Mascot Image -
@@ -153,20 +133,14 @@ export function HackathonData({ {/* QR Code and Apple Wallet */}
- - + +
{/* Hacker Guide Link */}
- - - Hacker's Guide - - + +
@@ -178,7 +152,7 @@ export function HackathonData({ {/* Decorative gradient overlay */}
@@ -186,11 +160,11 @@ export function HackathonData({
@@ -209,15 +183,15 @@ export function HackathonData({
{hacker?.points || 0} @@ -229,28 +203,19 @@ export function HackathonData({ - - - Leaderboard - - -
diff --git a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/issue-dialog.tsx b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/issue-dialog.tsx index 486d9b51f..2dbd7d119 100644 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/issue-dialog.tsx +++ b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/issue-dialog.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useState } from "react"; import { AlertCircle } from "lucide-react"; @@ -17,7 +19,7 @@ import { toast } from "@forge/ui/toast"; import { api } from "~/trpc/react"; -export default function AlertButton() { +export function BaseHackathonIssueButton() { const [open, setOpen] = useState(false); const [issue, setIssue] = useState(""); diff --git a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/point-leaderboard.tsx b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/point-leaderboard.tsx 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..abb189453 100644 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/upcoming-events.tsx +++ b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/upcoming-events.tsx @@ -12,7 +12,11 @@ import { time } from "@forge/utils"; import { api } from "~/trpc/server"; -export default async function UpcomingEvents() { +export async function BaseHackathonUpcomingEvents({ + hackathonId, +}: { + hackathonId: string; +}) { const events = await api.event.getEvents(); // eslint-disable-next-line react-hooks/purity @@ -24,7 +28,9 @@ export default async function UpcomingEvents() { const oneDayOffset = 24 * 60 * 60 * 1000; const start = new Date(event.start_datetime).getTime() + oneDayOffset; return ( - event.hackathonId != null && start >= now && start <= fiveHoursLater + event.hackathonId === hackathonId && + start >= now && + start <= fiveHoursLater ); }) .sort( diff --git a/apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-dashboard.tsx b/apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-dashboard.tsx index 9ccb5281b..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/member-dashboard.tsx b/apps/blade/src/app/_components/dashboard/member-dashboard/member-dashboard.tsx index b29f0c933..44ee66ad9 100644 --- a/apps/blade/src/app/_components/dashboard/member-dashboard/member-dashboard.tsx +++ b/apps/blade/src/app/_components/dashboard/member-dashboard/member-dashboard.tsx @@ -5,6 +5,7 @@ import { MemberAppCard } from "~/app/_components/option-cards"; import { api } from "~/trpc/server"; import { AlumniDiscord } from "./AlumniDiscord"; import { AlumniRecap } from "./AlumniRecap"; +import { CurrentHackathonNotice } from "./current-hackathon-notice"; import DayInHistory from "./day-in-history"; import EarlyAccessVolunteer from "./early-access-volunteer"; import { EventNumber } from "./event/event-number"; @@ -83,11 +84,13 @@ export default async function MemberDashboard({ const isAlumni = calcAlumniStatus(member.gradDate, member); - const [eventsValue, dues, hackathonsValue] = await Promise.all([ - api.member.getEvents(), - api.duesPayment.validatePaidDues(), - isAlumni ? api.hackathon.getPastHackathons() : Promise.resolve([]), - ]); + const [eventsValue, dues, hackathonsValue, currentHackathon] = + await Promise.all([ + api.member.getEvents(), + api.duesPayment.validatePaidDues(), + isAlumni ? api.hackathon.getPastHackathons() : Promise.resolve([]), + api.hackathon.getCurrentHackathon(), + ]); const duesPaid = dues.duesPaid; @@ -100,14 +103,11 @@ export default async function MemberDashboard({ return (
-
-
-

- Hello, {member.firstName}! -

-

Alumni Dashboard

-
-
+ {currentHackathon && ( + + )} {/* Unified View */}
@@ -139,14 +139,11 @@ export default async function MemberDashboard({ return (
-
-
-

- Hello, {member.firstName}! -

-

Member Dashboard

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

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

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

- Select Your Dashboard -

- - - {!memberValue ? ( - "Become a Member" - ) : ( - <> - Member - Member - - )} - - - - {currentHackathon ? currentHackathon.displayName : "Hacker"} - - - {currentHackathon - ? `${currentHackathon.displayName}` - : "Hacker Dashboard"} - - - -
-
- -
- -
-
- -
- {hackerValue && (hackerValue.status as string) === "checkedin" ? ( - - ) : ( - - )} -
-
-
+
+ +
); } diff --git a/apps/blade/src/app/hackathon/bloomknights/page.tsx b/apps/blade/src/app/hackathon/bloomknights/page.tsx new file mode 100644 index 000000000..0cf30f84d --- /dev/null +++ b/apps/blade/src/app/hackathon/bloomknights/page.tsx @@ -0,0 +1,52 @@ +import type { Metadata } from "next"; +import { redirect } from "next/navigation"; + +import { auth } from "@forge/auth"; + +import { BaseHackathonDashboard } from "~/app/_components/dashboard/hackathon-dashboard/components"; +import HackerDashboard from "~/app/_components/dashboard/hacker-dashboard/hacker-dashboard"; +import { SessionNavbar } from "~/app/_components/navigation/session-navbar"; +import NotFoundPage from "~/app/[...not-found]/page"; +import { api, HydrateClient } from "~/trpc/server"; + +export const metadata: Metadata = { + title: "Blade | BloomKnights Dashboard", + description: "The official BloomKnights hackathon dashboard.", +}; + +export default async function BloomKnightsHackathonPage() { + const session = await auth(); + + if (!session) { + redirect("/"); + } + + const hackathon = await api.hackathon.getHackathon({ + hackathonName: "bloomknights", + }); + + if (!hackathon) { + return ; + } + + const hacker = await api.hackerQuery.getHacker({ + hackathonName: hackathon.name, + }); + + return ( + + +
+
+
+ {hacker?.status === "checkedin" ? ( + + ) : ( + + )} +
+
+
+
+ ); +} diff --git a/apps/blade/src/app/hackathon/page.tsx b/apps/blade/src/app/hackathon/page.tsx new file mode 100644 index 000000000..ef7d6ca68 --- /dev/null +++ b/apps/blade/src/app/hackathon/page.tsx @@ -0,0 +1,48 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { redirect } from "next/navigation"; + +import { auth } from "@forge/auth"; + +import { api } from "~/trpc/server"; + +export const metadata: Metadata = { + title: "Blade | Hackathon", + description: "Open the currently running Knight Hacks hackathon dashboard.", +}; + +export default async function CurrentHackathonPage() { + const session = await auth(); + + if (!session) { + redirect("/"); + } + + const currentHackathon = await api.hackathon.getCurrentHackathon(); + + if (!currentHackathon) { + return ( +
+
+

+ There's no Hackathon running right now. +

+

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

+
+
+ ); + } + + redirect(`/hackathon/${currentHackathon.name}`); +} diff --git a/packages/api/src/routers/hackathon.ts b/packages/api/src/routers/hackathon.ts index 321baa31b..d83d80a8a 100644 --- a/packages/api/src/routers/hackathon.ts +++ b/packages/api/src/routers/hackathon.ts @@ -3,7 +3,7 @@ import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { HACKATHONS } from "@forge/consts"; -import { and, count, desc, eq, getTableColumns, lt } from "@forge/db"; +import { and, count, desc, eq, getTableColumns, gte, lt, lte } from "@forge/db"; import { db } from "@forge/db/client"; import { Hackathon, @@ -137,12 +137,12 @@ export const hackathonRouter = { }), getCurrentHackathon: publicProcedure.query(async () => { - // Find first hackathon that hasnt ended yet + const now = new Date(); const hackathon = await db.query.Hackathon.findFirst({ orderBy: (t, { asc }) => asc(t.endDate), - where: (t, { and, gte, lte }) => - and(gte(t.endDate, new Date()), lte(t.applicationOpen, new Date())), + where: and(lte(Hackathon.startDate, now), gte(Hackathon.endDate, now)), }); + return hackathon ?? null; }), diff --git a/packages/api/src/routers/hackers/mutations.ts b/packages/api/src/routers/hackers/mutations.ts index 4263b77c0..47bb1acdd 100644 --- a/packages/api/src/routers/hackers/mutations.ts +++ b/packages/api/src/routers/hackers/mutations.ts @@ -529,11 +529,7 @@ export const hackerMutationRouter = { hackathonId: z.string().uuid(), assignedClassCheckin: z.string().superRefine((v, ctx) => { //Idk man leave me alone - if ( - !( - AssignedClassCheckinSchema.options as unknown as string[] - ).includes(v) - ) { + if (!AssignedClassCheckinSchema.safeParse(v).success) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Invalid assignedClassCheckin",