From 7b1037b2aac589b00d0da3fc9efb2965bb192691 Mon Sep 17 00:00:00 2001 From: oliverhuangcode Date: Wed, 15 Jul 2026 22:48:36 +1000 Subject: [PATCH 1/9] docs: frontend handover guide + stub Past/FindTeam as learning exercises Hand-off branch for a first-year redesigning the frontend. Adds FRONTEND_GUIDE.md (data flow, request lifecycle, file map, local dev, Tailwind, git basics) and empties Past.tsx + FindTeam.tsx into guided TODO stubs so she rebuilds the fetch-then-render loop herself. Landing and Dashboard stay intact as reference implementations; the original Past/FindTeam remain on mac-hackathon-mvp as solutions. Co-Authored-By: Claude Opus 4.8 --- FRONTEND_GUIDE.md | 271 +++++++++++++++++++++++++++++++++++++ README.md | 4 + web/src/pages/FindTeam.tsx | 248 +++++---------------------------- web/src/pages/Past.tsx | 53 ++++---- 4 files changed, 335 insertions(+), 241 deletions(-) create mode 100644 FRONTEND_GUIDE.md diff --git a/FRONTEND_GUIDE.md b/FRONTEND_GUIDE.md new file mode 100644 index 0000000..4db3c6b --- /dev/null +++ b/FRONTEND_GUIDE.md @@ -0,0 +1,271 @@ +# Frontend guide & handover + +Welcome! πŸ‘‹ You're taking over the **frontend** of the MAC Hackathon platform β€” the part +people actually see and click. This doc teaches you how the whole thing fits together (so +the frontend makes sense, not just "magic that works"), how to run it on your laptop, and +gives you **two hands-on exercises** to get your hands dirty before you start redesigning. + +You don't need to touch the backend to redesign the frontend. But you *should* understand +how they talk, because every screen you build is really "fetch some data, then draw it." + +> New to git? There's a **Git cheat-sheet** at the bottom. Read that first if you've never +> made a branch or a pull request. + +--- + +## 1. The big picture: where does the data come from? + +The most important thing to understand: **the website does not invent its own data.** Every +screen is drawing numbers and text that came from somewhere else. There are three sources: + +``` + Notion (a fancy doc) Humanitix (ticket sales) Organisers (admin panel) + prizes, judges, FAQ, who bought a ticket create the event, + schedule, sponsors… for the hackathon trigger syncs + β”‚ β”‚ β”‚ + β”‚ (synced on a timer) β”‚ (synced on a timer) β”‚ (saved directly) + β–Ό β–Ό β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Postgres (our database) β”‚ + β”‚ one place that holds a *copy* of everything, always fast β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”‚ the backend reads Postgres and hands out JSON + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Backend API (Express, in src/server/) β”‚ + β”‚ e.g. GET /api/public/event β†’ { event: {...}, content: {...} } β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”‚ the frontend fetches that JSON + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Frontend (React, in web/) ← THIS IS YOUR PATCH β”‚ + β”‚ turns JSON into buttons, cards, and text on the page β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**A common misconception to clear up:** the public info (prizes, judges, schedule…) comes +from **Notion**, not from the admin panel. The admin panel just lets organisers create the +event and press "sync now." The content itself lives in a Notion database, gets copied into +Postgres on a timer, and the website reads it from Postgres. (Why the copy? So the site +stays up and fast even if Notion is slow or down.) Ticket info works the same way, but the +source is Humanitix. + +You almost never care *which* original source something came from. By the time it reaches +your React code, it's just JSON from our own API. + +--- + +## 2. The request lifecycle (the loop you'll repeat all day) + +Every interactive screen is the same four steps. Learn this once and every page makes sense: + +``` +1. React page loads ──▢ 2. calls a function in web/src/api.ts + β”‚ + β–Ό + 3. that does fetch("/api/…") to the backend + β”‚ + β–Ό backend reads Postgres, returns JSON +4. React stores the JSON in state and renders it β—€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +If the user *changes* something (joins a team, ticks a box), it's the same loop with one +extra step: send the change (a `POST`/`PATCH`), then **re-fetch** so the screen matches the +new reality. You'll do exactly this in Exercise 2. + +**You should never write `fetch("/api/…")` directly in a page.** All the network calls live +in one file β€” `web/src/api.ts` β€” as tidy named functions like `api.pastEvents()`. Your pages +call those. This keeps auth, error handling, and URLs in one place. If you need a new call, +add it to `api.ts` first, then use it. + +--- + +## 3. The frontend file map + +Everything you own is under `web/`. You can mostly ignore `src/server/` (that's the backend). + +``` +web/ + index.html the single HTML page everything mounts into + src/ + main.tsx ROUTER: which URL shows which page. Start here. + api.ts ALL calls to the backend live here (api.pastEvents(), etc.) + auth.ts sign-in / token plumbing β€” you rarely touch this + format.ts date/time helpers (fmtDateRange, fmtTime) + styles.css Tailwind theme + shared component classes (colours, buttons…) + pages/ + Landing.tsx public homepage βœ… WORKS β€” read this as your example + Dashboard.tsx "am I in the hackathon?" page βœ… WORKS β€” best example + Admin.tsx organiser control panel βœ… WORKS + Past.tsx archive of old events 🚧 EXERCISE 1 (stubbed for you) + FindTeam.tsx the teammate pool 🚧 EXERCISE 2 (stubbed for you) + components/ + SignInPanel.tsx "please sign in" box + ClaimForm.tsx ticket-claiming form + CustomFieldsForm.tsx extra event questions +``` + +The two 🚧 files have been **deliberately emptied out into guided stubs** so you can rebuild +them yourself β€” that's how you'll learn the fetch-then-render loop. The βœ… pages are complete +and are your reference: whenever you're stuck, open `Landing.tsx` or `Dashboard.tsx` and see +how they did it. + +--- + +## 4. Running it on your laptop + +You need [Node 22](https://nodejs.org) and [Docker Desktop](https://www.docker.com/products/docker-desktop/) +installed. Then: + +```bash +npm install # once, to grab dependencies +cp .env.example .env # then open .env and set: + # DATABASE_URL=...@localhost:5433/mac_hackathon + # DEV_AUTH=1 +docker compose up -d db # starts just the Postgres database (in Docker) +npm run db:migrate # creates the database tables +npm run dev # starts backend (:3000) + frontend (:5173) +``` + +Then open **http://localhost:5173** in your browser. That's the Vite dev server β€” it +**hot-reloads**, meaning when you save a `.tsx` file the page updates instantly. This is +where you'll do all your work. + +**Signing in locally.** Real sign-in only works on the live `monashcoding.com` site. On your +laptop, because you set `DEV_AUTH=1`, there's a **fake dev sign-in**: a panel lets you type +any name/email and tick an "organiser" box, and it just works. This is only ever on in dev β€” +it can't be turned on in production. + +**Two useful checks if something looks broken:** +- Open the browser DevTools (F12) β†’ **Console** tab for React errors, and **Network** tab to + watch the `/api/...` calls and see what JSON came back. This is your #1 debugging tool. +- `http://localhost:3000/api/health` should say `{"status":"ok"}` β€” that confirms the + backend is alive. + +If your local DB has no events/content in it, pages will look empty β€” that's expected, not a +bug. Ask Oliver for a way to seed some sample data, or just build against the empty/loading +states first. + +--- + +## 5. Styling: Tailwind v4 + +We use **Tailwind CSS**. Instead of writing a separate `.css` file per component, you put +utility classes right on the element: + +```tsx +
…
+// ^rounded ^a border ^our colour ^our bg ^padding +``` + +Our brand colours are defined once in `web/src/styles.css` as tokens you can use anywhere: +`bg-bg`, `bg-panel`, `text-text`, `text-muted`, `text-accent`, `border-border`, +`text-danger`, `text-ok`. So `text-accent` = our blue, `bg-panel` = the card background, etc. + +That same file also defines a few **shortcut classes** built from those utilities, so common +things stay consistent: `.wrap` (centered page column), `.panel` (a card), `.muted` (grey +sub-text), `.topnav`, `.btn`, `.card`. You'll see these all over the existing pages. You're +free to redesign these β€” since it's your job to make it look good β€” but they're a comfortable +starting point. + +New to Tailwind? The [official docs](https://tailwindcss.com/docs) have a search box; type +what you want ("padding", "flex", "rounded") and it shows the class. + +--- + +## 6. Your two exercises + +Do these **before** the big redesign. They're small, and they teach you the whole data loop +on the real codebase. Both files are already stubbed with detailed comments β€” open them. + +### Exercise 1: the Past Events page (`web/src/pages/Past.tsx`) + +**Goal:** a read-only page listing past hackathons. + +The data call already exists: `api.pastEvents()` returns `{ events: [...] }` (or `null` if +none). Your job is the React: fetch on load, show "Loading…", then map over the events and +draw each one (name, dates via `fmtDateRange`, venue, tagline, Devpost link). + +**How to approach it:** +1. Open `web/src/pages/Landing.tsx`. Notice the shape: a `useState` to hold the data, a + `useEffect` that calls the API once on load, and JSX that renders it. That's the whole + trick β€” you're copying that shape. +2. Look at what `api.pastEvents()` returns and what fields a `PublicEvent` has (both are in + `web/src/api.ts` β€” hover the types in your editor). +3. Build it. Handle three states: still loading, loaded-but-empty, and loaded-with-events. + +You'll know it works when you can add a past event (ask Oliver / use the admin panel) and it +shows up on `/past`. + +### Exercise 2: the Find-a-Team page (`web/src/pages/FindTeam.tsx`) + +**Goal:** a page that both reads *and* writes. Harder β€” this is the real skill. + +It shows a pool of people looking for a team, lets you tick "I'm looking for a team" (which +saves to the server), and β€” if you lead a team with a spare seat β€” lets you invite someone. + +**How to approach it:** +1. This time read `web/src/pages/Dashboard.tsx` as your model β€” it does the full + **load β†’ let the user act β†’ send the change β†’ re-fetch** cycle. +2. The calls you need are already in `api.ts`: `api.findTeam()` (load), `api.updateProfile(...)` + (opt in/out), `api.inviteFromPool(...)` (invite). The stub comment lists them. +3. Two things that trip people up, and how the reference page handles them: + - **Signed out?** `api.findTeam()` throws a `NotSignedInError`. Catch it and show + `` instead of crashing. + - **After a write, always re-fetch.** Don't try to hand-edit local state to match β€” just + call your load function again. It's simpler and always correct. + +You'll know it works when ticking the box and reloading keeps the box ticked (it saved), and +the pool list updates after you invite someone. + +> **Stuck? The original, working versions of both files exist** in git on the +> `mac-hackathon-mvp` branch. Try it yourself first β€” but if you want to peek at a solution: +> `git show mac-hackathon-mvp:web/src/pages/Past.tsx`. Learning to read someone else's +> solution *after* attempting it is a real skill; use it that way. + +--- + +## 7. Then: the redesign + +Once those two work, you understand the whole frontend. Now make it beautiful. Suggested +order: +1. Start with `Landing.tsx` (the public homepage β€” most eyes on it, most fun to design). +2. Then `Dashboard.tsx` β€” but be careful: read the top comment in that file. It's the single + most important page (a participant must never leave it unsure whether they're in the + hackathon). Redesign the *look*, keep every piece of *information* it shows. +3. Keep it mobile-friendly β€” lots of people open this on their phone. + +Design freely, but keep the data each page shows intact β€” you're changing how it looks, not +what it says. If you find you need data that isn't there, that's a backend change: write it +down and talk to Oliver rather than faking it in the frontend. + +--- + +## 8. Git cheat-sheet (if you're new to this) + +You're on a branch called `frontend-redesign` β€” your own copy where you can't break anyone +else's work. The normal loop: + +```bash +git status # what have I changed? +git add -A # stage all my changes +git commit -m "Rebuild Past page" # save a snapshot, with a message +git push # upload your branch to GitHub +``` + +Commit **little and often** β€” every time something works, commit it. Good messages describe +what you did ("Add loading state to Find a Team"), not "stuff" or "wip". + +When a chunk of work is ready for Oliver to look at, open a **Pull Request** (PR) on GitHub +from your branch β€” that's how you ask "please review and merge my changes." Don't commit +straight to `main`. + +If you get into a mess, **don't panic and don't force anything** β€” stop and ask. Almost +nothing in git is truly unrecoverable, but the fixes are much easier before you try random +commands. + +--- + +Any questions, ask Oliver. Have fun β€” this is a real thing real people will use. πŸŽ‰ diff --git a/README.md b/README.md index 95dbf29..8e23abe 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ MAC's hackathon platform: a public info site (Notion-driven) plus team registrat Read [`SPEC_hackathon.md`](./SPEC_hackathon.md) β€” it is the source of truth. This README covers running the thing. +> **Redesigning the frontend?** Start with [`FRONTEND_GUIDE.md`](./FRONTEND_GUIDE.md) β€” a +> from-scratch walkthrough of how the frontend works, how it talks to the backend, and two +> hands-on exercises to get going. + ## Stack Node 22 Β· TypeScript Β· Express Β· React + Vite (built, served same-origin by Express) Β· diff --git a/web/src/pages/FindTeam.tsx b/web/src/pages/FindTeam.tsx index e8fe7c2..14dd747 100644 --- a/web/src/pages/FindTeam.tsx +++ b/web/src/pages/FindTeam.tsx @@ -1,225 +1,43 @@ -import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; -import { api, NotSignedInError, type DashboardResponse, type FindTeamResponse } from "../api.ts"; -import { SignInPanel } from "../components/SignInPanel.tsx"; -import { TopNav } from "../components/TopNav.tsx"; -import { CustomFieldsForm } from "../components/CustomFieldsForm.tsx"; -import { InvitesPanel, NoTeamPanel, TeamPanel } from "../components/TeamPanels.tsx"; -// The Team page (spec Β§9 + team formation). Everything about teams lives here: -// your current team (or the create/join controls), pending invites, team -// questions, and the looking-for-a-team pool. The dashboard is left to the -// ticket + personal details; this page needs both the dashboard payload (team, -// invites, custom fields) and the find-team payload (the pool), so it loads -// both. All team options are gated behind ticket verification. +// πŸ‘‰ EXERCISE 2 β€” see FRONTEND_GUIDE.md ("Exercise 2: the Find-a-Team page"). +// +// A step up from Exercise 1: this page both READS and WRITES data, so you'll +// practise the full loop β€” load data, let the user change something, send that +// change to the server, then re-load so the screen matches reality. +// +// Rebuild it so it: +// 1. Loads the pool of people looking for a team: api.findTeam() +// 2. Handles being signed out: if the fetch throws a NotSignedInError, +// show the component instead of the pool. +// 3. Has a checkbox to opt in / out of the pool: +// api.updateProfile({ lookingForTeam: true|false }) +// 4. If the signed-in user leads a team with a free slot, shows an +// "Invite to my team" button next to each person: +// api.inviteFromPool(myTeamId, participantId) +// 5. After ANY write, re-fetches so the UI reflects the new state. +// +// Things you'll use (all already built): +// β€’ api.findTeam / updateProfile / inviteFromPool, the FindTeamResponse type, +// and NotSignedInError β†’ web/src/api.ts +// β€’ β†’ web/src/components/SignInPanel.tsx +// β€’ The "load β†’ mutate β†’ refresh" pattern, done in full: +// web/src/pages/Dashboard.tsx +// +// Delete this placeholder and the TODO note once your version works. export function FindTeam() { - const [dash, setDash] = useState(null); - const [find, setFind] = useState(null); - const [signedOut, setSignedOut] = useState(false); - const [error, setError] = useState(""); - const [busy, setBusy] = useState(false); - - async function refresh() { - setError(""); - try { - const [d, f] = await Promise.all([api.dashboard(), api.findTeam()]); - setDash(d); - setFind(f); - setSignedOut(false); - } catch (e) { - if (e instanceof NotSignedInError) { - setSignedOut(true); - } else { - setError((e as Error).message); - } - } - } - useEffect(() => { - void refresh(); - }, []); - - async function toggleLooking(v: boolean) { - setBusy(true); - try { - await api.updateProfile({ lookingForTeam: v }); - await refresh(); - } finally { - setBusy(false); - } - } - async function invite(participantId: string) { - if (!find?.myTeamId) return; - setBusy(true); - try { - await api.inviteFromPool(find.myTeamId, participantId); - await refresh(); - } catch (e) { - alert((e as Error).message); - } finally { - setBusy(false); - } - } - - const status = dash?.participant?.verificationStatus; - const verified = status === "verified" || status === "override"; - return (
- -
-

Team

-

- Form your team, manage members, and browse others looking for teammates. Teams need at - least 2 people β€” keep the conversation going in the MAC Discord. -

- - {signedOut && } - {error &&

{error}

} - - {dash && dash.event === null && ( -

There's no active hackathon right now.

- )} - - {dash && dash.event && !verified && ( -
-

Verify your ticket first

-

- Team registration unlocks once your Humanitix ticket is verified. Head to your{" "} - dashboard{" "} - and claim your ticket with your order reference β€” it takes ten seconds. -

-
- )} - - {dash && dash.event && verified && ( - <> - - - - - {dash.team ? ( - <> - - {dash.team.isLead && (dash.teamCustomFields?.length ?? 0) > 0 && ( - api.saveTeamCustomFields(dash.team!.id, r).then(refresh)} - /> - )} - - ) : ( - - )} - - {find && ( - - )} - - )} -
-
- ); -} - -// Team formation happens in the MAC Discord β€” this is the one-tap way in. Only -// rendered when the event has a Discord invite configured (admin UI). -function DiscordCard({ url }: { url: string | null }) { - if (!url) return null; - return ( -
-
- Team up in the MAC Discord -
- This is where teams actually form β€” introduce yourself, find teammates, and ask - organisers anything. There's no chat here, so head to Discord to connect. + +
+

Find a team

+

🚧 TODO: build this page β€” see FRONTEND_GUIDE.md (Exercise 2).

- - Join the Discord β†’ -
); } - -// The looking-for-a-team pool: the browsable list of verified, teamless -// participants who opted in. When you're teamless it also shows the opt-in -// toggle (put me in the pool); when you're already in a team that toggle is -// hidden β€” you can't be in the pool while teamed β€” and a lead can invite -// people straight into their team. NO chat β€” the conversation is on Discord. -function Pool({ - find, - inTeam, - busy, - onToggle, - onInvite, -}: { - find: FindTeamResponse; - inTeam: boolean; - busy: boolean; - onToggle: (v: boolean) => void; - onInvite: (participantId: string) => void; -}) { - return ( - <> - {!inTeam && ( -
- -

- Opt in and other participants (and team leads with a spare slot) can find you. -

-
- )} - -
-

Looking for a team ({find.pool.length})

- {find.pool.length === 0 && ( -

- {inTeam - ? "No one's in the pool right now. As participants opt into β€œlooking for a team” they'll show up here and you can invite them straight into your team." - : "No one else is in the pool yet β€” you won't see yourself here. Check back as more people opt in, and say hi in the Discord above."} -

- )} - {find.pool.map((p) => ( -
-
- {p.displayName ?? "(no name)"} -
- {[p.university, p.studyLevel, p.githubHandle ? `github: ${p.githubHandle}` : null] - .filter(Boolean) - .join(" Β· ") || "β€”"} -
-
- {find.myTeamId && find.hasOpenSlot && ( - - )} -
- ))} - {find.myTeamId && !find.hasOpenSlot && ( -

Your team is full β€” no open slots to invite into.

- )} -
- - ); -} diff --git a/web/src/pages/Past.tsx b/web/src/pages/Past.tsx index cd9e0bf..d7ec95d 100644 --- a/web/src/pages/Past.tsx +++ b/web/src/pages/Past.tsx @@ -1,35 +1,36 @@ -import { useEffect, useState } from "react"; -import { api, type PublicEvent } from "../api.ts"; -import { TopNav } from "../components/TopNav.tsx"; -import { fmtDateRange } from "../format.ts"; +import { Link } from "react-router-dom"; -// Archive of previous events. Grows for free every year. +// πŸ‘‰ EXERCISE 1 β€” see FRONTEND_GUIDE.md ("Exercise 1: the Past Events page"). +// +// This is your warm-up: a read-only page. Rebuild it so it: +// 1. Fetches the list of past events when the page loads. +// 2. Shows a "Loading…" message while the request is in flight. +// 3. Renders each event: name, dates, venue, tagline, and (if present) a +// link to its Devpost. +// 4. Says something friendly if there are no past events yet. +// +// Everything you need already exists β€” you are only writing the React part: +// β€’ Data: api.pastEvents() β†’ { events: PublicEvent[] } | null +// (defined in web/src/api.ts β€” go read it) +// β€’ Dates: fmtDateRange(startsAt, endsAt) +// (defined in web/src/format.ts) +// β€’ A page that already does exactly this shape of fetch-then-render: +// web/src/pages/Landing.tsx ← copy this pattern +// +// Delete this placeholder and the TODO note once your version works. export function Past() { - const [events, setEvents] = useState(null); - - useEffect(() => { - api.pastEvents().then((d) => setEvents(d?.events ?? [])).catch(() => setEvents([])); - }, []); - return (
- +

Past events

- {events === null &&

Loading…

} - {events && events.length === 0 &&

No past events yet.

} - {events?.map((e) => ( -
- {e.name} /{e.slug} -
{fmtDateRange(e.startsAt, e.endsAt)}{e.venue ? ` Β· ${e.venue}` : ""}
- {e.tagline &&
{e.tagline}
} - {e.devpostUrl && ( - - Projects on Devpost - - )} -
- ))} +

🚧 TODO: build this page β€” see FRONTEND_GUIDE.md (Exercise 1).

); From 8557e179daa6be5a6f4f579790557b35569e4bb1 Mon Sep 17 00:00:00 2001 From: oliverhuangcode Date: Wed, 15 Jul 2026 22:53:08 +1000 Subject: [PATCH 2/9] docs: backend teaching guide + a first-endpoint exercise Companion to FRONTEND_GUIDE.md. BACKEND_GUIDE.md walks through the request lifecycle, the file map, Drizzle/Postgres, and the project's backend principles (whitelisting, server-side auth, serve-from-cache, no hard deletes, derived state). Adds a stubbed, wired-in GET /api/public/stats (routes/stats.ts) as a safe read-only exercise that mirrors frontend Exercise 1 and can be surfaced on the redesigned landing page. Co-Authored-By: Claude Opus 4.8 --- BACKEND_GUIDE.md | 223 +++++++++++++++++++++++++++++++++++++ README.md | 7 +- src/server/index.ts | 2 + src/server/routes/stats.ts | 31 ++++++ 4 files changed, 260 insertions(+), 3 deletions(-) create mode 100644 BACKEND_GUIDE.md create mode 100644 src/server/routes/stats.ts diff --git a/BACKEND_GUIDE.md b/BACKEND_GUIDE.md new file mode 100644 index 0000000..cfc0fb2 --- /dev/null +++ b/BACKEND_GUIDE.md @@ -0,0 +1,223 @@ +# Backend guide + +You don't need this to redesign the frontend β€” but you asked to understand the whole stack, +and it'll make you a much stronger engineer to know what's happening on the *other side* of +every `fetch` you write. This is the companion to [`FRONTEND_GUIDE.md`](./FRONTEND_GUIDE.md); +read that one first, because it sets up the big picture this doc drills into. + +The backend's one job: **keep a fast, safe copy of the truth in Postgres, and hand out +exactly the right slice of it as JSON.** That's it. Everything below is detail on how. + +> Everything backend lives under `src/server/`. It's plain **TypeScript + Express** (the web +> server) talking to **Postgres** (the database) through **Drizzle** (a type-safe query +> builder). No magic frameworks. + +--- + +## 1. What a request actually does + +Remember the frontend loop (page β†’ `api.ts` β†’ `fetch("/api/…")`). Here's what happens the +instant that `fetch` reaches the server: + +``` + fetch("/api/public/past") + β”‚ + β–Ό + src/server/index.ts ← the front door. Matches the URL to a "router". + β”‚ "/api/public/…" is handled by publicRouter. + β–Ό + src/server/routes/public.ts ← the route handler. THE code that runs for this URL. + β”‚ + β–Ό + Drizzle query on Postgres ← db.select().from(events).where(...) + β”‚ + β–Ό + res.json({ events: [...] }) ← turn the DB rows into JSON and send them back +``` + +So for any endpoint, there are only ever two questions: +1. **Which file handles this URL?** (Look in `index.ts` β€” it maps URL prefixes to routers.) +2. **What does that handler do?** (Read the route file β€” it's usually 5–15 lines.) + +--- + +## 2. The file map + +``` +src/server/ + index.ts THE FRONT DOOR. Wires every router to a URL prefix. Read this first. + env.ts Reads + validates environment variables. Fails loudly at boot if a + required secret is missing (better than a mystery crash at 2am). + + db/ + schema.ts THE MOST IMPORTANT FILE. Defines every database table as TypeScript. + index.ts The database connection (the `db` object you query with). + migrate.ts Applies migrations on startup. + + auth/ + jwt.ts Verifies mac-auth sign-in tokens. We never build our own login. + middleware.ts requireAuth / requireOrganiser β€” the guards you put on routes. + + routes/ One file per area. Each exports a "router" wired up in index.ts. + health.ts "is the server alive?" + public.ts public site data (no login needed) + me.ts "who am I?" + dashboard.ts the participant dashboard payload + teams.ts create/join/invite/leave teams + events.ts organiser event CRUD + tickets.ts trigger ticket syncs / CSV import + organiser.ts the gap report, override queue, exports + stats.ts πŸ‘ˆ YOUR EXERCISE (a stub β€” see Β§6) + + content/ Notion β†’ Postgres sync (the public-site content) + tickets/ Humanitix β†’ Postgres sync (who holds a ticket) + teams/ team status rules (status is derived, never set by hand) + participants/ verification logic + lib/ small shared helpers (audit log, discord alerts, codes…) +``` + +If you only read two files to "get" the backend, read **`index.ts`** (how URLs map to code) +and **`db/schema.ts`** (what data exists). Everything else is variations on a theme. + +--- + +## 3. The database, via Drizzle + +Postgres stores the data in **tables** (like spreadsheets: `events`, `participants`, `teams`, +`tickets`…). We never write raw SQL by hand β€” we use **Drizzle**, which lets us describe +tables in TypeScript (`db/schema.ts`) and query them with autocomplete and type-checking. + +A table definition (trimmed from `schema.ts`): + +```ts +export const events = pgTable("events", { + id: uuid("id").primaryKey().defaultRandom(), + slug: text("slug").notNull(), // e.g. "2026" + name: text("name").notNull(), + isPublished: boolean("is_published").notNull().default(false), + isArchived: boolean("is_archived").notNull().default(false), + // …more columns +}); +``` + +Querying it (from `routes/public.ts`) reads almost like English: + +```ts +const rows = await db + .select() + .from(events) + .where(and(eq(events.isPublished, true), eq(events.isArchived, true))) + .orderBy(desc(events.startsAt)); +``` + +`eq` = equals, `and` = both conditions, `desc` = newest first. That's 90% of what you need. + +**Changing the shape of the database** (adding a column, a table) is a two-step ritual: +1. Edit `db/schema.ts`. +2. Run `npm run db:generate` β€” Drizzle writes a **migration** (a `.sql` file in `drizzle/`) + describing the change, then `npm run db:migrate` applies it. + +Migrations are how the database changes safely and repeatably, on every machine and in +production β€” never by hand-editing the live database. You won't need this for the stats +exercise (it only reads), but it's good to know the ritual exists. + +--- + +## 4. A few backend principles worth understanding + +These come straight from the project spec, and they explain *why* the code looks careful in +places. You don't have to memorise them β€” just recognise them when you see them: + +- **Never expose raw database rows.** Public endpoints hand back a hand-picked *whitelist* of + fields (see `toPublicEvent()` in `public.ts`). Internal stuff (a Humanitix event id, ticket + PII) must never leak into a public JSON response. When you add an endpoint, decide on + purpose what goes in it. +- **Auth is checked on the server, every time.** A hidden button in the UI is not security. + Protected routes put a guard in front: `router.get("/me", requireAuth, handler)` (see + `me.ts`). Organiser-only routes add `requireOrganiser`. The token's signature is + re-verified on every request β€” we never trust what the browser claims. +- **Read from Postgres, not from Notion/Humanitix, on a page request.** Those are synced into + Postgres on a timer (`content/`, `tickets/`). If Notion is down, the site is fine β€” it's + serving the last good copy. A page render must be fast and must not depend on someone + else's API being up. +- **Nothing is hard-deleted.** "Deleting" flips a flag (`isArchived`, a `withdrawn` status). + The row stays. And every consequential action is written to an append-only `audit_log`, so + we can always answer "who changed this, and when?" +- **Some things are computed, not stored.** A team's status (`forming` / `confirmed` / …) is + *derived* from its members and their tickets (`teams/status.ts`), never set by hand β€” so it + can't drift out of sync with reality. + +You'll notice the ticket-sync code (`tickets/sync.ts`) is especially defensive β€” it has a +"safety gate" that refuses to un-verify a huge chunk of attendees in one sweep. That's +deliberate: it's the difference between a bug and 200 people locked out the night before the +event. Read the comments there if you're curious; it's a great example of *defensive backend +thinking*. + +--- + +## 5. Running & poking at the backend + +Same setup as the frontend guide (`npm install`, `docker compose up -d db`, `npm run dev`). +Once it's running, the backend is at **http://localhost:3000** and you can hit endpoints +directly from your terminal β€” no frontend needed: + +```bash +curl http://localhost:3000/api/health # {"status":"ok","db":"ok"} +curl http://localhost:3000/api/public/past # {"events":[...]} +curl http://localhost:3000/api/public/stats # your exercise β€” see below +``` + +`curl` is just "make an HTTP request from the command line." It's the fastest way to check a +backend endpoint in isolation. (For endpoints that need login, it's easier to test through +the running site with dev sign-in β€” don't worry about auth for the exercise.) + +When the backend crashes or misbehaves, look at the **terminal running `npm run dev`** β€” that's +where server errors and `console.log` output appear (the browser console only shows frontend +errors). + +--- + +## 6. Exercise: your first endpoint (`src/server/routes/stats.ts`) + +A tiny, self-contained backend task that mirrors frontend Exercise 1 β€” but on the server +side. The file is already created, stubbed, and wired into `index.ts`, so it's live right now: +`curl http://localhost:3000/api/public/stats` returns `{"pastEventCount":0}`. Your job is to +make that number real. + +**Goal:** make `GET /api/public/stats` return the actual count of past events. + +**How to approach it:** +1. Open `src/server/routes/public.ts` and find the `/public/past` handler. It already queries + for exactly the rows you want (published **and** archived events). You're reusing that + `.where(...)` filter. +2. In `stats.ts`, run that query and return the *count* instead of the list. Simplest version: + fetch the rows and return `rows.length`. (Uncomment the imports at the top of the file as + you need them.) +3. Restart isn't needed β€” `npm run dev` reloads on save. Re-run the `curl` and watch the + number change. + +**How you'll know it works:** add/archive an event via the admin panel, then +`curl .../api/public/stats` and see the count reflect it. + +**Ties back to the frontend:** once it works, you could call it from a new `api.stats()` in +`web/src/api.ts` and show "N hackathons and counting" on your redesigned landing page β€” a +complete feature you built through *every* layer of the stack. That's the whole thing. πŸŽ‰ + +> Stretch: also return `publishedEventCount` (published but not archived). And if you want to +> see the full "add a column" ritual, ask Oliver for a small schema exercise. + +--- + +## 7. What NOT to change (for now) + +While you're finding your feet, steer clear of these unless you're pairing with Oliver β€” they +have sharp edges and real consequences: + +- `tickets/sync.ts` and the safety gate β€” getting this wrong can lock real people out. +- `auth/` β€” we never build auth; mac-auth owns it. +- Anything that writes to `audit_log` or that hard-deletes a row (don't add hard deletes). +- `db/schema.ts` migrations on production data. + +Adding *read-only* endpoints (like the stats exercise) is always safe. Start there. + +Questions β†’ ask Oliver. Welcome to the backend. πŸš€ diff --git a/README.md b/README.md index 8e23abe..b5ccdfe 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,10 @@ MAC's hackathon platform: a public info site (Notion-driven) plus team registrat Read [`SPEC_hackathon.md`](./SPEC_hackathon.md) β€” it is the source of truth. This README covers running the thing. -> **Redesigning the frontend?** Start with [`FRONTEND_GUIDE.md`](./FRONTEND_GUIDE.md) β€” a -> from-scratch walkthrough of how the frontend works, how it talks to the backend, and two -> hands-on exercises to get going. +> **New here / redesigning the frontend?** Start with [`FRONTEND_GUIDE.md`](./FRONTEND_GUIDE.md) β€” +> a from-scratch walkthrough of how the frontend works, how it talks to the backend, and two +> hands-on exercises. Then [`BACKEND_GUIDE.md`](./BACKEND_GUIDE.md) explains the server side +> (Express + Drizzle + Postgres) with a matching exercise. ## Stack diff --git a/src/server/index.ts b/src/server/index.ts index 4a9d0d6..a0e78e0 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -6,6 +6,7 @@ import { healthRouter } from "./routes/health.ts"; import { meRouter } from "./routes/me.ts"; import { eventsRouter } from "./routes/events.ts"; import { publicRouter } from "./routes/public.ts"; +import { statsRouter } from "./routes/stats.ts"; // BACKEND_GUIDE.md exercise import { contentRouter } from "./routes/content.ts"; import { ticketsRouter } from "./routes/tickets.ts"; import { dashboardRouter } from "./routes/dashboard.ts"; @@ -23,6 +24,7 @@ app.use(express.json()); app.use("/api", healthRouter); app.use("/api", meRouter); app.use("/api", publicRouter); +app.use("/api", statsRouter); // BACKEND_GUIDE.md exercise β€” GET /api/public/stats app.use("/api", contentRouter); app.use("/api", dashboardRouter); app.use("/api/organiser", organiserRouter); diff --git a/src/server/routes/stats.ts b/src/server/routes/stats.ts new file mode 100644 index 0000000..3c43e58 --- /dev/null +++ b/src/server/routes/stats.ts @@ -0,0 +1,31 @@ +import { Router } from "express"; +// You'll need these once you start writing the real query β€” uncomment as you go: +// import { and, eq } from "drizzle-orm"; +// import { db } from "../db/index.ts"; +// import { events } from "../db/schema.ts"; + +export const statsRouter = Router(); + +// πŸ‘‰ BACKEND EXERCISE β€” see BACKEND_GUIDE.md ("Exercise: your first endpoint"). +// +// Build a public, read-only endpoint that reports a couple of simple counts the +// homepage could show off (e.g. "12 past events"). This teaches the whole +// backend loop end-to-end: a route β†’ a Drizzle query on Postgres β†’ JSON out. +// +// Make GET /api/public/stats return something like: +// { "pastEventCount": 12 } +// +// Steps (all the pieces already exist elsewhere in this file tree): +// 1. Query the `events` table for rows that are published AND archived β€” that's +// what "a past event" means. Copy the WHERE clause from the /public/past +// handler in src/server/routes/public.ts (it does exactly this filter). +// 2. Count them and return the number as JSON. +// 3. Keep it PUBLIC β€” no requireAuth here. Anyone can hit the homepage. +// +// Stretch goal: also return `publishedEventCount` (published, not archived). +// +// Replace the placeholder below with your real implementation. +statsRouter.get("/public/stats", async (_req, res) => { + // TODO: run the real query and return real numbers. + res.json({ pastEventCount: 0 }); +}); From 3f4921d40258adde786d4ff8760f643e448fe831 Mon Sep 17 00:00:00 2001 From: oliverhuangcode Date: Wed, 15 Jul 2026 23:05:49 +1000 Subject: [PATCH 3/9] feat(dev): idempotent local seed script + wire into guides npm run db:seed fills an empty local DB with one upcoming event, two past events, and a full set of content blocks (prizes/judges/schedule/ sponsors/FAQ) so the landing, /past, and dashboard pages render while the frontend is being redesigned. Upserts by slug / notion page id, so re-running is safe; refuses to run under NODE_ENV=production. Guides now point at it instead of "ask Oliver". Co-Authored-By: Claude Opus 4.8 --- BACKEND_GUIDE.md | 7 ++- FRONTEND_GUIDE.md | 17 +++-- package.json | 1 + src/server/db/seed.ts | 140 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 157 insertions(+), 8 deletions(-) create mode 100644 src/server/db/seed.ts diff --git a/BACKEND_GUIDE.md b/BACKEND_GUIDE.md index cfc0fb2..77e4175 100644 --- a/BACKEND_GUIDE.md +++ b/BACKEND_GUIDE.md @@ -157,7 +157,8 @@ thinking*. ## 5. Running & poking at the backend -Same setup as the frontend guide (`npm install`, `docker compose up -d db`, `npm run dev`). +Same setup as the frontend guide (`npm install`, `docker compose up -d db`, `npm run db:seed`, +`npm run dev`). Once it's running, the backend is at **http://localhost:3000** and you can hit endpoints directly from your terminal β€” no frontend needed: @@ -196,8 +197,8 @@ make that number real. 3. Restart isn't needed β€” `npm run dev` reloads on save. Re-run the `curl` and watch the number change. -**How you'll know it works:** add/archive an event via the admin panel, then -`curl .../api/public/stats` and see the count reflect it. +**How you'll know it works:** after `npm run db:seed` there are 2 past events, so a correct +implementation returns `{"pastEventCount":2}` (not `0`). **Ties back to the frontend:** once it works, you could call it from a new `api.stats()` in `web/src/api.ts` and show "N hackathons and counting" on your redesigned landing page β€” a diff --git a/FRONTEND_GUIDE.md b/FRONTEND_GUIDE.md index 4db3c6b..b27ed65 100644 --- a/FRONTEND_GUIDE.md +++ b/FRONTEND_GUIDE.md @@ -126,6 +126,7 @@ cp .env.example .env # then open .env and set: # DEV_AUTH=1 docker compose up -d db # starts just the Postgres database (in Docker) npm run db:migrate # creates the database tables +npm run db:seed # fills the empty DB with sample data (see Β§4) npm run dev # starts backend (:3000) + frontend (:5173) ``` @@ -144,9 +145,16 @@ it can't be turned on in production. - `http://localhost:3000/api/health` should say `{"status":"ok"}` β€” that confirms the backend is alive. -If your local DB has no events/content in it, pages will look empty β€” that's expected, not a -bug. Ask Oliver for a way to seed some sample data, or just build against the empty/loading -states first. +A fresh local database is **empty**, so pages look blank at first β€” that's expected, not a +bug. Fill it with realistic sample data (one upcoming event, two past events, prizes/judges/ +schedule/FAQ) with one command: + +```bash +npm run db:seed +``` + +It's safe to run repeatedly. Now the landing page, `/past`, and the dashboard all have +something to render (and to redesign). --- @@ -196,8 +204,7 @@ draw each one (name, dates via `fmtDateRange`, venue, tagline, Devpost link). `web/src/api.ts` β€” hover the types in your editor). 3. Build it. Handle three states: still loading, loaded-but-empty, and loaded-with-events. -You'll know it works when you can add a past event (ask Oliver / use the admin panel) and it -shows up on `/past`. +You'll know it works when the seeded past events (from `npm run db:seed`) show up on `/past`. ### Exercise 2: the Find-a-Team page (`web/src/pages/FindTeam.tsx`) diff --git a/package.json b/package.json index 6b813a8..3defa7e 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "start": "tsx src/server/index.ts", "db:generate": "drizzle-kit generate", "db:migrate": "tsx --env-file-if-exists=.env src/server/db/migrate.ts", + "db:seed": "tsx --env-file-if-exists=.env src/server/db/seed.ts", "typecheck": "tsc --noEmit", "test": "vitest run", "test:watch": "vitest" diff --git a/src/server/db/seed.ts b/src/server/db/seed.ts new file mode 100644 index 0000000..86b0674 --- /dev/null +++ b/src/server/db/seed.ts @@ -0,0 +1,140 @@ +// --------------------------------------------------------------------------- +// Dev seed β€” fills an empty local database with a realistic-looking event so the +// public site, past-events page, and dashboard aren't blank while you work on +// the frontend. +// +// npm run db:seed +// +// DEV ONLY. It writes plausible fake data, never touches Notion/Humanitix, and +// is safe to run repeatedly β€” it upserts by a stable key (event slug / a fake +// "notion page id"), so re-running just refreshes the same rows rather than +// piling up duplicates. It refuses to run when NODE_ENV=production. +// +// This is NOT how real content gets in β€” that comes from Notion via the sync +// (see BACKEND_GUIDE.md Β§4). This is scaffolding so you have something to style. +// --------------------------------------------------------------------------- +import { db, closeDb } from "./index.ts"; +import { contentBlocks, events } from "./schema.ts"; +import { isProduction } from "../env.ts"; + +async function upsertEvent(row: typeof events.$inferInsert) { + const [saved] = await db + .insert(events) + .values(row) + .onConflictDoUpdate({ target: events.slug, set: row }) + .returning(); + return saved!; +} + +async function upsertBlock(row: typeof contentBlocks.$inferInsert) { + await db + .insert(contentBlocks) + .values(row) + .onConflictDoUpdate({ target: contentBlocks.notionPageId, set: row }); +} + +async function main() { + if (isProduction) { + throw new Error("Refusing to seed: NODE_ENV=production. The seed is dev-only."); + } + + // --- The current event (published, not archived β†’ this is what the landing + // page and dashboard show). Dates are in the near future. --- + const current = await upsertEvent({ + slug: "2026", + name: "MACATHON 2026", + tagline: "48 hours. One idea. Build something that matters.", + startsAt: new Date("2026-09-19T09:00:00+10:00"), + endsAt: new Date("2026-09-21T17:00:00+10:00"), + venue: "Monash University, Clayton", + registrationOpensAt: new Date("2026-08-01T00:00:00+10:00"), + registrationClosesAt: new Date("2026-09-15T23:59:00+10:00"), + minTeamSize: 2, + maxTeamSize: 4, + devpostUrl: "https://macathon-2026.devpost.com", + isPublished: true, + isArchived: false, + }); + + // --- Past events (published AND archived β†’ these show on /past). --- + await upsertEvent({ + slug: "2025", + name: "MACATHON 2025", + tagline: "Where it all came together.", + startsAt: new Date("2025-09-20T09:00:00+10:00"), + endsAt: new Date("2025-09-22T17:00:00+10:00"), + venue: "Monash University, Clayton", + minTeamSize: 2, + maxTeamSize: 4, + devpostUrl: "https://macathon-2025.devpost.com", + isPublished: true, + isArchived: true, + }); + await upsertEvent({ + slug: "2024", + name: "MACATHON 2024", + tagline: "The one that started the streak.", + startsAt: new Date("2024-09-21T09:00:00+10:00"), + endsAt: new Date("2024-09-23T17:00:00+10:00"), + venue: "Monash University, Caulfield", + minTeamSize: 2, + maxTeamSize: 4, + isPublished: true, + isArchived: true, + }); + + // --- Content for the current event. In production these rows come from Notion + // via the sync; here we fake a handful so every section on the landing + // page has something to render. `payload` matches the ContentItem shape + // the frontend expects (see web/src/api.ts). --- + const blocks: Array<{ + kind: (typeof contentBlocks.$inferInsert)["kind"]; + id: string; + sort: number; + payload: Record; + }> = [ + { kind: "prize", id: "seed-prize-1", sort: 0, payload: { title: "Best Overall", subtitle: "$2,000 + interviews", bodyHtml: "

The team that best nails idea, execution, and demo.

" } }, + { kind: "prize", id: "seed-prize-2", sort: 1, payload: { title: "Best Use of AI", subtitle: "$1,000", bodyHtml: "

Most thoughtful application of AI to a real problem.

" } }, + { kind: "prize", id: "seed-prize-3", sort: 2, payload: { title: "People's Choice", subtitle: "$500", bodyHtml: "

Voted by fellow hackers at the expo.

" } }, + + { kind: "judge", id: "seed-judge-1", sort: 0, payload: { title: "Dr Alex Chen", subtitle: "Senior Engineer, Atlassian", bodyHtml: "

Distributed systems and developer tools.

" } }, + { kind: "judge", id: "seed-judge-2", sort: 1, payload: { title: "Priya Nair", subtitle: "Founder, Northlight AI", bodyHtml: "

Building applied ML products.

" } }, + + { kind: "schedule_item", id: "seed-sched-1", sort: 0, payload: { title: "Doors open & check-in", time: "2026-09-19T09:00:00+10:00" } }, + { kind: "schedule_item", id: "seed-sched-2", sort: 1, payload: { title: "Opening ceremony & team forming", time: "2026-09-19T10:00:00+10:00" } }, + { kind: "schedule_item", id: "seed-sched-3", sort: 2, payload: { title: "Hacking begins", time: "2026-09-19T12:00:00+10:00" } }, + { kind: "schedule_item", id: "seed-sched-4", sort: 3, payload: { title: "Submissions due & expo", time: "2026-09-21T12:00:00+10:00" } }, + + { kind: "sponsor", id: "seed-sponsor-1", sort: 0, payload: { title: "Atlassian", subtitle: "Platinum sponsor", url: "https://atlassian.com" } }, + { kind: "sponsor", id: "seed-sponsor-2", sort: 1, payload: { title: "AWS", subtitle: "Cloud credits", url: "https://aws.amazon.com" } }, + + { kind: "faq", id: "seed-faq-1", sort: 0, payload: { question: "Do I need a team to sign up?", answerHtml: "

No β€” come solo and use the Find a Team page. Teams are 2–4 people.

" } }, + { kind: "faq", id: "seed-faq-2", sort: 1, payload: { question: "How much does it cost?", answerHtml: "

A small ticket via Humanitix, which covers food for the whole weekend.

" } }, + { kind: "faq", id: "seed-faq-3", sort: 2, payload: { question: "Who can attend?", answerHtml: "

Anyone, from any university. All skill levels welcome.

" } }, + ]; + + for (const b of blocks) { + await upsertBlock({ + eventId: current.id, + kind: b.kind, + notionPageId: b.id, + payload: b.payload, + sortOrder: b.sort, + isPublished: true, + isPresent: true, + }); + } + + console.log( + `[seed] done β€” current event "${current.name}", 2 past events, ${blocks.length} content blocks.`, + ); + console.log("[seed] open http://localhost:5173 (landing) and /past to see it."); +} + +main() + .then(() => closeDb()) + .catch(async (err) => { + console.error("[seed] failed:", err); + await closeDb(); + process.exit(1); + }); From 0d075ed6c570c36617339e8f5be20f8c4e61dd7b Mon Sep 17 00:00:00 2001 From: oliverhuangcode Date: Wed, 15 Jul 2026 23:24:42 +1000 Subject: [PATCH 4/9] feat(deploy): staging preview service for the frontend redesign MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds docker-compose.staging.yml β€” a separate Dokploy Compose stack that runs the frontend-redesign branch at staging.hackathons.monashcoding.com with its own Postgres, unique Traefik router/service names, and no Humanitix/Notion keys. It self-seeds demo content on boot: the entrypoint runs db:seed when ALLOW_SEED=1, and the seed guard now permits NODE_ENV=production only under that same flag β€” so real production is untouched. Runbook section added to docs/deploy-dokploy.md. Co-Authored-By: Claude Opus 4.8 --- docker-compose.staging.yml | 74 ++++++++++++++++++++++++++++++++++++++ docker-entrypoint.sh | 9 +++++ docs/deploy-dokploy.md | 34 ++++++++++++++++++ src/server/db/seed.ts | 11 ++++-- 4 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 docker-compose.staging.yml diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml new file mode 100644 index 0000000..b0071c4 --- /dev/null +++ b/docker-compose.staging.yml @@ -0,0 +1,74 @@ +# Staging compose for Dokploy β€” a persistent PREVIEW of the frontend-redesign +# branch at https://staging.hackathons.monashcoding.com, kept completely separate +# from production. See docs/deploy-dokploy.md β†’ "Staging preview". +# +# Differences from docker-compose.dokploy.yml (production): +# β€’ Its own domain + Traefik router/service NAMES (must be unique on the host). +# β€’ Its own Postgres + db_data volume β€” Dokploy scopes these per compose stack, +# so staging data never mixes with production. +# β€’ ALLOW_SEED=1 β†’ the entrypoint auto-seeds demo content on boot, so the +# preview is never blank. Real production never sets this. +# β€’ No Humanitix / Notion keys β€” staging shows seeded content, touches nothing +# real. (Add them later only if you deliberately want live sync in staging.) +# +# Still NODE_ENV=production: that's what makes the app serve the built SPA. It is +# "production mode running throwaway data", not a dev server. +services: + db: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_USER: mac_hackathon + POSTGRES_PASSWORD: mac_hackathon + POSTGRES_DB: mac_hackathon + volumes: + - db_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U mac_hackathon -d mac_hackathon"] + interval: 5s + timeout: 5s + retries: 20 + + app: + build: . + restart: unless-stopped + depends_on: + db: + condition: service_healthy + environment: + DATABASE_URL: postgres://mac_hackathon:mac_hackathon@db:5432/mac_hackathon + NODE_ENV: production + PORT: "3000" + PUBLIC_URL: https://staging.hackathons.monashcoding.com + + # Auto-seed demo content on every deploy (idempotent). Staging only. + ALLOW_SEED: "1" + + # mac-auth is shared with production. Public pages (landing, past) work + # without sign-in; auth-gated pages (dashboard/find-team/admin) only work + # if this origin is in mac-auth's TRUSTED_ORIGINS β€” see the runbook note. + MAC_AUTH_URL: https://auth.monashcoding.com + MAC_AUTH_JWKS_URL: https://auth.monashcoding.com/api/auth/jwks + JWT_AUDIENCE: mac-suite + ORGANISER_ROLES: ${ORGANISER_ROLES:-committee,exec,admin} + + # Deliberately no HUMANITIX/NOTION keys β€” staging never hits real services. + networks: + - default # reach the db + - dokploy-network # be reachable by Dokploy's Traefik + # Router/service names are SUFFIXED "-staging" so they never collide with the + # production stack's Traefik config on the same host. + labels: + - traefik.enable=true + - traefik.docker.network=dokploy-network + - traefik.http.routers.mac-hackathon-staging.rule=Host(`staging.hackathons.monashcoding.com`) + - traefik.http.routers.mac-hackathon-staging.entrypoints=websecure + - traefik.http.routers.mac-hackathon-staging.tls.certresolver=letsencrypt + - traefik.http.services.mac-hackathon-staging.loadbalancer.server.port=3000 + +networks: + dokploy-network: + external: true + +volumes: + db_data: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 7e3c6e5..ca88c4a 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -8,5 +8,14 @@ set -e echo "[entrypoint] running migrations…" npm run db:migrate +# Staging-only: auto-populate demo content on boot so the preview site isn't +# blank. Gated behind ALLOW_SEED=1 (set only on the staging service) β€” the seed +# script itself also refuses to run in production without it, so real production +# is never touched. Idempotent (upserts), so re-seeding every deploy is fine. +if [ "$ALLOW_SEED" = "1" ]; then + echo "[entrypoint] ALLOW_SEED=1 β†’ seeding demo content…" + npm run db:seed +fi + echo "[entrypoint] starting server…" exec npm start diff --git a/docs/deploy-dokploy.md b/docs/deploy-dokploy.md index bd4a05a..6f09f87 100644 --- a/docs/deploy-dokploy.md +++ b/docs/deploy-dokploy.md @@ -100,6 +100,40 @@ Then check: --- +## Staging preview (persistent link for frontend work) + +A **separate, throwaway** Dokploy service that runs the `frontend-redesign` branch at +`https://staging.hackathons.monashcoding.com`, so work-in-progress can be previewed on a +real URL **without ever touching production**. It uses its own compose file +([`docker-compose.staging.yml`](../docker-compose.staging.yml)), its own database, and +auto-seeds demo content on boot β€” so it's never blank and never hits Humanitix/Notion. + +1. **DNS (once).** Add an A record for `staging.hackathons.monashcoding.com` β†’ the same + Oracle VM IP. +2. **Create a second Compose service** in Dokploy (name it e.g. `hackathons-staging`): + - Provider = GitHub, repo `monashcoding/hackathons`, branch **`frontend-redesign`**. + - **Compose Path**: `docker-compose.staging.yml`. +3. **Domain**: add `staging.hackathons.monashcoding.com`, HTTPS on, Let's Encrypt, port 3000. +4. **Environment**: none required β€” the staging compose hard-sets everything, including + `ALLOW_SEED=1` (which makes the entrypoint seed demo content on every deploy) and + `NODE_ENV=production` (so the built SPA is served). Leave the env box empty. +5. **Auto-deploy on push**: enable Dokploy's native **Auto Deploy** on this staging service. + Then every push to `frontend-redesign` redeploys the preview automatically. (The CI-gated + `deploy.yml` only fires for the production branch, so it won't interfere.) +6. **Deploy.** Watch the logs for `ALLOW_SEED=1 β†’ seeding demo content…` then the usual + `listening on :3000`. Visit the staging URL. + +> **Public pages just work** on staging (landing, `/past`) β€” no sign-in needed, so that's the +> whole redesign surface covered. The **auth-gated pages** (`/dashboard`, `/find-team`, +> `/admin`) additionally need `staging.hackathons.monashcoding.com` added to **mac-auth's** +> `TRUSTED_ORIGINS`; until then they'll fail to sign in on staging (they still work locally +> via dev sign-in). Ask whoever administers mac-auth if she needs those pages live. + +**Tearing it down** when the redesign lands: delete the staging service in Dokploy and remove +the `staging.` DNS record. `docker-compose.staging.yml` can stay in the repo for next time. + +--- + ## Operating notes - **The database volume `db_data` is the only stateful thing.** Back it up before major diff --git a/src/server/db/seed.ts b/src/server/db/seed.ts index 86b0674..2d0caa4 100644 --- a/src/server/db/seed.ts +++ b/src/server/db/seed.ts @@ -8,7 +8,10 @@ // DEV ONLY. It writes plausible fake data, never touches Notion/Humanitix, and // is safe to run repeatedly β€” it upserts by a stable key (event slug / a fake // "notion page id"), so re-running just refreshes the same rows rather than -// piling up duplicates. It refuses to run when NODE_ENV=production. +// piling up duplicates. It refuses to run when NODE_ENV=production UNLESS +// ALLOW_SEED=1 is explicitly set β€” that override is how the throwaway STAGING +// deploy (which runs NODE_ENV=production so it serves the built SPA) seeds +// itself. Real production never sets ALLOW_SEED, so its database is safe. // // This is NOT how real content gets in β€” that comes from Notion via the sync // (see BACKEND_GUIDE.md Β§4). This is scaffolding so you have something to style. @@ -34,8 +37,10 @@ async function upsertBlock(row: typeof contentBlocks.$inferInsert) { } async function main() { - if (isProduction) { - throw new Error("Refusing to seed: NODE_ENV=production. The seed is dev-only."); + if (isProduction && process.env.ALLOW_SEED !== "1") { + throw new Error( + "Refusing to seed: NODE_ENV=production. Set ALLOW_SEED=1 to override (staging only).", + ); } // --- The current event (published, not archived β†’ this is what the landing From d41824fbfc18bb6ecd66ff321ca85239f9591f8b Mon Sep 17 00:00:00 2001 From: oliverhuangcode Date: Wed, 15 Jul 2026 23:43:59 +1000 Subject: [PATCH 5/9] docs: rewrite frontend/backend guides to be warmer and cohesive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reworks both handover guides for a genuine fullstack beginner: a single shared restaurant metaphor (dining room / waiter / kitchen / pantry / suppliers) runs through both, framed as Part 1 β†’ Part 2 of one story. The backend guide now picks up the request journey exactly where the frontend guide's waiter hands off, and the exercises are cross-linked as mirror images. Same technical content, friendlier voice. Co-Authored-By: Claude Opus 4.8 --- BACKEND_GUIDE.md | 301 ++++++++++++++++++--------------- FRONTEND_GUIDE.md | 412 ++++++++++++++++++++++++++-------------------- README.md | 9 +- 3 files changed, 408 insertions(+), 314 deletions(-) diff --git a/BACKEND_GUIDE.md b/BACKEND_GUIDE.md index 77e4175..eabc365 100644 --- a/BACKEND_GUIDE.md +++ b/BACKEND_GUIDE.md @@ -1,93 +1,111 @@ -# Backend guide +# Backend guide β€” the other half (Part 2 of 2) -You don't need this to redesign the frontend β€” but you asked to understand the whole stack, -and it'll make you a much stronger engineer to know what's happening on the *other side* of -every `fetch` you write. This is the companion to [`FRONTEND_GUIDE.md`](./FRONTEND_GUIDE.md); -read that one first, because it sets up the big picture this doc drills into. +You made it to Part 2. πŸ‘‹ If you've done the frontend guide and its two exercises, you already +know more than you think β€” you just know it from the *dining room* side. This guide walks you +through the kitchen door. -The backend's one job: **keep a fast, safe copy of the truth in Postgres, and hand out -exactly the right slice of it as JSON.** That's it. Everything below is detail on how. +You don't strictly need any of this to make the frontend beautiful. But every `api.…` call you +wrote in Part 1 disappeared through a door, and I don't want that door to feel like magic. +Understanding what's behind it will make you noticeably better at the frontend too β€” you'll +know *why* an endpoint returns what it does, and what's easy vs. hard to change. -> Everything backend lives under `src/server/`. It's plain **TypeScript + Express** (the web -> server) talking to **Postgres** (the database) through **Drizzle** (a type-safe query -> builder). No magic frameworks. +> **Read [`FRONTEND_GUIDE.md`](./FRONTEND_GUIDE.md) first.** This guide reuses its restaurant +> picture (dining room / waiter / kitchen / pantry / suppliers) and starts exactly where it +> stopped: the moment the waiter pushes through the kitchen door. + +The kitchen's whole job, in one sentence: **keep a fast, safe copy of the truth in the pantry, +and plate up exactly the right slice of it as JSON.** Everything below is just *how*. + +> It's all under `src/server/`, and it's refreshingly boring tech on purpose: **TypeScript + +> Express** (the web server) talking to **Postgres** (the database) through **Drizzle** (a +> type-safe way to write database queries). No mysterious framework magic. --- -## 1. What a request actually does +## 1. What happens after the waiter pushes through the door -Remember the frontend loop (page β†’ `api.ts` β†’ `fetch("/api/…")`). Here's what happens the -instant that `fetch` reaches the server: +In Part 1, a page asked the waiter (`web/src/api.ts`) for data and the waiter did a `fetch` to +`/api/…`. Here's the rest of that same journey β€” the kitchen side: ``` - fetch("/api/public/past") + the waiter's order arrives: GET /api/public/past β”‚ β–Ό - src/server/index.ts ← the front door. Matches the URL to a "router". - β”‚ "/api/public/…" is handled by publicRouter. + src/server/index.ts ← the kitchen door. Sends each order to the right station. + β”‚ "/api/public/…" β†’ the publicRouter station. β–Ό - src/server/routes/public.ts ← the route handler. THE code that runs for this URL. + src/server/routes/public.ts ← the station. The actual code that fills this order. β”‚ β–Ό - Drizzle query on Postgres ← db.select().from(events).where(...) - β”‚ + a Drizzle query on Postgres ← "grab the matching rows from the pantry" + β”‚ db.select().from(events).where(...) β–Ό - res.json({ events: [...] }) ← turn the DB rows into JSON and send them back + res.json({ events: [...] }) ← plate it up as JSON and hand it back to the waiter ``` -So for any endpoint, there are only ever two questions: -1. **Which file handles this URL?** (Look in `index.ts` β€” it maps URL prefixes to routers.) -2. **What does that handler do?** (Read the route file β€” it's usually 5–15 lines.) +That's the whole shape. For *any* endpoint in the app, you only ever need to answer two +questions: + +1. **Which file fills this order?** β†’ open `src/server/index.ts`; it maps each URL prefix to a + station (a "router"). +2. **What does that station do?** β†’ open the route file; most handlers are 5–15 readable lines. + +If you can answer those two, you can find and understand any behaviour in the backend. --- -## 2. The file map +## 2. The kitchen, room by room + +Everything's under `src/server/`. You can ignore most of it at first β€” the two files that +matter most are marked. ``` src/server/ - index.ts THE FRONT DOOR. Wires every router to a URL prefix. Read this first. - env.ts Reads + validates environment variables. Fails loudly at boot if a - required secret is missing (better than a mystery crash at 2am). + index.ts ⭐ THE KITCHEN DOOR. Wires every station to a URL. Read this first. + env.ts Reads + checks environment variables. Refuses to start if a required + secret is missing β€” a loud failure now beats a mystery crash at 2am. db/ - schema.ts THE MOST IMPORTANT FILE. Defines every database table as TypeScript. - index.ts The database connection (the `db` object you query with). - migrate.ts Applies migrations on startup. + schema.ts ⭐ THE MOST IMPORTANT FILE. Describes every pantry shelf (table). + index.ts The pantry connection β€” the `db` object you run queries on. + migrate.ts Applies database changes on startup. auth/ - jwt.ts Verifies mac-auth sign-in tokens. We never build our own login. - middleware.ts requireAuth / requireOrganiser β€” the guards you put on routes. + jwt.ts Checks sign-in tokens from mac-auth. We never build our own login. + middleware.ts requireAuth / requireOrganiser β€” the "members only" guards for routes. - routes/ One file per area. Each exports a "router" wired up in index.ts. - health.ts "is the server alive?" - public.ts public site data (no login needed) + routes/ One file per area. Each is a station wired up in index.ts. + health.ts "is the kitchen alive?" + public.ts public site data (no sign-in) me.ts "who am I?" - dashboard.ts the participant dashboard payload - teams.ts create/join/invite/leave teams - events.ts organiser event CRUD + dashboard.ts everything the participant dashboard needs + teams.ts create / join / invite / leave teams + events.ts organiser event management tickets.ts trigger ticket syncs / CSV import - organiser.ts the gap report, override queue, exports + organiser.ts the gap report, override queue, CSV exports stats.ts πŸ‘ˆ YOUR EXERCISE (a stub β€” see Β§6) - content/ Notion β†’ Postgres sync (the public-site content) - tickets/ Humanitix β†’ Postgres sync (who holds a ticket) - teams/ team status rules (status is derived, never set by hand) - participants/ verification logic - lib/ small shared helpers (audit log, discord alerts, codes…) + content/ the Notion β†’ pantry delivery (public-site content) + tickets/ the Humanitix β†’ pantry delivery (who holds a ticket) + teams/ team status rules (status is worked out, never set by hand) + participants/ ticket-verification logic + lib/ small shared helpers (audit log, Discord alerts, codes…) ``` -If you only read two files to "get" the backend, read **`index.ts`** (how URLs map to code) -and **`db/schema.ts`** (what data exists). Everything else is variations on a theme. +Honestly, if you read just two files to "get" the backend, read **`index.ts`** (how orders +find their station) and **`db/schema.ts`** (what data exists at all). Everything else is a +variation on the pattern in Β§1. --- -## 3. The database, via Drizzle +## 3. The pantry, via Drizzle -Postgres stores the data in **tables** (like spreadsheets: `events`, `participants`, `teams`, -`tickets`…). We never write raw SQL by hand β€” we use **Drizzle**, which lets us describe -tables in TypeScript (`db/schema.ts`) and query them with autocomplete and type-checking. +The database keeps data in **tables** β€” think spreadsheets: an `events` table, a +`participants` table, a `teams` table, and so on. We never hand-write SQL; instead we use +**Drizzle**, which lets us describe each table in TypeScript and then query it with real +autocomplete and type-checking (so your editor catches typos before the code ever runs). -A table definition (trimmed from `schema.ts`): +Here's a table, trimmed from `db/schema.ts`: ```ts export const events = pgTable("events", { @@ -100,7 +118,8 @@ export const events = pgTable("events", { }); ``` -Querying it (from `routes/public.ts`) reads almost like English: +And here's a real query against it, from `routes/public.ts` β€” notice it reads almost like a +sentence: ```ts const rows = await db @@ -110,115 +129,133 @@ const rows = await db .orderBy(desc(events.startsAt)); ``` -`eq` = equals, `and` = both conditions, `desc` = newest first. That's 90% of what you need. +`eq` = "equals", `and` = "both of these are true", `desc` = "newest first". That small +vocabulary covers most of what you'll ever write. + +**Changing the pantry's shelves** β€” adding a column or a whole table β€” is a little two-step +ritual worth knowing exists (you won't need it for the exercise, which only reads): -**Changing the shape of the database** (adding a column, a table) is a two-step ritual: 1. Edit `db/schema.ts`. -2. Run `npm run db:generate` β€” Drizzle writes a **migration** (a `.sql` file in `drizzle/`) - describing the change, then `npm run db:migrate` applies it. +2. Run `npm run db:generate`. Drizzle writes a **migration** β€” a `.sql` file in `drizzle/` + describing the change β€” and `npm run db:migrate` applies it. -Migrations are how the database changes safely and repeatably, on every machine and in -production β€” never by hand-editing the live database. You won't need this for the stats -exercise (it only reads), but it's good to know the ritual exists. +Migrations are how the database changes the same way everywhere: your laptop, staging, and +production, all in the right order, without anyone hand-editing a live database at midnight. --- -## 4. A few backend principles worth understanding - -These come straight from the project spec, and they explain *why* the code looks careful in -places. You don't have to memorise them β€” just recognise them when you see them: - -- **Never expose raw database rows.** Public endpoints hand back a hand-picked *whitelist* of - fields (see `toPublicEvent()` in `public.ts`). Internal stuff (a Humanitix event id, ticket - PII) must never leak into a public JSON response. When you add an endpoint, decide on - purpose what goes in it. -- **Auth is checked on the server, every time.** A hidden button in the UI is not security. - Protected routes put a guard in front: `router.get("/me", requireAuth, handler)` (see - `me.ts`). Organiser-only routes add `requireOrganiser`. The token's signature is - re-verified on every request β€” we never trust what the browser claims. -- **Read from Postgres, not from Notion/Humanitix, on a page request.** Those are synced into - Postgres on a timer (`content/`, `tickets/`). If Notion is down, the site is fine β€” it's - serving the last good copy. A page render must be fast and must not depend on someone - else's API being up. -- **Nothing is hard-deleted.** "Deleting" flips a flag (`isArchived`, a `withdrawn` status). - The row stays. And every consequential action is written to an append-only `audit_log`, so - we can always answer "who changed this, and when?" -- **Some things are computed, not stored.** A team's status (`forming` / `confirmed` / …) is - *derived* from its members and their tickets (`teams/status.ts`), never set by hand β€” so it - can't drift out of sync with reality. - -You'll notice the ticket-sync code (`tickets/sync.ts`) is especially defensive β€” it has a -"safety gate" that refuses to un-verify a huge chunk of attendees in one sweep. That's -deliberate: it's the difference between a bug and 200 people locked out the night before the -event. Read the comments there if you're curious; it's a great example of *defensive backend -thinking*. +## 4. Why the kitchen looks so careful (the house rules) + +You'll notice the backend is written defensively in places. That's not paranoia β€” each rule +below comes from a real "imagine if this went wrong the night before the event" scenario. You +don't need to memorise them; just recognise them when you see them, because they explain a lot +of the code's shape: + +- **Never send raw pantry rows out to guests.** Public endpoints hand back a hand-picked + *whitelist* of fields (see `toPublicEvent()` in `public.ts`). Internal things β€” a Humanitix + id, someone's personal details β€” must never slip into a public response. When you write an + endpoint, choose on purpose what goes in it. +- **Check "members only" on the server, every single time.** A hidden button in the dining + room is *not* security β€” anyone can call the API directly. Protected routes put a guard in + front: `router.get("/me", requireAuth, handler)` (see `me.ts`), and organiser-only routes + add `requireOrganiser`. The sign-in token is re-checked on every request; we never just + trust what the browser claims to be. +- **Cook from the pantry, not from the supplier, on a page load.** Notion and Humanitix are + synced into Postgres on a timer (that's the `content/` and `tickets/` folders). So if Notion + is down, the site shrugs and serves the last good copy. A page must be fast and must never + depend on someone else's API being up at that exact second. +- **Nothing is ever truly deleted.** "Deleting" flips a flag (`isArchived`, a `withdrawn` + status); the row stays. And every meaningful action gets written to an append-only + `audit_log`, so we can always answer "who changed this, and when?" +- **Some things are worked out, not stored.** A team's status (`forming` / `confirmed` / …) + is *calculated* from its members and their tickets (`teams/status.ts`), never set by hand β€” + so it can never drift out of step with reality. + +The clearest example of this mindset is `tickets/sync.ts`. It has a **safety gate** that flat +-out refuses to un-verify a big chunk of attendees in a single sync β€” because the difference +between a normal bug and "200 people are locked out an hour before doors open" is enormous. +Have a read of the comments there someday; it's a lovely example of thinking about what happens +when things go wrong, which is most of what senior backend work actually is. --- -## 5. Running & poking at the backend +## 5. Running and poking at the kitchen -Same setup as the frontend guide (`npm install`, `docker compose up -d db`, `npm run db:seed`, -`npm run dev`). -Once it's running, the backend is at **http://localhost:3000** and you can hit endpoints -directly from your terminal β€” no frontend needed: +Setup is identical to Part 1 (`npm install`, `docker compose up -d db`, `npm run db:seed`, +`npm run dev`). The nice thing about the backend is you can talk to it directly from the +terminal β€” no browser needed β€” using `curl`, which is just "make a web request from the +command line": ```bash -curl http://localhost:3000/api/health # {"status":"ok","db":"ok"} -curl http://localhost:3000/api/public/past # {"events":[...]} -curl http://localhost:3000/api/public/stats # your exercise β€” see below +curl http://localhost:3000/api/health # {"status":"ok","db":"ok"} +curl http://localhost:3000/api/public/past # {"events":[...]} +curl http://localhost:3000/api/public/stats # your exercise β€” see Β§6 ``` -`curl` is just "make an HTTP request from the command line." It's the fastest way to check a -backend endpoint in isolation. (For endpoints that need login, it's easier to test through -the running site with dev sign-in β€” don't worry about auth for the exercise.) +Poking one endpoint at a time like this is the fastest way to understand it in isolation. (For +routes that need sign-in it's easier to test through the running site with dev sign-in β€” but +you won't need auth for the exercise.) -When the backend crashes or misbehaves, look at the **terminal running `npm run dev`** β€” that's -where server errors and `console.log` output appear (the browser console only shows frontend -errors). +**When the backend misbehaves, watch the terminal running `npm run dev`.** That's where server +errors and any `console.log` you add show up. (The *browser* console only shows dining-room +errors β€” a common early confusion. Frontend problems: browser console. Backend problems: +terminal.) --- -## 6. Exercise: your first endpoint (`src/server/routes/stats.ts`) +## 6. Exercise β€” build your first endpoint (`src/server/routes/stats.ts`) -A tiny, self-contained backend task that mirrors frontend Exercise 1 β€” but on the server -side. The file is already created, stubbed, and wired into `index.ts`, so it's live right now: +This is the mirror image of frontend Exercise 1: there you drew data the kitchen already sent; +here you build the kitchen end of a brand-new order. The file is already created, stubbed, and +wired into `index.ts`, so it's *live right now* β€” `curl http://localhost:3000/api/public/stats` returns `{"pastEventCount":0}`. Your job is to -make that number real. +make that number honest. -**Goal:** make `GET /api/public/stats` return the actual count of past events. +**Goal:** make `GET /api/public/stats` return the real number of past events. -**How to approach it:** -1. Open `src/server/routes/public.ts` and find the `/public/past` handler. It already queries - for exactly the rows you want (published **and** archived events). You're reusing that - `.where(...)` filter. -2. In `stats.ts`, run that query and return the *count* instead of the list. Simplest version: - fetch the rows and return `rows.length`. (Uncomment the imports at the top of the file as - you need them.) -3. Restart isn't needed β€” `npm run dev` reloads on save. Re-run the `curl` and watch the - number change. +**A gentle way in:** +1. Open `src/server/routes/public.ts` and find the `/public/past` handler. It already asks the + pantry for exactly the rows you care about (published **and** archived events). You're going + to reuse that same `.where(...)` filter. +2. In `stats.ts`, run that query and return the *count* rather than the list. The simplest + version: fetch the rows and return `rows.length`. (Uncomment the imports at the top of the + file as you reach for them.) +3. No restart needed β€” `npm run dev` reloads on save. Re-run the `curl` and watch the number + change. That instant feedback loop is the fun part. -**How you'll know it works:** after `npm run db:seed` there are 2 past events, so a correct -implementation returns `{"pastEventCount":2}` (not `0`). +**You'll know it worked:** after `npm run db:seed` there are 2 past events, so a correct +version returns `{"pastEventCount":2}` instead of `0`. -**Ties back to the frontend:** once it works, you could call it from a new `api.stats()` in -`web/src/api.ts` and show "N hackathons and counting" on your redesigned landing page β€” a -complete feature you built through *every* layer of the stack. That's the whole thing. πŸŽ‰ +**And here's the whole point** β€” go back to the dining room and finish the circle: add an +`api.stats()` function to `web/src/api.ts` (the waiter learns a new order) and show +"N hackathons and counting" somewhere on your redesigned landing page. That's one small +feature you built through *every* layer β€” pantry, kitchen, waiter, dining room. Once you've +done that, none of this is magic anymore. πŸŽ‰ -> Stretch: also return `publishedEventCount` (published but not archived). And if you want to -> see the full "add a column" ritual, ask Oliver for a small schema exercise. +> Want more? Stretch goal: also return `publishedEventCount` (published but not archived). And +> if you'd like to see the full "add a new column" ritual from Β§3 for real, grab me β€” it's a +> great next exercise. --- -## 7. What NOT to change (for now) +## 7. What to leave alone for now + +While you're finding your feet, steer clear of these unless we're pairing on it. They have +sharp edges and real-world consequences: -While you're finding your feet, steer clear of these unless you're pairing with Oliver β€” they -have sharp edges and real consequences: +- **`tickets/sync.ts` and the safety gate** β€” get this wrong and real people get locked out. +- **`auth/`** β€” we never build authentication; mac-auth owns it entirely. +- **Anything that writes to `audit_log`, or that hard-deletes a row** β€” please don't add hard + deletes; it breaks a promise the whole system relies on. +- **`db/schema.ts` migrations against production data.** -- `tickets/sync.ts` and the safety gate β€” getting this wrong can lock real people out. -- `auth/` β€” we never build auth; mac-auth owns it. -- Anything that writes to `audit_log` or that hard-deletes a row (don't add hard deletes). -- `db/schema.ts` migrations on production data. +Adding **read-only** endpoints β€” exactly like the stats exercise β€” is always safe. That's the +corner of the kitchen to play in first. + +--- -Adding *read-only* endpoints (like the stats exercise) is always safe. Start there. +That's the whole machine, both halves. You came in as "the frontend person" and now you can +read a request from the button a guest clicks all the way down to the pantry shelf and back. +That's genuinely full-stack β€” well done. -Questions β†’ ask Oliver. Welcome to the backend. πŸš€ +Any question, however small, come find me. Welcome to the kitchen. πŸš€ diff --git a/FRONTEND_GUIDE.md b/FRONTEND_GUIDE.md index b27ed65..6bfbc93 100644 --- a/FRONTEND_GUIDE.md +++ b/FRONTEND_GUIDE.md @@ -1,278 +1,334 @@ -# Frontend guide & handover +# Frontend guide β€” start here (Part 1 of 2) -Welcome! πŸ‘‹ You're taking over the **frontend** of the MAC Hackathon platform β€” the part -people actually see and click. This doc teaches you how the whole thing fits together (so -the frontend makes sense, not just "magic that works"), how to run it on your laptop, and -gives you **two hands-on exercises** to get your hands dirty before you start redesigning. +Hey! πŸ‘‹ Welcome to the MAC Hackathon platform. You're taking over the **frontend** β€” the part +people actually see, click, and (fingers crossed) enjoy using. This guide gets you from +"I've just cloned this repo" to "I understand how it works and I've changed real code," and +then turns you loose on the redesign. -You don't need to touch the backend to redesign the frontend. But you *should* understand -how they talk, because every screen you build is really "fetch some data, then draw it." +You genuinely don't need to know the backend to make the frontend beautiful. But I don't want +any of this to feel like magic you're afraid to touch, so this guide explains the whole shape +of the thing, and its companion β€” **[`BACKEND_GUIDE.md`](./BACKEND_GUIDE.md)** (Part 2) β€” walks +the other half when you're curious. Read them in order; Part 2 literally picks up the story +where this one leaves off. -> New to git? There's a **Git cheat-sheet** at the bottom. Read that first if you've never -> made a branch or a pull request. +Here's the plan for your first day or two: +1. Read Β§1–§2 to get the mental model (10 minutes, no typing). +2. Get it running on your laptop (Β§4). +3. Do the two small **exercises** (Β§6) β€” this is where it clicks. +4. Start redesigning (Β§7). + +> **Brand new to git?** Skip to the **cheat-sheet in Β§8** and read that first β€” it's the one +> tool you'll use constantly, and a little confidence there makes everything else calmer. --- -## 1. The big picture: where does the data come from? +## 1. The big picture (a restaurant) + +The single most useful thing to understand up front: **the website doesn't make up its own +data.** Every price, name, and date on the screen came from somewhere else and travelled to +the browser. Once you can picture that journey, every page makes sense. + +The easiest way to hold it in your head is a restaurant: + +- **The dining room** is the **frontend** (React, in `web/`) β€” the tables, the menus, the + stuff guests see and touch. **This is your patch.** +- **The waiter** is one small file, **`web/src/api.ts`** β€” they carry your order to the + kitchen and bring the food back. Guests don't wander into the kitchen themselves. +- **The kitchen** is the **backend** (Express, in `src/server/`) β€” it does the actual work. + Guests never go in, but every dish comes from there. +- **The pantry** is the **database** (Postgres) β€” stocked shelves the kitchen cooks from. + It's right there, so it's fast. +- **The suppliers** are **Notion** and **Humanitix** β€” they deliver fresh stock on a + schedule. The kitchen keeps the pantry stocked so it never has to phone a supplier in the + middle of dinner service. -The most important thing to understand: **the website does not invent its own data.** Every -screen is drawing numbers and text that came from somewhere else. There are three sources: +Drawn out, a plate of data travels like this: ``` - Notion (a fancy doc) Humanitix (ticket sales) Organisers (admin panel) - prizes, judges, FAQ, who bought a ticket create the event, - schedule, sponsors… for the hackathon trigger syncs + Notion (a shared doc) Humanitix (ticket sales) Organisers (admin panel) + prizes, judges, FAQ, who bought a ticket create the event, + schedule, sponsors… for the hackathon press "sync now" β”‚ β”‚ β”‚ - β”‚ (synced on a timer) β”‚ (synced on a timer) β”‚ (saved directly) + β”‚ delivered on a timer β”‚ delivered on a timer β”‚ saved directly β–Ό β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Postgres (our database) β”‚ - β”‚ one place that holds a *copy* of everything, always fast β”‚ + β”‚ THE PANTRY β€” Postgres (our database) β”‚ + β”‚ one fast, local copy of everything the site needs β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β–² + β”‚ the KITCHEN reads the pantry and plates up JSON β”‚ - β”‚ the backend reads Postgres and hands out JSON - β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Backend API (Express, in src/server/) β”‚ - β”‚ e.g. GET /api/public/event β†’ { event: {...}, content: {...} } β”‚ + β”‚ THE KITCHEN β€” backend API (Express, src/server/) β”‚ + β”‚ e.g. GET /api/public/event β†’ { event: {…}, content: {…} } β”‚ ← Part 2 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β–² + β”‚ the WAITER (web/src/api.ts) carries the order and brings JSON back β”‚ - β”‚ the frontend fetches that JSON - β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Frontend (React, in web/) ← THIS IS YOUR PATCH β”‚ + β”‚ THE DINING ROOM β€” frontend (React, web/) ← YOU ARE HERE β”‚ β”‚ turns JSON into buttons, cards, and text on the page β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` -**A common misconception to clear up:** the public info (prizes, judges, schedule…) comes -from **Notion**, not from the admin panel. The admin panel just lets organisers create the -event and press "sync now." The content itself lives in a Notion database, gets copied into -Postgres on a timer, and the website reads it from Postgres. (Why the copy? So the site -stays up and fast even if Notion is slow or down.) Ticket info works the same way, but the -source is Humanitix. +Why keep a *copy* in the pantry instead of asking Notion every time someone loads the page? +Same reason a kitchen keeps stock: it's faster, and if a supplier's truck is late (Notion is +down), you can still serve dinner from what's on the shelf. That "copy it on a schedule" job +is the backend's world β€” it's the whole second half of the story, so don't worry about it yet. -You almost never care *which* original source something came from. By the time it reaches -your React code, it's just JSON from our own API. +**The one thing to take away:** by the time data reaches your React code, you don't care +whether it started in Notion or Humanitix. It's just JSON, handed to you by the waiter. --- -## 2. The request lifecycle (the loop you'll repeat all day) +## 2. The loop you'll repeat all day -Every interactive screen is the same four steps. Learn this once and every page makes sense: +Almost every screen you build is the same four steps. Learn this rhythm once and the rest is +detail: ``` -1. React page loads ──▢ 2. calls a function in web/src/api.ts - β”‚ - β–Ό - 3. that does fetch("/api/…") to the backend - β”‚ - β–Ό backend reads Postgres, returns JSON -4. React stores the JSON in state and renders it β—€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +1. A page loads ──▢ 2. it asks the waiter for data (a function in web/src/api.ts) + β”‚ + β–Ό + 3. the waiter fetches it from the kitchen ( /api/… ) + β”‚ + β–Ό kitchen reads the pantry, returns JSON +4. the page saves that JSON and draws it on screen β—€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` -If the user *changes* something (joins a team, ticks a box), it's the same loop with one -extra step: send the change (a `POST`/`PATCH`), then **re-fetch** so the screen matches the -new reality. You'll do exactly this in Exercise 2. +When the user *changes* something β€” joins a team, ticks a box β€” it's the same loop with one +extra beat: **send the change, then ask for the data again** so the screen matches reality. +(That "ask again" habit saves you a world of confusing bugs. More on it in Exercise 2.) -**You should never write `fetch("/api/…")` directly in a page.** All the network calls live -in one file β€” `web/src/api.ts` β€” as tidy named functions like `api.pastEvents()`. Your pages -call those. This keeps auth, error handling, and URLs in one place. If you need a new call, -add it to `api.ts` first, then use it. +**One rule that matters:** never call `fetch("/api/…")` straight from a page. All the kitchen +orders live in one place β€” **`web/src/api.ts`**, our waiter β€” as tidy named functions like +`api.pastEvents()`. Your pages call *those*. It keeps every network detail (the URL, the +sign-in token, error handling) in one file instead of scattered everywhere. Need something the +waiter doesn't offer yet? Add the function to `api.ts` first, then use it in your page. + +> That `api.ts` file is exactly the seam where this guide hands off to Part 2: the waiter is +> the last thing on *your* side of the kitchen door. What happens after they push through it +> is the backend guide's job. --- -## 3. The frontend file map +## 3. The files you'll live in -Everything you own is under `web/`. You can mostly ignore `src/server/` (that's the backend). +Everything you own is under `web/`. You can happily ignore `src/server/` for now β€” that's the +kitchen, and Part 2 gives you the tour. ``` web/ - index.html the single HTML page everything mounts into + index.html the single HTML page everything loads into src/ - main.tsx ROUTER: which URL shows which page. Start here. - api.ts ALL calls to the backend live here (api.pastEvents(), etc.) - auth.ts sign-in / token plumbing β€” you rarely touch this + main.tsx the ROUTER: which URL shows which page. A good first read. + api.ts the WAITER: every call to the backend lives here + auth.ts sign-in / token plumbing β€” you'll rarely touch this format.ts date/time helpers (fmtDateRange, fmtTime) - styles.css Tailwind theme + shared component classes (colours, buttons…) + styles.css Tailwind theme: our colours + shared classes (buttons, cards…) pages/ - Landing.tsx public homepage βœ… WORKS β€” read this as your example - Dashboard.tsx "am I in the hackathon?" page βœ… WORKS β€” best example - Admin.tsx organiser control panel βœ… WORKS - Past.tsx archive of old events 🚧 EXERCISE 1 (stubbed for you) + Landing.tsx public homepage βœ… WORKS β€” your best worked example + Dashboard.tsx "am I in the hackathon?" βœ… WORKS β€” the read-and-write example + Admin.tsx organiser control panel βœ… WORKS + Past.tsx archive of old events 🚧 EXERCISE 1 (stubbed for you) FindTeam.tsx the teammate pool 🚧 EXERCISE 2 (stubbed for you) components/ - SignInPanel.tsx "please sign in" box - ClaimForm.tsx ticket-claiming form - CustomFieldsForm.tsx extra event questions + SignInPanel.tsx the "please sign in" box + ClaimForm.tsx ticket-claiming form + CustomFieldsForm.tsx extra event questions ``` -The two 🚧 files have been **deliberately emptied out into guided stubs** so you can rebuild -them yourself β€” that's how you'll learn the fetch-then-render loop. The βœ… pages are complete -and are your reference: whenever you're stuck, open `Landing.tsx` or `Dashboard.tsx` and see -how they did it. +The two 🚧 pages have been **deliberately hollowed out into guided stubs** β€” you're going to +rebuild them, and that's how the whole loop from Β§2 stops being theory. The βœ… pages are +finished and working, and they're your safety net: whenever you're unsure how to do +something, open `Landing.tsx` or `Dashboard.tsx` and copy how *they* did it. Reading working +code is not cheating β€” it's most of the job. --- -## 4. Running it on your laptop +## 4. Getting it running -You need [Node 22](https://nodejs.org) and [Docker Desktop](https://www.docker.com/products/docker-desktop/) -installed. Then: +You'll need [Node 22](https://nodejs.org) and +[Docker Desktop](https://www.docker.com/products/docker-desktop/) installed (Docker just runs +the pantry β€” the Postgres database β€” so you don't have to install it by hand). Then, from the +project folder: ```bash -npm install # once, to grab dependencies -cp .env.example .env # then open .env and set: +npm install # once β€” downloads the project's dependencies +cp .env.example .env # then open .env and set two things: # DATABASE_URL=...@localhost:5433/mac_hackathon # DEV_AUTH=1 -docker compose up -d db # starts just the Postgres database (in Docker) -npm run db:migrate # creates the database tables -npm run db:seed # fills the empty DB with sample data (see Β§4) -npm run dev # starts backend (:3000) + frontend (:5173) -``` - -Then open **http://localhost:5173** in your browser. That's the Vite dev server β€” it -**hot-reloads**, meaning when you save a `.tsx` file the page updates instantly. This is -where you'll do all your work. - -**Signing in locally.** Real sign-in only works on the live `monashcoding.com` site. On your -laptop, because you set `DEV_AUTH=1`, there's a **fake dev sign-in**: a panel lets you type -any name/email and tick an "organiser" box, and it just works. This is only ever on in dev β€” -it can't be turned on in production. - -**Two useful checks if something looks broken:** -- Open the browser DevTools (F12) β†’ **Console** tab for React errors, and **Network** tab to - watch the `/api/...` calls and see what JSON came back. This is your #1 debugging tool. -- `http://localhost:3000/api/health` should say `{"status":"ok"}` β€” that confirms the - backend is alive. - -A fresh local database is **empty**, so pages look blank at first β€” that's expected, not a -bug. Fill it with realistic sample data (one upcoming event, two past events, prizes/judges/ -schedule/FAQ) with one command: - -```bash -npm run db:seed +docker compose up -d db # start just the database (in Docker) +npm run db:migrate # create the empty tables +npm run db:seed # stock the pantry with sample data (see the note below) +npm run dev # start the kitchen (:3000) and the dining room (:5173) ``` -It's safe to run repeatedly. Now the landing page, `/past`, and the dashboard all have -something to render (and to redesign). +Now open **http://localhost:5173**. That's the Vite dev server, and its superpower is +**hot reload**: save a `.tsx` file and the page updates in the browser instantly, no refresh. +This is where you'll spend all your time. + +**A fresh database is empty**, so without that `npm run db:seed` step the pages look blank β€” +which is expected, not a bug you caused. The seed command stocks the pantry with realistic +sample data (one upcoming event, two past ones, plus prizes/judges/schedule/FAQ) so every +page has something to show and to restyle. It's safe to re-run any time. + +**Signing in on your laptop.** Real sign-in only works on the live `monashcoding.com` site, so +locally there's a stand-in: because you set `DEV_AUTH=1`, a little dev sign-in panel lets you +type any name/email (and tick "organiser" if you want to see the admin pages). This shortcut +only exists in dev β€” it physically can't be switched on in production, so don't worry about +it leaking. + +**Your two best friends when something looks broken:** +- **Browser DevTools** (press F12). The **Console** tab shows React errors in red; the + **Network** tab lets you watch each `/api/…` call and click it to see exactly what JSON came + back. When a page misbehaves, look here *first* β€” it usually tells you whether the problem + is your React or the data it received. +- **http://localhost:3000/api/health** should say `{"status":"ok"}`. If it does, the kitchen + is alive and the problem is on your side of the door. --- -## 5. Styling: Tailwind v4 +## 5. Styling: Tailwind -We use **Tailwind CSS**. Instead of writing a separate `.css` file per component, you put +We style with **Tailwind CSS**. Instead of writing a separate stylesheet, you put small utility classes right on the element: ```tsx
…
-// ^rounded ^a border ^our colour ^our bg ^padding +// ^rounded ^a border ^our bg colour ^padding ``` -Our brand colours are defined once in `web/src/styles.css` as tokens you can use anywhere: -`bg-bg`, `bg-panel`, `text-text`, `text-muted`, `text-accent`, `border-border`, -`text-danger`, `text-ok`. So `text-accent` = our blue, `bg-panel` = the card background, etc. +Our brand colours live in one place β€” `web/src/styles.css` β€” as named tokens you can use +anywhere: `bg-bg`, `bg-panel`, `text-text`, `text-muted`, `text-accent`, `border-border`, +`text-danger`, `text-ok`. So `text-accent` is our blue, `bg-panel` is the card background, and +so on. Using the tokens (instead of hard-coding a colour) keeps the whole site consistent and +makes a future theme change a one-file edit. -That same file also defines a few **shortcut classes** built from those utilities, so common -things stay consistent: `.wrap` (centered page column), `.panel` (a card), `.muted` (grey -sub-text), `.topnav`, `.btn`, `.card`. You'll see these all over the existing pages. You're -free to redesign these β€” since it's your job to make it look good β€” but they're a comfortable -starting point. +That same file defines a few **shortcut classes** built from those utilities β€” `.wrap` (a +centered page column), `.panel` (a card), `.muted` (grey sub-text), `.topnav`, `.btn`, +`.card`. You'll spot them all over the existing pages. Redesigning them is fair game β€” making +it look good is literally your job β€” but they're a comfortable place to start. -New to Tailwind? The [official docs](https://tailwindcss.com/docs) have a search box; type -what you want ("padding", "flex", "rounded") and it shows the class. +New to Tailwind? The [docs](https://tailwindcss.com/docs) have a search box β€” type what you +want ("padding", "flex", "rounded corners") and it shows you the class. You'll memorise the +common ones within a week. --- -## 6. Your two exercises +## 6. Your two exercises (do these before redesigning) -Do these **before** the big redesign. They're small, and they teach you the whole data loop -on the real codebase. Both files are already stubbed with detailed comments β€” open them. +These are small on purpose. They walk you through the whole Β§2 loop on real code, so that by +the end you're not *reading* about how the app works β€” you've done it. Both files are already +open-able with detailed comments inside; this section is the friendly version. -### Exercise 1: the Past Events page (`web/src/pages/Past.tsx`) +### Exercise 1 β€” the Past Events page (`web/src/pages/Past.tsx`) -**Goal:** a read-only page listing past hackathons. +**What you're building:** a read-only page that lists past hackathons. No writing data yet, +just fetching and drawing β€” the gentlest possible version of the loop. -The data call already exists: `api.pastEvents()` returns `{ events: [...] }` (or `null` if -none). Your job is the React: fetch on load, show "Loading…", then map over the events and -draw each one (name, dates via `fmtDateRange`, venue, tagline, Devpost link). +The waiter already knows this order: `api.pastEvents()` hands you `{ events: [...] }` (or +`null` if there aren't any). Your job is the React around it: fetch when the page loads, show +a "Loading…" line while you wait, then map over the events and draw each one (name; dates via +the `fmtDateRange` helper; venue; tagline; a Devpost link if there is one). -**How to approach it:** -1. Open `web/src/pages/Landing.tsx`. Notice the shape: a `useState` to hold the data, a - `useEffect` that calls the API once on load, and JSX that renders it. That's the whole - trick β€” you're copying that shape. -2. Look at what `api.pastEvents()` returns and what fields a `PublicEvent` has (both are in - `web/src/api.ts` β€” hover the types in your editor). -3. Build it. Handle three states: still loading, loaded-but-empty, and loaded-with-events. +**A gentle way in:** +1. Open `web/src/pages/Landing.tsx` and look at its shape β€” a `useState` to hold the data, a + `useEffect` that calls the waiter once when the page loads, and some JSX that draws the + result. That shape *is* the trick. You're copying it. +2. Peek at `api.pastEvents()` and the `PublicEvent` type in `web/src/api.ts` so you know what + fields you're getting (hover them in your editor β€” the types tell you). +3. Build it, handling three moments: still loading, loaded-but-empty, and loaded-with-events. + Real pages always think about all three. -You'll know it works when the seeded past events (from `npm run db:seed`) show up on `/past`. +**You'll know it worked** when the seeded past events show up at `/past` in your browser. πŸŽ‰ -### Exercise 2: the Find-a-Team page (`web/src/pages/FindTeam.tsx`) +### Exercise 2 β€” the Find-a-Team page (`web/src/pages/FindTeam.tsx`) -**Goal:** a page that both reads *and* writes. Harder β€” this is the real skill. +**What you're building:** a page that both *reads and writes*. This is the real skill, and +it's the boss level of the loop β€” take your time. It shows a pool of people looking for a team, lets you tick "I'm looking for a team" (which -saves to the server), and β€” if you lead a team with a spare seat β€” lets you invite someone. - -**How to approach it:** -1. This time read `web/src/pages/Dashboard.tsx` as your model β€” it does the full - **load β†’ let the user act β†’ send the change β†’ re-fetch** cycle. -2. The calls you need are already in `api.ts`: `api.findTeam()` (load), `api.updateProfile(...)` - (opt in/out), `api.inviteFromPool(...)` (invite). The stub comment lists them. -3. Two things that trip people up, and how the reference page handles them: - - **Signed out?** `api.findTeam()` throws a `NotSignedInError`. Catch it and show - `` instead of crashing. - - **After a write, always re-fetch.** Don't try to hand-edit local state to match β€” just - call your load function again. It's simpler and always correct. - -You'll know it works when ticking the box and reloading keeps the box ticked (it saved), and -the pool list updates after you invite someone. - -> **Stuck? The original, working versions of both files exist** in git on the -> `mac-hackathon-mvp` branch. Try it yourself first β€” but if you want to peek at a solution: -> `git show mac-hackathon-mvp:web/src/pages/Past.tsx`. Learning to read someone else's -> solution *after* attempting it is a real skill; use it that way. +**saves** to the server), and β€” if you lead a team with a spare seat β€” lets you invite +someone from the pool. + +**A gentle way in:** +1. This time, read `web/src/pages/Dashboard.tsx` as your model. It does the full dance: + **load β†’ let the user do something β†’ send the change β†’ ask for the data again.** +2. The waiter already has every order you need (they're listed in the stub's comments): + `api.findTeam()` to load, `api.updateProfile(...)` to opt in/out, `api.inviteFromPool(...)` + to invite. +3. Two things that trip everyone up the first time β€” and how the reference page handles them: + - **Not signed in?** `api.findTeam()` throws a `NotSignedInError`. Catch it and show the + `` component instead of letting the page crash. + - **After you save a change, re-fetch.** Resist the urge to hand-edit the on-screen data to + match what you just sent. Just call your load function again and let fresh data redraw the + page. It's less code and it's never wrong. + +**You'll know it worked** when ticking the box and reloading keeps it ticked (proof it saved), +and the pool updates after you invite someone. + +> **Stuck, and want to see how it's done?** The original working versions of both pages are +> still in git on the `mac-hackathon-mvp` branch. Have a real go first β€” struggling for a bit +> is where the learning happens β€” but when you want to check your thinking: +> `git show mac-hackathon-mvp:web/src/pages/Past.tsx`. Reading a solution *after* you've +> attempted it is a genuine skill; that's the way to use it. +> +> **Curious what happens after the waiter disappears into the kitchen?** That exact question +> is Part 2 β€” **[`BACKEND_GUIDE.md`](./BACKEND_GUIDE.md)** β€” and it has a matching little +> exercise that builds the *other* end of an `api.…` call. --- -## 7. Then: the redesign +## 7. The redesign -Once those two work, you understand the whole frontend. Now make it beautiful. Suggested +Once those two work, you understand the frontend β€” really. Now go make it lovely. A sensible order: -1. Start with `Landing.tsx` (the public homepage β€” most eyes on it, most fun to design). -2. Then `Dashboard.tsx` β€” but be careful: read the top comment in that file. It's the single - most important page (a participant must never leave it unsure whether they're in the - hackathon). Redesign the *look*, keep every piece of *information* it shows. -3. Keep it mobile-friendly β€” lots of people open this on their phone. -Design freely, but keep the data each page shows intact β€” you're changing how it looks, not -what it says. If you find you need data that isn't there, that's a backend change: write it -down and talk to Oliver rather than faking it in the frontend. +1. **`Landing.tsx`** first β€” the public homepage. Most eyes land here, and it's the most fun + to design. +2. **`Dashboard.tsx`** next β€” but read the comment at the top of that file before you start. + It's the most important page in the whole app: a participant should never leave it unsure + whether they're actually in the hackathon. Restyle the *look* all you like; keep every + piece of *information* it currently shows. +3. Design **mobile-first** β€” a lot of people open this on their phone between classes. + +The golden rule: change how a page *looks*, not what it *says*. If you find yourself wanting +data that isn't there, that's a backend change β€” jot it down and talk to me rather than faking +it in the frontend. (And if you're curious how you'd add it yourself, that's Part 2. πŸ˜‰) --- -## 8. Git cheat-sheet (if you're new to this) +## 8. Git cheat-sheet (if this is new) -You're on a branch called `frontend-redesign` β€” your own copy where you can't break anyone -else's work. The normal loop: +You're working on a branch called `frontend-redesign` β€” think of it as your own copy of the +project where you can experiment freely without breaking anyone else's work. The everyday +rhythm: ```bash -git status # what have I changed? -git add -A # stage all my changes -git commit -m "Rebuild Past page" # save a snapshot, with a message -git push # upload your branch to GitHub +git status # what have I changed? +git add -A # stage all my changes, ready to save +git commit -m "Rebuild Past page" # save a snapshot, with a short message +git push # upload your branch to GitHub ``` -Commit **little and often** β€” every time something works, commit it. Good messages describe -what you did ("Add loading state to Find a Team"), not "stuff" or "wip". +**Commit little and often** β€” every time something works, save it. Future-you will thank +present-you. Good messages say what you did ("Add loading state to Find a Team"), not "stuff" +or "wip". -When a chunk of work is ready for Oliver to look at, open a **Pull Request** (PR) on GitHub -from your branch β€” that's how you ask "please review and merge my changes." Don't commit -straight to `main`. +When a piece of work is ready for me to look at, open a **Pull Request** on GitHub from your +branch β€” that's the "hey, please review this" button. Don't commit straight to the main +branch. -If you get into a mess, **don't panic and don't force anything** β€” stop and ask. Almost -nothing in git is truly unrecoverable, but the fixes are much easier before you try random -commands. +And if you ever end up in a tangle: **stop, don't force anything, and ask.** Almost nothing in +git is truly unrecoverable, but the fixes are far easier *before* trying random commands you +found online. Getting stuck is completely normal β€” reaching out early is the pro move, not the +beginner one. --- -Any questions, ask Oliver. Have fun β€” this is a real thing real people will use. πŸŽ‰ +That's everything you need to start. When you're comfortable here and want to see the other +half of the machine, **[`BACKEND_GUIDE.md`](./BACKEND_GUIDE.md)** is waiting. + +Any questions at all, ask me β€” no question is too small. Have fun with it; real people are +going to use what you build. πŸŽ‰ diff --git a/README.md b/README.md index b5ccdfe..7381f53 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,11 @@ MAC's hackathon platform: a public info site (Notion-driven) plus team registrat Read [`SPEC_hackathon.md`](./SPEC_hackathon.md) β€” it is the source of truth. This README covers running the thing. -> **New here / redesigning the frontend?** Start with [`FRONTEND_GUIDE.md`](./FRONTEND_GUIDE.md) β€” -> a from-scratch walkthrough of how the frontend works, how it talks to the backend, and two -> hands-on exercises. Then [`BACKEND_GUIDE.md`](./BACKEND_GUIDE.md) explains the server side -> (Express + Drizzle + Postgres) with a matching exercise. +> **New here / redesigning the frontend?** There's a two-part, beginner-friendly walkthrough +> that reads as one story. Start with **[`FRONTEND_GUIDE.md`](./FRONTEND_GUIDE.md)** (Part 1) β€” +> how the frontend works, how it talks to the backend, and two hands-on exercises β€” then +> **[`BACKEND_GUIDE.md`](./BACKEND_GUIDE.md)** (Part 2) picks up where it leaves off and walks +> the server side (Express + Drizzle + Postgres) with a matching exercise. ## Stack From b50a87d463ea1803cffa046bb5d4a97f69744247 Mon Sep 17 00:00:00 2001 From: oliverhuangcode Date: Mon, 20 Jul 2026 21:51:01 +1000 Subject: [PATCH 6/9] fix(staging): use single-level subdomain so Cloudflare TLS covers it staging.hackathons.monashcoding.com is a nested subdomain that Cloudflare's free Universal SSL cert (monashcoding.com + *.monashcoding.com) doesn't cover, so the TLS handshake fails at Cloudflare's edge with ERR_SSL_VERSION_OR_CIPHER_MISMATCH before ever reaching Traefik. Move the preview to the single-level host staging-hackathons.monashcoding.com, which Universal SSL covers with no paid Advanced Certificate Manager to renew. Updates the compose PUBLIC_URL + Traefik Host rule and the deploy runbook, with a note so the next committee doesn't recreate the nested-subdomain bug. Co-Authored-By: Claude Opus 4.8 --- docker-compose.staging.yml | 14 +++++++++++--- docs/deploy-dokploy.md | 23 +++++++++++++++++------ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml index b0071c4..3d01be5 100644 --- a/docker-compose.staging.yml +++ b/docker-compose.staging.yml @@ -1,7 +1,15 @@ # Staging compose for Dokploy β€” a persistent PREVIEW of the frontend-redesign -# branch at https://staging.hackathons.monashcoding.com, kept completely separate +# branch at https://staging-hackathons.monashcoding.com, kept completely separate # from production. See docs/deploy-dokploy.md β†’ "Staging preview". # +# NOTE ON THE HOSTNAME: this is a SINGLE-level subdomain on purpose. The domain +# sits behind Cloudflare, whose free Universal SSL cert covers monashcoding.com +# and *.monashcoding.com β€” but NOT a nested *.hackathons.monashcoding.com. A host +# like staging.hackathons.monashcoding.com would fail the TLS handshake at +# Cloudflare's edge (ERR_SSL_VERSION_OR_CIPHER_MISMATCH) before ever reaching +# Traefik. Keeping it one level deep means Cloudflare serves TLS with no extra +# cert and no paid Advanced Certificate Manager to remember/renew. +# # Differences from docker-compose.dokploy.yml (production): # β€’ Its own domain + Traefik router/service NAMES (must be unique on the host). # β€’ Its own Postgres + db_data volume β€” Dokploy scopes these per compose stack, @@ -39,7 +47,7 @@ services: DATABASE_URL: postgres://mac_hackathon:mac_hackathon@db:5432/mac_hackathon NODE_ENV: production PORT: "3000" - PUBLIC_URL: https://staging.hackathons.monashcoding.com + PUBLIC_URL: https://staging-hackathons.monashcoding.com # Auto-seed demo content on every deploy (idempotent). Staging only. ALLOW_SEED: "1" @@ -61,7 +69,7 @@ services: labels: - traefik.enable=true - traefik.docker.network=dokploy-network - - traefik.http.routers.mac-hackathon-staging.rule=Host(`staging.hackathons.monashcoding.com`) + - traefik.http.routers.mac-hackathon-staging.rule=Host(`staging-hackathons.monashcoding.com`) - traefik.http.routers.mac-hackathon-staging.entrypoints=websecure - traefik.http.routers.mac-hackathon-staging.tls.certresolver=letsencrypt - traefik.http.services.mac-hackathon-staging.loadbalancer.server.port=3000 diff --git a/docs/deploy-dokploy.md b/docs/deploy-dokploy.md index 6f09f87..b9ed3a6 100644 --- a/docs/deploy-dokploy.md +++ b/docs/deploy-dokploy.md @@ -103,17 +103,27 @@ Then check: ## Staging preview (persistent link for frontend work) A **separate, throwaway** Dokploy service that runs the `frontend-redesign` branch at -`https://staging.hackathons.monashcoding.com`, so work-in-progress can be previewed on a +`https://staging-hackathons.monashcoding.com`, so work-in-progress can be previewed on a real URL **without ever touching production**. It uses its own compose file ([`docker-compose.staging.yml`](../docker-compose.staging.yml)), its own database, and auto-seeds demo content on boot β€” so it's never blank and never hits Humanitix/Notion. -1. **DNS (once).** Add an A record for `staging.hackathons.monashcoding.com` β†’ the same - Oracle VM IP. +> **Why `staging-hackathons` and not `staging.hackathons`?** The domain sits behind +> Cloudflare, whose free Universal SSL cert covers `monashcoding.com` and `*.monashcoding.com` +> but **not** a nested `*.hackathons.monashcoding.com`. A host like +> `staging.hackathons.monashcoding.com` fails the TLS handshake at Cloudflare's edge +> (`ERR_SSL_VERSION_OR_CIPHER_MISMATCH`) before it ever reaches Traefik. Keeping the preview a +> **single-level** subdomain means Cloudflare serves TLS automatically β€” no paid Advanced +> Certificate Manager to remember or renew. + +1. **DNS (once).** In Cloudflare, add a record for `staging-hackathons.monashcoding.com` + pointing at the same Oracle VM as production (mirror however the prod `hackathons` record is + set up β€” same target, **proxied / orange cloud on**). Because it's one level deep, Universal + SSL covers it with no extra certificate. 2. **Create a second Compose service** in Dokploy (name it e.g. `hackathons-staging`): - Provider = GitHub, repo `monashcoding/hackathons`, branch **`frontend-redesign`**. - **Compose Path**: `docker-compose.staging.yml`. -3. **Domain**: add `staging.hackathons.monashcoding.com`, HTTPS on, Let's Encrypt, port 3000. +3. **Domain**: add `staging-hackathons.monashcoding.com`, HTTPS on, Let's Encrypt, port 3000. 4. **Environment**: none required β€” the staging compose hard-sets everything, including `ALLOW_SEED=1` (which makes the entrypoint seed demo content on every deploy) and `NODE_ENV=production` (so the built SPA is served). Leave the env box empty. @@ -125,12 +135,13 @@ auto-seeds demo content on boot β€” so it's never blank and never hits Humanitix > **Public pages just work** on staging (landing, `/past`) β€” no sign-in needed, so that's the > whole redesign surface covered. The **auth-gated pages** (`/dashboard`, `/find-team`, -> `/admin`) additionally need `staging.hackathons.monashcoding.com` added to **mac-auth's** +> `/admin`) additionally need `staging-hackathons.monashcoding.com` added to **mac-auth's** > `TRUSTED_ORIGINS`; until then they'll fail to sign in on staging (they still work locally > via dev sign-in). Ask whoever administers mac-auth if she needs those pages live. **Tearing it down** when the redesign lands: delete the staging service in Dokploy and remove -the `staging.` DNS record. `docker-compose.staging.yml` can stay in the repo for next time. +the `staging-hackathons` DNS record. `docker-compose.staging.yml` can stay in the repo for +next time. --- From 66fa79409dd18f5e1cd2647f831db57b56a67689 Mon Sep 17 00:00:00 2001 From: oliverhuangcode Date: Wed, 29 Jul 2026 00:01:55 +1000 Subject: [PATCH 7/9] docs(guide): accent token is MAC yellow now, not blue Co-Authored-By: Claude Opus 4.8 --- FRONTEND_GUIDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FRONTEND_GUIDE.md b/FRONTEND_GUIDE.md index 6bfbc93..b0abb34 100644 --- a/FRONTEND_GUIDE.md +++ b/FRONTEND_GUIDE.md @@ -201,7 +201,7 @@ utility classes right on the element: Our brand colours live in one place β€” `web/src/styles.css` β€” as named tokens you can use anywhere: `bg-bg`, `bg-panel`, `text-text`, `text-muted`, `text-accent`, `border-border`, -`text-danger`, `text-ok`. So `text-accent` is our blue, `bg-panel` is the card background, and +`text-danger`, `text-ok`. So `text-accent` is our MAC yellow, `bg-panel` is the card background, and so on. Using the tokens (instead of hard-coding a colour) keeps the whole site consistent and makes a future theme change a one-file edit. From 4911c2dfb070b47759a3ee6a91d8557c3a70b4b3 Mon Sep 17 00:00:00 2001 From: oliverhuangcode Date: Wed, 29 Jul 2026 00:06:17 +1000 Subject: [PATCH 8/9] docs(guide): add TopNav + TeamPanels to the file map Co-Authored-By: Claude Opus 4.8 --- FRONTEND_GUIDE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/FRONTEND_GUIDE.md b/FRONTEND_GUIDE.md index b0abb34..83f6a70 100644 --- a/FRONTEND_GUIDE.md +++ b/FRONTEND_GUIDE.md @@ -133,9 +133,11 @@ web/ Past.tsx archive of old events 🚧 EXERCISE 1 (stubbed for you) FindTeam.tsx the teammate pool 🚧 EXERCISE 2 (stubbed for you) components/ + TopNav.tsx the shared top bar (Dashboard + Team tabs) on every page SignInPanel.tsx the "please sign in" box ClaimForm.tsx ticket-claiming form CustomFieldsForm.tsx extra event questions + TeamPanels.tsx the team views (your team, invites, the pool) ``` The two 🚧 pages have been **deliberately hollowed out into guided stubs** β€” you're going to From dbf55877aaa6a8e3b1c4bf79f69c9512a7edb45a Mon Sep 17 00:00:00 2001 From: oliverhuangcode Date: Wed, 29 Jul 2026 00:22:06 +1000 Subject: [PATCH 9/9] docs: high-level PROJECT_OVERVIEW as the first-year orientation A 10-minute, no-typing big-picture doc that sits above the two hands-on guides: what the platform is, why it exists, how it works, the rules that must not break, and what a new frontend committee member should do next. Wired into the README and FRONTEND_GUIDE as the true starting point. Co-Authored-By: Claude Opus 4.8 --- FRONTEND_GUIDE.md | 3 + PROJECT_OVERVIEW.md | 184 ++++++++++++++++++++++++++++++++++++++++++++ README.md | 3 + 3 files changed, 190 insertions(+) create mode 100644 PROJECT_OVERVIEW.md diff --git a/FRONTEND_GUIDE.md b/FRONTEND_GUIDE.md index 83f6a70..72dc1fc 100644 --- a/FRONTEND_GUIDE.md +++ b/FRONTEND_GUIDE.md @@ -1,5 +1,8 @@ # Frontend guide β€” start here (Part 1 of 2) +> Haven't read **[`PROJECT_OVERVIEW.md`](./PROJECT_OVERVIEW.md)** yet? Do that first (10 min, +> no typing) β€” it's the big-picture map. This guide is the hands-on follow-up. + Hey! πŸ‘‹ Welcome to the MAC Hackathon platform. You're taking over the **frontend** β€” the part people actually see, click, and (fingers crossed) enjoy using. This guide gets you from "I've just cloned this repo" to "I understand how it works and I've changed real code," and diff --git a/PROJECT_OVERVIEW.md b/PROJECT_OVERVIEW.md new file mode 100644 index 0000000..2c6bf6c --- /dev/null +++ b/PROJECT_OVERVIEW.md @@ -0,0 +1,184 @@ +# Project overview β€” read this first πŸ—ΊοΈ + +Welcome to the MAC Hackathon platform! This is the **10-minute, no-typing** orientation: +what this website is, why it exists, how it works at a high level, the rules you must not +break, and β€” most importantly β€” **what you should actually do next**. Once you've read this, +the hands-on guides ([`FRONTEND_GUIDE.md`](./FRONTEND_GUIDE.md) and +[`BACKEND_GUIDE.md`](./BACKEND_GUIDE.md)) take over and get you writing real code. + +You don't need to memorise any of this. Just come away with the *shape* of the thing. + +--- + +## 1. What is this website? + +It's two things bolted together: + +1. **A public info site** for a MAC hackathon β€” prizes, judges, schedule, sponsors, FAQ. + A non-developer edits it in **Notion**, and it updates on the live site with no deploy. +2. **A team registration system** whose one defining feature is: **every registered + participant is checked against a real, paid ticket** (bought on **Humanitix**, the + ticketing site). + +That second part is the whole point. Hold onto it. + +### Why it exists β€” the two problems + +Straight from the Hackathon Director: + +- **"We don't know if these people actually exist until the very end."** Teams sign up + without their whole team, or with people who never bought a ticket. Nobody notices the + gaps until event day. +- **"People keep asking if they're even in the hackathon."** Someone buys a ticket, hears + nothing back, and DMs the director to ask if they're registered. + +Both have the *same* root cause: **the ticket and the team roster are two separate lists +with no link between them.** This website links them. The ticket becomes the source of +truth, and the team roster is forced to constantly reconcile against it. That's the entire +idea in one sentence. + +### What this is NOT (so you don't build the wrong thing) + +- ❌ **Not** project submissions or judging β€” that stays on **Devpost**. +- ❌ **Not** ticket sales or payments β€” that's **Humanitix**. +- ❌ **Not** an email system β€” MAC deliberately doesn't send email. Notifications happen + **in-app** and via **Discord**. +- ❌ **Not** a general CMS β€” **Notion** is the CMS. + +If you ever catch yourself building one of these, stop β€” it belongs somewhere else. + +--- + +## 2. How it works, at a high level + +The single most useful idea: **the website never makes up its own data.** Everything on the +screen came from somewhere else and travelled to the browser. There are three "somewhere +else"s: + +``` + Notion Humanitix Organisers + (the info: prizes, (who bought a (create the event and + judges, schedule) ticket) press "sync now" in /admin) + β”‚ β”‚ β”‚ + β”‚ copied on a β”‚ copied on a β”‚ saved directly + β”‚ timer β”‚ timer β”‚ + β–Ό β–Ό β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ OUR DATABASE (Postgres) β€” one fast, local copy of β”‚ + β”‚ everything the site needs β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”‚ the backend reads the database and hands out JSON + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ THE WEBSITE (React) β€” turns JSON into pages people see β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +Why keep our own *copy* instead of asking Notion/Humanitix live on every page load? Speed, +and safety: if Notion is down, we can still serve the page from our copy. Keeping the copy +fresh (on a timer) is the backend's job β€” you'll meet it in Part 2. + +**The takeaway:** by the time data reaches the React code you'll be editing, you don't care +where it started. It's just JSON. + +### The three big ideas layered on top + +1. **Verification.** A participant signs in (with their Monash account), and we match them + to their Humanitix ticket β€” by email, or by them typing in their **order reference** + (the short `7QVD6HEL`-style code from their confirmation email) plus surname. Once + matched, they see an unambiguous **"you're registered"** state and stop DMing anyone. +2. **Teams.** Verified people form teams (2–4 people). A team lead can see, at any time, + exactly which members haven't **accepted** the invite and which haven't **bought a + ticket** β€” weeks early, not on the day. +3. **The organiser gap report.** The admin view lists every ticket-holder with no team, + every team member with no ticket, and every unaccepted invite. This one screen is the + thing the director used to rebuild by hand β€” it's *why this project exists*. + +--- + +## 3. The pages + +Two pages for participants, split by the question each one answers: + +- **`/dashboard` β†’ "Am I actually in the hackathon?"** Ticket state, the verify/claim flow, + and personal details. Nothing else. If someone reads this page and still has to ask, the + page has failed. +- **`/find-team` (the "Team" tab) β†’ everything about teams.** Your team, who hasn't accepted + or bought a ticket yet, invites, team questions, and the pool of people looking for a + team. All of it is locked until you're verified. + +Plus **`/admin`** for organisers β€” the team board and the gap report above. It's reachable +by URL but deliberately not shown in the nav. + +--- + +## 4. The rules you must not break + +These aren't style preferences β€” they're load-bearing. Most were learned the hard way. + +- **Humanitix is read-only.** We only ever *read* ticket data. Never write to it. +- **No email, ever.** No SMTP, no "send a confirmation email". In-app state + Discord. +- **We never build auth.** Sign-in is handled by `mac-auth`, a separate MAC service. Don't + reinvent it. +- **Everything must survive handover.** The committee changes every year. No personal + accounts, no paid subscriptions that can lapse, and next year's team must be able to run + the whole thing from a clean clone with one command β€” *without talking to whoever built + it.* If a decision relies on someone *remembering* something, it's the wrong decision. +- **Nothing is truly deleted.** Things get marked as gone and preserved, so mistakes are + recoverable. + +The full, authoritative version of all of this lives in +**[`SPEC_hackathon.md`](./SPEC_hackathon.md)** β€” that document is the source of truth. You +don't need to read all 593 lines today, but know it's there when a "can I...?" question +comes up. + +--- + +## 5. What YOU should do (your first week) + +Here's the path. Don't skip steps β€” each one makes the next make sense. + +1. **Read this page** (done! βœ…) so you have the mental model. +2. **Get it running on your laptop.** Full instructions are in + [`FRONTEND_GUIDE.md`](./FRONTEND_GUIDE.md) Β§4 β€” it's a handful of copy-paste commands and + a sample-data seed so the pages aren't blank. +3. **Do the two exercises** in the frontend guide (Β§6). Two pages β€” + [`Past.tsx`](./web/src/pages/Past.tsx) and [`FindTeam.tsx`](./web/src/pages/FindTeam.tsx) β€” + have been **deliberately hollowed out** for you to rebuild. This is where everything + clicks from "reading about it" to "I did it." The finished versions of both pages exist + on the `mac-hackathon-mvp` branch if you get stuck and want to peek *after* trying. +4. **Then start the redesign** (frontend guide Β§7) β€” make it look great. The golden rule: + change how a page *looks*, not what it *says*. If a page is missing data you wish it had, + that's a backend change β€” write it down and ask, don't fake it in the frontend. +5. **Curious about the other half?** [`BACKEND_GUIDE.md`](./BACKEND_GUIDE.md) (Part 2) walks + the server side and has its own small exercise. Optional, but it demystifies the whole + machine. + +### A few practical notes + +- **Where you'll live:** everything you own is under `web/`. You can ignore `src/server/` + (the backend) until you're curious. +- **Your branch:** you're on `frontend-redesign` β€” your own safe copy. Commit little and + often, push, and open a Pull Request when something's ready for review. Never commit + straight to `main`. (Git cheat-sheet: frontend guide Β§8.) +- **Seeing your work live:** this branch auto-deploys to + **[staging-hackathons.monashcoding.com](https://staging-hackathons.monashcoding.com)** β€” a + throwaway preview with sample data, completely separate from the real site. Push, wait a + minute, and your changes are there to show people. +- **When you're stuck:** that's normal and expected β€” reaching out early is the pro move, + not the beginner one. No question is too small. + +--- + +## 6. The map of docs + +| File | What it's for | +|------|---------------| +| **`PROJECT_OVERVIEW.md`** (this file) | The 10-minute big picture. Start here. | +| [`FRONTEND_GUIDE.md`](./FRONTEND_GUIDE.md) | Part 1 β€” how the frontend works + your two exercises. **Your main guide.** | +| [`BACKEND_GUIDE.md`](./BACKEND_GUIDE.md) | Part 2 β€” the server side, when you're curious. | +| [`SPEC_hackathon.md`](./SPEC_hackathon.md) | The full source-of-truth spec. Reference, not bedtime reading. | +| [`README.md`](./README.md) | Setup, deployment, and stack details for the whole repo. | + +Welcome aboard β€” real people are going to use what you build. Have fun with it. πŸŽ‰ diff --git a/README.md b/README.md index 7381f53..9d731e2 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,9 @@ MAC's hackathon platform: a public info site (Notion-driven) plus team registrat Read [`SPEC_hackathon.md`](./SPEC_hackathon.md) β€” it is the source of truth. This README covers running the thing. +> **Brand new here?** Read **[`PROJECT_OVERVIEW.md`](./PROJECT_OVERVIEW.md)** first β€” a +> 10-minute, no-typing big-picture map of what this is, how it works, and what to do next. +> > **New here / redesigning the frontend?** There's a two-part, beginner-friendly walkthrough > that reads as one story. Start with **[`FRONTEND_GUIDE.md`](./FRONTEND_GUIDE.md)** (Part 1) β€” > how the frontend works, how it talks to the backend, and two hands-on exercises β€” then