T/T
Back to work

Client work · 2026 · In production

Academia Combate de Gaia

Raça. Humildade. Audácia.

The academy home page: the three values set large in condensed uppercase against a near-black ground, with the crest to the right.
The public site. The academy's colours are black and white, so hierarchy comes from weight, case and tracking.

What it is

Academia Combate de Gaia is a non-profit combat sports academy in Vila Nova de Gaia, running since 2019. Just under four hundred people are registered, and somewhere between a hundred and fifty and two hundred train on any given day: competitive athletes preparing for national championships, adults training for fitness, and children from age five, all on the same mat in the same sessions.

The platform runs the academy in one place. A public site with the disciplines, schedules, news, events and the competition record. A member area for booking classes, following payments and holding a membership. An admin panel for the one person who runs all of it. Underneath: subscriptions, Portuguese payment rails, class scheduling, trial classes, event ticketing and staged bulk email.

I owned the frontend from the start: the design system, every screen on all three surfaces, and every integration point between them and the data. A second developer owned the backend, and partway through the project I absorbed that scope as well, along with the infrastructure. It began as freelance work in April 2026 and became my internship placement in July, so I could work on it full time.

The problem

The academy was running on three separate services that did not talk to each other. One held membership, one held class booking, one held payments. Nothing was shared between them, so the same person existed three times and matching them up was manual work.

Two things made it worse than inconvenient. Booking eligibility was tied to whether the current month had been paid, rather than to the period a subscription actually covered, so members could not book ahead. That was the single most-cited complaint. And the membership service held the academy's previous domain name registered in its own name, which meant the academy did not own its own address on the internet.

So the brief was not really "build a website". It was: bring everything under one roof, and make sure the roof belongs to the academy. Whose name is on the domain, and whose account holds the keys.

The decisions

A palette that was handed to me, not chosen

The academy's colours are black and white. That was a client requirement, not a design decision, and it is more interesting than the version where I claim credit for it. One background colour across every surface, no section fills, no card backgrounds. Hierarchy comes from size, weight, case and tracking, with opacity as a secondary lever. White is the only accent, used at most once per section.

Colour appears in exactly one place on the entire platform: gold, silver and bronze in the competition results, where medals have earned it. Working inside a constraint that tight is what produced whatever discipline the design has.

The one significant visual decision that was mine is the diagonal seam. The academy's equipment and team kit carry a diagonal, so the site echoes it as its structural device: a single angle constant, alternating direction down the page, separating sections in a system where nothing sits inside a box.

The competition record page: a table of championships by year with counts of gold, silver and bronze medals.
The competition record, the only page on the platform where colour appears.

Giving the academy its own history back

The academy had five years of competition results sitting in nobody's particular care. I built a page that totals them: medals by competition, by year, and overall, with the current season marked as still in progress.

It became the most successful thing in the build, and not in a way I planned. The owner liked it enough to rebuild something close to it on the academy's own Instagram, and has since talked about painting the year-by-year record onto a wall at the academy.

I have no metric that beats that. A client taking a page you made and propagating it into channels you have nothing to do with is the clearest evidence I have that presenting an organisation's own history back to it was worth more than another feature.

Building against source files, not documentation

With a split codebase, the contract between frontend and backend was written down: endpoint shapes, field names, enums, pagination. It drifted anyway. The document said one field name where the schema used another. A form sent Portuguese field names where English was expected. One endpoint rejected an explicit null where the key simply had to be absent.

None of that is anyone's fault in particular; it is what happens when a written document and running code are allowed to be two different things. I stopped treating the document as authoritative and verified every call against the actual route handlers and validation schemas. All of them, line by line, which is how the mismatches above surfaced at all. Generated types would have made the drift impossible rather than merely detectable, and that is what I would reach for next time.

The admin panel dashboard, showing summary figures, expiring memberships and quick actions, with the navigation grouped by area.
The admin panel, built for a single administrator. Shown with an empty test account rather than real member data.

Specified is not implemented

The most useful thing this project taught me came a week before launch. A new requirement arrived, and instead of assuming the rules it depended on already worked, I went and read the code that enforced them.

Three of them were not in it. The annual membership was supposed to be mandatory before anyone could book, and nothing checked for it. Booking was supposed to close when a class started, and no time check existed anywhere, so a morning session was still bookable that evening. Per-plan weekly limits could not be expressed at all.

They had been in the brief from day one and absent for four months, in the enforcement layer I had taken over weeks earlier. Every automated check was green the entire time, because nobody writes a test for a rule nobody implemented. The first was invisible in practice because an administrator assigned every plan by hand, so the situation that would expose it could not arise, right up until self-service registration was requested. All three shipped the day I found them.

The lesson is not that rules can go missing. It is that nothing surfaces them. A checklist that walks the brief and asserts each rule against real behaviour would have caught all three in an afternoon, and I now treat a green test suite as evidence about the code someone wrote, not about the code they did not.

Checking it, and what checking could not tell me

I owned the test strategy. There is a suite of contract tests asserting that what the frontend sends is what the backend actually accepts, which is what stopped the drift above from recurring, and a crawler that walks every page checking links, images, copy rules, metadata and accessibility. Zero findings is the bar before anything ships.

Both were running and green through the entire four months in which three business rules did not exist. That is the part worth carrying forward. Automated checks tell you the code you wrote behaves as written; they say nothing at all about code nobody wrote. End-to-end coverage against the live database is the tier still missing, and it is the next thing I want on this project.

What I'd do differently

Walk the brief against real behaviour, early and on a schedule. That one habit would have caught the three missing rules, and it costs an afternoon.

Get a browser into the loop sooner. An entire mobile pass was done by reasoning about class names without rendering anything, and on-device testing showed it had fixed neither of the two problems it targeted. Visual work is not finished until it has been seen.

Plan the data migration alongside the schema rather than after it. The importer creates accounts, which is not the same thing as migrating members, and nobody asked what a migrated member actually needs until the rules that depend on it were being enforced.

More work on the home page, or the full history on the CV.