Skip to main content
Christian Crawford

Case study

Routine partner work, without an engineer in the loop.

Role
Senior Software Engineer
Focus
Product shaping · UX · Frontend architecture
Stack
Next.js · React · TypeScript · GraphQL · Tailwind
Status
In progress

The problem

Everything went through email.

Pharmacy partners had no tool of their own. Looking up an order, correcting a patient record, pulling a report, or rotating an API key meant emailing support and waiting for someone to query the database. Partners couldn't check status, requests had no priority or history, and engineers were in the path of routine work.

Before

  1. A partner emails support
  2. Support waits on someone who can query the database, often an engineer
  3. The answer comes back by email, with no status or history

With the portal

  1. A partner signs in to the portal
  2. They look up the order, fix the record, or pull the report themselves
  3. Support and engineers stay out of routine requests
The portal is still being built, so the second path is the design, not yet the daily routine.

Shaping

A written pitch before any code.

The team shapes work as a written pitch before building it. I wrote this one: the problem, a proposed solution for each area, the rabbit holes, and what was out of scope. Frontend and backend engineers reviewed it before work started.

Out of scope, on purpose

Role and permission management, and partner account administration. Everyone gets one role, so there's no permission UI to design, build, or test.

Gaps named up front

Parts of the API didn't exist yet. The pitch listed each gap and said to stub the screen and tag it, so frontend work never sat waiting on a backend decision.

Works without a backend

Every list page renders from mock data when no API is configured, so screens could be reviewed and tested before the API behind them was ready.

The key decision

Labeled filters, not a search box.

Stripe, Linear, and Shopify Admin filter with one search bar and removable chips. I weighed three versions of that pattern and kept plain labeled fields, because of who uses this portal.

Who it's for

Operations staff doing the same lookups all day, not engineers who already know the product. A box that says “Search orders…” doesn't say which fields it searches. A field labeled “Customer email” does.

What research said

Baymard and Nielsen Norman Group both found users miss filtering hidden behind a generic control, and non-technical users often don't recognize a chip as something they can remove.

When I'd revisit it

If the people using it get more technical, or the API gains multi-field search. Filters come from one config per resource, so moving to chips would change that config, not the screens.

Architecture

Open a record, share the link.

Records open as panels over the list instead of on a new page, so people keep their place. The address bar holds which panels are open.

  1. /customers

    The list

    Filtered and paged, like any other list page.

  2. /customers?customer=42

    A record opens over it

    The list stays put underneath. Refresh, or send the link, and the same customer is open.

  3. /customers?customer=42&order=7

    A related record stacks on top

    One of that customer's orders opens as a second panel. Back closes it, and the customer is still there.

The address bar holds which panels are open, so the view survives a refresh, the back button, and a shared link.

What I ruled out

Component state, a global store, and route segments. Each one loses the open panel on refresh or needs custom back-button handling.

What it costs

Which panels close together has to be listed explicitly, and a link with a child panel but no parent has to be caught and corrected.

The hard part

Rows you could click but not reach.

Every list opened a record when you clicked a row, and none of that worked from a keyboard. The two quick fixes were both wrong.

A tab stop on every row

Twenty-five stops to get past one page of results, and a focusable row outside a grid gives screen readers no defined way to present it.

A button in every row

Reachable by keyboard, but each row becomes its own stop, and the table stops reading as one collection.

What I built

The WAI-ARIA grid pattern: one tab stop into the table, arrow keys between rows, Enter to open. Closing the panel returns focus to the row you came from.

Where it stands

Still being built.

Screens for every area in the pitch exist but one. API keys, and the choice of a multi-factor sign-in provider, wait on decisions the pitch flagged at the start.

Areas with working screens

  • Customers
  • Orders
  • Support requests
  • Inventory
  • Reports
  • Webhooks
  • Onboarding
  • API docs
  • API playground

Role

What I actually did

  • Wrotethe pitch: the problem, the solution area by area, the rabbit holes, and what was out of scope.
  • Reviewedit with frontend and backend engineers before any of it was built.
  • Builtmost of the frontend, on Next.js, React, TypeScript, Apollo, and Tailwind.
  • Recordedeach real design and architecture choice in a decision log, with the alternatives and what would make me revisit it.
  • Testedaccessibility with Playwright and axe-core, including keyboard access, contrast, and reflow.