From bdabf3ebff20c1605c12c5d8824f8f13d9b4e0b5 Mon Sep 17 00:00:00 2001 From: aamoghS Date: Fri, 6 Nov 2026 12:39:27 -0500 Subject: [PATCH 1/9] perf(mainweb): prefetch the hackathons list on the server The list drew only after hydration plus a tRPC batch. A server page now reads the list, the visitor's registrations and projects and hands them to the unchanged client page; its dates print on the venue calendar so the server (UTC) and the browser agree. Cold load to the first card, 5 runs, 1440px, median: 436 to 334 ms (warm 190 to 158). --- .../(portal)/hackathons/HackathonsPage.tsx | 376 +++++++++++++++++ .../mainweb/app/(portal)/hackathons/page.tsx | 388 +----------------- 2 files changed, 388 insertions(+), 376 deletions(-) create mode 100644 sites/mainweb/app/(portal)/hackathons/HackathonsPage.tsx diff --git a/sites/mainweb/app/(portal)/hackathons/HackathonsPage.tsx b/sites/mainweb/app/(portal)/hackathons/HackathonsPage.tsx new file mode 100644 index 00000000..4768dc68 --- /dev/null +++ b/sites/mainweb/app/(portal)/hackathons/HackathonsPage.tsx @@ -0,0 +1,376 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { loginHref } from "@/lib/safe-callback"; +import { useSession } from "next-auth/react"; +import { trpc } from "@/lib/trpc"; +import { hackathonSlug } from "@/lib/hackathon-slug"; +import { useRouter } from "next/navigation"; +import { LoadingScreen } from "@/components/portal/LoadingScreen"; +import Link from "next/link"; +import { StatusBadge } from "@/components/hackathon/StatusBadge"; +import { hackathonStatus } from "@/lib/hackathon-status"; +import { venueFormat, venueParts } from "@/lib/venue-time"; +import { + body, + btnSecondary, + chip, + itemTitle, + kicker, + meta, + object, + page, + pageDek, + pageTitle, + sectionTitle, + status, + tab, + tabList, + textLink, +} from "@/components/portal/ui"; + +type Tab = "browse" | "registrations" | "projects"; + +function formatDate(d: Date | string) { + return venueFormat(d, { month: "short", day: "numeric", year: "numeric" }); +} + +function formatDateRange(start: Date | string, end: Date | string) { + const s = venueParts(start); + const e = venueParts(end); + const short = { month: "short", day: "numeric" } as const; + if (s.month === e.month && s.year === e.year) { + return `${venueFormat(start, short)} – ${e.day}, ${e.year}`; + } + if (s.year === e.year) { + return `${venueFormat(start, short)} – ${venueFormat(end, short)}, ${e.year}`; + } + return `${formatDate(start)} – ${formatDate(end)}`; +} + +export default function HackathonsPage() { + const { data: session, status: authStatus } = useSession(); + const router = useRouter(); + + const [activeTab, setActiveTab] = useState("browse"); + const [statusFilter, setStatusFilter] = useState< + "all" | "open" | "live" | "completed" + >("all"); + + const { data: hackathons, isLoading } = trpc.hackathon.list.useQuery({}); + const { data: myRegs } = trpc.hackathon.myRegistrations.useQuery(undefined, { + enabled: !!session, + }); + + // Covers solo submissions too, which have no team to read them off. + const { data: myProjects } = trpc.team.myProjects.useQuery(undefined, { + enabled: !!session, + }); + const projects = myProjects ?? []; + + useEffect(() => { + const handleHashChange = () => { + const hash = window.location.hash.replace("#", "") as Tab; + if (["browse", "registrations", "projects"].includes(hash)) { + setActiveTab(hash); + } + }; + handleHashChange(); + window.addEventListener("hashchange", handleHashChange); + return () => window.removeEventListener("hashchange", handleHashChange); + }, []); + + useEffect(() => { + if (authStatus === "unauthenticated") router.push(loginHref()); + }, [authStatus, router]); + + if (authStatus === "loading" || isLoading) + return ; + if (!session) return null; + + const regStatusById = new Map( + myRegs?.map((r) => [r.hackathonId, r.registrationStatus]) ?? [], + ); + + const tabs = [ + { id: "browse", label: "Browse" }, + { id: "registrations", label: "My registrations" }, + { id: "projects", label: "My projects" }, + ]; + + const filters = [ + { key: "all", label: "All" }, + { key: "open", label: "Open" }, + { key: "live", label: "Live" }, + { key: "completed", label: "Past" }, + ] as const; + + const filtered = (hackathons ?? []).filter((h) => { + if (statusFilter === "all") return true; + if (statusFilter === "open") return h.status === "open"; + if (statusFilter === "live") return h.status === "in_progress"; + return h.status === "completed"; + }); + const filterLabel = + filters.find((f) => f.key === statusFilter)?.label.toLowerCase() ?? ""; + + return ( +
+ + ← Dashboard + + +
+

Hackathons

+

+ Events you can register for, your registrations, and projects + you've submitted. +

+
+ + + + {/* Browse */} + {activeTab === "browse" && ( +
+
+ {filters.map(({ key, label }) => ( + + ))} +
+ + {!hackathons || hackathons.length === 0 ? ( +

+ No hackathons are listed yet. New events show up here when + they're announced. +

+ ) : filtered.length === 0 ? ( +
+

No {filterLabel} hackathons right now.

+ +
+ ) : ( +
+ {filtered.map((h) => { + const conf = hackathonStatus(h.status); + const regStatus = regStatusById.get(h.id); + return ( + +
+ {conf.label} + {regStatus && } +
+ +
+ {h.theme &&

{h.theme}

} +

{h.name}

+ {h.description && ( +

+ {h.description} +

+ )} +
+ +
+

{formatDateRange(h.startDate, h.endDate)}

+ {h.location &&

{h.location}

} + {h.maxParticipants && ( +

+ {h.currentParticipants} of {h.maxParticipants} spots + taken +

+ )} +
+ + View + + ); + })} +
+ )} + +
+
+

Judge a hackathon

+

+ Industry professionals and experienced students score projects + at the expo. You apply per event, and an organiser reviews each + application. +

+
+ + Apply to judge + +
+
+ )} + + {/* My registrations */} + {activeTab === "registrations" && ( +
+ {myRegs && myRegs.length > 0 ? ( +
    + {myRegs.map((reg) => ( +
  • + +
    + +

    + {reg.hackathon.name} +

    + {reg.hackathon.theme && ( +

    + Theme: {reg.hackathon.theme} +

    + )} +

    + {reg.hackathon.description} +

    +
    +
    + + {reg.hackathon.startDate + ? venueFormat(reg.hackathon.startDate) + : "TBA"} + + View +
    + +
  • + ))} +
+ ) : ( +
+

+ You haven't registered for any hackathons yet. +

+ +
+ )} +
+ )} + + {/* My projects */} + {activeTab === "projects" && ( +
+ {projects.length === 0 ? ( +
+

+ Projects you submit to a hackathon show up here. +

+ +
+ ) : ( +
    + {projects.map((project) => ( +
  • + + {project.status === "winner" + ? "Winner" + : project.status === "judging" + ? "In judging" + : "Submitted"} + +

    {project.name}

    +

    + {project.description} +

    + + {(project.githubUrl || project.demoUrl) && ( +
    + {project.githubUrl && ( + + Code + + )} + {project.demoUrl && ( + + Demo + + )} +
    + )} +
  • + ))} +
+ )} +
+ )} +
+ ); +} diff --git a/sites/mainweb/app/(portal)/hackathons/page.tsx b/sites/mainweb/app/(portal)/hackathons/page.tsx index 5ba2dc4e..575ddb0f 100644 --- a/sites/mainweb/app/(portal)/hackathons/page.tsx +++ b/sites/mainweb/app/(portal)/hackathons/page.tsx @@ -1,380 +1,16 @@ -"use client"; - -import React, { useEffect, useState } from "react"; -import { loginHref } from "@/lib/safe-callback"; -import { useSession } from "next-auth/react"; -import { trpc } from "@/lib/trpc"; -import { hackathonSlug } from "@/lib/hackathon-slug"; -import { useRouter } from "next/navigation"; -import { LoadingScreen } from "@/components/portal/LoadingScreen"; -import Link from "next/link"; -import { StatusBadge } from "@/components/hackathon/StatusBadge"; -import { hackathonStatus } from "@/lib/hackathon-status"; -import { - body, - btnSecondary, - chip, - itemTitle, - kicker, - meta, - object, - page, - pageDek, - pageTitle, - sectionTitle, - status, - tab, - tabList, - textLink, -} from "@/components/portal/ui"; - -type Tab = "browse" | "registrations" | "projects"; - -function formatDate(d: Date | string) { - return new Date(d).toLocaleDateString("en-US", { - month: "short", - day: "numeric", - year: "numeric", - }); -} - -function formatDateRange(start: Date | string, end: Date | string) { - const s = new Date(start); - const e = new Date(end); - if (s.getMonth() === e.getMonth() && s.getFullYear() === e.getFullYear()) { - return `${s.toLocaleDateString("en-US", { month: "short", day: "numeric" })} – ${e.getDate()}, ${e.getFullYear()}`; - } - if (s.getFullYear() === e.getFullYear()) { - return `${s.toLocaleDateString("en-US", { month: "short", day: "numeric" })} – ${e.toLocaleDateString("en-US", { month: "short", day: "numeric" })}, ${e.getFullYear()}`; - } - return `${formatDate(s)} – ${formatDate(e)}`; -} - -export default function HackathonsPage() { - const { data: session, status: authStatus } = useSession(); - const router = useRouter(); - - const [activeTab, setActiveTab] = useState("browse"); - const [statusFilter, setStatusFilter] = useState< - "all" | "open" | "live" | "completed" - >("all"); - - const { data: hackathons, isLoading } = trpc.hackathon.list.useQuery({}); - const { data: myRegs } = trpc.hackathon.myRegistrations.useQuery(undefined, { - enabled: !!session, - }); - - // Covers solo submissions too, which have no team to read them off. - const { data: myProjects } = trpc.team.myProjects.useQuery(undefined, { - enabled: !!session, - }); - const projects = myProjects ?? []; - - useEffect(() => { - const handleHashChange = () => { - const hash = window.location.hash.replace("#", "") as Tab; - if (["browse", "registrations", "projects"].includes(hash)) { - setActiveTab(hash); - } - }; - handleHashChange(); - window.addEventListener("hashchange", handleHashChange); - return () => window.removeEventListener("hashchange", handleHashChange); - }, []); - - useEffect(() => { - if (authStatus === "unauthenticated") router.push(loginHref()); - }, [authStatus, router]); - - if (authStatus === "loading" || isLoading) - return ; - if (!session) return null; - - const regStatusById = new Map( - myRegs?.map((r) => [r.hackathonId, r.registrationStatus]) ?? [], - ); - - const tabs = [ - { id: "browse", label: "Browse" }, - { id: "registrations", label: "My registrations" }, - { id: "projects", label: "My projects" }, - ]; - - const filters = [ - { key: "all", label: "All" }, - { key: "open", label: "Open" }, - { key: "live", label: "Live" }, - { key: "completed", label: "Past" }, - ] as const; - - const filtered = (hackathons ?? []).filter((h) => { - if (statusFilter === "all") return true; - if (statusFilter === "open") return h.status === "open"; - if (statusFilter === "live") return h.status === "in_progress"; - return h.status === "completed"; - }); - const filterLabel = - filters.find((f) => f.key === statusFilter)?.label.toLowerCase() ?? ""; +import { PrefetchedPage } from "@/components/portal/PrefetchedPage"; +import HackathonsPage from "./HackathonsPage"; +export default function Page() { return ( -
- - ← Dashboard - - -
-

Hackathons

-

- Events you can register for, your registrations, and projects - you've submitted. -

-
- - - - {/* Browse */} - {activeTab === "browse" && ( -
-
- {filters.map(({ key, label }) => ( - - ))} -
- - {!hackathons || hackathons.length === 0 ? ( -

- No hackathons are listed yet. New events show up here when - they're announced. -

- ) : filtered.length === 0 ? ( -
-

No {filterLabel} hackathons right now.

- -
- ) : ( -
- {filtered.map((h) => { - const conf = hackathonStatus(h.status); - const regStatus = regStatusById.get(h.id); - return ( - -
- {conf.label} - {regStatus && } -
- -
- {h.theme &&

{h.theme}

} -

{h.name}

- {h.description && ( -

- {h.description} -

- )} -
- -
-

{formatDateRange(h.startDate, h.endDate)}

- {h.location &&

{h.location}

} - {h.maxParticipants && ( -

- {h.currentParticipants} of {h.maxParticipants} spots - taken -

- )} -
- - View - - ); - })} -
- )} - -
-
-

Judge a hackathon

-

- Industry professionals and experienced students score projects - at the expo. You apply per event, and an organiser reviews each - application. -

-
- - Apply to judge - -
-
- )} - - {/* My registrations */} - {activeTab === "registrations" && ( -
- {myRegs && myRegs.length > 0 ? ( -
    - {myRegs.map((reg) => ( -
  • - -
    - -

    - {reg.hackathon.name} -

    - {reg.hackathon.theme && ( -

    - Theme: {reg.hackathon.theme} -

    - )} -

    - {reg.hackathon.description} -

    -
    -
    - - {reg.hackathon.startDate - ? new Date( - reg.hackathon.startDate, - ).toLocaleDateString() - : "TBA"} - - View -
    - -
  • - ))} -
- ) : ( -
-

- You haven't registered for any hackathons yet. -

- -
- )} -
- )} - - {/* My projects */} - {activeTab === "projects" && ( -
- {projects.length === 0 ? ( -
-

- Projects you submit to a hackathon show up here. -

- -
- ) : ( -
    - {projects.map((project) => ( -
  • - - {project.status === "winner" - ? "Winner" - : project.status === "judging" - ? "In judging" - : "Submitted"} - -

    {project.name}

    -

    - {project.description} -

    - - {(project.githubUrl || project.demoUrl) && ( -
    - {project.githubUrl && ( - - Code - - )} - {project.demoUrl && ( - - Demo - - )} -
    - )} -
  • - ))} -
- )} -
- )} -
+ [ + ["hackathon.list", {}, c.hackathon.list({})], + ["hackathon.myRegistrations", undefined, c.hackathon.myRegistrations()], + ["team.myProjects", undefined, c.team.myProjects()], + ]} + > + + ); } From 788d44475e6eabb9c2938bac4a817d7680ab389d Mon Sep 17 00:00:00 2001 From: aamoghS Date: Fri, 6 Nov 2026 12:39:27 -0500 Subject: [PATCH 2/9] perf(mainweb): prefetch the submit page and the event it opens on The page waited on the registrations, then on the submission, the event and the submission window for the event it picked. The server page reads the registrations, picks the same event (the URL's, else the first still open), and reads the other three with it. PrefetchedPage accepts an async reads function for this. Cold load to the submission notice, median of 5: 878 to 488 ms (warm 412 to 232). --- .../app/(portal)/submit/SubmitPage.tsx | 764 +++++++++++++++++ sites/mainweb/app/(portal)/submit/page.tsx | 811 ++---------------- .../components/portal/PrefetchedPage.tsx | 13 +- 3 files changed, 824 insertions(+), 764 deletions(-) create mode 100644 sites/mainweb/app/(portal)/submit/SubmitPage.tsx diff --git a/sites/mainweb/app/(portal)/submit/SubmitPage.tsx b/sites/mainweb/app/(portal)/submit/SubmitPage.tsx new file mode 100644 index 00000000..c73a669b --- /dev/null +++ b/sites/mainweb/app/(portal)/submit/SubmitPage.tsx @@ -0,0 +1,764 @@ +"use client"; + +import { useSession } from "next-auth/react"; +import { loginHref } from "@/lib/safe-callback"; +import { trpc } from "@/lib/trpc"; +import { useRouter, useSearchParams } from "next/navigation"; +import { Suspense, useState } from "react"; +import { LoadingScreen } from "@/components/portal/LoadingScreen"; +import Link from "next/link"; +import { + body, + btnDanger, + btnPrimary, + btnSecondary, + chip, + fieldLabel, + input, + itemTitle, + kicker, + page, + pageTitle, + textLink, +} from "@/components/portal/ui"; + +/** + * Deadlines are rendered in the event's own time zone, not the viewer's. Half + * the field is remote, and a submission deadline shown in the wrong zone is the + * one rendering mistake that costs someone their entry. + */ +const formatMoment = (moment: Date) => + moment.toLocaleString("en-US", { + weekday: "short", + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", + timeZoneName: "short", + timeZone: "America/New_York", + }); + +/** A deadline that never breaks across lines: "EST." alone on a line is unreadable. */ +const Moment = ({ at }: { at: Date }) => ( + {formatMoment(at)} +); + +/** A teammate's view of the form: filed values read as values, not empty fields. */ +const readOnlyField = + "read-only:bg-transparent read-only:border-[var(--border-subtle)]"; + +function SubmitPortalContent() { + const { data: session, status } = useSession(); + const router = useRouter(); + const searchParams = useSearchParams(); + const urlHackathonId = searchParams.get("id"); + + const utils = trpc.useUtils(); + + // Context & State + const [selectedHackathonId, setSelectedHackathonId] = useState( + urlHackathonId || "", + ); + // The id can come straight from the URL or the picker, so it is encoded + // wherever it becomes part of a link path. + const hackathonPath = `/hackathons/${encodeURIComponent(selectedHackathonId)}`; + const [confirmWithdraw, setConfirmWithdraw] = useState(false); + + const [projectName, setProjectName] = useState(""); + const [projectDesc, setProjectDesc] = useState(""); + const [githubUrl, setGithubUrl] = useState(""); + const [videoUrl, setVideoUrl] = useState(""); + const [demoUrl, setDemoUrl] = useState(""); + // Judge routing filters on exactly these three. A submission without them + // reaches no track judge and no sponsor judge at all. + const [tracks, setTracks] = useState([]); + const [challenges, setChallenges] = useState([]); + const [isCreateX, setIsCreateX] = useState(false); + + const [error, setError] = useState(""); + const [successMessage, setSuccessMessage] = useState(""); + const [projectSubmitted, setProjectSubmitted] = useState(false); + // Which hackathon the form below has already been filled from, so a + // background refetch cannot overwrite what someone is part-way through + // typing. + const [prefilledFor, setPrefilledFor] = useState(""); + + // Queries + const { data: myRegs, isLoading: loadingRegs } = + trpc.hackathon.myRegistrations.useQuery(undefined, { + enabled: !!session, + // An accepted attendee gets checked in at the venue, which is what + // unlocks the form. Poll for it only on the event's days, so the page + // updates without a reload and idle tabs stay quiet. Day granularity on + // the viewer's clock, as the event page does: a badge scanned before + // doors open still counts. + refetchInterval: (query) => { + const reg = query.state.data?.find( + (r) => r.hackathonId === selectedHackathonId, + ); + if (reg?.registrationStatus !== "approved") return false; + const startOfDay = (d: Date | string) => + new Date(d).setHours(0, 0, 0, 0); + const today = startOfDay(new Date()); + const started = startOfDay(reg.hackathon.startDate) <= today; + const ended = + !!reg.hackathon.endDate && today > startOfDay(reg.hackathon.endDate); + return started && !ended ? 30_000 : false; + }, + }); + + // The project a submit would overwrite — the team's entry, or the solo one + // this participant filed. Reading it from the team alone leaves every solo + // hacker staring at a blank form over a live submission. + const mySubmission = trpc.team.mySubmission.useQuery( + { hackathonId: selectedHackathonId }, + { enabled: !!session && !!selectedHackathonId }, + ); + + // The event's own track and challenge lists. Offering free text instead + // would be worse than offering nothing: judge assignment matches these + // strings exactly, so a typo silently removes a project from a judge's pool. + const { data: hackathonDetail } = trpc.hackathon.getById.useQuery( + { id: selectedHackathonId }, + { enabled: !!session && !!selectedHackathonId }, + ); + + // The window the submit mutation gates on. The page never queried it, so the + // refusal only ever arrived after the form was filled in. + const submissionWindow = trpc.team.submissionWindow.useQuery( + { hackathonId: selectedHackathonId }, + { enabled: !!session && !!selectedHackathonId }, + ); + + const availableTracks = hackathonDetail?.tracks ?? []; + const availableChallenges = hackathonDetail?.challenges ?? []; + + const toggle = ( + value: string, + current: string[], + set: (next: string[]) => void, + ) => + set( + current.includes(value) + ? current.filter((v) => v !== value) + : [...current, value], + ); + + // We get the specific registration / team context based on selected hackathon + const currentReg = myRegs?.find((r) => r.hackathonId === selectedHackathonId); + // Same gate as TeamsTab: forming a team needs acceptance, and the server + // refuses anyone else, so the controls are not offered until then. + const admitted = + currentReg?.registrationStatus === "approved" || + currentReg?.registrationStatus === "checked_in"; + // A withdrawn project stays as a draft row, so the row alone is not a + // submission. + const hasSubmitted = + (!!mySubmission.data && mySubmission.data.status !== "draft") || + currentReg?.hasSubmittedProject || + projectSubmitted; + // submitProject and withdrawProject refuse everyone but the captain, so + // teammates see the entry but cannot change it. + const readOnly = + !!currentReg?.team && currentReg.team.captainId !== session?.user?.id; + // The team list already carries the captain; it is read only to name them. + const teamRoster = trpc.team.list.useQuery( + { hackathonId: selectedHackathonId }, + { enabled: readOnly && !!selectedHackathonId }, + ); + const captainName = teamRoster.data?.find( + (t) => t.id === currentReg?.team?.id, + )?.captain?.name; + + // Mutations + const submitProject = trpc.team.submitProject.useMutation({ + onSuccess: () => { + utils.hackathon.myRegistrations.invalidate(); + utils.team.mySubmission.invalidate(); + setError(""); + setSuccessMessage( + "Project submitted. Judging is in person: your team presents to judges for each challenge, and the event's Judging tab shows where and when.", + ); + setProjectSubmitted(true); + }, + onError: (err) => setError(err.message), + }); + + // Leaving or disbanding a team refuses while a submission stands and tells + // the user to withdraw it first, so there has to be a way to do that. + const withdrawProject = trpc.team.withdrawProject.useMutation({ + onSuccess: () => { + utils.hackathon.myRegistrations.invalidate(); + utils.team.mySubmission.invalidate(); + setError(""); + setProjectSubmitted(false); + setConfirmWithdraw(false); + setSuccessMessage( + "Submission withdrawn. It is back to draft and no longer entered for judging.", + ); + }, + onError: (err) => { + setConfirmWithdraw(false); + setError(err.message); + }, + }); + + // Still submittable: until 12 hours after the event ends. The default pick + // and the dropdown use the same rule; defaulting to myRegs[0] could select + // a past edition the dropdown did not even list. + const isActiveReg = (reg: NonNullable[number]) => + reg.hackathon.endDate + ? new Date( + new Date(reg.hackathon.endDate).getTime() + 12 * 60 * 60 * 1000, + // oxlint-disable-next-line react/purity + ) >= new Date() + : true; + + if (!selectedHackathonId && myRegs && myRegs.length > 0) { + const firstReg = myRegs.find(isActiveReg); + if (firstReg) { + setSelectedHackathonId(firstReg.hackathonId); + } + } + + // Fill the form from the existing submission, once per selected event. + // Clearing when there is none matters as much as filling: the form used to + // keep the previous event's answers after switching, so a submit could file + // one hackathon's project against another. + if ( + selectedHackathonId && + !mySubmission.isPending && + prefilledFor !== selectedHackathonId + ) { + const p = mySubmission.data; + setProjectName(p?.name ?? ""); + setProjectDesc(p?.description ?? ""); + setGithubUrl(p?.githubUrl ?? ""); + setVideoUrl(p?.videoUrl ?? ""); + setDemoUrl(p?.demoUrl ?? ""); + setTracks(p?.tracks ?? []); + setChallenges(p?.challenges ?? []); + setIsCreateX(p?.isCreateX ?? false); + setPrefilledFor(selectedHackathonId); + } + + if (status === "loading" || loadingRegs) { + return ; + } + + if (!session) { + router.push(loginHref()); + return null; + } + + const activeRegs = myRegs?.filter(isActiveReg) || []; + // One live event is already named by the kicker; the picker earns its place + // only when there is something to choose. + const showEventPicker = + activeRegs.length > 1 || + !activeRegs.some((r) => r.hackathonId === selectedHackathonId); + + // A teammate reads what was filed; a row of dimmed buttons says it badly. + const filedChips = (picked: string[]) => + picked.length > 0 ? ( + picked.map((v) => ( + + {v} + + )) + ) : ( +

None picked

+ ); + + return ( +
+ + ← Dashboard + + +
+ {currentReg &&

{currentReg.hackathon.name}

} +

Submit your project

+
+ + {!myRegs || myRegs.length === 0 ? ( +
+

+ Once you register for a hackathon, you can submit your project + here. +

+ + Browse hackathons + +
+ ) : ( +
+ {/* Context and team */} + + + {/* Project submission */} +
+
+ {/* Once the project is in judging, the team's times and rooms + live on the hackathon's Judging tab. */} + {mySubmission.data?.inJudging && ( +

+ Your project is in judging. Your team presents to judges for + each challenge it entered, and the{" "} + + Judging tab + {" "} + shows where and when once organisers publish the schedule. +

+ )} + + {/* Admission, said before the form is filled in. Acceptance + lets you form a team; submitting needs the badge scan, + because judging happens in person. */} + {currentReg && currentReg.registrationStatus !== "checked_in" && ( +

+ {currentReg.registrationStatus === "approved" + ? "You're accepted. Check in at the event before submitting: find a volunteer and have your badge scanned." + : currentReg.registrationStatus === "pending" + ? "Your registration is still being reviewed. You can form a team once you have been accepted." + : currentReg.registrationStatus === "waitlisted" + ? "You're on the waitlist. If a seat opens, you'll be accepted and can submit." + : "Your registration wasn't accepted, so you can't submit to this hackathon."} +

+ )} + + {/* The window this form is gated on, said before it is filled + in. Without it an attendee wrote a full description and + learned it was refused only on submit. */} + {submissionWindow.data && ( +

+ {submissionWindow.data.cancelled ? ( + "This hackathon has been cancelled, so nothing can be submitted." + ) : submissionWindow.data.notYetOpen ? ( + <> + Submission opens{" "} + . The form is + here early so you can see what it asks for. + + ) : !submissionWindow.data.isOpen ? ( + <> + Submission closed{" "} + . + + ) : !hasSubmitted ? ( + <> + Submissions close{" "} + . + + ) : submissionWindow.data.canEditExisting ? ( + <> + {readOnly + ? "Your team can edit its entry" + : "You can edit your entry"}{" "} + until . + + ) : ( + <> + Edits closed{" "} + , so + your entry stands as filed. + + )} +

+ )} +
+ +
{ + e.preventDefault(); + if (readOnly) return; + submitProject.mutate({ + hackathonId: selectedHackathonId, + teamId: currentReg?.team?.id, + name: projectName, + description: projectDesc, + githubUrl, + demoUrl, + videoUrl, + tracks, + challenges, + isCreateX, + }); + }} + className="space-y-10" + > + {/* Name & Desc */} +
+
+ + setProjectName(e.target.value)} + placeholder="e.g. Transit delay predictor" + className={`${input} ${readOnlyField}`} + /> +
+
+ +