Skip to content

As case study

BDC Professionals dashboard

Full frontend build for a dealership BDC operations platform — 41 pages, role-based permission UX, design system, delivered solo on an agency engagement.

Role

Frontend developer · Agency engagement

Timeline

2025

Status

Internal

BDC Professionals dashboard — cover screenshot

At a glance

41
Pages built
40+
Routes wired
60+
Permissions designed
40+
shadcn components
01 · The brief

The brief

BDC Professionals runs Business Development Center operations for car dealerships — the team that handles inbound leads, books test drives, coordinates with sales floors, and keeps the relationship between dealer and customer warm between visits. Their internal platform had grown into a tangle of spreadsheets, shared inboxes, and tribal knowledge.

The engagement scope was specific: deliver the full frontend of a replacement platform — every page, every form, the full permission UX, the dashboards — as if the backend already existed. The backend integration phase would follow with a separate team. My job was to lock the UX with realistic data flows before a database schema got committed to.

02 · Approach

Approach

I picked a stack that let one person move fast without owning infrastructure:

  • React + TypeScript for the UI, Vite for the build.
  • Wouter for routing — small enough that 40+ routes don’t bloat the bundle, expressive enough to handle nested admin patterns.
  • TanStack Query wired up against a localStorage shim. The cache, mutation, and invalidation patterns are real — when the API lands, only the fetch function inside each query changes.
  • shadcn/ui + Tailwind for the component layer. Accessible primitives, ownable code (no library upgrades to manage), restyled to the dealership’s brand tokens.
  • Drizzle ORM + PostgreSQL schema defined and committed alongside the frontend, even though the storage was still localStorage. The backend team would inherit a real schema, not a sketch.

The store architecture used a typed IStorage interface with a localStorage implementation. When the backend phase begins, swapping in a FetchStorage implementation is a one-file change, not a refactor.

03 · The interface

The interface

Four representative screens from the platform. Click any image to see it full-size.

04 · The permission system

The permission system

The trickiest part wasn’t routing or forms — it was the role and permission system. BDC operations have overlapping accountability: agents handle their own dealers, supervisors see across agents, dealership admins see only their own location. The UX had to communicate exactly what each user could and couldn’t do without surfacing controls that would just throw errors.

I designed it as:

  • 11 functional modules (Dealers, Appointments, SMS, Roles, Dealerships, Settings, Reports, etc.).
  • ~60 individual permissions across those modules — view, create, edit, delete, plus module-specific actions like send_sms, assign_dealer, reassign_appointment.
  • A localStorage-backed role store with a usePermission() hook wrapping every action.
  • Conditional rendering at three levels: sidebar items, page-level guards (designed for the auth phase), and per-control visibility (e.g., the “Delete” button on a dealer row only renders if the role has dealers.delete).

Even though the backend will ultimately enforce these, having the UX check them early meant the support team could see each role’s view, give feedback, and lock the permission matrix before a single API endpoint got built.

05 · Outcomes

Outcomes

What shipped at the end of the engagement window:

  • 41 functional pages with realistic data flows, validated forms, and full keyboard navigation.
  • The full sidebar + responsive header layout, with collapsible sections matching the dealership’s mental model.
  • Client-side validated forms for every CRUD entity (dealers, appointments, SMS, roles, dealerships).
  • A documented role / permission spec that the backend team could implement against directly — no follow-up clarifications needed.
  • A migration path from localStorage to PostgreSQL with the Drizzle schema already in place.
  • Component-level documentation so the next person picking it up could find what they needed without reading the whole codebase.

The agency moved the platform into its backend-integration phase on schedule.

06 · What I'd do differently

What I’d do differently

Two things, in hindsight.

Build the auth wrapper before the pages. I built the permission system as a hook layer, but I left the actual page guards as “designed, to be enforced when auth lands.” That worked, but every page had to be retroactively wrapped. Doing it once at the layout level on day one would have saved a couple of days at handoff.

Commit to TanStack Query’s optimistic updates earlier. I built the forms with synchronous localStorage writes, then realized halfway through that the UI was going to feel sluggish once a real network round-trip was involved. Going back to add optimistic update patterns to forms that were already “done” was the part of the engagement I’d most like to redo.

Next case study

Royal Subz — building solo, end-to-end

Read case study →