mirror of
https://github.com/System-End/campfire.git
synced 2026-04-19 16:28:22 +00:00
Feat: phoenix!
This commit is contained in:
parent
a17f4d9bb0
commit
cc0bfe1cb9
29 changed files with 686 additions and 3502 deletions
|
|
@ -1,25 +1,23 @@
|
|||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
import { defineConfig } from "astro/config";
|
||||
|
||||
import react from '@astrojs/react';
|
||||
import react from "@astrojs/react";
|
||||
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
import node from '@astrojs/node';
|
||||
|
||||
import svelte from '@astrojs/svelte';
|
||||
import node from "@astrojs/node";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
integrations: [react(), svelte()],
|
||||
integrations: [react()],
|
||||
|
||||
vite: {
|
||||
plugins: [tailwindcss()]
|
||||
plugins: [tailwindcss()],
|
||||
},
|
||||
|
||||
adapter: node({
|
||||
mode: 'standalone'
|
||||
mode: "standalone",
|
||||
}),
|
||||
|
||||
output: "server",
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,41 +1,23 @@
|
|||
{
|
||||
"name": "astro",
|
||||
"name": "campfire-phoenix",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro",
|
||||
"sync:once": "tsx -e \"import('./src/lib/airtable.js').then(m => m.airtableSyncWorker.syncEvents())\""
|
||||
"preview": "astro preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^9.5.1",
|
||||
"@astrojs/react": "^4.4.2",
|
||||
"@astrojs/svelte": "^7.2.3",
|
||||
"@prisma/adapter-pg": "7.1.0",
|
||||
"@prisma/client": "7.1.0",
|
||||
"@prisma/config": "7.1.0",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/leaflet": "^1.9.21",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"astro": "^5.16.5",
|
||||
"better-auth": "^1.4.6",
|
||||
"dotenv": "^17.2.3",
|
||||
"leaflet": "^1.9.4",
|
||||
"lockfile": "^1.0.4",
|
||||
"clsx": "^2.1.1",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"react-leaflet": "^5.0.0",
|
||||
"svelte": "^5.45.10",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/pg": "^8.16.0",
|
||||
"airtable": "^0.12.2",
|
||||
"prisma": "7.1.0",
|
||||
"tsx": "^4.21.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1314
astro/pnpm-lock.yaml
generated
1314
astro/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
3
astro/pnpm-workspace.yaml
Normal file
3
astro/pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
- sharp
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
// This file was generated by Prisma and assumes you have installed the following:
|
||||
// npm install --save-dev prisma dotenv
|
||||
import "dotenv/config";
|
||||
import { defineConfig, env } from "@prisma/config";
|
||||
|
||||
export default defineConfig({
|
||||
schema: "prisma/schema.prisma",
|
||||
migrations: {
|
||||
path: "prisma/migrations",
|
||||
},
|
||||
datasource: {
|
||||
url: env("DATABASE_URL"),
|
||||
},
|
||||
});
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "Satellite" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"data" JSONB NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Satellite_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Satellite_slug_key" ON "Satellite"("slug");
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"slackId" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"role" TEXT NOT NULL DEFAULT 'user',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"satelliteId" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Session" (
|
||||
"id" TEXT NOT NULL,
|
||||
"secretHash" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "User" ADD CONSTRAINT "User_satelliteId_fkey" FOREIGN KEY ("satelliteId") REFERENCES "Satellite"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `Session` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `User` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "User" DROP CONSTRAINT "User_satelliteId_fkey";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "Session";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "User";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "user" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"emailVerified" BOOLEAN NOT NULL DEFAULT false,
|
||||
"image" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"satelliteId" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "user_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "session" (
|
||||
"id" TEXT NOT NULL,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"token" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"ipAddress" TEXT,
|
||||
"userAgent" TEXT,
|
||||
"userId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "session_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "account" (
|
||||
"id" TEXT NOT NULL,
|
||||
"accountId" TEXT NOT NULL,
|
||||
"providerId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"accessToken" TEXT,
|
||||
"refreshToken" TEXT,
|
||||
"idToken" TEXT,
|
||||
"accessTokenExpiresAt" TIMESTAMP(3),
|
||||
"refreshTokenExpiresAt" TIMESTAMP(3),
|
||||
"scope" TEXT,
|
||||
"password" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "account_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "verification" (
|
||||
"id" TEXT NOT NULL,
|
||||
"identifier" TEXT NOT NULL,
|
||||
"value" TEXT NOT NULL,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "verification_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "user_email_key" ON "user"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "session_userId_idx" ON "session"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "session_token_key" ON "session"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "account_userId_idx" ON "account"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "verification_identifier_idx" ON "verification"("identifier");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "user" ADD CONSTRAINT "user_satelliteId_fkey" FOREIGN KEY ("satelliteId") REFERENCES "Satellite"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "session" ADD CONSTRAINT "session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "account" ADD CONSTRAINT "account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
-- DropForeignKey
|
||||
ALTER TABLE "user" DROP CONSTRAINT "user_satelliteId_fkey";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "user" ALTER COLUMN "satelliteId" DROP NOT NULL;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "user" ADD CONSTRAINT "user_satelliteId_fkey" FOREIGN KEY ("satelliteId") REFERENCES "Satellite"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `account` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `session` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `user` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `verification` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "account" DROP CONSTRAINT "account_userId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "session" DROP CONSTRAINT "session_userId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "user" DROP CONSTRAINT "user_satelliteId_fkey";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Satellite" ADD COLUMN "active" BOOLEAN NOT NULL DEFAULT true;
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "account";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "session";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "user";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "verification";
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[recordId]` on the table `Satellite` will be added. If there are existing duplicate values, this will fail.
|
||||
- Added the required column `recordId` to the `Satellite` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "Satellite" ADD COLUMN "recordId" TEXT NOT NULL;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Satellite_recordId_key" ON "Satellite"("recordId");
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
|
||||
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client"
|
||||
output = "../src/generated/prisma"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
}
|
||||
|
||||
model Satellite {
|
||||
id Int @id @default(autoincrement())
|
||||
recordId String @unique
|
||||
slug String @unique
|
||||
data Json
|
||||
active Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
|
@ -1,745 +0,0 @@
|
|||
import '../../styles/global.css';
|
||||
import FaqQuestion from '../primitives/FaqQuestion.js';
|
||||
import FaqButton from '../primitives/FaqButton.js';
|
||||
import Step from '../primitives/Step.js';
|
||||
import StoryCard from '../primitives/StoryCard.js';
|
||||
import GameCard from '../primitives/GameCard.js';
|
||||
import NavbarLink from '../primitives/NavbarLink.tsx';
|
||||
import MapEmbed from '../primitives/MapEmbed.tsx';
|
||||
import { Map } from '../primitives/Map.tsx';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import type { EventLocation } from '../../lib/events.ts';
|
||||
|
||||
const FORM_URL_ORGANIZER_APPLICATION = "https://forms.hackclub.com/t/8L51MzWyrHus";
|
||||
const FORM_URL_RSVP = "https://forms.hackclub.com/t/a3QSt8MuvHus";
|
||||
const FORM_URL_SIGN_UP = "https://forms.hackclub.com/campfire-signup";
|
||||
|
||||
function FlagshipCTA({ className, compact, maxWidth }: { className?: string; compact?: boolean; maxWidth?: boolean }) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<a
|
||||
href="https://flagship.hackclub.com?utm_source=campfire-website"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className={clsx(
|
||||
"flex items-center rounded-[20px] transition-transform hover:scale-105 active:scale-95 cursor-pointer",
|
||||
compact ? "gap-4 px-6 py-3" : "gap-8 px-8 py-4",
|
||||
maxWidth && "max-w-2xl"
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: '#F9E2CA',
|
||||
border: '4px solid #D5A16C',
|
||||
boxShadow: '-6px 10px 0px 0px rgba(0,0,0,0.25)'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/compressed/branding/logo-flagship.png"
|
||||
alt="Campfire Flagship"
|
||||
className={clsx("object-contain select-none", compact ? "h-10" : "h-12 md:h-14")}
|
||||
/>
|
||||
<span
|
||||
className={clsx(
|
||||
"text-[#854d16] font-bold font-ember-and-fire",
|
||||
compact ? "text-xl" : "text-2xl md:text-3xl"
|
||||
)}
|
||||
>
|
||||
{compact ? "Join our flagship in LA with Open Sauce creators!" : "Come to our flagship event in Los Angeles with William Osman and Michael Reeves!"}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function App({ events }: { events: EventLocation[] }) {
|
||||
const [email, setEmail] = useState("");
|
||||
const [scrollY, setScrollY] = useState(document.body.scrollTop);
|
||||
const [isLargeScreen, setIsLargeScreen] = useState(window.innerWidth >= 1280);
|
||||
const [windowHeight, setWindowHeight] = useState(window.innerHeight);
|
||||
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
||||
const [isMapOpen, setIsMapOpen] = useState(false);
|
||||
const emailRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (window.location.pathname === '/flagship') {
|
||||
window.location.href = 'https://flagship.hackclub.com?utm_source=campfire_landing_page';
|
||||
return;
|
||||
}
|
||||
|
||||
// Redirect with r parameter to signup form
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const rParam = urlParams.get('r');
|
||||
if (rParam) {
|
||||
window.location.href = `${FORM_URL_SIGN_UP}?r=${encodeURIComponent(rParam)}`;
|
||||
return;
|
||||
}
|
||||
|
||||
document.addEventListener("scroll", () => {
|
||||
setScrollY(window.scrollY);
|
||||
});
|
||||
|
||||
const handleResize = () => {
|
||||
setIsLargeScreen(window.innerWidth >= 1280);
|
||||
setWindowHeight(window.innerHeight);
|
||||
setWindowWidth(window.innerWidth);
|
||||
};
|
||||
window.addEventListener("resize", handleResize);
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, []);
|
||||
|
||||
const scrollToSection = (sectionId: string) => {
|
||||
const element = document.getElementById(sectionId);
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function openWithEmail(url: string) {
|
||||
if (!emailRef?.current?.reportValidity() || !email)
|
||||
return;
|
||||
|
||||
window.open(`${url}?email=${encodeURIComponent(email)}`, "_blank");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full min-h-screen flex flex-col overflow-x-hidden">
|
||||
<div className="absolute -top-16 -left-8 w-1/3 z-20 pointer-events-none hidden min-[860px]:block">
|
||||
<img
|
||||
src="/compressed/backgrounds/corner-cloud.webp"
|
||||
alt=""
|
||||
className="w-full h-full object-cover select-none transform -rotate-12"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={clsx("w-full relative", windowHeight > windowWidth && windowWidth < 860 ? "h-full" : "h-screen")}>
|
||||
<header className="relative h-[60px] md:h-[115px] bg-[#45b4f5] justify-end items-center content-center md:pr-16 hidden sm:flex">
|
||||
<nav className="flex gap-4 w-full justify-between px-8 md:px-0 text-2xl md:gap-12 items-center md:justify-end text-white md:text-3xl xl:text-5xl font-bold font-ember-and-fire">
|
||||
<NavbarLink onClick={() => scrollToSection('steps')}>Steps</NavbarLink>
|
||||
{/* <NavbarLink onClick={() => scrollToSection('map')}>Map</NavbarLink> */}
|
||||
{/* <NavbarLink onClick={() => scrollToSection('letter')}>Letter</NavbarLink> */}
|
||||
<NavbarLink onClick={() => scrollToSection('previous-events')}>Past events</NavbarLink>
|
||||
<NavbarLink onClick={() => scrollToSection('games-made')}>Games made</NavbarLink>
|
||||
<NavbarLink onClick={() => scrollToSection('faq')}>FAQ</NavbarLink>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<section className={clsx(
|
||||
"relative min-h-[700px] px-6 md:px-16 md:px-24 2xl:px-32 bg-[url(/backgrounds/blue-sky.webp)] bg-center bg-cover",
|
||||
windowHeight > windowWidth && windowWidth < 860 ? "flex items-stretch pb-0" : "h-full flex items-end pb-32 2xl:pb-48"
|
||||
)}>
|
||||
<div className="absolute top-0 left-0 w-full h-full pointer-events-none">
|
||||
<img src="/backgrounds/sky-shine.webp" alt="" className="w-full h-full object-cover select-none" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="absolute bottom-[32px] right-[40px] md:right-[140px] w-full h-full pointer-events-none"
|
||||
style={{
|
||||
transform: isLargeScreen ? `translateY(${-scrollY / 12}px)` : undefined
|
||||
}}
|
||||
>
|
||||
{/* fishy on the right. His name is frederick. */}
|
||||
<div className="absolute top-[128px] md:top-[96px] right-[-50px] md:right-[100px] w-1/3 md:w-[200px]">
|
||||
<img src="/compressed/characters/fish-2.webp" alt="Fish named Frederick" className="w-full h-full object-cover select-none" />
|
||||
</div>
|
||||
|
||||
{/* fishy on the left. His name is gubson */}
|
||||
<div className="absolute top-[96px] md:top-[60px] right-[-10px] md:right-[300px] w-1/3 md:w-[200px]">
|
||||
<img src="/compressed/characters/fish-1.webp" alt="Fish named Gubson" className="w-full h-full object-cover select-none" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-0 left-0 w-full max-h-[120vh] overflow-hidden pointer-events-none md:animate-cloud-float-right">
|
||||
<img
|
||||
src="/backgrounds/bottom-cloud.webp"
|
||||
alt=""
|
||||
className="w-full h-full object-cover object-top select-none"
|
||||
style={{
|
||||
transform: isLargeScreen ? `translateY(${-scrollY / 5}px)` : undefined
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* <div className="absolute -bottom-[50px] left-0 w-full h-[120px] pointer-events-none">
|
||||
<img
|
||||
src="/decorative/vines.webp"
|
||||
alt=""
|
||||
className="w-full h-full object-cover object-top select-none"
|
||||
/>
|
||||
</div> */}
|
||||
|
||||
<div className={clsx(
|
||||
"flex flex-col md:flex-row justify-between items-center md:items-start xl:items-center w-full gap-8 z-30 pt-16 md:pt-0",
|
||||
windowHeight > windowWidth && windowWidth < 860 ? "pb-32" : "pb-16 h-full md:h-auto"
|
||||
)}>
|
||||
<div className={clsx(
|
||||
"flex flex-col gap-4 w-full md:w-[648px]",
|
||||
windowHeight > windowWidth && windowWidth < 860 && "justify-between h-full"
|
||||
)}>
|
||||
<div className="mb-6">
|
||||
{/* {windowWidth >= 400 && <FlagshipCTA className="min-[860px]:hidden -mt-12 mb-8" compact={windowWidth < 500} />} */}
|
||||
|
||||
<div className="flex items-center gap-3 mb-4 relative z-30">
|
||||
<a href='https://hackclub.com' className='transition-transform hover:scale-105 active:scale-95'>
|
||||
<img
|
||||
src="/decorative/flag-standalone-wtransparent.png"
|
||||
alt="Hack Club"
|
||||
className="w-[151px] h-[53px] object-cover transform rotate-[-4.8deg] select-none"
|
||||
style={{
|
||||
filter: "drop-shadow(3px 3px 0px rgba(0,0,0,0.25))"
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
<div className="w-[2px] h-8 bg-white opacity-60"></div>
|
||||
<a href='https://opensauce.com' className='transition-transform scale-125 hover:scale-130 active:scale-130'>
|
||||
<img
|
||||
src="/branding/logo-opensauce.webp"
|
||||
alt="Open Sauce"
|
||||
className="h-[70px] object-contain select-none pl-4"
|
||||
style={{
|
||||
filter: "drop-shadow(3px 3px 0px rgba(0,0,0,0.25))"
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="transform md:rotate-[-2.97deg]">
|
||||
<h1
|
||||
className="text-[#fcf5ed] text-[80px] md:text-[100px] xl:text-[150px] font-normal leading-none mb-4 font-dream-planner"
|
||||
style={{
|
||||
textShadow: "5px 8px 0px rgba(0,0,0,0.25)"
|
||||
}}
|
||||
>
|
||||
CAMPFIRE
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="pl-2 md:pl-4">
|
||||
<p
|
||||
className="text-white text-4xl md:text-3xl xl:text-4xl font-bold mb-2 font-ember-and-fire"
|
||||
style={{
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)"
|
||||
}}
|
||||
>
|
||||
Game jam for high schoolers in 200+ cities
|
||||
</p>
|
||||
<p
|
||||
className="text-white text-4xl md:text-3xl xl:text-4xl font-bold font-ember-and-fire"
|
||||
style={{
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)"
|
||||
}}
|
||||
>
|
||||
Feb 28 - Mar 1, 2026
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className="flex flex-col md:flex-row items-center gap-2">
|
||||
<div
|
||||
className={clsx(
|
||||
"bg-[#f9e2ca] border-4 border-[#d5a16c] rounded-[20px] px-4 md:px-8 py-4 flex items-center gap-3 md:gap-6 w-full transform md:rotate-[-1.2deg] shadow-[0_8px_20px_rgba(0,0,0,0.25)]",
|
||||
"transition-transform hover:scale-105"
|
||||
)}
|
||||
>
|
||||
<img src="/icons/email.svg" alt="" className="w-6 h-5 flex-shrink-0 select-none" />
|
||||
<input
|
||||
required
|
||||
ref={emailRef}
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
type="email"
|
||||
className="text-[#854d16] text-2xl md:text-4xl font-bold truncate bg-transparent border-none outline-none flex-1 cursor-text font-ember-and-fire"
|
||||
placeholder="you@hackclub.com"
|
||||
defaultValue="you@hackclub.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="bg-[#fca147] border-[5px] border-[rgba(0,0,0,0.2)] rounded-[20px] px-8 md:px-14 py-4 hover:scale-105 transition-transform w-full md:w-auto transform md:rotate-[1.5deg] shadow-[0_8px_20px_rgba(0,0,0,0.25)] cursor-pointer active:scale-95"
|
||||
type="button"
|
||||
onClick={() => openWithEmail(FORM_URL_SIGN_UP)}
|
||||
>
|
||||
<p
|
||||
className="text-[#8d3f34] text-3xl md:text-5xl font-normal font-dream-planner whitespace-nowrap"
|
||||
>
|
||||
SIGN UP!
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='font-ember-and-fire text-white text-4xl pl-2 -translate-y-1 md:rotate-[-1.2deg]' style={{
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)"
|
||||
}}>
|
||||
Looking for our <a
|
||||
className='underline inline-block cursor-pointer transition-transform hover:scale-105 active:scale-95'
|
||||
href='https://flagship.hackclub.com?utm_source=campfire-website'
|
||||
>flagship</a> event?<br />...or <span
|
||||
className='underline inline-block cursor-pointer transition-transform hover:scale-105 active:scale-95'
|
||||
onClick={() => openWithEmail(FORM_URL_ORGANIZER_APPLICATION)}
|
||||
>
|
||||
organize
|
||||
</span> your own event!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MapEmbed className="hidden xl:block self-end mb-8 relative z-50" onOpenMap={() => setIsMapOpen(true)} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="absolute bottom-0 left-0 w-full pointer-events-none">
|
||||
<img
|
||||
src="/compressed/backgrounds/landing-grass.webp"
|
||||
alt=""
|
||||
className="w-full h-full object-cover select-none"
|
||||
style={{
|
||||
transform: isLargeScreen ? `translateY(${scrollY / 10}px)` : undefined
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="relative pb-96 bg-[url(/backgrounds/underwater-gradient.webp)] bg-cover bg-top">
|
||||
<div className="xl:hidden pt-16 pb-8 relative z-50">
|
||||
<MapEmbed className="px-6 relative z-50 max-w-sm mx-auto" onOpenMap={() => setIsMapOpen(true)} />
|
||||
</div>
|
||||
<div className="pt-[8vw] xl:pt-[13vw]"></div>
|
||||
<div className="absolute top-0 left-0 w-screen h-[200px] bg-gradient-to-b from-[#004b2a] to-transparent pointer-events-none"></div>
|
||||
<div className="absolute top-0 xl:top-[30px] left-0 w-full scale-125 pointer-events-none z-50">
|
||||
<img src="/decorative/vines.webp" alt="" className="w-full h-full object-cover select-none" />
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-[30px] left-0 w-full h-[800px] z-0 pointer-events-none">
|
||||
<img src="/decorative/clouds-3.webp" alt="" className="w-full h-full object-cover select-none object-top" />
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-0 left-0 w-full h-[613px] z-0 pointer-events-none">
|
||||
<img src="/backgrounds/seafloor.webp" alt="" className="w-full h-full object-cover select-none" />
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-[200px] left-32 w-[569px] h-[676px] pointer-events-none">
|
||||
<img
|
||||
src="/compressed/characters/astronaut.webp"
|
||||
alt=""
|
||||
className="w-full h-full object-cover transform rotate-[172deg] scale-y-[-1] select-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="absolute top-[0px] z-45 right-0 w-[150px] md:w-[352px] pointer-events-none">
|
||||
<img
|
||||
src="/compressed/decorative/cogs-top-right.webp" alt=""
|
||||
className="w-full h-full object-contain select-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="absolute invisible md:visible top-[55%] z-45 left-0 w-[260px] pointer-events-none">
|
||||
<img src="/compressed/decorative/cogs-mid-left.webp" alt="" className="w-full h-full object-contain select-none" />
|
||||
</div>
|
||||
|
||||
<div className="absolute invisible md:visible top-[75%] z-45 right-0 translate-x-20 w-[280px] pointer-events-none">
|
||||
<img src="/compressed/decorative/single-cog-1.webp" alt="" className="w-full h-full object-contain select-none" />
|
||||
</div>
|
||||
|
||||
<div className="absolute invisible md:visible top-[82%] z-45 left-0 translate-x-10 w-[280px] pointer-events-none">
|
||||
<img src="/compressed/decorative/single-cog-2.webp" alt="" className="w-full h-full object-contain select-none" />
|
||||
</div>
|
||||
|
||||
<div id="steps" className="relative z-40 flex flex-col gap-24 items-center px-12 max-w-7xl mx-auto pt-12 md:pt-0">
|
||||
<Step
|
||||
stepNumber={1}
|
||||
imageSrc="/compressed/ui/step-signup.jpeg"
|
||||
imageAlt="Step 1"
|
||||
>
|
||||
Sign up for a <br></br><span className="font-bold text-[#F77034]">Campfire</span> near you
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
stepNumber={2}
|
||||
imageSrc="/compressed/ui/step-team.webp"
|
||||
imageAlt="Step 2"
|
||||
isReversed={true}
|
||||
>
|
||||
Grab <span className="font-bold text-[#F77034]">friends</span> (or make new friends!) and <span className='font-bold'>form a <span className='text-[#F77034]'>team of 2-3</span></span>
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
stepNumber={3}
|
||||
imageSrc="/compressed/ui/step-workshops.webp"
|
||||
imageAlt="Step 3"
|
||||
>
|
||||
Learn from <span className="text-[#F77034] font-bold">workshops</span>, enjoy <span className='font-bold'>free food and merch!</span>
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
stepNumber={4}
|
||||
imageSrc="/compressed/ui/step-build.webp"
|
||||
imageAlt="Step 4"
|
||||
isReversed={true}
|
||||
>
|
||||
<span className="text-[#F77034] font-bold">Build</span> your game & <span className="text-[#F77034] font-bold">publish</span> it on itch.io!
|
||||
</Step>
|
||||
|
||||
<div className="flex justify-end mt-12">
|
||||
<button
|
||||
className="bg-[#E77232] rounded-[20px] px-16 py-8 transform rotate-[2deg] hover:scale-105 transition-transform shadow-[0_8px_20px_rgba(0,0,0,0.25)] cursor-pointer active:scale-95 animate-float-up-down"
|
||||
type="button"
|
||||
onClick={() => window.open("https://docs.google.com/document/d/14sMLsvxpBFtdzNOvmMJyjIrggKdaXLJ2GMiOoBcE8-M/", "_blank")}
|
||||
>
|
||||
<p
|
||||
className="text-5xl md:text-6xl font-normal font-dream-planner"
|
||||
style={{ color: "rgba(255, 255, 255, 0.69)" }}
|
||||
>
|
||||
READ MORE
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="relative h-full md:h-[800px] pb-8 px-8 z-30 -mt-96">
|
||||
<div className="absolute top-0 left-0 w-full h-[900px] pointer-events-none">
|
||||
<img src="/decorative/clouds-3-symmetric.webp" alt="" className="w-full h-full select-none" />
|
||||
</div>
|
||||
|
||||
<div id="letter" className="relative w-full h-full z-50 translate-y-20 min-[1200px]:translate-y-64 flex justify-center">
|
||||
<img src='/compressed/backgrounds/world-map-left.webp' alt='' className='h-full hidden min-[1200px]:block' />
|
||||
<div className='flex items-center min-[1200px]:block min-[1200px]:relative'>
|
||||
<img src='/backgrounds/world-map-right.webp' alt='' className='h-full hidden min-[1200px]:block' />
|
||||
<div className='min-[1200px]:absolute min-[1200px]:top-0 min-[1200px]:left-0 py-12 min-[1200px]:py-16 min-[1200px]:pb-0 rounded-xl shadow-[0_8px_20px_rgba(0,0,0,0.25)] min-[1200px]:rounded-none min-[1200px]:shadow-none min-[1200px]:pt-30 pl-6 min-[1200px]:pl-12 pr-6 min-[1200px]:pr-64 text-xl bg-[#EAD6BE] border-[#DCA87E] border-4 min-[1200px]:border-0 min-[1200px]:bg-transparent flex flex-col gap-6 font-solway'>
|
||||
<h1>Dear hacker,</h1>
|
||||
<p>
|
||||
You can make a change: inspire someone to build a game for the first time, help someone
|
||||
fall in love with computers, run an <i>incredible</i> game jam that you can invite all your
|
||||
friends to.
|
||||
</p>
|
||||
|
||||
<p><b>This February, what if you organized a game jam in your city?</b></p>
|
||||
|
||||
<p>
|
||||
Hack Club will provide guides, funding, merch, and 1-on-1 mentorship. Our goal?
|
||||
Run 200 game jams in 200 cities worldwide. All on the same day. All run by high
|
||||
schoolers like us.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
To kick off 2026, we’re so excited to invite you to Campfire. In just a couple
|
||||
months, you will learn how to raise money for your event, buy food and drinks
|
||||
for your attendees, and make your own video games with your friends!
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Let’s go on an adventure together.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
With love, <br />
|
||||
The Campfire Team
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="relative py-24 bg-[#384fbc] bg-cover bg-center z-20 pt-90 md:pt-180 -mt-[25vw] pb-0 bg-[url(/backgrounds/blue-gradient.webp)]">
|
||||
<div className="absolute bottom-48 md:bottom-0 left-0 w-full pointer-events-none scale-250 md:scale-100">
|
||||
<img
|
||||
src="/decorative/clouds-1.webp"
|
||||
alt=""
|
||||
className="w-full h-full object-contain select-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-48 left-32 md:bottom-0 md:left-0 w-[80vw] pointer-events-none scale-250 md:scale-100">
|
||||
<img
|
||||
src="/decorative/moon-composite.webp"
|
||||
alt=""
|
||||
className="w-full h-full object-contain transform rotate-[346deg] select-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-[-160px] md:bottom-[64px] translate-y-full scale-300 md:scale-105 left-0 w-full pointer-events-none">
|
||||
<img
|
||||
src="/decorative/puzzle-composite.webp"
|
||||
alt=""
|
||||
className="w-full h-full object-cover select-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex flex-col gap-12 items-center md:px-24 w-full md:w-auto md:max-w-7xl mx-auto">
|
||||
<h2
|
||||
id="previous-events"
|
||||
className="text-[#d7cfeb] text-6xl font-bold text-center mb-8 font-ember-and-fire"
|
||||
style={{
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)"
|
||||
}}
|
||||
>
|
||||
Stories from past events
|
||||
</h2>
|
||||
|
||||
<div className="flex flex-col min-[1050px]:flex-row gap-12 justify-between w-full items-center">
|
||||
<StoryCard
|
||||
imageSrc="/stories/counterspell.webp"
|
||||
imageAlt=""
|
||||
titleImageSrc="/compressed/branding/logo-counterspell.webp"
|
||||
titleImageAlt="Counterspell logo"
|
||||
description="Our first worldwide game jam! In 50 cities including Toronto, Boston, & Singapore"
|
||||
videoButtonText='Watch the video'
|
||||
videoUrl="https://www.youtube.com/watch?v=H5RPsCMl3uM"
|
||||
videoButtonColor="#FF4186"
|
||||
eventUrl="https://counterspell.hackclub.com/"
|
||||
/>
|
||||
|
||||
<StoryCard
|
||||
imageSrc="/stories/scrapyard.webp"
|
||||
imageAlt=""
|
||||
titleImageSrc="/branding/logo-scrapyard.svg"
|
||||
titleImageAlt="Scrapyard logo"
|
||||
description="Build stupid stuff, get stupid prizes! In-person hackathon in 70+ cities."
|
||||
videoButtonText='Check out vid!'
|
||||
videoUrl="https://www.youtube.com/watch?v=8iM1W8kXrQA"
|
||||
videoButtonColor="#AF8D67"
|
||||
eventUrl="https://scrapyard.hackclub.com/"
|
||||
/>
|
||||
|
||||
<StoryCard
|
||||
imageSrc="/compressed/stories/daydream.webp"
|
||||
imageAlt=""
|
||||
titleImageSrc="/compressed/branding/logo-daydream.webp"
|
||||
titleImageAlt="Daydream logo"
|
||||
description="Game jam in 100 cities worldwide -- from London to NYC to Penang!"
|
||||
videoButtonText='Video here!'
|
||||
videoUrl="https://www.youtube.com/watch?v=vvdoW2gh9YU"
|
||||
videoButtonColor="#3F709A"
|
||||
eventUrl="https://daydream.hackclub.com/"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="bg-[#384fbc] pt-[40vw] md:pt-[25vw] z-10">
|
||||
<div className="flex flex-col gap-8 items-center px-8 max-w-7xl mx-auto pb-16">
|
||||
<h2
|
||||
id="games-made"
|
||||
className="text-[#f1ebff] text-6xl font-bold text-center mb-8 font-ember-and-fire"
|
||||
style={{
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)"
|
||||
}}
|
||||
>
|
||||
Favorite games from past events
|
||||
</h2>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-16 md:gap-5 w-full justify-between">
|
||||
<GameCard
|
||||
imageSrc="/games/office-click-clack.webp"
|
||||
imageAlt="Office Click Clack"
|
||||
title="Office Click Clack"
|
||||
author="bunnyguy - Daydream DFW"
|
||||
href="https://theavgeekbee.itch.io/office-click-clack"
|
||||
/>
|
||||
|
||||
<GameCard
|
||||
imageSrc="/games/zero-sum.webp"
|
||||
imageAlt="Zero Sum"
|
||||
title="Zero Sum"
|
||||
author="ARandomPsi - Daydream SV"
|
||||
href="https://arandompsi.itch.io/zero-sum"
|
||||
/>
|
||||
|
||||
<GameCard
|
||||
imageSrc="/games/return-to-sender.webp"
|
||||
imageAlt="Return to The Sender"
|
||||
title="Return to The Sender"
|
||||
author="i1rs7 - Daydream Global"
|
||||
href="https://i1rs7.itch.io/return-to-the-sender"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center mt-12">
|
||||
<button
|
||||
className="bg-[#45B4F5] rounded-[20px] px-12 py-6 transform rotate-[-2deg] hover:scale-105 transition-transform shadow-[0_8px_20px_rgba(0,0,0,0.25)] cursor-pointer active:scale-95"
|
||||
type="button"
|
||||
onClick={() => window.open("https://itch.io/jam/daydream-global", "_blank")}
|
||||
>
|
||||
<p className="text-white text-4xl md:text-5xl font-normal font-dream-planner">
|
||||
CHECK OUT TOP SUBMISSIONS!
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="scale-250 translate-y-64 pb-48 md:pb-0 md:translate-y-0 md:scale-105 left-0 w-full pointer-events-none">
|
||||
<img
|
||||
src="/decorative/puzzle-cloud-bottom.webp"
|
||||
alt=""
|
||||
className="w-full h-full object-cover select-none"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="relative pb-64 bg-[#384FBC] md:bg-[#081F8B]">
|
||||
<div className="absolute w-full h-full z-10 pointer-events-none invisible md:visible">
|
||||
<img
|
||||
src="/compressed/decorative/speech-bubble-bg.webp"
|
||||
alt=""
|
||||
className="w-full h-full select-none pointer-events-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 md:px-32">
|
||||
<h2
|
||||
id="faq"
|
||||
className="text-[#d7cfeb] text-[128px] font-bold text-center mb-16 font-ember-and-fire"
|
||||
style={{
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)"
|
||||
}}
|
||||
>
|
||||
FAQ
|
||||
</h2>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-32 md:gap-10 justify-center items-center md:items-start">
|
||||
<div className="relative w-full md:w-auto">
|
||||
<div className="absolute inset-0 md:w-[608px] pointer-events-none flex flex-col min-h-[105%]">
|
||||
<img src="/ui/woodboard-1-top.svg" alt="" className="w-full flex-shrink-0 select-none" />
|
||||
<div className="bg-[#AD684F] flex-1 w-full py-4"></div>
|
||||
<img src="/ui/woodboard-1-bottom.svg" alt="" className="w-full flex-shrink-0 select-none" />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex flex-col gap-10 items-center md:px-16 pt-8 w-[100%] md:w-[608px]">
|
||||
<p
|
||||
className="text-[#d7cfeb] text-6xl font-bold text-center mb-4 font-ember-and-fire"
|
||||
style={{
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)"
|
||||
}}
|
||||
>
|
||||
Participant
|
||||
</p>
|
||||
|
||||
<FaqQuestion question="What is a game jam?">
|
||||
A game jam is an event where you build a game from scratch in a short time period! It's all about creativity, teamwork, and having fun while learning new skills.
|
||||
</FaqQuestion>
|
||||
<FaqQuestion question="Am I eligible?">
|
||||
If you're a high schooler (or younger), you're eligible! No prior experience required - just bring your enthusiasm and willingness to learn.
|
||||
</FaqQuestion>
|
||||
<FaqQuestion question="But I've never hacked before!">
|
||||
Perfect! Game jams are designed for beginners. You'll have workshops, mentors, and teammates to help you every step of the way.
|
||||
</FaqQuestion>
|
||||
<FaqQuestion question="All this, for free?">
|
||||
Yes! Everything is completely free - venue, food, swag, workshops, and prizes. Hack Club covers all costs so you can focus on creating.
|
||||
</FaqQuestion>
|
||||
<FaqQuestion question="What do I need to bring?">
|
||||
Just bring yourself, a laptop, charger, and any personal items you need. We'll provide food, drinks, and everything else!
|
||||
</FaqQuestion>
|
||||
<FaqButton content="Check out the parent guide" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative w-full md:w-auto">
|
||||
<div className="absolute inset-0 md:w-[608px] pointer-events-none flex flex-col min-h-[105%]">
|
||||
<img src="/ui/woodboard-2-top.svg" alt="" className="w-full flex-shrink-0 select-none" />
|
||||
<div className="bg-[#AD684F] flex-1 w-full"></div>
|
||||
<img src="/ui/woodboard-2-bottom.svg" alt="" className="w-full flex-shrink-0 select-none" />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex flex-col gap-10 items-center px-4 md:px-16 pt-9 w-full md:w-[608px]">
|
||||
<p
|
||||
className="text-[#d7cfeb] text-6xl font-bold text-center mb-4 font-ember-and-fire"
|
||||
style={{
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)"
|
||||
}}
|
||||
>
|
||||
Organizer
|
||||
</p>
|
||||
|
||||
<FaqQuestion question="Can I organize a Campfire in my city?">
|
||||
Absolutely! We're always looking for passionate organizers. If you're ready to bring the magic of game development to your community, we'd love to help.
|
||||
</FaqQuestion>
|
||||
<FaqQuestion question="What are the steps to organizing?">
|
||||
First, apply through our organizer form. Then we'll guide you through venue booking, team building, workshop planning, and day-of coordination.
|
||||
</FaqQuestion>
|
||||
<FaqQuestion question="Do we get funding?">
|
||||
Yes! Hack Club provides funding for venue, food, swag, and other event costs. We want to remove financial barriers for amazing events.
|
||||
</FaqQuestion>
|
||||
<FaqQuestion question="Do we get volunteer hours?">
|
||||
Many schools accept organizing hours as community service. Check with your school's requirements - we can provide documentation.
|
||||
</FaqQuestion>
|
||||
<FaqQuestion question="Can I join an organizing team?">
|
||||
Of course! Many cities have organizing teams. Reach out to organizers in your area or apply to join an existing team.
|
||||
</FaqQuestion>
|
||||
<FaqButton href={FORM_URL_ORGANIZER_APPLICATION} content="Apply to be an organizer" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer className="relative flex flex-col z-0 -mt-80">
|
||||
<img
|
||||
src="/backgrounds/footer-background.webp"
|
||||
alt=""
|
||||
className="w-full h-[50vw] object-bottom object-cover select-none scale-105"
|
||||
/>
|
||||
|
||||
<div className="relative w-full px-4 pb-32 md:pb-16 md:px-0 bg-[#0f371d] -mt-3 flex flex-col items-center justify-center gap-16 md:gap-6 z-10">
|
||||
<p
|
||||
className="text-white text-3xl md:text-3xl text-center font-bold font-ember-and-fire"
|
||||
style={{
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)"
|
||||
}}
|
||||
>
|
||||
made with love by Hack Club & Open Sauce
|
||||
</p>
|
||||
|
||||
<div className="mt-8 flex flex-col md:flex-row gap-16 max-w-6xl mx-auto px-4">
|
||||
<div className="flex flex-col items-center md:items-end gap-4 text-white text-4xl md:text-3xl font-ember-and-fire font-bold z-20">
|
||||
<a href="https://hackclub.com/" target="_blank" className="hover:underline">Hack Club</a>
|
||||
<a href="https://hackclub.com/slack" target="_blank" className="hover:underline">Slack</a>
|
||||
<a href="https://hackclub.com/clubs" target="_blank" className="hover:underline">Clubs</a>
|
||||
<a href="https://hackclub.com/conduct/" target="_blank" className="hover:underline">Code of Conduct</a>
|
||||
|
||||
<p className="text-white text-sm md:text-md text-right max-w-96 font-ember-and-fire">
|
||||
© 2026 Hack Club. 501(c)(3) nonprofit (EIN: 81-2908499)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 text-left">
|
||||
<p className="text-white text-lg md:text-xl font-ember-and-fire leading-relaxed mb-4">
|
||||
Hack Club is a 501(c)(3) nonprofit and network of 60k+ technical high schoolers. We believe you learn best by building so we're creating community and providing grants so you can make awesome projects. In the past few years, we've partnered with GitHub to run <a href="https://summer.hackclub.com/" target="_blank" className="underline hover:text-gray-300">Summer of Making</a>, hosted the <a href="https://github.com/hackclub/the-hacker-zephyr" target="_blank" className="underline hover:text-gray-300">world's longest hackathon on land</a>, and ran <a href="https://www.youtube.com/watch?v=QvCoISXfcE8" target="_blank" className="underline hover:text-gray-300">Canada's largest high school hackathon</a>.
|
||||
</p>
|
||||
<p className="text-white text-lg md:text-xl font-ember-and-fire font-bold">
|
||||
At Hack Club, students aren't just learning, they're shipping.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{isMapOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/80 z-[9999] flex items-center justify-center p-4"
|
||||
onClick={() => setIsMapOpen(false)}
|
||||
>
|
||||
<div
|
||||
className="relative w-[90vw] h-[80vh]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
onClick={() => setIsMapOpen(false)}
|
||||
className="absolute -top-10 right-0 text-white text-3xl font-bold hover:opacity-70 cursor-pointer"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
<iframe
|
||||
src="/map"
|
||||
className="w-full h-full rounded-2xl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
@ -1,42 +1,67 @@
|
|||
import FaqQuestion from '../primitives/FaqQuestion.js';
|
||||
import FaqButton from '../primitives/FaqButton.js';
|
||||
import Step from '../primitives/Step.js';
|
||||
import GameCard from '../primitives/GameCard.js';
|
||||
import NavbarLink from '../primitives/NavbarLink.tsx';
|
||||
import MapEmbed from '../primitives/MapEmbed.tsx';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import type { SatelliteContent } from '../../lib/satellite.ts';
|
||||
import FaqQuestion from "../primitives/FaqQuestion";
|
||||
import FaqButton from "../primitives/FaqButton";
|
||||
import Step from "../primitives/Step";
|
||||
import NavbarLink from "../primitives/NavbarLink";
|
||||
import MapEmbed from "../primitives/MapEmbed";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import type { SatelliteContent } from "../../lib/satellite.ts";
|
||||
|
||||
function FormattedText({ text }: { text: string }) {
|
||||
const parts = text.split(/(\*\*[^*]+\*\*|__[^_]+__)/g);
|
||||
return (
|
||||
<>
|
||||
{parts.map((part, i) => {
|
||||
if (part.startsWith('**') && part.endsWith('**')) {
|
||||
return <span key={i} className="font-bold text-[#F77034]">{part.slice(2, -2)}</span>;
|
||||
if (part.startsWith("**") && part.endsWith("**")) {
|
||||
return (
|
||||
<span key={i} className="font-bold text-[#F77034]">
|
||||
{part.slice(2, -2)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (part.startsWith('__') && part.endsWith('__')) {
|
||||
return <span key={i} className="font-bold">{part.slice(2, -2)}</span>;
|
||||
if (part.startsWith("__") && part.endsWith("__")) {
|
||||
return (
|
||||
<span key={i} className="font-bold text-[#000]">
|
||||
{part.slice(2, -2)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return part;
|
||||
return (
|
||||
<span key={i} className="text-[#000]">
|
||||
{part}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const FORM_URL_ORGANIZER_APPLICATION = "https://forms.hackclub.com/t/8L51MzWyrHus";
|
||||
const FORM_URL_RSVP = "https://forms.hackclub.com/t/a3QSt8MuvHus";
|
||||
const FORM_URL_SIGN_UP = "https://forms.hackclub.com/campfire-signup";
|
||||
const FORM_URL_ORGANIZER_APPLICATION =
|
||||
"https://forms.hackclub.com/t/8L51MzWyrHus";
|
||||
const FORM_URL_SIGN_UP = "https://forms.hackclub.com/campfire-signup?ref=28";
|
||||
|
||||
function App({slug, content, record_id}: {slug: string | undefined, content: SatelliteContent, record_id: string | undefined}) {
|
||||
function App({
|
||||
content,
|
||||
record_id,
|
||||
}: {
|
||||
slug?: string;
|
||||
content: SatelliteContent;
|
||||
record_id?: string;
|
||||
}) {
|
||||
const [email, setEmail] = useState("");
|
||||
const [scrollY, setScrollY] = useState(document.body.scrollTop);
|
||||
const [isLargeScreen, setIsLargeScreen] = useState(window.innerWidth >= 1280);
|
||||
const [scrollY, setScrollY] = useState(0);
|
||||
const [isLargeScreen, setIsLargeScreen] = useState(
|
||||
typeof window !== "undefined" ? window.innerWidth >= 1280 : true,
|
||||
);
|
||||
const [isMapOpen, setIsMapOpen] = useState(false);
|
||||
|
||||
const emailRef = useRef<HTMLInputElement>(null);
|
||||
const [windowHeight, setWindowHeight] = useState(window.innerHeight);
|
||||
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
||||
const [windowHeight, setWindowHeight] = useState(
|
||||
typeof window !== "undefined" ? window.innerHeight : 800,
|
||||
);
|
||||
const [windowWidth, setWindowWidth] = useState(
|
||||
typeof window !== "undefined" ? window.innerWidth : 1200,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("scroll", () => {
|
||||
|
|
@ -54,17 +79,19 @@ function App({slug, content, record_id}: {slug: string | undefined, content: Sat
|
|||
const element = document.getElementById(sectionId);
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function openWithEmail(url: string) {
|
||||
if (!emailRef?.current?.reportValidity() || !email)
|
||||
return;
|
||||
if (!emailRef?.current?.reportValidity() || !email) return;
|
||||
|
||||
window.open(`${url}?email=${encodeURIComponent(email)}&event=${encodeURIComponent(record_id || "")}`, "_blank");
|
||||
window.open(
|
||||
`${url}&email=${encodeURIComponent(email)}&event=${encodeURIComponent(record_id || "")}`,
|
||||
"_blank",
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -86,36 +113,54 @@ function App({slug, content, record_id}: {slug: string | undefined, content: Sat
|
|||
{/* <NavbarLink onClick={() => scrollToSection('schedule')}>{content.nav.schedule}</NavbarLink> */}
|
||||
{/* <NavbarLink onClick={() => scrollToSection('games-made')}>{content.nav.gamesMade}</NavbarLink> */}
|
||||
{/* <NavbarLink onClick={() => scrollToSection('faq')}>{content.nav.faq}</NavbarLink> */}
|
||||
{
|
||||
Object.keys(content.localization.nav).map(key => (
|
||||
<NavbarLink key={key} onClick={() => scrollToSection(key)}>{content.localization.nav[key]}</NavbarLink>
|
||||
))
|
||||
}
|
||||
{Object.keys(content.localization.nav).map((key) => (
|
||||
<NavbarLink key={key} onClick={() => scrollToSection(key)}>
|
||||
{content.localization.nav[key]}
|
||||
</NavbarLink>
|
||||
))}
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<section className={clsx(
|
||||
<section
|
||||
className={clsx(
|
||||
"relative min-h-[750px] px-6 md:px-16 md:px-24 2xl:px-32 bg-[url(/backgrounds/blue-sky.webp)] bg-center bg-cover",
|
||||
windowHeight > windowWidth && windowWidth < 860 ? "flex items-stretch pb-0" : "h-full flex items-end pb-32 2xl:pb-48"
|
||||
)}>
|
||||
windowHeight > windowWidth && windowWidth < 860
|
||||
? "flex items-stretch pb-0"
|
||||
: "h-full flex items-end pb-32 2xl:pb-48",
|
||||
)}
|
||||
>
|
||||
<div className="absolute top-0 left-0 w-full h-full pointer-events-none">
|
||||
<img src="/backgrounds/sky-shine.webp" alt="" className="w-full h-full object-cover select-none" />
|
||||
<img
|
||||
src="/backgrounds/sky-shine.webp"
|
||||
alt=""
|
||||
className="w-full h-full object-cover select-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="absolute bottom-[32px] right-[40px] md:right-[140px] w-full h-full pointer-events-none"
|
||||
style={{
|
||||
transform: isLargeScreen ? `translateY(${-scrollY / 12}px)` : undefined
|
||||
transform: isLargeScreen
|
||||
? `translateY(${-scrollY / 12}px)`
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
{/* fishy on the right. His name is frederick. */}
|
||||
<div className="absolute top-[128px] md:top-[96px] right-[-50px] md:right-[100px] w-1/3 md:w-[200px]">
|
||||
<img src="/compressed/characters/fish-2.webp" alt="Fish named Frederick" className="w-full h-full object-cover select-none" />
|
||||
<img
|
||||
src="/compressed/characters/fish-2.webp"
|
||||
alt="Fish named Frederick"
|
||||
className="w-full h-full object-cover select-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* fishy on the left. His name is gubson */}
|
||||
<div className="absolute top-[96px] md:top-[60px] right-[-10px] md:right-[300px] w-1/3 md:w-[200px]">
|
||||
<img src="/compressed/characters/fish-1.webp" alt="Fish named Gubson" className="w-full h-full object-cover select-none" />
|
||||
<img
|
||||
src="/compressed/characters/fish-1.webp"
|
||||
alt="Fish named Gubson"
|
||||
className="w-full h-full object-cover select-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -125,7 +170,9 @@ function App({slug, content, record_id}: {slug: string | undefined, content: Sat
|
|||
alt=""
|
||||
className="w-full h-full object-cover object-top select-none"
|
||||
style={{
|
||||
transform: isLargeScreen ? `translateY(${-scrollY / 5}px)` : undefined
|
||||
transform: isLargeScreen
|
||||
? `translateY(${-scrollY / 5}px)`
|
||||
: undefined,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -134,24 +181,30 @@ function App({slug, content, record_id}: {slug: string | undefined, content: Sat
|
|||
<div className="flex flex-col gap-4 w-full md:w-[648px]">
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-3 mb-4 relative z-30">
|
||||
<a href='https://hackclub.com' className='transition-transform hover:scale-105 active:scale-95'>
|
||||
<a
|
||||
href="https://hackclub.com"
|
||||
className="transition-transform hover:scale-105 active:scale-95"
|
||||
>
|
||||
<img
|
||||
src="/decorative/flag-standalone-wtransparent.png"
|
||||
alt="Hack Club"
|
||||
className="w-[151px] h-[53px] object-cover transform rotate-[-4.8deg] select-none"
|
||||
style={{
|
||||
filter: "drop-shadow(3px 3px 0px rgba(0,0,0,0.25))"
|
||||
filter: "drop-shadow(3px 3px 0px rgba(0,0,0,0.25))",
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
<div className="w-[2px] h-8 bg-white opacity-60"></div>
|
||||
<a href='https://opensauce.com' className='transition-transform scale-125 hover:scale-130 active:scale-130'>
|
||||
<a
|
||||
href="https://opensauce.com"
|
||||
className="transition-transform scale-125 hover:scale-130 active:scale-130"
|
||||
>
|
||||
<img
|
||||
src="/branding/logo-opensauce.webp"
|
||||
alt="Open Sauce"
|
||||
className="h-[70px] object-contain select-none pl-4"
|
||||
style={{
|
||||
filter: "drop-shadow(3px 3px 0px rgba(0,0,0,0.25))"
|
||||
filter: "drop-shadow(3px 3px 0px rgba(0,0,0,0.25))",
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
|
|
@ -161,7 +214,7 @@ function App({slug, content, record_id}: {slug: string | undefined, content: Sat
|
|||
<h1
|
||||
className="text-[#fcf5ed] text-[80px] md:text-[100px] xl:text-[150px] font-normal leading-none -mb-4 font-dream-planner"
|
||||
style={{
|
||||
textShadow: "5px 8px 0px rgba(0,0,0,0.25)"
|
||||
textShadow: "5px 8px 0px rgba(0,0,0,0.25)",
|
||||
}}
|
||||
>
|
||||
{content.localization.hero.campfire}
|
||||
|
|
@ -169,7 +222,7 @@ function App({slug, content, record_id}: {slug: string | undefined, content: Sat
|
|||
<h3
|
||||
className="text-[#fcf5ed] text-[40px] md:text-[50px] xl:text-[60px] font-normal leading-none mb-4 font-dream-planner text-right"
|
||||
style={{
|
||||
textShadow: "5px 8px 0px rgba(0,0,0,0.25)"
|
||||
textShadow: "5px 8px 0px rgba(0,0,0,0.25)",
|
||||
}}
|
||||
>
|
||||
{content.event.city.toUpperCase()}
|
||||
|
|
@ -180,7 +233,7 @@ function App({slug, content, record_id}: {slug: string | undefined, content: Sat
|
|||
<p
|
||||
className="text-white text-4xl md:text-3xl xl:text-4xl font-bold mb-2 font-ember-and-fire"
|
||||
style={{
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)"
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)",
|
||||
}}
|
||||
>
|
||||
{content.localization.hero.subtitle}
|
||||
|
|
@ -188,18 +241,23 @@ function App({slug, content, record_id}: {slug: string | undefined, content: Sat
|
|||
<p
|
||||
className="text-white text-4xl md:text-3xl xl:text-4xl font-bold mb-2 font-ember-and-fire"
|
||||
style={{
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)"
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)",
|
||||
}}
|
||||
>
|
||||
{content.localization.hero.hostedAt}
|
||||
<a href={content.event.venue.link} target="_blank" rel="noopener noreferrer" className="underline">
|
||||
<a
|
||||
href={content.event.venue.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
{content.event.venue.name}
|
||||
</a>
|
||||
</p>
|
||||
<p
|
||||
className="text-white text-4xl md:text-3xl xl:text-4xl font-bold font-ember-and-fire"
|
||||
style={{
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)"
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)",
|
||||
}}
|
||||
>
|
||||
{content.event.date}
|
||||
|
|
@ -207,53 +265,64 @@ function App({slug, content, record_id}: {slug: string | undefined, content: Sat
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col md:flex-row items-center gap-2">
|
||||
<div
|
||||
className={clsx(
|
||||
"bg-[#f9e2ca] border-4 border-[#d5a16c] rounded-[20px] px-4 md:px-8 py-4 flex items-center gap-3 md:gap-6 w-full transform md:rotate-[-1.2deg] shadow-[0_8px_20px_rgba(0,0,0,0.25)]",
|
||||
"transition-transform hover:scale-105"
|
||||
"transition-transform hover:scale-105",
|
||||
)}
|
||||
>
|
||||
<img src="/icons/email.svg" alt="" className="w-6 h-5 flex-shrink-0 select-none" />
|
||||
<img
|
||||
src="/icons/email.svg"
|
||||
alt=""
|
||||
className="w-6 h-5 flex-shrink-0 select-none"
|
||||
/>
|
||||
<input
|
||||
required
|
||||
ref={emailRef}
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
type="email"
|
||||
className="text-[#854d16] text-2xl md:text-4xl font-bold truncate bg-transparent border-none outline-none flex-1 cursor-text font-ember-and-fire"
|
||||
placeholder={content.localization.hero.emailPlaceholder}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
<button
|
||||
className="bg-[#fca147] border-[5px] border-[rgba(0,0,0,0.2)] rounded-[20px] px-8 md:px-14 py-4 hover:scale-105 transition-transform w-full md:w-auto transform md:rotate-[1.5deg] shadow-[0_8px_20px_rgba(0,0,0,0.25)] cursor-pointer active:scale-95"
|
||||
type="button"
|
||||
onClick={() => openWithEmail(FORM_URL_SIGN_UP)}
|
||||
>
|
||||
<p
|
||||
className="text-[#8d3f34] text-3xl md:text-5xl font-normal font-dream-planner whitespace-nowrap"
|
||||
>
|
||||
<p className="text-[#8d3f34] text-3xl md:text-5xl font-normal font-dream-planner whitespace-nowrap">
|
||||
{content.localization.hero.ctaPrimary}
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='font-ember-and-fire text-white text-4xl pl-2 -translate-y-1 md:rotate-[-1.2deg]' style={{
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)"
|
||||
}}>
|
||||
{content.localization.hero.ctaSecondaryPrefix}<a
|
||||
className='underline inline-block cursor-pointer transition-transform hover:scale-105 active:scale-95'
|
||||
href='https://flagship.hackclub.com?utm_source=campfire-website'
|
||||
<div
|
||||
className="font-ember-and-fire text-white text-4xl pl-2 -translate-y-1 md:rotate-[-1.2deg]"
|
||||
style={{
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)",
|
||||
}}
|
||||
>
|
||||
{content.localization.hero.ctaSecondaryPrefix}
|
||||
<a
|
||||
className="underline inline-block cursor-pointer transition-transform hover:scale-105 active:scale-95"
|
||||
href="https://flagship.hackclub.com?utm_source=campfire-website"
|
||||
>
|
||||
{content.localization.hero.ctaSecondary}
|
||||
</a>{content.localization.hero.ctaSecondarySuffix}
|
||||
</a>
|
||||
{content.localization.hero.ctaSecondarySuffix}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MapEmbed className="hidden xl:block self-end mb-8" onOpenMap={() => setIsMapOpen(true)} label={content.localization.hero.mapLabel}/>
|
||||
<MapEmbed
|
||||
className="hidden xl:block self-end mb-8"
|
||||
onOpenMap={() => setIsMapOpen(true)}
|
||||
label={content.localization.hero.mapLabel}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
@ -263,7 +332,9 @@ function App({slug, content, record_id}: {slug: string | undefined, content: Sat
|
|||
alt=""
|
||||
className="w-full h-full object-cover select-none"
|
||||
style={{
|
||||
transform: isLargeScreen ? `translateY(${scrollY / 10}px)` : undefined
|
||||
transform: isLargeScreen
|
||||
? `translateY(${scrollY / 10}px)`
|
||||
: undefined,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -271,20 +342,36 @@ function App({slug, content, record_id}: {slug: string | undefined, content: Sat
|
|||
|
||||
<section className="relative pb-96 bg-[url(/backgrounds/underwater-gradient.webp)] bg-cover bg-top">
|
||||
<div className="xl:hidden pt-16 pb-8 relative z-50">
|
||||
<MapEmbed className="px-6 relative z-50 max-w-sm mx-auto" onOpenMap={() => setIsMapOpen(true)} label={content.localization.hero.mapLabel}/>
|
||||
<MapEmbed
|
||||
className="px-6 relative z-50 max-w-sm mx-auto"
|
||||
onOpenMap={() => setIsMapOpen(true)}
|
||||
label={content.localization.hero.mapLabel}
|
||||
/>
|
||||
</div>
|
||||
<div className="pt-[8vw] xl:pt-[13vw]"></div>
|
||||
<div className="absolute top-0 left-0 w-screen h-[200px] bg-gradient-to-b from-[#004b2a] to-transparent pointer-events-none"></div>
|
||||
<div className="absolute top-0 xl:top-[30px] left-0 w-full scale-125 pointer-events-none z-50">
|
||||
<img src="/decorative/vines.webp" alt="" className="w-full h-full object-cover select-none" />
|
||||
<img
|
||||
src="/decorative/vines.webp"
|
||||
alt=""
|
||||
className="w-full h-full object-cover select-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-[30px] left-0 w-full h-[800px] z-0 pointer-events-none">
|
||||
<img src="/decorative/clouds-3.webp" alt="" className="w-full h-full object-cover select-none object-top" />
|
||||
<img
|
||||
src="/decorative/clouds-3.webp"
|
||||
alt=""
|
||||
className="w-full h-full object-cover select-none object-top"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-0 left-0 w-full h-[613px] z-0 pointer-events-none">
|
||||
<img src="/backgrounds/seafloor.webp" alt="" className="w-full h-full object-cover select-none" />
|
||||
<img
|
||||
src="/backgrounds/seafloor.webp"
|
||||
alt=""
|
||||
className="w-full h-full object-cover select-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-[200px] left-32 w-[569px] h-[676px] pointer-events-none">
|
||||
|
|
@ -297,24 +384,40 @@ function App({slug, content, record_id}: {slug: string | undefined, content: Sat
|
|||
|
||||
<div className="absolute top-[0px] z-45 right-0 w-[150px] md:w-[352px] pointer-events-none">
|
||||
<img
|
||||
src="/compressed/decorative/cogs-top-right.webp" alt=""
|
||||
src="/compressed/decorative/cogs-top-right.webp"
|
||||
alt=""
|
||||
className="w-full h-full object-contain select-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="absolute invisible md:visible top-[55%] z-45 left-0 w-[260px] pointer-events-none">
|
||||
<img src="/compressed/decorative/cogs-mid-left.webp" alt="" className="w-full h-full object-contain select-none" />
|
||||
<img
|
||||
src="/compressed/decorative/cogs-mid-left.webp"
|
||||
alt=""
|
||||
className="w-full h-full object-contain select-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="absolute invisible md:visible top-[75%] z-45 right-0 translate-x-20 w-[280px] pointer-events-none">
|
||||
<img src="/compressed/decorative/single-cog-1.webp" alt="" className="w-full h-full object-contain select-none" />
|
||||
<img
|
||||
src="/compressed/decorative/single-cog-1.webp"
|
||||
alt=""
|
||||
className="w-full h-full object-contain select-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="absolute invisible md:visible top-[82%] z-45 left-0 translate-x-10 w-[280px] pointer-events-none">
|
||||
<img src="/compressed/decorative/single-cog-2.webp" alt="" className="w-full h-full object-contain select-none" />
|
||||
<img
|
||||
src="/compressed/decorative/single-cog-2.webp"
|
||||
alt=""
|
||||
className="w-full h-full object-contain select-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="steps" className="relative z-40 flex flex-col gap-24 items-center px-12 max-w-7xl mx-auto pt-12 md:pt-0">
|
||||
<div
|
||||
id="steps"
|
||||
className="relative z-40 flex flex-col gap-24 items-center px-12 max-w-7xl mx-auto pt-12 md:pt-0"
|
||||
>
|
||||
<Step
|
||||
stepNumber={1}
|
||||
imageSrc="/compressed/ui/step-signup.jpeg"
|
||||
|
|
@ -353,18 +456,37 @@ function App({slug, content, record_id}: {slug: string | undefined, content: Sat
|
|||
|
||||
<section className="relative h-full md:h-[800px] pb-8 px-8 z-30 -mt-96">
|
||||
<div className="absolute top-0 left-0 w-full h-[900px] pointer-events-none">
|
||||
<img src="/decorative/clouds-3-symmetric.webp" alt="" className="w-full h-full select-none" />
|
||||
<img
|
||||
src="/decorative/clouds-3-symmetric.webp"
|
||||
alt=""
|
||||
className="w-full h-full select-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="letter" className="relative w-full h-full z-50 translate-y-8 min-[1200px]:translate-y-40 flex justify-center">
|
||||
<img src='/compressed/backgrounds/world-map-left.webp' alt='' className='h-full hidden min-[1200px]:block' />
|
||||
<div className='flex items-center min-[1200px]:block min-[1200px]:relative'>
|
||||
<img src='/backgrounds/world-map-right.webp' alt='' className='h-full hidden min-[1200px]:block' />
|
||||
<div className='min-[1200px]:absolute min-[1200px]:top-0 min-[1200px]:left-0 py-12 min-[1200px]:py-16 min-[1200px]:pb-0 rounded-xl shadow-[0_8px_20px_rgba(0,0,0,0.25)] min-[1200px]:rounded-none min-[1200px]:shadow-none min-[1200px]:pt-30 pl-6 min-[1200px]:pl-12 pr-6 min-[1200px]:pr-64 text-xl bg-[#EAD6BE] border-[#DCA87E] border-4 min-[1200px]:border-0 min-[1200px]:bg-transparent flex flex-col gap-6 font-solway'>
|
||||
<h1 className="text-2xl md:text-3xl font-bold">{content.localization.letter.greeting}</h1>
|
||||
<div
|
||||
id="letter"
|
||||
className="relative w-full h-full z-50 translate-y-8 min-[1200px]:translate-y-40 flex justify-center"
|
||||
>
|
||||
<img
|
||||
src="/compressed/backgrounds/world-map-left.webp"
|
||||
alt=""
|
||||
className="h-full hidden min-[1200px]:block"
|
||||
/>
|
||||
<div className="flex items-center min-[1200px]:block min-[1200px]:relative">
|
||||
<img
|
||||
src="/backgrounds/world-map-right.webp"
|
||||
alt=""
|
||||
className="h-full hidden min-[1200px]:block"
|
||||
/>
|
||||
<div className="min-[1200px]:absolute min-[1200px]:top-0 min-[1200px]:left-0 py-12 min-[1200px]:py-16 min-[1200px]:pb-0 rounded-xl shadow-[0_8px_20px_rgba(0,0,0,0.25)] min-[1200px]:rounded-none min-[1200px]:shadow-none min-[1200px]:pt-30 pl-6 min-[1200px]:pl-12 pr-6 min-[1200px]:pr-64 text-xl bg-[#EAD6BE] border-[#DCA87E] border-4 min-[1200px]:border-0 min-[1200px]:bg-transparent flex flex-col gap-6 font-solway">
|
||||
<h1 className="text-2xl md:text-3xl font-bold">
|
||||
{content.localization.letter.greeting}
|
||||
</h1>
|
||||
<p>{content.localization.letter.paragraph1}</p>
|
||||
|
||||
<p className="text-xl"><b>{content.localization.letter.paragraph2}</b></p>
|
||||
<p className="text-xl">
|
||||
<b>{content.localization.letter.paragraph2}</b>
|
||||
</p>
|
||||
|
||||
<p>{content.localization.letter.paragraph3}</p>
|
||||
|
||||
|
|
@ -409,9 +531,7 @@ function App({slug, content, record_id}: {slug: string | undefined, content: Sat
|
|||
id="schedule"
|
||||
className="relative w-full bg-[#3154B9] rounded-xl border-4 border-[#7B9FF5] p-8 md:p-16 min-h-[500px] shadow-[0_8px_20px_rgba(0,0,0,0.25)]"
|
||||
>
|
||||
<h2
|
||||
className="text-[#FFD999] text-6xl md:text-7xl font-bold text-center mb-4 font-ember-and-fire relative z-10"
|
||||
>
|
||||
<h2 className="text-[#FFD999] text-6xl md:text-7xl font-bold text-center mb-4 font-ember-and-fire relative z-10">
|
||||
{content.localization.schedule.title}
|
||||
</h2>
|
||||
|
||||
|
|
@ -453,53 +573,57 @@ function App({slug, content, record_id}: {slug: string | undefined, content: Sat
|
|||
id="games-made"
|
||||
className="text-[#f1ebff] text-6xl font-bold text-center mb-8 font-ember-and-fire"
|
||||
style={{
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)"
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)",
|
||||
}}
|
||||
>
|
||||
{content.localization.sponsors.title}
|
||||
</h2>
|
||||
|
||||
<div className="flex flex-wrap gap-8 justify-center">
|
||||
{
|
||||
content.event.sponsors.cards.map((sponsor, index) => (
|
||||
<>
|
||||
<a key={index} href={sponsor.link} target="_blank" className='flex flex-col justify-center items-center bg-white/10 rounded-xl p-4 hover:bg-white/15 transition-all'>
|
||||
<img
|
||||
src={sponsor.logo}
|
||||
alt={sponsor.sponsor}
|
||||
className="w-40 m-4 object-cover select-none"
|
||||
/>
|
||||
<p className="text-white text-lg md:text-xl font-solway">{sponsor.sponsor}</p>
|
||||
</a>
|
||||
</>
|
||||
))
|
||||
}
|
||||
{content.event.sponsors.cards.map((sponsor, index) => (
|
||||
<>
|
||||
<a
|
||||
key={index}
|
||||
href={sponsor.link}
|
||||
target="_blank"
|
||||
className="flex flex-col justify-center items-center bg-white/10 rounded-xl p-4 hover:bg-white/15 transition-all"
|
||||
>
|
||||
<img
|
||||
src={sponsor.logo}
|
||||
alt={sponsor.sponsor}
|
||||
className="w-40 m-4 object-cover select-none"
|
||||
/>
|
||||
<p className="text-white text-lg md:text-xl font-solway">
|
||||
{sponsor.sponsor}
|
||||
</p>
|
||||
</a>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{
|
||||
content.event.signatures ?
|
||||
(
|
||||
<div>
|
||||
<h2
|
||||
id="games-made"
|
||||
className="text-[#f1ebff] text-6xl font-bold text-center mt-16 mb-8 font-ember-and-fire"
|
||||
style={{
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)"
|
||||
}}
|
||||
>
|
||||
{content.localization.signatures.title}
|
||||
</h2>
|
||||
{content.event.signatures ? (
|
||||
<div>
|
||||
<h2
|
||||
id="games-made"
|
||||
className="text-[#f1ebff] text-6xl font-bold text-center mt-16 mb-8 font-ember-and-fire"
|
||||
style={{
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)",
|
||||
}}
|
||||
>
|
||||
{content.localization.signatures.title}
|
||||
</h2>
|
||||
|
||||
<img
|
||||
src={content.event.signatures.img}
|
||||
alt={content.localization.signatures.title}
|
||||
className="w-[60%] object-cover select-none"
|
||||
/>
|
||||
</div>)
|
||||
: (<></>)
|
||||
}
|
||||
<img
|
||||
src={content.event.signatures.img}
|
||||
alt={content.localization.signatures.title}
|
||||
className="w-[60%] object-cover select-none"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<div className="scale-250 translate-y-80 pb-48 md:pb-0 md:translate-y-0 md:scale-105 left-0 w-full pointer-events-none">
|
||||
<img
|
||||
src="/decorative/puzzle-cloud-bottom.webp"
|
||||
|
|
@ -523,7 +647,7 @@ function App({slug, content, record_id}: {slug: string | undefined, content: Sat
|
|||
id="faq"
|
||||
className="text-[#d7cfeb] text-[128px] font-bold text-center mb-16 font-ember-and-fire"
|
||||
style={{
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)"
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)",
|
||||
}}
|
||||
>
|
||||
{content.event.faq.title}
|
||||
|
|
@ -532,16 +656,24 @@ function App({slug, content, record_id}: {slug: string | undefined, content: Sat
|
|||
<div className="flex flex-col md:flex-row gap-32 md:gap-10 justify-center items-center md:items-start">
|
||||
<div className="relative w-full md:w-auto">
|
||||
<div className="absolute inset-0 md:w-[608px] pointer-events-none flex flex-col min-h-[105%]">
|
||||
<img src="/ui/woodboard-1-top.svg" alt="" className="w-full flex-shrink-0 select-none" />
|
||||
<img
|
||||
src="/ui/woodboard-1-top.svg"
|
||||
alt=""
|
||||
className="w-full flex-shrink-0 select-none"
|
||||
/>
|
||||
<div className="bg-[#AD684F] flex-1 w-full py-4"></div>
|
||||
<img src="/ui/woodboard-1-bottom.svg" alt="" className="w-full flex-shrink-0 select-none" />
|
||||
<img
|
||||
src="/ui/woodboard-1-bottom.svg"
|
||||
alt=""
|
||||
className="w-full flex-shrink-0 select-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex flex-col gap-10 items-center md:px-16 pt-8 w-[100%] md:w-[608px]">
|
||||
<p
|
||||
className="text-[#d7cfeb] text-6xl font-bold text-center mb-4 font-ember-and-fire"
|
||||
style={{
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)"
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)",
|
||||
}}
|
||||
>
|
||||
{content.event.faq.participant.title}
|
||||
|
|
@ -559,16 +691,24 @@ function App({slug, content, record_id}: {slug: string | undefined, content: Sat
|
|||
{content.event.faq.organizer && (
|
||||
<div className="relative w-full md:w-auto">
|
||||
<div className="absolute inset-0 md:w-[608px] pointer-events-none flex flex-col min-h-[105%]">
|
||||
<img src="/ui/woodboard-2-top.svg" alt="" className="w-full flex-shrink-0 select-none" />
|
||||
<img
|
||||
src="/ui/woodboard-2-top.svg"
|
||||
alt=""
|
||||
className="w-full flex-shrink-0 select-none"
|
||||
/>
|
||||
<div className="bg-[#AD684F] flex-1 w-full"></div>
|
||||
<img src="/ui/woodboard-2-bottom.svg" alt="" className="w-full flex-shrink-0 select-none" />
|
||||
<img
|
||||
src="/ui/woodboard-2-bottom.svg"
|
||||
alt=""
|
||||
className="w-full flex-shrink-0 select-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex flex-col gap-10 items-center px-4 md:px-16 pt-9 w-full md:w-[608px]">
|
||||
<p
|
||||
className="text-[#d7cfeb] text-6xl font-bold text-center mb-4 font-ember-and-fire"
|
||||
style={{
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)"
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)",
|
||||
}}
|
||||
>
|
||||
{content.event.faq.organizer.title}
|
||||
|
|
@ -579,7 +719,10 @@ function App({slug, content, record_id}: {slug: string | undefined, content: Sat
|
|||
{q.answer}
|
||||
</FaqQuestion>
|
||||
))}
|
||||
<FaqButton href={FORM_URL_ORGANIZER_APPLICATION} content={content.event.faq.organizer.buttonText} />
|
||||
<FaqButton
|
||||
href={FORM_URL_ORGANIZER_APPLICATION}
|
||||
content={content.event.faq.organizer.buttonText}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -598,7 +741,7 @@ function App({slug, content, record_id}: {slug: string | undefined, content: Sat
|
|||
<p
|
||||
className="text-white text-3xl md:text-3xl text-center font-bold font-ember-and-fire"
|
||||
style={{
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)"
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)",
|
||||
}}
|
||||
>
|
||||
{content.localization.footer.tagline}
|
||||
|
|
@ -607,7 +750,15 @@ function App({slug, content, record_id}: {slug: string | undefined, content: Sat
|
|||
<div className="mt-8 flex flex-col md:flex-row gap-16 max-w-6xl mx-auto px-4">
|
||||
<div className="flex flex-col items-center md:items-end gap-4 text-white text-4xl md:text-3xl font-ember-and-fire font-bold z-20">
|
||||
{content.localization.footer.links.map((link, index) => (
|
||||
<a key={index} href={link.href} target="_blank" className="hover:underline">{link.text}</a>
|
||||
<a
|
||||
key={index}
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:underline"
|
||||
>
|
||||
{link.text}
|
||||
</a>
|
||||
))}
|
||||
|
||||
<p className="text-white text-sm md:text-md text-right max-w-96 font-ember-and-fire">
|
||||
|
|
@ -617,7 +768,34 @@ function App({slug, content, record_id}: {slug: string | undefined, content: Sat
|
|||
|
||||
<div className="flex-1 text-left">
|
||||
<p className="text-white text-lg md:text-xl font-ember-and-fire leading-relaxed mb-4">
|
||||
{content.localization.footer.description}<a href="https://summer.hackclub.com/" target="_blank" className="underline hover:text-gray-300">Summer of Making</a>, hosted the <a href="https://github.com/hackclub/the-hacker-zephyr" target="_blank" className="underline hover:text-gray-300">world's longest hackathon on land</a>, and ran <a href="https://www.youtube.com/watch?v=QvCoISXfcE8" target="_blank" className="underline hover:text-gray-300">Canada's largest high school hackathon</a>.
|
||||
{content.localization.footer.description}
|
||||
<a
|
||||
href="https://summer.hackclub.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-gray-300"
|
||||
>
|
||||
Summer of Making
|
||||
</a>
|
||||
, hosted the{" "}
|
||||
<a
|
||||
href="https://github.com/hackclub/the-hacker-zephyr"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-gray-300"
|
||||
>
|
||||
world's longest hackathon on land
|
||||
</a>
|
||||
, and ran{" "}
|
||||
<a
|
||||
href="https://www.youtube.com/watch?v=QvCoISXfcE8"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-gray-300"
|
||||
>
|
||||
Canada's largest high school hackathon
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<p className="text-white text-lg md:text-xl font-ember-and-fire font-bold">
|
||||
{content.localization.footer.closing}
|
||||
|
|
@ -628,11 +806,11 @@ function App({slug, content, record_id}: {slug: string | undefined, content: Sat
|
|||
</footer>
|
||||
|
||||
{isMapOpen && (
|
||||
<div
|
||||
<div
|
||||
className="fixed inset-0 bg-black/80 z-[9999] flex items-center justify-center p-4"
|
||||
onClick={() => setIsMapOpen(false)}
|
||||
>
|
||||
<div
|
||||
<div
|
||||
className="relative w-[90vw] h-[80vh]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
|
|
@ -642,8 +820,8 @@ function App({slug, content, record_id}: {slug: string | undefined, content: Sat
|
|||
>
|
||||
✕
|
||||
</button>
|
||||
<iframe
|
||||
src="/map"
|
||||
<iframe
|
||||
src="https://campfire.hackclub.com/map"
|
||||
className="w-full h-full rounded-2xl"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,163 +0,0 @@
|
|||
import '../../styles/global.css';
|
||||
import { useState, useRef } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
const FORM_URL_SIGN_UP = "https://forms.hackclub.com/campfire-signup";
|
||||
|
||||
interface UnderConstructionProps {
|
||||
event_name: string;
|
||||
record_id?: string;
|
||||
error?: string;
|
||||
is404?: boolean;
|
||||
}
|
||||
|
||||
function UnderConstruction({ event_name, record_id, error = "", is404 = false }: UnderConstructionProps) {
|
||||
const [email, setEmail] = useState("");
|
||||
const emailRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
function openWithEmail(url: string) {
|
||||
if (!emailRef?.current?.reportValidity() || !email)
|
||||
return;
|
||||
|
||||
window.open(`${url}?email=${encodeURIComponent(email)}&event=${encodeURIComponent(record_id || "")}`, "_blank");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full min-h-screen flex flex-col overflow-hidden relative bg-[#fca84a]">
|
||||
{/* Background layers */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<img
|
||||
src="/backgrounds/blue-sky.webp"
|
||||
alt=""
|
||||
className="absolute inset-0 w-full h-full object-cover select-none"
|
||||
/>
|
||||
<img
|
||||
src="/backgrounds/sky-shine.webp"
|
||||
alt=""
|
||||
className="absolute inset-0 w-full h-full object-cover select-none"
|
||||
/>
|
||||
<img
|
||||
src="/backgrounds/landing-grass.png"
|
||||
alt=""
|
||||
className="absolute bottom-0 left-0 w-full h-auto object-cover select-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative z-10 flex flex-col items-center justify-center flex-1 px-6 py-16">
|
||||
<div className="flex flex-col gap-8 items-center max-w-[648px] w-full">
|
||||
{/* Logo section */}
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<div className='w-fit'>
|
||||
<div className="flex items-center gap-3 mb-4 relative z-30">
|
||||
<a href='https://hackclub.com' className='transition-transform hover:scale-105 active:scale-95'>
|
||||
<img
|
||||
src="/decorative/flag-standalone-wtransparent.png"
|
||||
alt="Hack Club"
|
||||
className="w-[151px] h-[53px] object-cover transform rotate-[-4.8deg] select-none"
|
||||
style={{
|
||||
filter: "drop-shadow(3px 3px 0px rgba(0,0,0,0.25))"
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
<div className="w-[2px] h-8 bg-white opacity-60"></div>
|
||||
<a href='https://opensauce.com' className='transition-transform scale-125 hover:scale-130 active:scale-130'>
|
||||
<img
|
||||
src="/branding/logo-opensauce.webp"
|
||||
alt="Open Sauce"
|
||||
className="h-[70px] object-contain select-none pl-4"
|
||||
style={{
|
||||
filter: "drop-shadow(3px 3px 0px rgba(0,0,0,0.25))"
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="transform md:rotate-[-2.97deg] w-min">
|
||||
<h1
|
||||
className="text-[#fcf5ed] text-[80px] md:text-[100px] xl:text-[150px] font-normal leading-none -mb-4 font-dream-planner"
|
||||
style={{
|
||||
textShadow: "5px 8px 0px rgba(0,0,0,0.25)"
|
||||
}}
|
||||
>
|
||||
CAMPFIRE
|
||||
</h1>
|
||||
<h3
|
||||
className="text-[#fcf5ed] text-[40px] md:text-[50px] xl:text-[60px] font-normal leading-none mb-4 font-dream-planner text-right"
|
||||
style={{
|
||||
textShadow: "5px 8px 0px rgba(0,0,0,0.25)"
|
||||
}}
|
||||
>
|
||||
{event_name.toUpperCase().replaceAll("-", " ").replace("CAMPFIRE", "")}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pl-2 md:pl-4">
|
||||
<p
|
||||
className="text-white text-3xl md:text-2xl xl:text-3xl font-bold mb-2 font-ember-and-fire text-center"
|
||||
style={{
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)"
|
||||
}}
|
||||
>
|
||||
{
|
||||
error ? (
|
||||
<>
|
||||
This site is temporarily down! <br /> (Organizers: {error})
|
||||
</>
|
||||
) : is404 ? "This page doesn't exist!" : "This event is confirmed, but we’re still finalizing the exact schedule! The site will be live in 3-4 business days"
|
||||
}
|
||||
</p>
|
||||
<p
|
||||
className="text-white text-4xl md:text-3xl xl:text-4xl font-bold font-ember-and-fire text-center"
|
||||
style={{
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)"
|
||||
}}
|
||||
>
|
||||
{is404 ? "Sign up for campfire below:" : "You can still sign up below:"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RSVP form */}
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className="flex flex-col md:flex-row items-center gap-2">
|
||||
<div
|
||||
className={clsx(
|
||||
"bg-[#f9e2ca] border-4 border-[#d5a16c] rounded-[20px] px-4 md:px-8 py-4 flex items-center gap-3 md:gap-6 w-full transform md:rotate-[-1.2deg] shadow-[0_8px_20px_rgba(0,0,0,0.25)]",
|
||||
"transition-transform hover:scale-105"
|
||||
)}
|
||||
>
|
||||
<img src="/icons/email.svg" alt="" className="w-6 h-5 flex-shrink-0 select-none" />
|
||||
<input
|
||||
required
|
||||
ref={emailRef}
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
type="email"
|
||||
className="text-[#854d16] text-2xl md:text-4xl font-bold truncate bg-transparent border-none outline-none flex-1 cursor-text font-ember-and-fire"
|
||||
placeholder="you@hackclub.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="bg-[#fca147] border-[5px] border-[rgba(0,0,0,0.2)] rounded-[20px] px-8 md:px-14 py-4 hover:scale-105 transition-transform w-full md:w-auto transform md:rotate-[1.5deg] shadow-[0_8px_20px_rgba(0,0,0,0.25)] cursor-pointer active:scale-95"
|
||||
type="button"
|
||||
onClick={() => openWithEmail(FORM_URL_SIGN_UP)}
|
||||
>
|
||||
<p
|
||||
className="text-[#8d3f34] text-3xl md:text-5xl font-normal font-dream-planner whitespace-nowrap"
|
||||
>
|
||||
SIGN UP!
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UnderConstruction;
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
interface GameCardProps {
|
||||
imageSrc: string;
|
||||
imageAlt: string;
|
||||
title: string;
|
||||
author: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
function GameCard({ imageSrc, imageAlt, title, author, href }: GameCardProps) {
|
||||
return (
|
||||
<div className="flex flex-col gap-9 items-center md:w-[400px]">
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
className="block"
|
||||
>
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt={imageAlt}
|
||||
className="w-[391px] h-[263px] shadow-[12px_12px_0px_0px_rgba(0,0,0,0.25)] object-cover hover:scale-105 transition-transform"
|
||||
/>
|
||||
</a>
|
||||
<div className="text-center text-[#d7cfeb]">
|
||||
<p
|
||||
className="text-6xl font-bold mb-2 font-ember-and-fire"
|
||||
style={{
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)"
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
<p
|
||||
className="text-3xl font-bold font-ember-and-fire"
|
||||
style={{
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)"
|
||||
}}
|
||||
>
|
||||
{author}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GameCard;
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import type { EventLocation } from '../../lib/events';
|
||||
import '../../styles/map.css';
|
||||
|
||||
interface MapProps {
|
||||
events: EventLocation[];
|
||||
center?: [number, number];
|
||||
zoom?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Map({ events, center = [20, 0], zoom = 2, className }: MapProps) {
|
||||
const [MapComponents, setMapComponents] = useState<{
|
||||
MapContainer: any;
|
||||
TileLayer: any;
|
||||
Marker: any;
|
||||
Popup: any;
|
||||
useMap: any;
|
||||
flagIcon: any;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
import('leaflet/dist/leaflet.css');
|
||||
Promise.all([import('leaflet'), import('react-leaflet')]).then(([L, module]) => {
|
||||
const flagIcon = L.icon({
|
||||
iconUrl: '/map/map-flag.png',
|
||||
iconSize: [32, 32],
|
||||
iconAnchor: [16, 32],
|
||||
popupAnchor: [0, -32]
|
||||
});
|
||||
setMapComponents({
|
||||
MapContainer: module.MapContainer,
|
||||
TileLayer: module.TileLayer,
|
||||
Marker: module.Marker,
|
||||
Popup: module.Popup,
|
||||
useMap: module.useMap,
|
||||
flagIcon,
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
const validEvents = events.filter(
|
||||
(event) => typeof event.lat === 'number' && typeof event.long === 'number'
|
||||
);
|
||||
|
||||
if (!MapComponents) {
|
||||
return <div style={{ minHeight: '400px', height: '100%', width: '100%', background: 'linear-gradient(135deg, #CCF4FD 0%, #B8D9F8 100%)', borderRadius: '16px' }} className={className} />;
|
||||
}
|
||||
|
||||
const { MapContainer, TileLayer, Marker, Popup, useMap, flagIcon } = MapComponents;
|
||||
|
||||
function InvalidateSize() {
|
||||
const map = useMap();
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => map.invalidateSize(), 0);
|
||||
const resizeTimer = setTimeout(() => map.invalidateSize(), 300);
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
clearTimeout(resizeTimer);
|
||||
};
|
||||
}, [map]);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MapContainer
|
||||
center={center}
|
||||
zoom={zoom}
|
||||
className={className}
|
||||
style={{ minHeight: '400px', height: '100%', width: '100%' }}
|
||||
>
|
||||
<InvalidateSize />
|
||||
<TileLayer
|
||||
attribution='© OpenStreetMap contributors © CARTO'
|
||||
url="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png"
|
||||
detectRetina={true}
|
||||
/>
|
||||
{validEvents.map((event) => (
|
||||
<Marker key={event.slug} position={[event.lat, event.long]} icon={flagIcon}>
|
||||
<Popup className="custom-popup">
|
||||
<div>
|
||||
<a href={`https://campfire.hackclub.com/${event.slug}`} target="_top" className="text-black text-lg font-bold font-ember-and-fire">Campfire {event.event_name}</a>
|
||||
</div>
|
||||
</Popup>
|
||||
</Marker>
|
||||
))}
|
||||
</MapContainer>
|
||||
);
|
||||
}
|
||||
|
|
@ -8,23 +8,23 @@ function MapEmbed({ className, onOpenMap, label = "find an event near you" }: Ma
|
|||
return (
|
||||
<div className={className}>
|
||||
<div className="relative mt-16 md:mt-0 mb-2 md:mb-10 2xl:mb-12">
|
||||
<p
|
||||
<p
|
||||
className="text-white text-4xl 2xl:text-5xl font-bold font-ember-and-fire text-center"
|
||||
style={{
|
||||
style={{
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)"
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</p>
|
||||
|
||||
<img
|
||||
src="/compressed/ui/arrow.webp"
|
||||
alt=""
|
||||
<img
|
||||
src="/compressed/ui/arrow.webp"
|
||||
alt=""
|
||||
className="hidden md:block absolute right-0 top-full w-[55px] h-[41px] rotate-[6.2deg] z-50 select-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
<button
|
||||
onClick={onOpenMap}
|
||||
className="relative transform rotate-[1.7deg] transition-transform hover:scale-105 w-[70vw] md:w-[50vw] xl:w-[442px] mx-auto cursor-pointer"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
function StoryCard({ imageSrc, imageAlt, titleImageSrc, titleImageAlt, description, videoUrl, videoButtonText, videoButtonColor, eventUrl }: {
|
||||
imageSrc: string;
|
||||
imageAlt: string;
|
||||
titleImageSrc: string;
|
||||
titleImageAlt: string;
|
||||
description: string;
|
||||
videoUrl: string;
|
||||
videoButtonText: string;
|
||||
videoButtonColor: string;
|
||||
eventUrl: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-[url(/compressed/ui/step-bg.webp)] bg-no-repeat bg-cover min-[1050px]:bg-none min-[1050px]:bg-white relative p-6 pb-24 flex items-center flex-1 basis-0 flex-col gap-4 min-[1050px]:gap-2 min-[1050px]:shadow-[16px_16px_0px_0px_rgba(0,0,0,0.3)] mx-8 min-[1050px]:mx-0">
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt={imageAlt}
|
||||
className="block w-[280px] h-[220px] max-w-none object-cover mb-2"
|
||||
/>
|
||||
|
||||
<div className="p-2 flex flex-col items-center min-[1050px]:items-start">
|
||||
<a className="transition-transform hover:scale-105 active:scale-95 cursor-pointer w-full flex justify-center min-[1050px]:justify-start" href={eventUrl} target="_blank">
|
||||
<img
|
||||
src={titleImageSrc}
|
||||
alt={titleImageAlt}
|
||||
className="mb-2 w-1/2 min-[1050px]:w-full object-contain"
|
||||
style={{
|
||||
filter: "drop-shadow(0px 4px 4px rgba(0,0,0,0.25))"
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
|
||||
<p
|
||||
className="text-black h-max w-full text-2xl font-source-serif-pro text-center min-[1050px]:text-left"
|
||||
style={{
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)"
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="absolute bottom-0 cursor-pointer block rounded-2xl p-4 px-6 font-ember-and-fire text-white font-bold text-4xl transition-transform hover:scale-105 active:scale-95 min-[1050px]:translate-y-6 shadow-[12px_12px_0px_0px_rgba(0,0,0,0.3)]"
|
||||
onClick={() => window.open(videoUrl, "_blank")}
|
||||
style={{ background: videoButtonColor }}
|
||||
>
|
||||
{videoButtonText.toUpperCase()}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default StoryCard;
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
interface VideoEmbedProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function VideoEmbed({ className }: VideoEmbedProps) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="flex items-center justify-center gap-3 mb-4 2xl:mb-8">
|
||||
<p
|
||||
className="text-white text-4xl 2xl:text-6xl font-bold font-ember-and-fire"
|
||||
style={{
|
||||
textShadow: "0px 4px 4px rgba(0,0,0,0.25)"
|
||||
}}
|
||||
>
|
||||
watch the video
|
||||
</p>
|
||||
|
||||
<img
|
||||
src="/compressed/ui/arrow.webp"
|
||||
alt=""
|
||||
className="w-[45px] md:w-[55px] h-[33px] md:h-[41px] translate-y-6 rotate-[6.2deg] z-50 select-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative transform rotate-[1.7deg] transition-transform hover:scale-105 w-[70vw] md:w-[50vw] xl:w-[442px] mx-auto">
|
||||
<iframe
|
||||
src="https://www.youtube.com/embed/yVgqQQ5xYJo?si=1PngS7-FtsjCfAGy"
|
||||
title="YouTube video player"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerPolicy="strict-origin-when-cross-origin"
|
||||
allowFullScreen
|
||||
className="w-full aspect-video rounded-2xl shadow-[12px_12px_0px_0px_rgba(0,0,0,0.25)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default VideoEmbed;
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
import 'dotenv/config';
|
||||
const { AIRTABLE_API_KEY, AIRTABLE_BASE_ID } = process.env;
|
||||
|
||||
import Airtable from 'airtable';
|
||||
let airtableBase: any = null;
|
||||
|
||||
if (AIRTABLE_API_KEY && AIRTABLE_BASE_ID) {
|
||||
airtableBase = new Airtable({ apiKey: AIRTABLE_API_KEY }).base(AIRTABLE_BASE_ID);
|
||||
}
|
||||
|
||||
export interface EventLocation {
|
||||
slug: string;
|
||||
lat: number;
|
||||
long: number;
|
||||
event_name: string;
|
||||
}
|
||||
|
||||
interface AirtableRecord {
|
||||
id: string;
|
||||
fields: EventLocation;
|
||||
}
|
||||
|
||||
export interface EventLocationWithDistance extends EventLocation {
|
||||
distance: number;
|
||||
}
|
||||
|
||||
export async function fetchEventsLoc() {
|
||||
return (await airtableBase('events').select({
|
||||
view: 'Everything',
|
||||
// filterByFormula: '{website_active} = 1',
|
||||
fields: [
|
||||
'slug', //string
|
||||
'event_name', //string
|
||||
'lat', //number
|
||||
'long', //number
|
||||
'website_active' //boolean
|
||||
]
|
||||
}).all()) as AirtableRecord[]
|
||||
}
|
||||
|
||||
|
||||
// Cache to avoid multiple geocoding during build
|
||||
let cachedEvents: EventLocation[] | null = null;
|
||||
|
||||
export async function loadEventsLoc(): Promise<EventLocation[]> {
|
||||
// do nothing if the API keys aren't set
|
||||
if (!AIRTABLE_API_KEY || !AIRTABLE_BASE_ID) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (cachedEvents) {
|
||||
return cachedEvents;
|
||||
}
|
||||
|
||||
console.log("Geocoding all events")
|
||||
|
||||
try {
|
||||
// Fetch all approved events from Airtable with pagination
|
||||
const locations = await fetchEventsLoc();
|
||||
|
||||
return locations.map(location => location.fields);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch event data:', error);
|
||||
cachedEvents = [];
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Haversine formula to calculate distance between two points in miles
|
||||
export function calculateDistance(lat1: number, lng1: number, lat2: number, lng2: number): number {
|
||||
const R = 3959; // Earth's radius in miles
|
||||
const dLat = (lat2 - lat1) * Math.PI / 180;
|
||||
const dLng = (lng2 - lng1) * Math.PI / 180;
|
||||
const a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
|
||||
Math.sin(dLng / 2) * Math.sin(dLng / 2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
return R * c; // Distance in miles
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import 'dotenv/config';
|
||||
import { PrismaClient } from '../generated/prisma/client';
|
||||
import { PrismaPg } from '@prisma/adapter-pg';
|
||||
import pg from 'pg';
|
||||
|
||||
const pool = new pg.Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
ssl: {
|
||||
rejectUnauthorized: false, // For most hosted databases
|
||||
},
|
||||
});
|
||||
|
||||
const adapter = new PrismaPg(pool);
|
||||
|
||||
export const prisma = new PrismaClient({
|
||||
adapter,
|
||||
});
|
||||
|
|
@ -1,418 +1,303 @@
|
|||
import { prisma } from "./prisma";
|
||||
export type SatelliteContent = {
|
||||
error?: string;
|
||||
localization: {
|
||||
hero: {
|
||||
campfire: string;
|
||||
subtitle: string;
|
||||
hostedAt: string;
|
||||
emailPlaceholder: string;
|
||||
ctaPrimary: string;
|
||||
ctaSecondary: string;
|
||||
ctaSecondaryPrefix: string;
|
||||
ctaSecondarySuffix: string;
|
||||
videoLabel?: string;
|
||||
mapLabel: string;
|
||||
};
|
||||
nav: {
|
||||
[key: string]: string;
|
||||
};
|
||||
steps: {
|
||||
step1: string;
|
||||
step2: string;
|
||||
step3: string;
|
||||
step4: string;
|
||||
guideButton: string;
|
||||
};
|
||||
letter: {
|
||||
greeting: string;
|
||||
paragraph1: string;
|
||||
paragraph2: string;
|
||||
paragraph3: string;
|
||||
paragraph4: string;
|
||||
closing: string;
|
||||
signature: string;
|
||||
};
|
||||
schedule: {
|
||||
title: string;
|
||||
};
|
||||
sponsors: {
|
||||
title: string;
|
||||
};
|
||||
signatures: {
|
||||
title: string;
|
||||
};
|
||||
footer: {
|
||||
tagline: string;
|
||||
copyright: string;
|
||||
description: string;
|
||||
closing: string;
|
||||
links: {
|
||||
text: string;
|
||||
href: string;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
event: {
|
||||
city: string;
|
||||
date: string;
|
||||
venue: {
|
||||
name: string;
|
||||
link: string;
|
||||
};
|
||||
schedule: {
|
||||
days: {
|
||||
date: string;
|
||||
items: {
|
||||
time: string;
|
||||
activity: string;
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
sponsors: {
|
||||
cards: {
|
||||
sponsor: string;
|
||||
logo: string;
|
||||
link: string;
|
||||
}[];
|
||||
};
|
||||
signatures?:
|
||||
| false
|
||||
| {
|
||||
img: string;
|
||||
};
|
||||
faq: {
|
||||
title: string;
|
||||
participant: {
|
||||
title: string;
|
||||
questions: {
|
||||
question: string;
|
||||
answer: string;
|
||||
}[];
|
||||
buttonText: string;
|
||||
};
|
||||
organizer?: {
|
||||
title: string;
|
||||
questions: {
|
||||
question: string;
|
||||
answer: string;
|
||||
}[];
|
||||
buttonText: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const PHOENIX_DATA: {
|
||||
slug: string;
|
||||
recordId: string;
|
||||
data: SatelliteContent;
|
||||
active: boolean;
|
||||
} = {
|
||||
slug: "phoenix",
|
||||
recordId: "recSrakXd1napVKai",
|
||||
active: true,
|
||||
data: {
|
||||
localization: {
|
||||
hero: {
|
||||
campfire: "CAMPFIRE",
|
||||
subtitle: "Game jam for high schoolers in 200+ cities",
|
||||
hostedAt: "Hosted @ ",
|
||||
emailPlaceholder: "you@hackclub.com",
|
||||
ctaPrimary: "SIGN UP!",
|
||||
ctaSecondary: "flagship event",
|
||||
ctaSecondaryPrefix: "Looking for our ",
|
||||
ctaSecondarySuffix: "?",
|
||||
videoLabel: "watch the video",
|
||||
mapLabel: "find another event near you",
|
||||
},
|
||||
nav: {
|
||||
faq: "FAQ",
|
||||
steps: "Steps",
|
||||
schedule: "Schedule",
|
||||
"games-made": "Games Made",
|
||||
},
|
||||
schedule: {
|
||||
title: "SCHEDULE",
|
||||
},
|
||||
sponsors: {
|
||||
title: "Our sponsors",
|
||||
},
|
||||
signatures: {
|
||||
title: "Signatures",
|
||||
},
|
||||
steps: {
|
||||
step1: "Sign up for a **Campfire** near you",
|
||||
step2:
|
||||
"Grab **friends** (or make new friends!) and __form a__ **team of 2-3**",
|
||||
step3: "Learn from **workshops**, enjoy __free food and merch!__",
|
||||
step4: "**Build** your game & **publish** it on itch.io!",
|
||||
guideButton: "READ MORE",
|
||||
},
|
||||
letter: {
|
||||
greeting: "Dear Hackers, Musicians, and Artists,",
|
||||
paragraph1:
|
||||
"Welcome to Hack Club's newest adventure. This winter we invite you to join us for Campfire, the world's biggest Game Jam happening simultaneously in 200 cities.",
|
||||
paragraph2: "Hack Club wants you to make a game this winter.",
|
||||
paragraph3:
|
||||
"Don't consider yourself a game dev? No problem - we have tons of online and in-person workshops for you to make your first game!",
|
||||
paragraph4:
|
||||
"This winter, we invite you to learn something new, make something you're really proud of, meet new friends, and go on an incredible adventure together.",
|
||||
closing: "With love,",
|
||||
signature: "The Campfire Team",
|
||||
},
|
||||
footer: {
|
||||
tagline: "made with love by Hack Club & Open Sauce",
|
||||
copyright: "© 2026 Hack Club. 501(c)(3) nonprofit (EIN: 81-2908499)",
|
||||
description:
|
||||
"Hack Club is a 501(c)(3) nonprofit and network of 60k+ technical high schoolers. We believe you learn best by building so we're creating community and providing grants so you can make awesome projects. In the past few years, we've partnered with GitHub to run ",
|
||||
closing:
|
||||
"At Hack Club, students aren't just learning, they're shipping.",
|
||||
links: [
|
||||
{ text: "Hack Club", href: "https://hackclub.com/" },
|
||||
{ text: "Slack", href: "https://hackclub.com/slack" },
|
||||
{ text: "Clubs", href: "https://hackclub.com/clubs" },
|
||||
{ text: "Code of Conduct", href: "https://hackclub.com/conduct/" },
|
||||
],
|
||||
},
|
||||
},
|
||||
event: {
|
||||
city: "Phoenix",
|
||||
date: "Feb 28 - Mar 1, 2026",
|
||||
venue: {
|
||||
name: "HeatSync Labs",
|
||||
link: "https://maps.app.goo.gl/1FgEXfLLJR39SsHg6",
|
||||
},
|
||||
schedule: {
|
||||
days: [
|
||||
{
|
||||
date: "Feb 28th",
|
||||
items: [
|
||||
{ time: "1 PM", activity: "Doors open" },
|
||||
{ time: "1 PM - 1:45 PM", activity: "Icebreakers" },
|
||||
{ time: "2 PM - 2:45 PM", activity: "Opening Ceremony" },
|
||||
{ time: "3:15 PM - 4:15 PM", activity: "Godot Workshop" },
|
||||
{ time: "4:15 PM - 6:30 PM", activity: "Coding!" },
|
||||
{ time: "7 PM - 8 PM", activity: "Dinner and Project help" },
|
||||
{ time: "8:30 PM - 9:30 PM", activity: "Just Dance" },
|
||||
{ time: "9:30 PM - 6 AM", activity: "Coding & Sleep" },
|
||||
],
|
||||
},
|
||||
{
|
||||
date: "Mar 1st",
|
||||
items: [
|
||||
{ time: "6:30 AM - 7:30 AM", activity: "Breakfast" },
|
||||
{ time: "8 AM - 10:30 AM", activity: "Finish game" },
|
||||
{ time: "11:00 AM - 12:30 PM", activity: "Voting" },
|
||||
{
|
||||
time: "12:30 PM - 1 PM",
|
||||
activity: "Closing Ceremony & Prizes",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
sponsors: {
|
||||
cards: [
|
||||
{
|
||||
sponsor: "Hack Club",
|
||||
logo: "https://assets.hackclub.com/flag-standalone-wtransparent.svg",
|
||||
link: "https://hackclub.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
signatures: false,
|
||||
faq: {
|
||||
title: "FAQ",
|
||||
participant: {
|
||||
title: "Participant",
|
||||
questions: [
|
||||
{
|
||||
question: "What is a game jam?",
|
||||
answer:
|
||||
"A game jam is an event where you build a game from scratch in a short time period! It's all about creativity, teamwork, and having fun while learning new skills.",
|
||||
},
|
||||
{
|
||||
question: "Am I eligible?",
|
||||
answer:
|
||||
"If you're a high schooler (or younger), you're eligible! No prior experience required - just bring your enthusiasm and willingness to learn.",
|
||||
},
|
||||
{
|
||||
question: "But I've never hacked before!",
|
||||
answer:
|
||||
"Perfect! Game jams are designed for beginners. You'll have workshops, mentors, and teammates to help you every step of the way.",
|
||||
},
|
||||
{
|
||||
question: "All this, for free?",
|
||||
answer:
|
||||
"Yes! Everything is completely free - venue, food, swag, workshops, and prizes. Hack Club covers all costs so you can focus on creating.",
|
||||
},
|
||||
{
|
||||
question: "What do I need to bring?",
|
||||
answer:
|
||||
"Just bring yourself, a laptop, charger, and any personal items you need. We'll provide food, drinks, and everything else!",
|
||||
},
|
||||
],
|
||||
buttonText: "Check out the parent guide",
|
||||
},
|
||||
organizer: {
|
||||
title: "Organizer",
|
||||
questions: [
|
||||
{
|
||||
question: "Can I organize a Campfire in my city?",
|
||||
answer:
|
||||
"Absolutely! We're always looking for passionate organizers. If you're ready to bring the magic of game development to your community, we'd love to help.",
|
||||
},
|
||||
{
|
||||
question: "What are the steps to organizing?",
|
||||
answer:
|
||||
"First, apply through our organizer form. Then we'll guide you through venue booking, team building, workshop planning, and day-of coordination.",
|
||||
},
|
||||
{
|
||||
question: "Do we get funding?",
|
||||
answer:
|
||||
"Yes! Hack Club provides funding for venue, food, swag, and other event costs. We want to remove financial barriers for amazing events.",
|
||||
},
|
||||
{
|
||||
question: "Do we get volunteer hours?",
|
||||
answer:
|
||||
"Many schools accept organizing hours as community service. Check with your school's requirements - we can provide documentation.",
|
||||
},
|
||||
{
|
||||
question: "Can I join an organizing team?",
|
||||
answer:
|
||||
"Of course! Many cities have organizing teams. Reach out to organizers in your area or apply to join an existing team.",
|
||||
},
|
||||
],
|
||||
buttonText: "Apply to be an organizer",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const HARDCODED_SATELLITES: Record<string, typeof PHOENIX_DATA> = {
|
||||
phoenix: PHOENIX_DATA,
|
||||
};
|
||||
|
||||
export async function getSatelliteSlugs(): Promise<string[]> {
|
||||
const slugs = await prisma.satellite.findMany({
|
||||
select: {
|
||||
slug: true,
|
||||
},
|
||||
})
|
||||
|
||||
return slugs.map((s: { slug: string }) => s.slug)
|
||||
return Object.keys(HARDCODED_SATELLITES);
|
||||
}
|
||||
|
||||
export async function getSatelliteData(slug: string) {
|
||||
return prisma.satellite.findUnique({
|
||||
where: {
|
||||
slug,
|
||||
},
|
||||
});
|
||||
return HARDCODED_SATELLITES[slug] || null;
|
||||
}
|
||||
|
||||
// {
|
||||
// "version": 2,
|
||||
// localization: {
|
||||
// hero: {
|
||||
// campfire: "CAMPFIRE",
|
||||
// subtitle: "Game jam for high schoolers in 200+ cities",
|
||||
// hostedAt: "Hosted @ {venue}",
|
||||
// emailPlaceholder: "you@hackclub.com",
|
||||
// ctaPrimary: "SIGN UP!",
|
||||
// ctaSecondary: "Find a Campfire near you",
|
||||
// ctaSecondaryPrefix: "Not in Shelburne? ",
|
||||
// ctaSecondarySuffix: ".",
|
||||
// videoLabel: "watch the video"
|
||||
// },
|
||||
// nav: {
|
||||
// "Steps": 'steps',
|
||||
// "Schedule": 'schedule',
|
||||
// "Games Made": 'games-made',
|
||||
// "FAQ": 'faq'
|
||||
// },
|
||||
// steps: {
|
||||
// step1: "Find a team of ",
|
||||
// step1Highlight: "CO-ORGANIZERS",
|
||||
// step2: "Find a ",
|
||||
// step2Highlight: "VENUE",
|
||||
// step2Suffix: " to host your hackathon",
|
||||
// step3: "Find ",
|
||||
// step3Highlight1: "SPONSORS",
|
||||
// step3Middle: " to buy merch and prizes, and make your event ",
|
||||
// step3Highlight2: "SPECIAL!",
|
||||
// step4: "Buy supplies, order food, learn ",
|
||||
// step4Highlight1: " GAME DEV",
|
||||
// step4Middle: ", and teach ",
|
||||
// step4Highlight2: "WORKSHOPS",
|
||||
// guideButton: "RSVP NOW"
|
||||
// },
|
||||
// letter: {
|
||||
// greeting: "Dear hacker,",
|
||||
// paragraph1: "You can make a change: inspire someone to build a game for the first time, help someone fall in love with computers, run an incredible game jam that you can invite all your friends to.",
|
||||
// paragraph2: "This February, what if you organized a game jam in your city?",
|
||||
// paragraph3: "Hack Club will provide guides, funding, merch, and 1-on-1 mentorship. Our goal? Run 200 game jams in 200 cities worldwide. All on the same day. All run by high schoolers like us.",
|
||||
// paragraph4: "To kick off 2026, we're so excited to invite you to Campfire. In just a couple months, you will learn how to raise money for your event, buy food and drinks for your attendees, and make your own video games with your friends!",
|
||||
// paragraph5: "Let's go on an adventure together.",
|
||||
// closing: "With love,",
|
||||
// signature: "The Campfire Team"
|
||||
// },
|
||||
// footer: {
|
||||
// tagline: "made with love by Hack Club & Open Sauce",
|
||||
// copyright: "© 2026 Hack Club. 501(c)(3) nonprofit (EIN: 81-2908499)",
|
||||
// description: "Hack Club is a 501(c)(3) nonprofit and network of 60k+ technical high schoolers. We believe you learn best by building so we're creating community and providing grants so you can make awesome projects. In the past few years, we've partnered with GitHub to run ",
|
||||
// closing: "At Hack Club, students aren't just learning, they're shipping."
|
||||
// }
|
||||
// },
|
||||
// event: {
|
||||
// city: "Shelburne",
|
||||
// date: "Feb 28 - Mar 1, 2026",
|
||||
// venue: "Hack Club HQ",
|
||||
// schedule: {
|
||||
// title: "SCHEDULE",
|
||||
// days: [
|
||||
// {
|
||||
// date: "Feb 28th",
|
||||
// items: [
|
||||
// { time: "7:45 AM", activity: "Doors open" },
|
||||
// { time: "8:00-8:45 AM", activity: "Icebreakers" },
|
||||
// { time: "9:00-9:45 AM", activity: "Opening Ceremony" },
|
||||
// { time: "10:15 AM-12:15 PM", activity: "Godot Workshop" },
|
||||
// { time: "12:45-1:45 PM", activity: "Lunch" },
|
||||
// { time: "2:00-3:00 PM", activity: "Ren'py Workshop" },
|
||||
// { time: "3:00-3:30 PM", activity: "Hackathon Hosting 101 With Hack Canada" },
|
||||
// { time: "4:00-4:15 PM", activity: "Typing Contest" },
|
||||
// { time: "6:00-7:00 PM", activity: "Project Pitch and Dinner" },
|
||||
// { time: "6:45-7:30 PM", activity: "Just Dance" },
|
||||
// { time: "7:35-7:45 PM", activity: "Semi-Closing" },
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// sponsors: {
|
||||
// title: "Our sponsors",
|
||||
// cards: [
|
||||
// {
|
||||
// sponsor: "Hack Club",
|
||||
// logo: "https://assets.hackclub.com/flag-standalone-wtransparent.svg",
|
||||
// link: "https://hackclub.com"
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// signatures: false as any,
|
||||
// // signatures: {
|
||||
// // title: "Campfire Shelburne is possible by...",
|
||||
// // img: "https://assets.hackclub.com/flag-standalone-wtransparent.svg"
|
||||
// // },
|
||||
// faq: {
|
||||
// title: "FAQ",
|
||||
// participant: {
|
||||
// title: "Participant",
|
||||
// questions: [
|
||||
// {
|
||||
// question: "What is a game jam?",
|
||||
// answer: "A game jam is an event where you build a game from scratch in a short time period! It's all about creativity, teamwork, and having fun while learning new skills."
|
||||
// },
|
||||
// {
|
||||
// question: "Am I eligible?",
|
||||
// answer: "If you're a high schooler (or younger), you're eligible! No prior experience required - just bring your enthusiasm and willingness to learn."
|
||||
// },
|
||||
// {
|
||||
// question: "But I've never hacked before!",
|
||||
// answer: "Perfect! Game jams are designed for beginners. You'll have workshops, mentors, and teammates to help you every step of the way."
|
||||
// },
|
||||
// {
|
||||
// question: "All this, for free?",
|
||||
// answer: "Yes! Everything is completely free - venue, food, swag, workshops, and prizes. Hack Club covers all costs so you can focus on creating."
|
||||
// },
|
||||
// {
|
||||
// question: "What do I need to bring?",
|
||||
// answer: "Just bring yourself, a laptop, charger, and any personal items you need. We'll provide food, drinks, and everything else!"
|
||||
// }
|
||||
// ],
|
||||
// buttonText: "Check out the parent guide"
|
||||
// },
|
||||
// organizer: {
|
||||
// title: "Organizer",
|
||||
// questions: [
|
||||
// {
|
||||
// question: "Can I organize a Campfire in my city?",
|
||||
// answer: "Absolutely! We're always looking for passionate organizers. If you're ready to bring the magic of game development to your community, we'd love to help."
|
||||
// },
|
||||
// {
|
||||
// question: "What are the steps to organizing?",
|
||||
// answer: "First, apply through our organizer form. Then we'll guide you through venue booking, team building, workshop planning, and day-of coordination."
|
||||
// },
|
||||
// {
|
||||
// question: "Do we get funding?",
|
||||
// answer: "Yes! Hack Club provides funding for venue, food, swag, and other event costs. We want to remove financial barriers for amazing events."
|
||||
// },
|
||||
// {
|
||||
// question: "Do we get volunteer hours?",
|
||||
// answer: "Many schools accept organizing hours as community service. Check with your school's requirements - we can provide documentation."
|
||||
// },
|
||||
// {
|
||||
// question: "Can I join an organizing team?",
|
||||
// answer: "Of course! Many cities have organizing teams. Reach out to organizers in your area or apply to join an existing team."
|
||||
// }
|
||||
// ],
|
||||
// buttonText: "Apply to be an organizer"
|
||||
// }
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
|
||||
export type SatelliteContent = {
|
||||
error?: string;
|
||||
localization: {
|
||||
hero: {
|
||||
campfire: string;
|
||||
subtitle: string;
|
||||
hostedAt: string;
|
||||
emailPlaceholder: string;
|
||||
ctaPrimary: string;
|
||||
ctaSecondary: string;
|
||||
ctaSecondaryPrefix: string;
|
||||
ctaSecondarySuffix: string;
|
||||
videoLabel?: string;
|
||||
mapLabel: string;
|
||||
};
|
||||
nav: {
|
||||
[key: string]: string;
|
||||
};
|
||||
steps: {
|
||||
step1: string;
|
||||
step2: string;
|
||||
step3: string;
|
||||
step4: string;
|
||||
guideButton: string;
|
||||
};
|
||||
letter: {
|
||||
greeting: string;
|
||||
paragraph1: string;
|
||||
paragraph2: string;
|
||||
paragraph3: string;
|
||||
paragraph4: string;
|
||||
closing: string;
|
||||
signature: string;
|
||||
};
|
||||
schedule: {
|
||||
title: string;
|
||||
}
|
||||
sponsors: {
|
||||
title: string;
|
||||
}
|
||||
signatures: {
|
||||
title: string;
|
||||
}
|
||||
footer: {
|
||||
tagline: string;
|
||||
copyright: string;
|
||||
description: string;
|
||||
closing: string;
|
||||
links: {
|
||||
text: string;
|
||||
href: string;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
event: {
|
||||
city: string;
|
||||
date: string;
|
||||
venue: {
|
||||
name: string;
|
||||
link: string;
|
||||
};
|
||||
schedule: {
|
||||
days: {
|
||||
date: string;
|
||||
items: {
|
||||
time: string;
|
||||
activity: string;
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
sponsors: {
|
||||
cards: {
|
||||
sponsor: string;
|
||||
logo: string;
|
||||
link: string;
|
||||
}[];
|
||||
};
|
||||
signatures?: false | {
|
||||
img: string;
|
||||
}
|
||||
faq: {
|
||||
title: string;
|
||||
participant: {
|
||||
title: string;
|
||||
questions: {
|
||||
question: string;
|
||||
answer: string;
|
||||
}[];
|
||||
buttonText: string;
|
||||
};
|
||||
organizer?: {
|
||||
title: string;
|
||||
questions: {
|
||||
question: string;
|
||||
answer: string;
|
||||
}[];
|
||||
buttonText: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// setSatelliteData('shelburne', {
|
||||
// hero: {
|
||||
// title: "CAMPFIRE",
|
||||
// city: "Shelburne",
|
||||
// subtitle: "Game jam for high schoolers in 200+ cities",
|
||||
// date: "Feb 28 - Mar 1, 2026",
|
||||
// emailPlaceholder: "you@hackclub.com",
|
||||
// ctaPrimary: "RSVP!",
|
||||
// ctaSecondary: "Find a Campfire near you",
|
||||
// ctaSecondaryPrefix: "Not in Shelburne? ",
|
||||
// ctaSecondarySuffix: ".",
|
||||
// videoLabel: "watch the video"
|
||||
// },
|
||||
// nav: {
|
||||
// howToOrganize: "How to organize",
|
||||
// schedule: "Schedule",
|
||||
// gamesMade: "Games made",
|
||||
// faq: "FAQ"
|
||||
// },
|
||||
// steps: {
|
||||
// step1: "Find a team of ",
|
||||
// step1Highlight: "CO-ORGANIZERS",
|
||||
// step1Suffix: " to help you organize your hackathon",
|
||||
// step2: "Find a ",
|
||||
// step2Highlight: "VENUE",
|
||||
// step2Suffix: " to host your hackathon",
|
||||
// step3: "Find ",
|
||||
// step3Highlight1: "SPONSORS",
|
||||
// step3Middle: " to buy merch and prizes, and make your event ",
|
||||
// step3Highlight2: "SPECIAL!",
|
||||
// step4: "Buy supplies, order food, learn ",
|
||||
// step4Highlight1: " GAME DEV",
|
||||
// step4Middle: ", and teach ",
|
||||
// step4Highlight2: "WORKSHOPS",
|
||||
// guideButton: "RSVP NOW"
|
||||
// },
|
||||
// letter: {
|
||||
// greeting: "Dear hacker,",
|
||||
// paragraph1: "You can make a change: inspire someone to build a game for the first time, help someone fall in love with computers, run an incredible game jam that you can invite all your friends to.",
|
||||
// paragraph2: "This February, what if you organized a game jam in your city?",
|
||||
// paragraph3: "Hack Club will provide guides, funding, merch, and 1-on-1 mentorship. Our goal? Run 200 game jams in 200 cities worldwide. All on the same day. All run by high schoolers like us.",
|
||||
// paragraph4: "To kick off 2026, we're so excited to invite you to Campfire. In just a couple months, you will learn how to raise money for your event, buy food and drinks for your attendees, and make your own video games with your friends!",
|
||||
// paragraph5: "Let's go on an adventure together.",
|
||||
// closing: "With love,",
|
||||
// signature: "The Campfire Team"
|
||||
// },
|
||||
// schedule: {
|
||||
// title: "SCHEDULE",
|
||||
// days: [
|
||||
// {
|
||||
// date: "Feb 28th",
|
||||
// items: [
|
||||
// { time: "7:45 AM", activity: "Doors open" },
|
||||
// { time: "8:00-8:45 AM", activity: "Icebreakers" },
|
||||
// { time: "9:00-9:45 AM", activity: "Opening Ceremony" },
|
||||
// { time: "10:15 AM-12:15 PM", activity: "Godot Workshop" },
|
||||
// { time: "12:45-1:45 PM", activity: "Lunch" },
|
||||
// { time: "2:00-3:00 PM", activity: "Ren'py Workshop" },
|
||||
// { time: "3:00-3:30 PM", activity: "Hackathon Hosting 101 With Hack Canada" },
|
||||
// { time: "4:00-4:15 PM", activity: "Typing Contest" },
|
||||
// { time: "6:00-7:00 PM", activity: "Project Pitch and Dinner" },
|
||||
// { time: "6:45-7:30 PM", activity: "Just Dance" },
|
||||
// { time: "7:35-7:45 PM", activity: "Semi-Closing" },
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// sponsors: {
|
||||
// title: "Our sponsors",
|
||||
// cards: [
|
||||
// {
|
||||
// sponsor: "Hack Club",
|
||||
// logo: "https://assets.hackclub.com/flag-standalone-wtransparent.svg",
|
||||
// link: "https://hackclub.com"
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// signatures: false as any,
|
||||
// // signatures: {
|
||||
// // title: "Campfire Shelburne is possible by...",
|
||||
// // img: "https://assets.hackclub.com/flag-standalone-wtransparent.svg"
|
||||
// // },
|
||||
// faq: {
|
||||
// title: "FAQ",
|
||||
// participant: {
|
||||
// title: "Participant",
|
||||
// questions: [
|
||||
// {
|
||||
// question: "What is a game jam?",
|
||||
// answer: "A game jam is an event where you build a game from scratch in a short time period! It's all about creativity, teamwork, and having fun while learning new skills."
|
||||
// },
|
||||
// {
|
||||
// question: "Am I eligible?",
|
||||
// answer: "If you're a high schooler (or younger), you're eligible! No prior experience required - just bring your enthusiasm and willingness to learn."
|
||||
// },
|
||||
// {
|
||||
// question: "But I've never hacked before!",
|
||||
// answer: "Perfect! Game jams are designed for beginners. You'll have workshops, mentors, and teammates to help you every step of the way."
|
||||
// },
|
||||
// {
|
||||
// question: "All this, for free?",
|
||||
// answer: "Yes! Everything is completely free - venue, food, swag, workshops, and prizes. Hack Club covers all costs so you can focus on creating."
|
||||
// },
|
||||
// {
|
||||
// question: "What do I need to bring?",
|
||||
// answer: "Just bring yourself, a laptop, charger, and any personal items you need. We'll provide food, drinks, and everything else!"
|
||||
// }
|
||||
// ],
|
||||
// buttonText: "Check out the parent guide"
|
||||
// },
|
||||
// organizer: {
|
||||
// title: "Organizer",
|
||||
// questions: [
|
||||
// {
|
||||
// question: "Can I organize a Campfire in my city?",
|
||||
// answer: "Absolutely! We're always looking for passionate organizers. If you're ready to bring the magic of game development to your community, we'd love to help."
|
||||
// },
|
||||
// {
|
||||
// question: "What are the steps to organizing?",
|
||||
// answer: "First, apply through our organizer form. Then we'll guide you through venue booking, team building, workshop planning, and day-of coordination."
|
||||
// },
|
||||
// {
|
||||
// question: "Do we get funding?",
|
||||
// answer: "Yes! Hack Club provides funding for venue, food, swag, and other event costs. We want to remove financial barriers for amazing events."
|
||||
// },
|
||||
// {
|
||||
// question: "Do we get volunteer hours?",
|
||||
// answer: "Many schools accept organizing hours as community service. Check with your school's requirements - we can provide documentation."
|
||||
// },
|
||||
// {
|
||||
// question: "Can I join an organizing team?",
|
||||
// answer: "Of course! Many cities have organizing teams. Reach out to organizers in your area or apply to join an existing team."
|
||||
// }
|
||||
// ],
|
||||
// buttonText: "Apply to be an organizer"
|
||||
// }
|
||||
// },
|
||||
// footer: {
|
||||
// tagline: "made with love by Hack Club & Open Sauce",
|
||||
// copyright: "© 2026 Hack Club. 501(c)(3) nonprofit (EIN: 81-2908499)",
|
||||
// description: "Hack Club is a 501(c)(3) nonprofit and network of 60k+ technical high schoolers. We believe you learn best by building so we're creating community and providing grants so you can make awesome projects. In the past few years, we've partnered with GitHub to run ",
|
||||
// closing: "At Hack Club, students aren't just learning, they're shipping."
|
||||
// }
|
||||
// })
|
||||
|
||||
|
|
|
|||
|
|
@ -1,41 +1,3 @@
|
|||
---
|
||||
import "../styles/global.css";
|
||||
//@ts-ignore
|
||||
import UnderConstruction from "../components/pages/UnderConstruction";
|
||||
return Astro.redirect("/phoenix");
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Hack Club Campfire</title>
|
||||
|
||||
<!-- OpenGraph meta tags -->
|
||||
<meta property="og:title" content="Hack Club Campfire" />
|
||||
<meta property="og:description" content="Join Hack Club's annual game jam! Build amazing games and compete for epic prizes." />
|
||||
<meta property="og:image" content="/og-banner.png" />
|
||||
<meta property="og:url" content="https://campfire.hackclub.com" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="Hack Club Campfire" />
|
||||
|
||||
<!-- Twitter Card meta tags -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content="@hackclub" />
|
||||
<meta name="twitter:title" content="Hack Club Campfire" />
|
||||
<meta name="twitter:description" content="Join Hack Club's annual game jam! Build amazing games and compete for epic prizes." />
|
||||
<meta name="twitter:image" content="/og-banner.png" />
|
||||
|
||||
<!-- Privacy-friendly analytics by Plausible
|
||||
<script async src="https://plausible.io/js/pa-N_oKgCwiYwq16gm7VPPeD.js"></script>
|
||||
<script>
|
||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||
plausible.init()
|
||||
</script> -->
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<UnderConstruction event_name="" is404={true}/>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
---
|
||||
import "../styles/global.css";
|
||||
//@ts-ignore
|
||||
import Satellite from "../components/pages/Satellite";
|
||||
import { getSatelliteData, getSatelliteSlugs } from "../lib/satellite";
|
||||
import UnderConstruction from "../components/pages/UnderConstruction";
|
||||
import type { SatelliteContent } from "../lib/satellite";
|
||||
|
||||
export const prerender = false;
|
||||
|
|
@ -12,7 +10,7 @@ const { slug } = Astro.params;
|
|||
const slugs = await getSatelliteSlugs();
|
||||
|
||||
if (!slug || !slugs.includes(slug)) {
|
||||
return Astro.redirect("/404");
|
||||
return Astro.redirect("/404");
|
||||
}
|
||||
|
||||
const satelliteData = await getSatelliteData(slug);
|
||||
|
|
@ -20,37 +18,45 @@ const satelliteData = await getSatelliteData(slug);
|
|||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Hack Club Campfire</title>
|
||||
|
||||
<!-- OpenGraph meta tags -->
|
||||
<meta property="og:title" content="Hack Club Campfire" />
|
||||
<meta property="og:description" content="Join Hack Club's annual game jam! Build amazing games and compete for epic prizes." />
|
||||
<meta property="og:image" content="/og-banner.png" />
|
||||
<meta property="og:url" content="https://campfire.hackclub.com" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="Hack Club Campfire" />
|
||||
|
||||
<!-- Twitter Card meta tags -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content="@hackclub" />
|
||||
<meta name="twitter:title" content="Hack Club Campfire" />
|
||||
<meta name="twitter:description" content="Join Hack Club's annual game jam! Build amazing games and compete for epic prizes." />
|
||||
<meta name="twitter:image" content="/og-banner.png" />
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Campfire Phoenix - Hack Club Game Jam</title>
|
||||
|
||||
<!-- Privacy-friendly analytics by Plausible
|
||||
<script async src="https://plausible.io/js/pa-N_oKgCwiYwq16gm7VPPeD.js"></script>
|
||||
<script>
|
||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||
plausible.init()
|
||||
</script> -->
|
||||
<!-- OpenGraph meta tags -->
|
||||
<meta
|
||||
property="og:title"
|
||||
content="Campfire Phoenix - Hack Club Game Jam"
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Join Hack Club's game jam in Phoenix! Build amazing games and compete for epic prizes."
|
||||
/>
|
||||
<meta property="og:image" content="/og-banner.png" />
|
||||
<meta property="og:url" content="https://campfire.hackclub.com" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="Hack Club Campfire" />
|
||||
|
||||
</head>
|
||||
<body>
|
||||
{(!satelliteData?.active || (satelliteData.data as SatelliteContent).error) && <UnderConstruction client:only="react" event_name={slug} record_id={satelliteData?.recordId} error={(satelliteData?.data as SatelliteContent)?.error || ""} />}
|
||||
{satelliteData?.active && <Satellite client:only="react" slug={slug} content={satelliteData.data as SatelliteContent} record_id={satelliteData?.recordId} />}
|
||||
</body>
|
||||
<!-- Twitter Card meta tags -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content="@hackclub" />
|
||||
<meta
|
||||
name="twitter:title"
|
||||
content="Campfire Phoenix - Hack Club Game Jam"
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Join Hack Club's game jam in Phoenix! Build amazing games and compete for epic prizes."
|
||||
/>
|
||||
<meta name="twitter:image" content="/og-banner.png" />
|
||||
</head>
|
||||
<body>
|
||||
<Satellite
|
||||
client:only="react"
|
||||
slug={slug}
|
||||
content={satelliteData?.data as SatelliteContent}
|
||||
record_id={satelliteData?.recordId}
|
||||
/>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
---
|
||||
import "../styles/global.css";
|
||||
import App from "../components/pages/App"
|
||||
|
||||
import { loadEventsLoc } from "../lib/events";
|
||||
|
||||
const events = await loadEventsLoc();
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Hack Club Campfire</title>
|
||||
|
||||
<!-- OpenGraph meta tags -->
|
||||
<meta property="og:title" content="Hack Club Campfire" />
|
||||
<meta property="og:description" content="Join Hack Club's annual game jam! Build amazing games and compete for epic prizes." />
|
||||
<meta property="og:image" content="/og-banner.png" />
|
||||
<meta property="og:url" content="https://campfire.hackclub.com" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="Hack Club Campfire" />
|
||||
|
||||
<!-- Twitter Card meta tags -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content="@hackclub" />
|
||||
<meta name="twitter:title" content="Hack Club Campfire" />
|
||||
<meta name="twitter:description" content="Join Hack Club's annual game jam! Build amazing games and compete for epic prizes." />
|
||||
<meta name="twitter:image" content="/og-banner.png" />
|
||||
|
||||
<!-- Privacy-friendly analytics by Plausible -->
|
||||
<script async src="https://plausible.io/js/pa-N_oKgCwiYwq16gm7VPPeD.js"></script>
|
||||
<script>
|
||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||
plausible.init()
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<App client:only="react" events={events} />
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
---
|
||||
import "../styles/global.css";
|
||||
import { Map } from "../components/primitives/Map";
|
||||
import { loadEventsLoc } from "../lib/events";
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
const events = await loadEventsLoc();
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Map - Hack Club Campfire</title>
|
||||
|
||||
<!-- OpenGraph meta tags -->
|
||||
<meta property="og:title" content="Campfire Map" />
|
||||
<meta property="og:description" content="Find Campfire events happening around the world!" />
|
||||
<meta property="og:image" content="/og-banner.png" />
|
||||
<meta property="og:url" content="https://campfire.hackclub.com/map" />
|
||||
<meta property="og:type" content="website" />
|
||||
</head>
|
||||
<body class="w-screen h-screen">
|
||||
<Map client:only="react" events={events} className="w-full h-full" />
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import { vitePreprocess } from '@astrojs/svelte';
|
||||
|
||||
export default {
|
||||
preprocess: vitePreprocess(),
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue