Skip to content
All projects
Team sprintfull-stack sprint· 2025 – 2026

Eventure

A React event app on its own Express API, built in three sprints

Eventure events page: event cards with large date tiles, titles, times, descriptions, and Learn more and Register now buttons on a blue gradient.

Problem

People need to browse events, register for one or more in a few steps, and later change which events they’re attending, without duplicate sign-ups or the UI drifting out of sync with the data.

My role

Sole developer: event components, React state, routing and data loading, registration and attendance logic, REST API design and JSON persistence.

Outcome

A working client and server: an event list built from my own components, registration that creates or updates attendees, email-based attendance changes sent as minimal diffs, and seven REST endpoints with validation and correct status codes.

Role
Full-stack developer (solo): React client and Express API
Team
Solo build. Every commit across the four repositories is mine
Timeline
Dec 2025 – Feb 2026 · three sprints
Tools
React 19, React Router 7, Vite, Axios, Node.js, Express 5, REST APIs, SCSS (BEM), dotenv, Postman, Git & GitHub

Note. A BrainStation sprint project built on a provided component starter and Figma mockups. The excerpts below are code I wrote (git blame on the repository shows the split). Event and attendee records are sample data. Screenshots are from the project running locally. The live demo stores registrations temporarily, so they reset from time to time.

1. Overview

Eventure lets people browse upcoming events, register for one or more of them through a short multi-step form, look up their registration by email to change what they’re attending, and see who else is going.

I built it alone over three BrainStation sprints, each adding a layer: event UI on a provided component starter, then the product flows against a provided mock API, then my own back end to replace that API.

  1. 1.

    Sprint 1: event UI on a starter

    BrainStation provided a component-library starter (form fields, buttons, typography, a docs page). I built the EventCard and EventList components and the Eventure landing page on top of it, and made the cards and buttons responsive.

  2. 2.

    Sprint 2: the product flows

    Routing and data loading in App, then the logic behind registration, attendee lists and email-based attendance management, all against the course’s mock REST API with Axios.

  3. 3.

    Sprint 3: my own API

    A Node and Express server with seven endpoints and JSON-file persistence that mirrors the mock API’s contract, so the client switched over by changing one base URL. I also fixed the sprint 2 feedback, a card layout that broke between 680 and 900px.

Eventure registration, step 1 of 3: personal details form with first name, last name, email, optional role and phone, two consent checkboxes, and Back and a disabled Next button.
Step 1 of 3. Next stays disabled until required fields pass validation.

2. Architecture

How the app is split
LayerBuilt withResponsibility
ClientReact 19, React Router 7, ViteRoutes for events, registration, attendees and manage-events. State is lifted to App and passed down.
Data layerAxios, one API_BASE_URL from envParallel initial load with Promise.all; POST and PATCH calls update local state from the server’s response.
ServerNode, Express 5, CORS, dotenvRouters for /events and /attendees, request validation and JSON error messages with the right status codes.
Storagefs/promises, JSON files, uuidRead and write helpers that return an empty list for a missing or empty file instead of crashing.
StylesSCSS with BEMMy EventCard and EventList styles build on the starter’s variables, breakpoint mixins and pxToRem function.
The REST API I wrote
MethodRouteWhat it does
GET/eventsAll events
GET/events/:eventIdOne event, or 404
POST/eventsCreate an event (title and date required), 201
GET/attendees?eventId=All attendees, optionally filtered to one event
GET/attendees/:attendeeIdOne attendee, or 404
POST/attendeesRegister: required-field check (400), duplicate email check (409), 201
PATCH/attendees/:attendeeIdChange attendance with { add, remove } arrays; rejects empty or malformed updates (400)

3. Code highlights

Diff-based attendance updatessrc/pages/ManageEvents/ManageEvents.jsx
const nextEvents = Object.entries(attendance)
  .filter(([, status]) => status === "attending")
  .map(([eventId]) => eventId);

const currentEvents = foundAttendee.attendingEvents || [];
const add = nextEvents.filter((id) => !currentEvents.includes(id));
const remove = currentEvents.filter((id) => !nextEvents.includes(id));

const res = await axios.patch(
  `${API_BASE_URL}/attendees/${foundAttendee.id}`,
  { add, remove }
);

// Trust the server's copy, then update the shared list.
onUpdateAttendees((prev) =>
  prev.map((a) => (a.id === foundAttendee.id ? res.data : a))
);

Instead of overwriting the whole record, the client sends only what changed. The server applies the same add and remove sets, so two views can’t silently undo each other’s changes.

Validation and duplicate protection on the servereventure-sprint3-server/routes/attendeesRoutes.js
if (!firstName || !lastName || !email ||
    !reasonForAttending || !Array.isArray(attendingEvents)) {
  return res.status(400).json({
    message: "firstName, lastName, email, reasonForAttending, and attendingEvents (array) are required",
  });
}

const attendees = await readJSONFile("attendees");

// Prevent duplicates by email
const exists = attendees.some(
  (a) => a.email?.toLowerCase() === email.toLowerCase()
);
if (exists) {
  return res.status(409).json({ message: "Attendee already exists" });
}

The client also trims and lower-cases emails before searching or posting, so the same person can’t register twice with different capitalisation.

Resilient JSON persistenceeventure-sprint3-server/utils/readWrite.js
export const readJSONFile = async (fileName) => {
  try {
    const data = await readFile(getFilePath(fileName), "utf-8");
    if (!data.trim()) return [];
    return JSON.parse(data);
  } catch (error) {
    if (error.code === "ENOENT") return [];
    throw error;
  }
};

4. Engineering decisions

  • One source of truth. Events and attendees load once in App (in parallel with Promise.all) and flow down as props. Every write replaces local state with the server’s response, so the UI never shows data the server didn’t accept.
  • Step-by-step validation. I wrote the input handlers that validate email and phone as you type and the step logic that blocks moving on until a step is valid, including requiring at least one event.
  • No wasted requests. If an existing attendee re-registers without changing anything, the client skips the PATCH; the server rejects empty updates too.
  • Configuration through the environment. The client reads its API base URL from Vite env variables; the server reads its port and allowed CORS origin from .env.
  • Tested by hand, end to end. Every endpoint exercised in Postman (GET, POST, PATCH), and the flows checked at mobile, tablet and desktop widths.
Eventure events page on a phone: event cards stacked in one column with the Learn more and Register now buttons below each description.
Mobile: cards stack and the action buttons wrap, the layout fixed after sprint 2 feedback.

What I’d do next

JSON files are fine for a sprint but don’t handle concurrent writes. Next steps would be a real database, automated API tests in place of manual Postman runs, and deploying the client and server.

Cartogra home page: a palm-lined beach behind the headline “Where do you feel today?”, with a four-step “Choose the vibe for your trip” section below.Next projectCartograResponsive layouts and a validated booking flow for a travel site