skip to content
naft.pink
2024archived

Codename ██████

Client work: a booking site for a dog grooming business. I migrated it off Create React App to Next.js single-handedly, built the calendar and the admin panel, and it never went live because the invoice never got paid. 2024.

  • Migrated the whole thing from Create React App to Next.js on my own — routing, data fetching and the build all changed shape.
  • Server actions for the mutations, in 2024, while they were still new enough to be a decision rather than a default.
  • A custom booking calendar, because the off-the-shelf ones model events and this needed to model availability.
  • Double-booking made impossible at the write: once a slot is taken, the second request for it loses.
  • An admin panel so the business could run the schedule without me.
  • Finished and never deployed — the client stopped paying.

outcome — Fully built, never deployed. The client stopped paying before launch, so it's a working codebase with no URL to point at.

  • NextJS
  • TypeScript
  • Prisma
  • TailwindCSS

A booking site for a dog grooming business: pick a service, pick a time, the owner sees it in an admin panel. I came onto it the same way as the art gallery — subcontracted, working with the same person — and it's the second and last thing that arrangement produced.

It's the most finished thing on this list that nobody has ever used.

the migration off create-react-app#

It arrived as a Create React App project and left as a Next.js one, and I did all of that myself.

A CRA migration sounds like a build-tool swap and isn't. Client-side routing becomes file-based routing. Data fetching that ran in useEffect after paint moves to the server and happens before it. The environment variable convention changes, the entry point stops existing, and every assumption about "this runs in the browser" needs checking individually, because the ones that are wrong don't fail at build time — they fail on the server on first render.

The reason to do it at all was that this is a site whose entire job is being found. A grooming business needs to come up in a local search, and a CRA app serves a blank page with a script tag to whatever comes to look. Everything else in the migration was downstream of that one requirement.

server actions, while they were new#

Mutations went through server actions. In 2024 that was a real call — the API was recent, the patterns weren't settled, and the safe answer was route handlers that nobody would question.

I've made this trade before, pushing for rspack while it was still the risky option, and the reasoning is the same both times: take the new thing where being wrong is survivable. Here it was. The mutation surface was small — book, cancel, edit availability — so the blast radius of the API changing under me was a handful of functions, not the architecture. Against that, deleting the entire fetch-wrapper-and-endpoint layer for a site this size was most of a week I didn't have to spend.

the slot that can only be taken once#

The interesting correctness problem was the same one the auction system had, wearing different clothes: two people want the same thing at the same instant, and exactly one of them can have it.

Two customers open the same Tuesday afternoon. Both see it free, because both loaded the page before either clicked. Both submit. Checking availability and then writing the booking as two separate steps is a race with a comfortable gap in the middle, and the failure mode isn't a stack trace — it's two dogs arriving at once and a groomer who has to turn one away. That's the kind of bug that costs the client a customer rather than costing me an afternoon.

So the check that a slot is free and the write that takes it had to be one indivisible thing, decided by the database rather than by the application reading state and hoping it still held. Get that right and the second request loses cleanly and gets told so. Get it wrong and you find out from a phone call.

the calendar, built rather than installed#

The calendar is custom. Most calendar libraries model events — things that exist and occupy a span — and a booking system needs the inverse, which is availability: the gaps a service of a given length could still fit into, given opening hours, the ones already taken, and how long the thing being booked actually runs for.

You can express that on top of an events library. It's just that by the time you've expressed it you've written the hard part yourself anyway and inherited someone else's rendering as well.

how it ended#

It was built. It worked. It never went live, because the client stopped paying and then didn't pay at all.

Not much more to say about it than that. The work was done to the point where deploying was the next step, and the deploy is the one thing that needed somebody else. I'd have liked it in a portfolio with a URL attached — instead it's a page describing software you can't go and look at, which is exactly as unsatisfying as it sounds.

It's here because it happened, and because leaving out the jobs that ended badly would make the record tidier than the record was. The technical calls in it are ones I'd make again.

all projects