diff --git a/apps/2025/src/app/_components/faq/faq.tsx b/apps/2025/src/app/_components/faq/faq.tsx index 0fadcba6b..0be3f718f 100644 --- a/apps/2025/src/app/_components/faq/faq.tsx +++ b/apps/2025/src/app/_components/faq/faq.tsx @@ -144,7 +144,7 @@ const faqData: FaqItem[] = [ id: "11", question: "Is food provided?", answer: - "We provide free meals, snacks, and drinks throughout the event to keep you energized. We also accommodate dietary restrictions—just let us know during registration.", + "We provide free meals, snacks, and drinks throughout the event to keep you energized. We also accommodate dietary restrictions, just let us know during registration.", category: "logistics", }, { @@ -165,7 +165,7 @@ const faqData: FaqItem[] = [ id: "14", question: "Can I use a past project or something I've built before?", answer: - "Nope—projects must be started after the hackathon begins. You're welcome to brainstorm ideas or learn tools ahead of time, but actual work should begin during the event to keep it fair for everyone.", + "Nope, projects must be started after the hackathon begins. You're welcome to brainstorm ideas or learn tools ahead of time, but actual work should begin during the event to keep it fair for everyone.", category: "event-details", }, { diff --git a/apps/blade/src/app/_components/admin/banquet-raffle/raffle-draw.tsx b/apps/blade/src/app/_components/admin/banquet-raffle/raffle-draw.tsx index df7ee9e55..1319243db 100644 --- a/apps/blade/src/app/_components/admin/banquet-raffle/raffle-draw.tsx +++ b/apps/blade/src/app/_components/admin/banquet-raffle/raffle-draw.tsx @@ -255,7 +255,7 @@ export default function RaffleDraw({ entries }: { entries: RaffleEntry[] }) { >
- {/* ———————————————————————— STATES ———————————————————————— */} + {/* STATES */} {!isDrawing && !winner ? (
diff --git a/apps/blade/src/app/_components/admin/club/events/create-event.tsx b/apps/blade/src/app/_components/admin/club/events/create-event.tsx index c9936b6cd..b74c2d539 100644 --- a/apps/blade/src/app/_components/admin/club/events/create-event.tsx +++ b/apps/blade/src/app/_components/admin/club/events/create-event.tsx @@ -691,7 +691,7 @@ export function CreateEventButton() { )} /> - {/* Discord Channel ID — shown only for internal events */} + {/* Discord Channel ID, shown only for internal events */} {form.watch("isOperationsCalendar") && (
- {/* End Date — NEW */} + {/* End Date, NEW */} - {/* Discord Channel ID — shown only for internal events */} + {/* Discord Channel ID, shown only for internal events */} {form.watch("isOperationsCalendar") && ( {response.member?.email ?? "N/A"} - + + No response + ); } diff --git a/apps/blade/src/app/_components/admin/forms/responses/PerUserResponsesView.tsx b/apps/blade/src/app/_components/admin/forms/responses/PerUserResponsesView.tsx index 21856fc78..765a22ba2 100644 --- a/apps/blade/src/app/_components/admin/forms/responses/PerUserResponsesView.tsx +++ b/apps/blade/src/app/_components/admin/forms/responses/PerUserResponsesView.tsx @@ -128,7 +128,7 @@ export function PerUserResponsesView({ const formatResponseValue = (value: unknown): string => { if (value === undefined || value === null) { - return "—"; + return "No response"; } if (Array.isArray(value)) { return value.join(", "); diff --git a/apps/blade/src/app/_components/admin/forms/responses/ResponsesTable.tsx b/apps/blade/src/app/_components/admin/forms/responses/ResponsesTable.tsx index 42a9640bb..ac0bcb5a8 100644 --- a/apps/blade/src/app/_components/admin/forms/responses/ResponsesTable.tsx +++ b/apps/blade/src/app/_components/admin/forms/responses/ResponsesTable.tsx @@ -74,7 +74,7 @@ export function ResponsesTable({ question, responses }: ResponsesTableProps) { let displayValue: React.ReactNode; if (answer === undefined || answer === null) { - displayValue = "—"; + displayValue = "No response"; } else if (Array.isArray(answer)) { displayValue = answer.join(", "); } else if (typeof answer === "boolean") { diff --git a/apps/blade/src/app/_components/admin/hackathon/events/update-event.tsx b/apps/blade/src/app/_components/admin/hackathon/events/update-event.tsx index fe21cc084..a856eeb2f 100644 --- a/apps/blade/src/app/_components/admin/hackathon/events/update-event.tsx +++ b/apps/blade/src/app/_components/admin/hackathon/events/update-event.tsx @@ -492,7 +492,7 @@ export function UpdateEventButton({ event }: { event: InsertEvent }) {
- {/* End Date — NEW */} + {/* End Date, NEW */} - {/* Discord Channel ID — shown only for internal events */} + {/* Discord Channel ID, shown only for internal events */} {form.watch("isOperationsCalendar") && ( Donate diff --git a/apps/blade/src/app/_components/dashboard/member/checkout-form.tsx b/apps/blade/src/app/_components/dashboard/member/checkout-form.tsx index 3762a84bf..15875f5c8 100644 --- a/apps/blade/src/app/_components/dashboard/member/checkout-form.tsx +++ b/apps/blade/src/app/_components/dashboard/member/checkout-form.tsx @@ -177,7 +177,7 @@ export function CheckoutForm() { return (
- {/* Left — order summary */} + {/* Left, order summary */}

@@ -202,7 +202,7 @@ export function CheckoutForm() {

- {/* Right — payment form */} + {/* Right, payment form */}
{intentError && (

{intentError}

diff --git a/apps/blade/src/app/_components/issue-calendar/calendar-issue-dialog.tsx b/apps/blade/src/app/_components/issue-calendar/calendar-issue-dialog.tsx index 0346d4878..9b9763282 100644 --- a/apps/blade/src/app/_components/issue-calendar/calendar-issue-dialog.tsx +++ b/apps/blade/src/app/_components/issue-calendar/calendar-issue-dialog.tsx @@ -44,7 +44,8 @@ function getAssigneeDisplayName( ) { const member = assignment.user.member; if (member) { - const fullName = `${member.firstName.trim()} ${member.lastName.trim()}`.trim(); + const fullName = + `${member.firstName.trim()} ${member.lastName.trim()}`.trim(); if (fullName.length > 0) return fullName; } return assignment.user.discordUserId; diff --git a/apps/blade/src/app/_components/issue-calendar/calendar.css b/apps/blade/src/app/_components/issue-calendar/calendar.css index 820bdb4d3..b67b57c29 100644 --- a/apps/blade/src/app/_components/issue-calendar/calendar.css +++ b/apps/blade/src/app/_components/issue-calendar/calendar.css @@ -143,7 +143,7 @@ background-color: hsl(var(--card)); } - /* Top row of weekday names — styled like the issues table header. */ + /* Top row of weekday names, styled like the issues table header. */ .calendar-theme .fc-col-header-cell { background-color: hsl(var(--muted) / 0.3); border-color: hsl(var(--border)); diff --git a/apps/blade/src/app/_components/issues/issue-fetcher-pane.tsx b/apps/blade/src/app/_components/issues/issue-fetcher-pane.tsx index f32cae57f..a7abd2ab7 100644 --- a/apps/blade/src/app/_components/issues/issue-fetcher-pane.tsx +++ b/apps/blade/src/app/_components/issues/issue-fetcher-pane.tsx @@ -49,7 +49,8 @@ function getAssigneeDisplayNames(issue: ISSUE.IssueFetcherPaneIssue): string[] { .map((assignment) => { const member = assignment.user.member; if (member) { - const fullName = `${member.firstName.trim()} ${member.lastName.trim()}`.trim(); + const fullName = + `${member.firstName.trim()} ${member.lastName.trim()}`.trim(); if (fullName) return fullName; } return assignment.user.discordUserId.trim(); diff --git a/apps/blade/src/app/_components/judge/projects-table.tsx b/apps/blade/src/app/_components/judge/projects-table.tsx index 67b21d6dc..5a819281b 100644 --- a/apps/blade/src/app/_components/judge/projects-table.tsx +++ b/apps/blade/src/app/_components/judge/projects-table.tsx @@ -118,7 +118,7 @@ export function ProjectsTable({ hackathonId }: { hackathonId?: string }) { try { regex = new RegExp(q, "i"); // 'i' for case-insensitive search } catch { - // Invalid regex input — fallback to simple substring check + // Invalid regex input, falling back to simple substring check return judgesList.filter((j) => `${j.name} ${j.challengeTitle} ${j.roomName}` .toLowerCase() @@ -197,7 +197,7 @@ export function ProjectsTable({ hackathonId }: { hackathonId?: string }) { disabled={judgesLoading} > {selectedJudge - ? `${selectedJudge.name} — ${selectedJudge.challengeTitle}${ + ? `${selectedJudge.name}, ${selectedJudge.challengeTitle}${ selectedJudge.roomName ? ` (${selectedJudge.roomName})` : "" @@ -240,7 +240,7 @@ export function ProjectsTable({ hackathonId }: { hackathonId?: string }) { />
- {judge.name} — {judge.challengeTitle} + {judge.name}, {judge.challengeTitle} {judge.roomName ? ( diff --git a/apps/blade/src/app/_components/judge/results-table.tsx b/apps/blade/src/app/_components/judge/results-table.tsx index 7d1a2f560..bcc835491 100644 --- a/apps/blade/src/app/_components/judge/results-table.tsx +++ b/apps/blade/src/app/_components/judge/results-table.tsx @@ -370,7 +370,7 @@ export default function ResultsTable() { Devpost ) : ( - "—" + "Not rated" )} @@ -487,7 +487,8 @@ export default function ResultsTable() { project.originality_rating, )} > - {project.originality_rating || "—"} + {project.originality_rating || + "Not rated"}
@@ -503,7 +504,7 @@ export default function ResultsTable() { project.design_rating, )} > - {project.design_rating || "—"} + {project.design_rating || "Not rated"}
@@ -520,7 +521,7 @@ export default function ResultsTable() { )} > {project.technical_understanding_rating || - "—"} + "Not rated"}
@@ -540,7 +541,8 @@ export default function ResultsTable() { project.implementation_rating, )} > - {project.implementation_rating || "—"} + {project.implementation_rating || + "Not rated"}
@@ -556,7 +558,7 @@ export default function ResultsTable() { project.wow_factor_rating, )} > - {project.wow_factor_rating || "—"} + {project.wow_factor_rating || "Not rated"} @@ -640,7 +642,7 @@ export default function ResultsTable() { } > {submission.originality_rating || - "—"} + "Not rated"}
@@ -656,7 +658,8 @@ export default function ResultsTable() { : "outline" } > - {submission.design_rating || "—"} + {submission.design_rating || + "Not rated"}
@@ -673,7 +676,7 @@ export default function ResultsTable() { } > {submission.technical_understanding_rating || - "—"} + "Not rated"}
@@ -692,7 +695,7 @@ export default function ResultsTable() { } > {submission.implementation_rating || - "—"} + "Not rated"}
@@ -709,7 +712,7 @@ export default function ResultsTable() { } > {submission.wow_factor_rating || - "—"} + "Not rated"}
diff --git a/apps/blade/src/app/_components/settings/hacker-profile-form.tsx b/apps/blade/src/app/_components/settings/hacker-profile-form.tsx index 2cfa53dd2..2b131b14b 100644 --- a/apps/blade/src/app/_components/settings/hacker-profile-form.tsx +++ b/apps/blade/src/app/_components/settings/hacker-profile-form.tsx @@ -364,7 +364,7 @@ export function HackerProfileForm({ Phone Number {" "} - — Optional + , Optional @@ -404,7 +404,7 @@ export function HackerProfileForm({ Gender {" "} - — Optional + , Optional @@ -439,7 +439,7 @@ export function HackerProfileForm({ Race or Ethnicity {" "} - — Optional + , Optional @@ -655,7 +655,7 @@ export function HackerProfileForm({ GitHub Profile {" "} - — Optional + , Optional @@ -677,7 +677,7 @@ export function HackerProfileForm({ LinkedIn Profile {" "} - — Optional + , Optional @@ -699,7 +699,7 @@ export function HackerProfileForm({ Personal Website {" "} - — Optional + , Optional @@ -718,7 +718,7 @@ export function HackerProfileForm({ Resume {" "} - — Optional + , Optional @@ -748,7 +748,7 @@ export function HackerProfileForm({ Food Allergies/Restrictions {" "} - — Optional + , Optional diff --git a/apps/blade/src/app/admin/issues/[id]/page.tsx b/apps/blade/src/app/admin/issues/[id]/page.tsx index b249aa996..fa62a4193 100644 --- a/apps/blade/src/app/admin/issues/[id]/page.tsx +++ b/apps/blade/src/app/admin/issues/[id]/page.tsx @@ -24,7 +24,8 @@ function getAssigneeDisplayName(assignment: { }) { const member = assignment.user.member; if (member) { - const fullName = `${member.firstName.trim()} ${member.lastName.trim()}`.trim(); + const fullName = + `${member.firstName.trim()} ${member.lastName.trim()}`.trim(); if (fullName.length > 0) return fullName; } return assignment.user.discordUserId; diff --git a/apps/bloomknights/components.json b/apps/bloomknights/components.json new file mode 100644 index 000000000..c9930e90d --- /dev/null +++ b/apps/bloomknights/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "~/components", + "utils": "~/lib/utils", + "ui": "~/components/ui", + "lib": "~/lib", + "hooks": "~/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/apps/bloomknights/eslint.config.js b/apps/bloomknights/eslint.config.js new file mode 100644 index 000000000..24a7bef4f --- /dev/null +++ b/apps/bloomknights/eslint.config.js @@ -0,0 +1,14 @@ +import baseConfig, { restrictEnvAccess } from "@forge/eslint-config/base"; +import nextjsConfig from "@forge/eslint-config/nextjs"; +import reactConfig from "@forge/eslint-config/react"; + +/** @type {import('typescript-eslint').Config} */ +export default [ + { + ignores: [".next/**"], + }, + ...baseConfig, + ...reactConfig, + ...nextjsConfig, + ...restrictEnvAccess, +]; diff --git a/apps/bloomknights/next.config.js b/apps/bloomknights/next.config.js new file mode 100644 index 000000000..fe9588103 --- /dev/null +++ b/apps/bloomknights/next.config.js @@ -0,0 +1,22 @@ +/** @type {import("next").NextConfig} */ +const config = { + reactStrictMode: true, + images: { + formats: ["image/avif", "image/webp"], + minimumCacheTTL: 60 * 60 * 24 * 30, + remotePatterns: [ + { + protocol: "https", + hostname: "assets.knighthacks.org", + }, + ], + }, + + /** Enables hot reloading for local packages without a build step */ + transpilePackages: ["@forge/ui"], + + /** We already do linting and typechecking as separate tasks in CI */ + typescript: { ignoreBuildErrors: true }, +}; + +export default config; diff --git a/apps/bloomknights/package.json b/apps/bloomknights/package.json new file mode 100644 index 000000000..eb0fdc616 --- /dev/null +++ b/apps/bloomknights/package.json @@ -0,0 +1,56 @@ +{ + "name": "@forge/bloomknights", + "version": "0.1.0", + "private": true, + "type": "module", + "packageManager": "pnpm@9.12.1", + "scripts": { + "build": "pnpm with-env next build", + "clean": "git clean -xdf .cache .next .turbo node_modules", + "dev": "pnpm with-env next dev --port 3006", + "format": "prettier --check . --ignore-path ../../.gitignore", + "lint": "eslint", + "start": "pnpm with-env next start --port 3006", + "typecheck": "tsc --noEmit", + "with-env": "dotenv -e ../../.env --" + }, + "dependencies": { + "@forge/api": "workspace:*", + "@forge/auth": "workspace:*", + "@forge/db": "workspace:*", + "@forge/ui": "workspace:*", + "@gsap/react": "^2.1.2", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "@svgr/webpack": "^8.1.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "framer-motion": "^12.34.3", + "geist": "^1.7.0", + "gsap": "^3.14.2", + "lucide-react": "^0.575.0", + "next": "^16.0.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "rsuite": "^6.1.2", + "tailwind-merge": "^3.5.0" + }, + "devDependencies": { + "@forge/eslint-config": "workspace:*", + "@forge/prettier-config": "workspace:*", + "@forge/tailwind-config": "workspace:*", + "@forge/tsconfig": "workspace:*", + "@tailwindcss/postcss": "^4.2.1", + "@types/node": "^25.3.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "dotenv-cli": "^11.0.0", + "eslint": "catalog:", + "prettier": "catalog:", + "tailwindcss": "catalog:", + "tw-animate-css": "^1.4.0", + "typescript": "catalog:" + }, + "prettier": "@forge/prettier-config" +} diff --git a/apps/bloomknights/postcss.config.cjs b/apps/bloomknights/postcss.config.cjs new file mode 100644 index 000000000..483f37854 --- /dev/null +++ b/apps/bloomknights/postcss.config.cjs @@ -0,0 +1,5 @@ +module.exports = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; diff --git a/apps/bloomknights/public/BloomKnights.svg b/apps/bloomknights/public/BloomKnights.svg new file mode 100644 index 000000000..249aa0e80 --- /dev/null +++ b/apps/bloomknights/public/BloomKnights.svg @@ -0,0 +1,894 @@ + + diff --git a/apps/bloomknights/public/BloomKnightsSigil.svg b/apps/bloomknights/public/BloomKnightsSigil.svg new file mode 100644 index 000000000..e2091a1e1 --- /dev/null +++ b/apps/bloomknights/public/BloomKnightsSigil.svg @@ -0,0 +1,600 @@ + + diff --git a/apps/bloomknights/public/Google_Gemini_logo.svg b/apps/bloomknights/public/Google_Gemini_logo.svg new file mode 100644 index 000000000..721d1536a --- /dev/null +++ b/apps/bloomknights/public/Google_Gemini_logo.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/apps/bloomknights/public/event-banner.png b/apps/bloomknights/public/event-banner.png new file mode 100644 index 000000000..8e39fb3be Binary files /dev/null and b/apps/bloomknights/public/event-banner.png differ diff --git a/apps/bloomknights/public/gemini (1).svg b/apps/bloomknights/public/gemini (1).svg new file mode 100644 index 000000000..526e06cdb --- /dev/null +++ b/apps/bloomknights/public/gemini (1).svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/bloomknights/public/mlh-logo-color.svg b/apps/bloomknights/public/mlh-logo-color.svg new file mode 100644 index 000000000..61abea883 --- /dev/null +++ b/apps/bloomknights/public/mlh-logo-color.svg @@ -0,0 +1 @@ +mlh-logo-color-dark \ No newline at end of file diff --git a/apps/bloomknights/public/mlh-logo-white.svg b/apps/bloomknights/public/mlh-logo-white.svg new file mode 100644 index 000000000..e7c5aee5f --- /dev/null +++ b/apps/bloomknights/public/mlh-logo-white.svg @@ -0,0 +1,38 @@ + + + + mlh-logo-white + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/bloomknights/public/oneethos.png b/apps/bloomknights/public/oneethos.png new file mode 100644 index 000000000..190a4425e Binary files /dev/null and b/apps/bloomknights/public/oneethos.png differ diff --git a/apps/bloomknights/src/app/_components/about/about.tsx b/apps/bloomknights/src/app/_components/about/about.tsx new file mode 100644 index 000000000..e8a060011 --- /dev/null +++ b/apps/bloomknights/src/app/_components/about/about.tsx @@ -0,0 +1,137 @@ +"use client"; + +import type { Variants } from "framer-motion"; +import Image from "next/image"; +import { motion } from "framer-motion"; + +const aboutCopy = [ + "BloomKnights is a 12-hour student hackathon in Orlando, Florida, hosted by Knight Hacks at the University of Central Florida. On July 11, 2026, UCF students will spend the day building software projects, learning new skills, attending workshops, meeting mentors, and collaborating with other hackers.", + "Whether you are new to hackathons or already shipping projects, BloomKnights is designed to be a fast, beginner-friendly way to create something real in one day. The event takes place on UCF campus at BA1, and participation is free for UCF students.", + "BloomKnights connects the energy of a Florida hackathon with the support of Knight Hacks, UCF's software development and hackathon organization. Bring a laptop, a charger, and an idea you want to explore.", +]; + +const aboutImages = [ + { + src: "https://assets.knighthacks.org/GemiKnight1.jpg", + alt: "GemiKnight event scene", + }, + { + src: "https://assets.knighthacks.org/GemiKnight2.jpg", + alt: "GemiKnight workshop scene", + }, +]; + +const revealEase = [0.22, 1, 0.36, 1] as const; + +const sectionReveal: Variants = { + hidden: {}, + visible: { + transition: { + staggerChildren: 0.14, + delayChildren: 0.05, + }, + }, +}; + +const focusPanelReveal: Variants = { + hidden: {}, + visible: { + transition: { + delayChildren: 0.1, + staggerChildren: 0.08, + }, + }, +}; + +const revealItem: Variants = { + hidden: { opacity: 0, y: 34 }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.85, + ease: revealEase, + }, + }, +}; + +const About = () => { + return ( + + +

+ ABOUT +

+
+ + +
+ {aboutCopy.map((paragraph) => ( + + {paragraph} + + ))} +
+ + +
+
+
+ ); +}; + +export default About; diff --git a/apps/bloomknights/src/app/_components/discord/discord.tsx b/apps/bloomknights/src/app/_components/discord/discord.tsx new file mode 100644 index 000000000..5676d1723 --- /dev/null +++ b/apps/bloomknights/src/app/_components/discord/discord.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { useEffect, useRef } from "react"; +import { gsap } from "gsap"; +import { ScrollTrigger } from "gsap/dist/ScrollTrigger"; + +import BloomButtonEdge from "../ui/BloomButtonEdge"; + +gsap.registerPlugin(ScrollTrigger); + +export default function DiscordCTAButton({ + label = "Join Our Discord!", +}: { + label?: string; +}) { + const buttonRef = useRef(null); + const containerRef = useRef(null); + + useEffect(() => { + if (!buttonRef.current || !containerRef.current) return; + const prefersReducedMotion = window.matchMedia( + "(prefers-reduced-motion: reduce)", + ).matches; + + if (prefersReducedMotion) { + gsap.set(buttonRef.current, { opacity: 1, x: 0, y: 0 }); + return; + } + + const context = gsap.context(() => { + gsap.fromTo( + buttonRef.current, + { opacity: 0, x: -120, y: 36 }, + { + opacity: 1, + x: 0, + y: 0, + duration: 1.35, + ease: "power3.out", + scrollTrigger: { + trigger: containerRef.current, + start: "top 75%", + once: true, + }, + }, + ); + }, containerRef); + + return () => context.revert(); + }, []); + + return ( +
+
+
+
+
+ +
+
+ +
+
+ ); +} diff --git a/apps/bloomknights/src/app/_components/faq/faq-data.ts b/apps/bloomknights/src/app/_components/faq/faq-data.ts new file mode 100644 index 000000000..5c28c82a0 --- /dev/null +++ b/apps/bloomknights/src/app/_components/faq/faq-data.ts @@ -0,0 +1,97 @@ +export const faqSections = [ + { + title: "General", + items: [ + { + question: "What is BloomKnights?", + answer: + "BloomKnights is a 12-hour beginner-friendly student hackathon hosted by Knight Hacks at the University of Central Florida, where participants create innovative projects in a single day.", + }, + { + question: "When and where is it happening?", + answer: + "BloomKnights will take place in person on July 11, 2026 at BA1 on UCF Main Campus in Orlando, Florida. For the most recent information on check-in and schedule, please refer to the Hackers Guide.", + }, + { + question: "How is BloomKnights different from Knight Hacks?", + answer: + "BloomKnights is a shorter 12-hour mini-hackathon focused on fast project building, workshops, mentorship, and beginner-friendly support. The main Knight Hacks hackathon is a larger multi-day event.", + }, + { + question: "My question was not answered, where can I ask?", + answer: + "Check out the Hackers Guide first. If your question still isn't answered, feel free to ask in our Discord!", + }, + ], + }, + { + title: "Registration", + items: [ + { + question: "Who can attend?", + answer: + "BloomKnights is open to current college students of all majors and experience levels. Beginners are welcome, and you do not need prior hackathon experience to participate.", + }, + { + question: "Is it free?", + answer: + "Yes, attending BloomKnights is completely free. Registration, workshops, mentorship, and event programming are free for accepted UCF students. Just bring your laptop, charger, and enthusiasm.", + }, + { + question: "How can I participate?", + answer: + "To participate, you MUST register for the event and join the Discord. Upon doing so, you may be accepted. If you are accepted, you MUST confirm your attendance by checking your dashboard. Failure to confirm will result in your spot being forfeit.", + }, + ], + }, + { + title: "Preparation", + items: [ + { + question: "Do I need a team?", + answer: + "No team? No problem. You can work solo or form a team at the event. We'll help with team matching at the start of the day.", + }, + { + question: "Do I need coding experience?", + answer: + "No coding experience is required. BloomKnights is a beginner-friendly UCF hackathon with workshops, mentors, and teammates who can help you learn as you build.", + }, + { + question: "What should I bring?", + answer: + "Bring your laptop, charger, water bottle, and anything else you need to stay productive and comfortable during the event.", + }, + { + question: "Will there be food?", + answer: + "Yes, meals and snacks will be provided throughout the day to keep you fueled.", + }, + { + question: "Can I show up late?", + answer: + "Yes, we will be allowing for check-in for the entirety of the event. However, we recommend arriving early to maximize your hacking time and take advantage of all the event resources.", + }, + ], + }, + { + title: "Projects & Judging", + items: [ + { + question: "Will there be prizes?", + answer: + "Yes! Prizes will be awarded to standout projects based on creativity, technical skill, and effective use of AI. Stay tuned for category announcements.", + }, + { + question: "Can I use past projects or something I've built before?", + answer: + "Nope, projects must be started after the hackathon begins. You're welcome to brainstorm ideas or learn tools ahead of time, but actual work should begin during the event to keep it fair for everyone.", + }, + { + question: "How does project submission and judging work?", + answer: + "We use Devpost for project submissions, and judging follows a science fair style format where you'll demo your project to judges who visit your table. For more detailed information and deeper explanations of the submission process, check out the Hackers Guide.", + }, + ], + }, +]; diff --git a/apps/bloomknights/src/app/_components/faq/faq.tsx b/apps/bloomknights/src/app/_components/faq/faq.tsx new file mode 100644 index 000000000..e9be09d32 --- /dev/null +++ b/apps/bloomknights/src/app/_components/faq/faq.tsx @@ -0,0 +1,134 @@ +"use client"; + +import type { Variants } from "framer-motion"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@radix-ui/react-accordion"; +import { motion } from "framer-motion"; + +import { faqSections } from "./faq-data"; + +const sectionReveal: Variants = { + hidden: {}, + visible: { + transition: { + staggerChildren: 0.1, + delayChildren: 0.05, + }, + }, +}; + +const focusPanelReveal: Variants = { + hidden: {}, + visible: { + transition: { + delayChildren: 0.08, + staggerChildren: 0.08, + }, + }, +}; + +const revealItem: Variants = { + hidden: { opacity: 0, y: 32 }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.78, + ease: [0.22, 1, 0.36, 1], + }, + }, +}; + +const questionReveal: Variants = { + hidden: { opacity: 0, y: 22, scale: 0.985 }, + visible: { + opacity: 1, + y: 0, + scale: 1, + transition: { + duration: 0.62, + ease: [0.22, 1, 0.36, 1], + }, + }, +}; + +const sparkNames = ["one", "two", "three", "four", "five", "six"] as const; + +const FAQ = () => { + return ( + + +

+ + +

+
+ +
+ {faqSections.map((section, sectionIndex) => ( + + + {section.title} + + + {section.items.map((item, itemIndex) => ( + + + + + ))} + + + ))} +
+
+
+ ); +}; + +export default FAQ; diff --git a/apps/bloomknights/src/app/_components/footer/footer.tsx b/apps/bloomknights/src/app/_components/footer/footer.tsx new file mode 100644 index 000000000..c6f49b144 --- /dev/null +++ b/apps/bloomknights/src/app/_components/footer/footer.tsx @@ -0,0 +1,70 @@ +import React from "react"; +import Link from "next/link"; + +import { Separator } from "@forge/ui/separator"; + +import { footerLinks, footerMessage } from "./footerContent"; + +const footerLinkClassName = + "wc-footer-link focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#fff7dc] focus-visible:ring-offset-2 focus-visible:ring-offset-[#245f34]"; +const footerLinkColors = ["#fff7dc", "#6fa04d", "#4a8139", "#245f35"]; + +export default function Footer() { + return ( +
+
+
+ +
+
+
+ {footerLinks.map((link, index) => { + const linkStyle = { + "--wc-footer-color": footerLinkColors[index], + } as React.CSSProperties; + + return ( + +
+ {link.href.startsWith("mailto:") ? ( + + {link.text} + + ) : ( + + {link.text} + + )} +
+ {index < footerLinks.length - 1 && ( +
+ +
+ )} +
+ ); + })} +
+
+ + {footerMessage} + +
+
+
+
+ ); +} diff --git a/apps/bloomknights/src/app/_components/footer/footerContent.ts b/apps/bloomknights/src/app/_components/footer/footerContent.ts new file mode 100644 index 000000000..e7d7b6f4d --- /dev/null +++ b/apps/bloomknights/src/app/_components/footer/footerContent.ts @@ -0,0 +1,25 @@ +const mlhcoc = "https://mlh.io/code-of-conduct"; +const sponsor = "https://blade.knighthacks.org/sponsor"; +const contact = "mailto:hack@knighthacks.org"; +const hackersGuide = "https://knight-hacks.notion.site/bloomknights2025"; + +export const footerLinks = [ + { + text: "MLH Code of Conduct", + href: mlhcoc, + }, + { + text: "Sponsor Us", + href: sponsor, + }, + { + text: "Contact Us", + href: contact, + }, + { + text: "Hackers Guide", + href: hackersGuide, + }, +]; + +export const footerMessage = "Made with love 💚 by the Knight Hacks team"; diff --git a/apps/bloomknights/src/app/_components/graphics/AnimatedBirds.tsx b/apps/bloomknights/src/app/_components/graphics/AnimatedBirds.tsx new file mode 100644 index 000000000..efd2246cf --- /dev/null +++ b/apps/bloomknights/src/app/_components/graphics/AnimatedBirds.tsx @@ -0,0 +1,205 @@ +import type { CSSProperties } from "react"; + +type BirdStyle = CSSProperties & Record<`--${string}`, string>; + +interface Bird { + className: string; + style: BirdStyle; +} + +const SKY_BIRDS: Bird[] = [ + { + className: "bloom-bird bloom-bird-soft", + style: { + "--bird-top": "18%", + "--bird-scale": "0.74", + "--bird-duration": "38s", + "--bird-delay": "-18s", + "--bird-start-y": "-12px", + "--bird-mid-y": "12px", + "--bird-end-y": "-8px", + }, + }, + { + className: "bloom-bird", + style: { + "--bird-top": "32%", + "--bird-scale": "0.92", + "--bird-duration": "44s", + "--bird-delay": "-7s", + "--bird-start-y": "10px", + "--bird-mid-y": "-18px", + "--bird-end-y": "4px", + }, + }, + { + className: "bloom-bird bloom-bird-small", + style: { + "--bird-top": "52%", + "--bird-scale": "0.62", + "--bird-duration": "41s", + "--bird-delay": "-29s", + "--bird-start-y": "-2px", + "--bird-mid-y": "6px", + "--bird-end-y": "-10px", + }, + }, + { + className: "bloom-bird bloom-bird-soft", + style: { + "--bird-top": "42%", + "--bird-scale": "0.58", + "--bird-duration": "47s", + "--bird-delay": "-35s", + "--bird-start-y": "8px", + "--bird-mid-y": "-10px", + "--bird-end-y": "2px", + }, + }, + { + className: "bloom-bird", + style: { + "--bird-top": "12%", + "--bird-scale": "0.68", + "--bird-duration": "52s", + "--bird-delay": "-24s", + "--bird-start-y": "-4px", + "--bird-mid-y": "10px", + "--bird-end-y": "-12px", + }, + }, +]; + +const ABOUT_BIRDS: Bird[] = [ + { + className: "bloom-bird bloom-bird-section bloom-bird-soft", + style: { + "--bird-top": "10%", + "--bird-scale": "0.66", + "--bird-duration": "46s", + "--bird-delay": "-11s", + "--bird-start-y": "8px", + "--bird-mid-y": "-12px", + "--bird-end-y": "6px", + }, + }, + { + className: "bloom-bird bloom-bird-section", + style: { + "--bird-top": "36%", + "--bird-scale": "0.86", + "--bird-duration": "54s", + "--bird-delay": "-31s", + "--bird-start-y": "-10px", + "--bird-mid-y": "14px", + "--bird-end-y": "-4px", + }, + }, + { + className: "bloom-bird bloom-bird-section bloom-bird-small", + style: { + "--bird-top": "64%", + "--bird-scale": "0.56", + "--bird-duration": "49s", + "--bird-delay": "-22s", + "--bird-start-y": "4px", + "--bird-mid-y": "-8px", + "--bird-end-y": "12px", + }, + }, + { + className: "bloom-bird bloom-bird-section bloom-bird-soft", + style: { + "--bird-top": "78%", + "--bird-scale": "0.48", + "--bird-duration": "58s", + "--bird-delay": "-42s", + "--bird-start-y": "-2px", + "--bird-mid-y": "9px", + "--bird-end-y": "-9px", + }, + }, +]; + +const FAQ_BIRDS: Bird[] = [ + { + className: "bloom-bird bloom-bird-section bloom-bird-small", + style: { + "--bird-top": "14%", + "--bird-scale": "0.58", + "--bird-duration": "48s", + "--bird-delay": "-28s", + "--bird-start-y": "-8px", + "--bird-mid-y": "10px", + "--bird-end-y": "-5px", + }, + }, + { + className: "bloom-bird bloom-bird-section", + style: { + "--bird-top": "42%", + "--bird-scale": "0.78", + "--bird-duration": "56s", + "--bird-delay": "-16s", + "--bird-start-y": "12px", + "--bird-mid-y": "-14px", + "--bird-end-y": "8px", + }, + }, + { + className: "bloom-bird bloom-bird-section bloom-bird-soft", + style: { + "--bird-top": "68%", + "--bird-scale": "0.52", + "--bird-duration": "51s", + "--bird-delay": "-39s", + "--bird-start-y": "2px", + "--bird-mid-y": "-6px", + "--bird-end-y": "14px", + }, + }, + { + className: "bloom-bird bloom-bird-section bloom-bird-soft", + style: { + "--bird-top": "24%", + "--bird-scale": "0.44", + "--bird-duration": "60s", + "--bird-delay": "-47s", + "--bird-start-y": "-5px", + "--bird-mid-y": "8px", + "--bird-end-y": "-11px", + }, + }, +]; + +function BirdField({ birds, className }: { birds: Bird[]; className: string }) { + return ( + + ); +} + +export function AboutBirdFlock() { + return ( + + ); +} + +export function FAQBirdFlock() { + return ( + + ); +} + +export default function AnimatedBirds() { + return ; +} diff --git a/apps/bloomknights/src/app/_components/graphics/FloatingFlowers.tsx b/apps/bloomknights/src/app/_components/graphics/FloatingFlowers.tsx new file mode 100644 index 000000000..4d50a60cc --- /dev/null +++ b/apps/bloomknights/src/app/_components/graphics/FloatingFlowers.tsx @@ -0,0 +1,287 @@ +"use client"; + +import type { CSSProperties } from "react"; +import { useEffect, useState } from "react"; + +interface Petal { + id: number; + left: string; + size: string; + duration: string; + delay: string; + drift: string; + driftReturn: string; + spin: string; + color: string; + type: "cherry" | "daisy" | "tulip" | "clover" | "star"; +} + +type NonEmptyArray = readonly [T, ...T[]]; + +const PALETTE = [ + "#fcbc4e", + "#a8d471", + "#fe73fe", + "#fdc0fd", + "#dae494", + "#b8d4e8", + "#c9b8d8", + "#a8c490", + "#c4a882", + "#f5d97a", +] as const satisfies NonEmptyArray; + +function FlowerSVG({ + type, + color, + size, +}: { + type: Petal["type"]; + color: string; + size: number; +}) { + switch (type) { + case "cherry": + return ( + + + + + + + + + + ); + case "daisy": + return ( + + + + + + + + + + + + ); + case "tulip": + return ( + + + + + + + + ); + case "clover": + return ( + + + + + + + ); + case "star": + return ( + + + + ); + } +} + +const TYPES = [ + "cherry", + "daisy", + "tulip", + "clover", + "star", +] as const satisfies NonEmptyArray; + +function pickFrom(items: NonEmptyArray, index: number): T { + return items[index % items.length] ?? items[0]; +} + +export default function FloatingFlowers() { + const [petals, setPetals] = useState([]); + + useEffect(() => { + const prefersReducedMotion = window.matchMedia( + "(prefers-reduced-motion: reduce)", + ).matches; + + if (prefersReducedMotion) { + return; + } + + const isCoarsePointer = window.matchMedia("(pointer: coarse)").matches; + const petalCount = isCoarsePointer ? 9 : 18; + const generated: Petal[] = Array.from({ length: petalCount }, (_, i) => { + const drift = (Math.random() > 0.5 ? 1 : -1) * (28 + Math.random() * 92); + + return { + id: i, + left: `${Math.random() * 97}%`, + size: `${(0.85 + Math.random() * 0.55) * 16}px`, + duration: `${24 + Math.random() * 24}s`, + delay: `${Math.random() * 30}s`, + drift: `${drift}px`, + driftReturn: `${drift * -0.55}px`, + spin: `${Math.random() > 0.5 ? 720 : -720}deg`, + color: pickFrom(PALETTE, i), + type: pickFrom(TYPES, i), + }; + }); + + const frame = requestAnimationFrame(() => setPetals(generated)); + return () => cancelAnimationFrame(frame); + }, []); + + return ( +
+ {petals.map((p) => ( + + ))} +
+ ); +} diff --git a/apps/bloomknights/src/app/_components/graphics/Flowercursor.tsx b/apps/bloomknights/src/app/_components/graphics/Flowercursor.tsx new file mode 100644 index 000000000..ec36123e5 --- /dev/null +++ b/apps/bloomknights/src/app/_components/graphics/Flowercursor.tsx @@ -0,0 +1,143 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; + +const PALETTE = [ + "#fcbc4e", + "#a8d471", + "#fe73fe", + "#b8d4e8", + "#c9b8d8", + "#f5d97a", +] as const; + +interface TrailFlower { + id: number; + x: number; + y: number; + color: string; +} + +function TrailFlowerSVG({ color, size }: { color: string; size: number }) { + return ( + + + + + + + + + + ); +} + +export default function FlowerCursor() { + const [trail, setTrail] = useState([]); + const counterRef = useRef(0); + const lastTrailPos = useRef({ x: -200, y: -200 }); + const cleanupTimers = useRef([]); + + useEffect(() => { + const prefersReducedMotion = window.matchMedia( + "(prefers-reduced-motion: reduce)", + ).matches; + const isCoarsePointer = window.matchMedia("(pointer: coarse)").matches; + + if (prefersReducedMotion || isCoarsePointer) { + return; + } + + const onMove = (e: MouseEvent) => { + const dx = e.clientX - lastTrailPos.current.x; + const dy = e.clientY - lastTrailPos.current.y; + const dist = Math.sqrt(dx * dx + dy * dy); + + if (dist > 36) { + lastTrailPos.current = { x: e.clientX, y: e.clientY }; + const id = counterRef.current++; + const color = PALETTE[id % PALETTE.length] ?? PALETTE[0]; + setTrail((prev) => [ + ...prev.slice(-14), + { id, x: e.clientX, y: e.clientY, color }, + ]); + const cleanupTimer = window.setTimeout(() => { + setTrail((prev) => prev.filter((f) => f.id !== id)); + }, 900); + cleanupTimers.current.push(cleanupTimer); + } + }; + + window.addEventListener("mousemove", onMove); + return () => { + window.removeEventListener("mousemove", onMove); + cleanupTimers.current.forEach((cleanupTimer) => { + window.clearTimeout(cleanupTimer); + }); + cleanupTimers.current = []; + }; + }, []); + + return ( + <> + {trail.map((f) => ( + + ))} + + ); +} diff --git a/apps/bloomknights/src/app/_components/graphics/ParallaxBackground.tsx b/apps/bloomknights/src/app/_components/graphics/ParallaxBackground.tsx new file mode 100644 index 000000000..0abf69392 --- /dev/null +++ b/apps/bloomknights/src/app/_components/graphics/ParallaxBackground.tsx @@ -0,0 +1,3 @@ +export default function ParallaxBackground() { + return