All projects Internal tool

Join

A board where a team can see who is doing what: tasks dragged between four columns, with contacts, deadlines and subtasks. The thing a company reaches for when work stops fitting into a group chat.

My role
Full rewrite — architecture, design, code
Size
~2,400 lines, 38 components
Stack
React 18, Vite, Tailwind, dnd-kit
Status
Live, open to guests
The Join board with tasks in four columns

Why it got rebuilt rather than patched

Join started life as a team project written in plain JavaScript. It worked, but every new feature meant threading changes through markup, state and styling that were never separated in the first place. Instead of tidying that up, I rebuilt the app from scratch as a React project — same idea, different foundation.

What you see here is an independent copy: the team's original repository is untouched, and the old vanilla version still sits in the project under legacy/ so the two can be compared honestly.

That is the same call a client eventually has to make about their own system, so it is worth stating plainly. A rewrite is the right answer when the cost of the next feature keeps rising; patching is the right answer when it does not. Here it kept rising.

What it does

On a desktop it is a sidebar layout; on a phone the navigation moves to the bottom of the screen, within thumb reach.

The hard part: drag and drop that is not a trap

Dragging cards is the feature everyone asks for and the one that quietly breaks the app for part of its users. A mouse-only drag is unusable with a keyboard, awkward with a screen reader, and on a phone it fights with page scrolling — every attempt to move a card scrolls the board instead.

So the drag is built on dnd-kit, and it is not the only way to move a task:

A gesture should never be the only route to an action. If dragging is the sole way to move a card, then anyone on a keyboard, a screen reader or an unsteady hand simply cannot use the board.

A data layer that works with or without a backend

All storage sits behind one file, backend.js. It exposes plain functions — load everything, save a task, delete a contact — and the rest of the app never learns where the data actually lives.

Underneath there are two implementations. If a Firebase address is set in the environment, everything goes to the Realtime Database over REST. If it is not, the same calls fall back to localStorage, and on first run the app seeds itself with demo data so the board is never empty on arrival.

This is worth more than it sounds on a client project. The demo can be handed to anyone without setting up infrastructure first; nothing has to be registered before the client sees their own product working. And when a real database is needed, it is one environment variable — not a rewrite of every screen.

What I would tell a client before they use it

Two limits, stated up front rather than discovered later:

Neither is hard to close — they are deliberately left where they are, because this is a portfolio piece rather than a system in production.

Press Guest log in to go straight to the board — no account needed.