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
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.
Screens
Next.js, dnd-kit, Recharts
Client pages that go through hooks only
Cache
TanStack Query
Holds reads, applies optimistic patches
Mock API
Repository modules
Delay 200 to 400 ms, validate, return cloned rows
Database
Zustand store
One state object, committed through a cloned draft
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.