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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
255 changes: 255 additions & 0 deletions apps/live/src/app/(landing)/CabinRace/CabinRace.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
"use client";

import React, { useEffect, useMemo, useState } from "react";
import RibbonTitle from "@repo/ui/RibbonTitle";
import useDevice from "@util/hooks/useDevice.ts";
import {
CabinRaceRedAirBalloon,
CabinRaceSquiggle,
} from "../../lib/Assets/SVG";
import CabinRaceCard from "../../components/CabinRaceComponents/CabinRaceCard.tsx";
import { Cabin, CabinLead } from "./CabinTypes.tsx";
import CabinRaceScoreTent from "../../components/CabinRaceComponents/CabinRaceScoreTent.tsx";

{
/* Airtable */
}
export type CabinInfoRecord = {
id: string;
createdTime: string;
fields: {
Name: string;
Points: number;
Description?: string;
CabinLeads?: string[];
};
};

export type CabinInfo = {
records: CabinInfoRecord[];
};

export type CabinLeadRecord = {
id: string;
createdTime: string;
fields: {
Name: string;
src: string;
url: string;
};
};

export type CabinLeadInfo = {
records: CabinLeadRecord[];
};

export default function CabinRace(): JSX.Element {
const { isDesktop, isTablet, isMobile } = useDevice();

const [cabinData, setCabinData] = useState<CabinInfo | null>(null);

async function getCabinPoints() {
const res = await fetch("/api/cabinPoints");
const jsonData: CabinInfo = await res.json();
const status = res.status;

if (status == 200) {
setCabinData(jsonData);
}
}

useEffect(() => {
const fetchCabins = async () => {
await getCabinPoints();
};

void fetchCabins();
}, []);

const [cabinLeads, setCabinLeads] = useState<CabinLeadRecord[]>([]);

useEffect(() => {
if (!cabinData) return;

const leadIds = cabinData.records.flatMap(
(record) => record.fields.CabinLeads ?? [],
);

if (!leadIds.length) return;

const fetchLeads = async () => {
const res = await fetch(`/api/cabinLeads?ids=${leadIds.join(",")}`);
const data: CabinLeadInfo = await res.json();
setCabinLeads(data.records);
};

void fetchLeads();
}, [cabinData]);

const fetchedCabins = useMemo<Cabin[]>(() => {
if (!cabinData) return [];

const leadsById: Record<string, CabinLead> = Object.fromEntries(
cabinLeads.map((lead) => [
lead.id,
{
name: lead.fields.Name,
src: lead.fields.src,
url: lead.fields.url,
},
]),
);

return cabinData.records.map((record) => ({
name: record.fields.Name,
points: record.fields.Points,
description: record.fields.Description ?? "",
cabinLeads: (record.fields.CabinLeads ?? [])
.map((id) => leadsById[id])
.filter((lead): lead is CabinLead => Boolean(lead)),
}));
}, [cabinData, cabinLeads]);

const cabinsByName: Record<string, Cabin> = Object.fromEntries(
fetchedCabins.map((cabin) => [cabin.name, cabin]),
);
const cabinNames: string[] = fetchedCabins.map((cabin) => cabin.name);
const [selectedCabinName, setSelectedCabinName] = useState<string | null>(
null,
);
useEffect(() => {
if (cabinNames.length && !selectedCabinName) {
setSelectedCabinName(cabinNames[0]);
}
}, [cabinNames, selectedCabinName]);

return (
<>
<div
className={`relative w-full bg-mossGreenDark flex flex-col items-center justify-center gap-[3vw] ${
isDesktop
? "aspect-[0.7]"
: isTablet
? "aspect-[0.55]"
: "aspect-[0.27]"
}`}
>
{/* Squiggle at top */}
<CabinRaceSquiggle
className={`absolute top-0 w-full -mt-[10vw]`}
></CabinRaceSquiggle>

{/* Ribbon Title */}
<div className={`relative w-[60vw] h-auto`}>
<RibbonTitle text={"CABIN RACE"}></RibbonTitle>
</div>

{/* Welcome Hackers Text*/}
<div
className={`relative flex flex-col
rounded-2xl bg-carouselCreamLight
justify-center px-6
${isDesktop ? "w-[60vw] h-[20vw]" : isTablet ? "w-[80vw] min-h-[40vw]" : "w-[60vw] min-h-[40vh]"}`}
>
<p
className={`text-firecrackerRed font-DMSans-Bold
${isDesktop ? "text-[2vw]" : isTablet ? "text-[3vw]" : "text-[4vw]"}`}
>
Welcome to the carnival, hackers!
</p>
<div
className={`flex w-full gap-4 ${isDesktop ? "flex-row" : "flex-col"}`}
>
<p
className={`text-charcoalFogDark font-DMSans-Regular
${isDesktop ? "text-[1.3vw]" : isTablet ? "text-[2vw]" : "text-[3vw]"}`}
>
Aside from hacking away and building an amazing project, you’re
also here to have fun — and what better way to do that than with
friends? In preparation for the carnival, you’ll be grouped into a
guild of performers who are interested in the same activities as
you.
</p>
<p
className={`text-charcoalFogDark font-DMSans-Regular
${isDesktop ? "text-[1.3vw]" : isTablet ? "text-[2vw]" : "text-[3vw]"}`}
>
<span className="font-DMSans-Bold">
Complete activities together
</span>{" "}
with your guild members to earn points and possibly win some cool
prizes! Most importantly, make some new friends who you can enjoy
the event with! All the while, you’ll be enjoying{" "}
<span className="font-DMSans-Bold">free food</span>, learning new
tricks from <span className="font-DMSans-Bold">workshops</span>,
and building your professional skills at{" "}
<span className="font-DMSans-Bold">career events</span>.
</p>
</div>
</div>

{/* Cabin Info Section*/}
{/* Buttons section */}
<div className="flex flex-wrap gap-3 justify-center z-30 p-6">
{cabinNames.map((name) => {
const isSelected = selectedCabinName === name;
return (
<button
key={name}
onClick={() => setSelectedCabinName(name)}
className={`px-[clamp(0.75rem,3vw,1.4rem)]
py-[clamp(0.4rem,2vw,0.8rem)]
rounded-[clamp(0.4rem,0.8vw,0.6rem)]
text-[clamp(0.75rem,1.2vw,1rem)]
font-DMSans-Bold
transition-colors duration-200
${
isSelected
? "bg-canopyGreen text-[#DEFFB8]"
: "bg-mossGreen text-charcoalFog hover:bg-mossGreen"
}`}
>
{name}
</button>
);
})}
</div>

{/* Card + balloons */}
<div
className={`relative mx-auto w-full`}
style={{
width: isDesktop ? "60vw" : isTablet ? "55vw" : "90vw",
height: isMobile ? "140vw" : isTablet ? "45vw" : "35vw",
}}
>
{/* Cabin card */}
{selectedCabinName && cabinsByName[selectedCabinName] && (
<div
className="relative z-10"
style={{ transform: isDesktop ? "translateX(8rem)" : "none" }}
>
<CabinRaceCard cabinInfo={cabinsByName[selectedCabinName]} />
</div>
)}

{/* Balloons */}
{isDesktop && (
<CabinRaceRedAirBalloon
className="absolute w-[18vw] h-auto top-0"
style={{ transform: "translateX(clamp(-7rem, -1vw, 10rem))" }}
/>
)}
{isTablet && (
<CabinRaceRedAirBalloon
className="absolute z-20 w-[15vw] top-0 h-auto"
style={{ transform: "translateX(45vw)" }}
/>
)}
</div>

<CabinRaceScoreTent cabinInfo={fetchedCabins}></CabinRaceScoreTent>
</div>
</>
);
}
13 changes: 13 additions & 0 deletions apps/live/src/app/(landing)/CabinRace/CabinTypes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// types
export interface CabinLead {
name: string;
src: string;
url: string;
}

export interface Cabin {
name: string;
points: number;
description: string;
cabinLeads: CabinLead[];
}
3 changes: 3 additions & 0 deletions apps/live/src/app/(landing)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import Footer from "@repo/ui/Footer";
// import ComingUp from "./ComingUp/ComingUp";
import OurTeam from "./OurTeam";
import Keynote from "./Keynote";
import CabinRace from "./CabinRace/CabinRace.tsx";

import SponsorFeature from "./SponsorFeature.tsx";
import CarnivalScene from "./CarnivalLanding/CarnivalScene.tsx";
export default function Page(): JSX.Element {
Expand All @@ -24,6 +26,7 @@ export default function Page(): JSX.Element {
<SponsorFeature />
<Welcome />
<EventSchedule />
<CabinRace />
<Keynote />
<MentorSection />
<Resources />
Expand Down
45 changes: 45 additions & 0 deletions apps/live/src/app/api/cabinLeads/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { NextRequest, NextResponse } from "next/server";

const BASE_URL = "https://api.airtable.com/v0";
const TABLE_NAME = "CabinLead";

export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const ids = searchParams.get("ids");

if (!ids) {
return NextResponse.json({ records: [] });
}

const CABIN_BASE_ID = process.env.CABIN_BASE_ID;

const filterFormula = `OR(${ids
.split(",")
.map((id) => `RECORD_ID()='${id}'`)
.join(",")})`;

const airtableUrl = `${BASE_URL}/${CABIN_BASE_ID}/${TABLE_NAME}?filterByFormula=${encodeURIComponent(
filterFormula,
)}`;

try {
const response = await fetch(airtableUrl, {
headers: {
Authorization: `Bearer ${process.env.AIRTABLE_TOKEN_ID}`,
"Content-Type": "application/json",
},
});

if (!response.ok) {
throw new Error("API request failed");
}

const data = await response.json();
return NextResponse.json(data);
} catch (err) {
return NextResponse.json(
{ error: `Request to get cabin leads failed ${err}` },
{ status: 500 },
);
}
}
Loading