diff --git a/BACKEND_GUIDE.md b/BACKEND_GUIDE.md new file mode 100644 index 0000000..eabc365 --- /dev/null +++ b/BACKEND_GUIDE.md @@ -0,0 +1,261 @@ +# Backend guide β the other half (Part 2 of 2) + +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. + +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. + +> **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 happens after the waiter pushes through the door + +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: + +``` + the waiter's order arrives: GET /api/public/past + β + βΌ + 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 station. The actual code that fills this order. + β + βΌ + a Drizzle query on Postgres β "grab the matching rows from the pantry" + β db.select().from(events).where(...) + βΌ + res.json({ events: [...] }) β plate it up as JSON and hand it back to the waiter +``` + +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 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 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. 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 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 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 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, CSV exports + stats.ts π YOUR EXERCISE (a stub β see Β§6) + + 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β¦) +``` + +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 pantry, via Drizzle + +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). + +Here's a table, trimmed from `db/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 +}); +``` + +And here's a real query against it, from `routes/public.ts` β notice it reads almost like a +sentence: + +```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 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): + +1. Edit `db/schema.ts`. +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 the same way everywhere: your laptop, staging, and +production, all in the right order, without anyone hand-editing a live database at midnight. + +--- + +## 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 and poking at the kitchen + +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 Β§6 +``` + +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 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 β build your first endpoint (`src/server/routes/stats.ts`) + +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 honest. + +**Goal:** make `GET /api/public/stats` return the real number of past events. + +**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. + +**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`. + +**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. π + +> 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 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: + +- **`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.** + +Adding **read-only** endpoints β exactly like the stats exercise β is always safe. That's the +corner of the kitchen to play in first. + +--- + +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. + +Any question, however small, come find me. Welcome to the kitchen. π diff --git a/FRONTEND_GUIDE.md b/FRONTEND_GUIDE.md new file mode 100644 index 0000000..6bfbc93 --- /dev/null +++ b/FRONTEND_GUIDE.md @@ -0,0 +1,334 @@ +# Frontend guide β start here (Part 1 of 2) + +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 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. + +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 (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. + +Drawn out, a plate of data travels like this: + +``` + 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" + β β β + β delivered on a timer β delivered on a timer β saved directly + βΌ βΌ βΌ + βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ + β THE PANTRY β Postgres (our database) β + β one fast, local copy of everything the site needs β + βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ + β² + β the KITCHEN reads the pantry and plates up JSON + β + βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ + β 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 DINING ROOM β frontend (React, web/) β YOU ARE HERE β + β turns JSON into buttons, cards, and text on the page β + βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ +``` + +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. + +**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 loop you'll repeat all day + +Almost every screen you build is the same four steps. Learn this rhythm once and the rest is +detail: + +``` +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 ββββββββββββββββ +``` + +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.) + +**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 files you'll live in + +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 loads into + src/ + 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: our colours + shared classes (buttons, cardsβ¦) + pages/ + 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 the "please sign in" box + ClaimForm.tsx ticket-claiming form + CustomFieldsForm.tsx extra event questions +``` + +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. Getting it running + +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 β 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 # 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) +``` + +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 + +We style with **Tailwind CSS**. Instead of writing a separate stylesheet, you put small +utility classes right on the element: + +```tsx +
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); + }); 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 }); +}); diff --git a/web/src/pages/FindTeam.tsx b/web/src/pages/FindTeam.tsx index 36ae068..14dd747 100644 --- a/web/src/pages/FindTeam.tsx +++ b/web/src/pages/FindTeam.tsx @@ -1,57 +1,31 @@ -import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; -import { api, NotSignedInError, type FindTeamResponse } from "../api.ts"; -import { SignInPanel } from "../components/SignInPanel.tsx"; -// The looking-for-a-team pool (spec Β§9). A browsable list of verified, teamless -// participants who opted in. NO chat β the conversation continues on Discord. -// Leads with an open slot get an "invite to my team" button per person. +// π 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- Teams need at least 2 people. Opt in below to appear in the pool, browse others looking - for a team, and keep the conversation going in the MAC Discord. -
- - {signedOut &&{error}
} - - {data && ( - <> -- You only appear once your ticket is verified. -
-Nobody's in the pool right now.
} - {data.pool.map((p) => ( -Your team is full β no open slots to invite into.
- )} -π§ TODO: build this page β see FRONTEND_GUIDE.md (Exercise 2).
Loadingβ¦
} - {events && events.length === 0 &&No past events yet.
} - {events?.map((e) => ( -π§ TODO: build this page β see FRONTEND_GUIDE.md (Exercise 1).