


The product, as designed: pick a brand, see today, book the next class.
| Company/Role: Freelance Project — Designer & App Developer | Target Platform: Mobile (iOS & Android) |
| Team Size: Solo product design and mobile app frontend development (design + 1 backend programmer working with API) | Tools: Figma, React Expo, API integration |
| Project Length: 4 months total (2 months design + 2 months frontend development) | Techniques: Persona, information architecture, booking state machine, UI design, component library, API-driven frontend |
| Impact: Designed and built end-to-end booking flow for HK gym members; state machine ensured valid actions per class state. (Note: Specific outcome metrics not captured during project; industry benchmark for gym no-show rates is ~20-30%. Impact framed through design decisions and lessons learned.) |
Before drawing a single screen I mapped booking as a state machine. A booking is never just “made” or “not made”; it moves through waiting, booked, cancellable, checked-in, attended and expired, and the state alone decides what the member is allowed to do next.
Impact: State machine enabled the "one card, one action" principle where every variant in the library has exactly one valid action — the state alone decides what the member is allowed to do next. This was especially critical given the API-driven constraints where the frontend could only show actions the backend permitted.
Writing the rules down first meant the interface could make a promise it kept everywhere: one class card, one primary action, always the right one. The member is never offered a button that will fail.
Book → Scan → Attend. The rules that govern each transition, mapped before any UI existed.

The wider product path around those rules: brand choice, guest vs member, then book, manage, check in.
That map collapses into a single lookup table. Every row below is a real variant in the library, not a special case drawn on one screen.

The class card in every legal state: Book Now, Join waitlist, Cancel, Show QR, Attended, Expired, Leave waitlist, Cancelled.
| Not booked, space available | Book Now |
| Not booked, class full | Join waitlist |
| On the waitlist | Leave waitlist |
| Booked, more than 60 minutes before start | Cancel Booking |
| Booked, less than 60 minutes before start | Show QR |
| Scanned at the gym | Attended |
| Never scanned | Expired |
| Cancelled | Cancelled. No action. |
The React Expo repo is private. This table is the spec the class card implemented: one state, one action.
The 60-minute cutoff is the one rule that is doing real work. Before it, cancelling is free and the seat goes back to the waitlist while someone can still act on it. After it, cancelling stops being offered at all and the card flips to the QR code — because at that point the only useful thing the app can do is get the member through the door. The same rule protects the gym's occupancy and removes the member's decision, which is exactly the trade a busy person wants.



The same rules on real screens: waiting, checking in, and the empty state that sends you back to book.
Check-in is a QR scan at reception, which introduced a constraint from the operations side: the scanning device has to be signed in as an admin account, so the member's phone displays the code and the gym's device reads it, never the other way round. And because a timetable is a living thing, any change to a class's time, location or trainer pushes a notification rather than waiting to be discovered.
AirFitness is the member-facing app for a fitness group that runs more than one brand under one roof — airstudio for boutique classes and airfitness for the gym floor. Members join a branch, hold a contract, and book into timetabled classes: hot stone yoga, pilates, zumba, toning fit, super core.
The operational side of that already worked. Staff had a schedule, a membership database and a front desk. What members had was a timetable they had to translate — is this class full, have I already booked it, can I still cancel, did last week's session actually count? Each of those questions has a definite answer sitting in the system, and none of them were being answered on a screen.
The result is the failure mode every gym knows: members book and then don't turn up. A no-show is not usually a change of heart. It is a class that slipped past someone who meant to be there, or a full class whose waitlist never moved because the person holding the slot forgot to release it.
How might we make attending the next class the easiest thing a member can do on their phone?
I owned the product end to end as the product designer — from who it is for, to the rules it keeps, to the screens that enforce them:
Version 3.0 settled on four tabs. Not a map of the gym's database objects — four jobs a member actually has.




Home, Classes, Bookings, Profile. Each tab answers one question: what is today, what can I book, what have I committed to, who am I here.
Two lines from the persona did the work. Reminders help plan the day and avoid missing classes, so notifications sit on Home, not in settings. Variety of classes at different times, so the timetable filters by time instead of asking the member to scroll.


The two persona requests, as screens: a reminder that reaches him, and a timetable that can be cut down to the hour he actually has.
Version 1.0's tab bar was Home, Classes, Booking, Profile, More. It was a faithful map of the system behind it, and that was the problem — it was organised around the gym's objects rather than the member's intent.
Two tabs were not earning their place. “Classes” and “Booking” describe the same act to a member who just wants to be in a room on Tuesday evening, and “More” had quietly become a drawer for everything without a home: branches, feedback, language, legal, logout. Meanwhile the two things the persona actually asked for — reminders, and a fast way through a busy timetable — had no permanent surface at all.
Version 1.0 to 2.0: dissolving two tabs to promote the ones the persona was asking for.
Version 2.0 tried Notifications and Shop as tabs. That was the right pressure test — reminders deserved a surface, and the business wanted a commercial one. Living with that IA made the next cut obvious.
Version 3.0 is the product that shipped: Home, Classes, Bookings, Profile. Bookings came back as its own tab because managing a commitment is a different job from discovering a class. Notifications moved onto Home, behind the bell, which is where a squeezed member already looks at the start of the day. Shop did not survive. It was a business wish, not a member job, and four tabs is the budget a thumb can keep. Three information architecture versions settled the tabs. Attendance was not measured.
The group runs two brands, so the app opens on a decision the identity has to make painless. A shared splash resolves into “Start your journey” with two doors: airstudio and airfitness.

Holding air constant and varying only the suffix makes the choice a member is being asked to make legible in a single glance.
The palette is a single confident blue carried across both brands, so a member who switches never feels like they have left the product.
The screens are assembled from a component library rather than drawn one by one: buttons, inputs, navigation and tab bars, class cards, calendars, filters, accordions, radios, toggles, loaders and top bars, each with its states defined.

Colour, type and token notes — Montserrat, one blue, a small set of neutrals and status colours.
Building the library first is what made the version 2.0 and 3.0 restructures affordable. Rearranging the navigation was a change to where components sit, not a redraw of every screen — which is the only reason a change that large was worth making at all. The class card's eight states are the same component; only the property changes.
Mapping booking as states and transitions before opening a canvas meant the class card had exactly one job: read the state, show the one action that is valid. Almost every ambiguity that would have surfaced late in design surfaced instead as a question in the flow, where it was cheap to answer.
The two lines that mattered — reminders, and variety across the day — are why notifications live on Home and why the timetable filters by time. Without going back to the persona I would have left both as buried preferences, which is where version 1.0 had put them.
The version 1.0 to 3.0 path was argued from first principles and the shipped IA holds up, but it was a judgement call. A card sort or a first-click test with a handful of members would have cost very little and would have either confirmed the change or caught Shop before a full set of screens was built around it.
Sixty minutes is a reasonable number, not a measured one. The right value depends on how late waitlisted members realistically arrive, and that is knowable from booking records. It is the first thing I would instrument after launch.
A no-network QR screen, a waitlist that never clears, a cancelled class — these are the moments where a booking app either keeps or loses trust. The empty bookings state made it into the library; the failure states still came after the happy path rather than alongside it.
Attendance was not measured in this project, which limited our ability to quantify the no-show reduction from the redesigned flow. In future projects, I would instrument check-in analytics from day one to establish a baseline and measure the impact of design improvements.
Reflection: The lack of attendance data was a deliberate trade-off given the operational constraints of the AirFitness HK app. Check-in required admin-signed-in QR scanners at reception, which meant member-side automated tracking wasn't technically feasible without backend changes. The lesson is: when design and implementation have mismatched capabilities (UI can show what backend doesn't yet support), make the trade-off explicit rather than leaving it invisible. In future projects, I'd evaluate analytics feasibility during the design phase, not after.