Hack Club's ten-week hardware programme. One repository, two worlds that are meant to look nothing alike:
- The landing page (
src/app/(site)) — the marketing frame, built to the Figma "landing page (claude)" (275:8) with the palette from "color scheme" (275:222). - The platform (
src/app/(platform)) — the logged-in product: a Duolingo-style trail of checkpoints, a journal, a shop, and the Doomscroller. Documented in docs/platform-ui.md.
Signed out, / is the landing page. Signed in, proxy.ts rewrites it to the
trail: two route groups cannot both define /, and each has its own root layout
and stylesheet. The cookie check there is optimistic and decides nothing that
matters — the real session guard is a server component.
pnpm install
./dev.sh # Postgres in Docker, migrated and seeded, then next devNew here? docs/BACKEND.md is the handoff — the shape, the invariants, and the traps that have already bitten. Beyond it: the visual system is DESIGN.md, the reasoning behind the invariants is docs/ARCHITECTURE.md, and operating it is docs/RUNBOOK.md.
src/app/(site)/ the landing page
src/app/(platform)/(app)/ the trail, projects, shop, explore, leaderboard
src/app/(platform)/(ops)/ admin, review, login — earlier design system
src/app/(handoff)/ the phone upload page a QR code opens
src/app/api/ every write in the product
src/components/site/ landing sections
src/components/platform/ the logged-in product's components
src/lib/config/ every tunable number: tiers, printers, programme
src/lib/curriculum.ts the shape of a week, as a pure function
src/lib/queries/store.ts the trail's data, read in one pass
src/lib/store.tsx that snapshot, plus every write the trail makes
prisma/schema.prisma the data model
public/art/ hero painting, scatter photos, comp SVG exports
pnpm typecheck && pnpm lint && pnpm build
pnpm verify:ledger # the economy: two pots, and every printer reachable
pnpm verify:store # the database-to-trail mapping
pnpm verify:onboarding # the first checkpoint cannot be skipped
pnpm verify:feed # moderation actually removes things
pnpm verify:handoff # the QR upload token is single-useThe verify scripts write to whatever DATABASE_URL points at and clean up after
themselves. Point them at a scratch database, never production.
src/components/wordmark.tsx sets the wordmark in live type by default. Drop
your artwork in public/brand/ and set one constant:
const LOGO_SRC = "/brand/half-life-wordmark.svg";
const LOGO_ASPECT = 981 / 261; // your file's width ÷ heightThe comp reserves a 981 × 261 box. The slot fills its width either way, so nothing else changes.
Every reserved photo is an <ImageSlot> — an ink plate with cyan registration
ticks and the artwork's native size printed in it, so an empty slot reads as a
deliberate footprint rather than a broken image. Each one tells you its size:
| Where | Native size | File |
|---|---|---|
| Step 2 — "get funding…" | 736 × 155 | src/components/process.tsx |
| Step 3 — "then spend 5 weeks…" | 736 × 269 | src/components/process.tsx |
| Step 4 — "get a 3D printer!" | 736 × 269 | src/components/process.tsx |
| Carousel cards (×5) | 329 × 377 | src/lib/content.ts → BUILD_CARDS |
Replace an <ImageSlot …/> with <Image …/> in the same wrapper — the box is
already the comp's box.
POST /api/subscribe validates, normalises and de-duplicates, then writes to
one of two sinks:
| Env | Behaviour |
|---|---|
SIGNUP_WEBHOOK_URL |
POST { email, source, at } to your list provider or relay |
SIGNUP_WEBHOOK_TOKEN |
optional Authorization: Bearer … for that webhook |
| (neither set) | appends JSON Lines to ./data/signups.jsonl (gitignored) |
The file sink is fine for local development and any host with a writable disk.
It is not durable on serverless — point SIGNUP_WEBHOOK_URL at a real
provider before launch. The rate limit is per-instance and coarse; put Vercel
BotID or a WAF rule in front of the route for real abuse protection.
src/app/api/slack/events/ and src/app/api/slack/interactions/ back a bot
that:
- invites anyone who joins the main Half Life channel into
#halflife-bulletinand#halflife-help - posts every new top-level message in
#halflife-helpinto a private tickets channel with a Mark as helped button (anyone with access to that channel can click it), and replies in-thread in#halflife-helpwith an I'm all set button that only the ticket's own author can click
Either button resolves the ticket: it edits its own message in place, and —
for the author's button — also updates the tickets-channel message so staff
can see it's already handled. There's no database; each button's own
value carries the pointer(s) it needs back to the other message(s). One
known gap: if staff resolve a ticket first, the author's button is left
active and clicking it afterward will just re-mark things as resolved by
the author — harmless, but slightly confusing if it happens.
Create the app from slack-app-manifest.yml at the repo root (see the
comments in that file for the exact steps), then set:
| Env | What |
|---|---|
SLACK_BOT_TOKEN |
Bot User OAuth Token (xoxb-…) |
SLACK_SIGNING_SECRET |
Basic Information → Signing Secret, verifies both routes |
SLACK_MAIN_CHANNEL_ID |
channel that triggers the auto-invite |
SLACK_BULLETIN_CHANNEL_ID |
#halflife-bulletin's channel ID |
SLACK_HELP_CHANNEL_ID |
#halflife-help's channel ID |
SLACK_TICKETS_CHANNEL_ID |
private channel new/resolved tickets get posted to |
The bot needs to be a member of all four channels — invite it manually after installing.
- The carousel credits are the comp's placeholder credit ("by Meghana, 17, from Ohio") repeated across five cards — replace with the real makers.
- Hack Club's EIN (
81-2908499) is in the footer. Confirm it and decide whether you also want the mailing address, which the Hack Club footer carries. - FAQ card six ("What do I get?") replaces a duplicated "What's Hack Club?" in the comp. Its copy is assembled from facts the comp already states.
Two layouts, on purpose:
- ≥ 1180px —
.stagereproduces the comp's 1728px grid exactly. Every offset is written as its literal Figma pixel and resolved incqw, so the composition scales continuously instead of snapping. - < 1180px — the same content stacked, with the connector bands re-drawn as short vertical tapers between plates.