HomeWorkWhat I doContact
All projects

AcmeCRM case study

Making a browser-only CRM demo feel real

AcmeCRM is a clickable CRM demo on sample data, built to show clients how such a system would work, with no backend behind it and a data layer that still acts like one.

Role
Front-end product demo
Platforms
Web
Stack
Next.js, TanStack Query, dnd-kit, Recharts
AcmeCRM dashboard with revenue, deals by stage and upcoming tasks

The problem

A demo clients can test

A CRM demo only helps a client if it responds like the system they might commission. Deals need to move, forms need to reject bad input, lists need loading states, and a change on one screen has to appear on the others. Static screens cannot show any of that.

AcmeCRM has no backend and runs on sample data in the browser. The engineering problem was giving the screens the same asynchronous contract they would have against a real API, one that can be slow and can say no, while letting anyone change the data and reset it.

Architecture

A mock server behind real hooks

I split the app the way a client-server product is split, then put the server in the browser. Pages call TanStack Query hooks, hooks call repository modules, and only the repositories touch the store. A repository validates input, applies side effects such as logging a deal update and joins names onto rows.

The database is a single Zustand store persisted to localStorage, so edits survive a reload and sync across tabs. Sample data comes from a fixed-seed generator with dates relative to today, and Settings has a reset back to the original data.

  1. Screens

    Next.js, dnd-kit, Recharts

    Client pages that go through hooks only

  2. Cache

    TanStack Query

    Holds reads, applies optimistic patches

  3. Mock API

    Repository modules

    Delay 200 to 400 ms, validate, return cloned rows

  4. Database

    Zustand store

    One state object, committed through a cloned draft

  5. Persistence

    localStorage

    Survives reloads, syncs tabs, resets to seed

All records and figures on screen are generated sample data. The app has no API routes, no database and no real authentication.

Key decisions

Three decisions behind the demo

01

Repositories that behave like an API

Screens built directly on in-memory arrays never have to deal with waiting, rejection or stale copies. Those are the behaviours a client needs to see to judge a real system.

Every repository call goes through one function that waits a random 200 to 400 ms, runs the work and returns a clone, so no screen holds a reference to stored data. Writes change a deep copy and commit only if nothing throws, so a failed validation acts like a rejected request and saves nothing.

  • Failures are real validation and not-found errors, none are randomly injected.
  • Queries retry once, but never on a not-found error.
  • Tests set latency to 0 and run the same repositories without timers.

02

Optimistic drags that survive overlap

A dropped card has to land at once even though the write takes a few hundred milliseconds. A refetch while a second drag is pending would briefly undo that second move.

On drop, a shared mutation hook cancels in-flight reads and patches the card's stage across every cached deals query, keeping a snapshot for rollback. Refetching is skipped while another mutation is pending, so the last one to settle reloads everything. A rejected write restores the snapshot and shows a toast.

  • The pointer must travel 6 px to start a drag, so a click still opens the deal.
  • Dropping on Lost waits for a reason, and the repository rejects a move without one.
  • Space starts a keyboard drag and Enter opens the deal.

03

One source for every dashboard number

A dashboard that keeps its own totals drifts as soon as someone drags a deal. The point of the demo is that a move on the board shows up in the numbers.

The dashboard stores nothing: one function computes KPIs, the revenue series and stage totals from the same state every other screen reads. A stage move updates the deal, the contact status and the activity log in one committed write. Because the current time is an argument, tests run against a fixed date.

  • KPI deltas compare the last 7 days with the 7 before.
  • A delta is null, not an infinite percentage, when the earlier window is empty.
  • Completing a task patches both the tasks and dashboard caches.

Let's build
something
together.

Open to senior roles: remote, hybrid or on-site in Ho Chi Minh City. The quickest way to reach me is email or LinkedIn.