Skip to content
AA

Adminix — Internal Admin Dashboard

Client-side admin dashboard for managing 100,000 users across 10 mock customer accounts — MSW for API simulation, React Virtual for the user list, and URL-as-state for persistent filters.

3 min read

An internal admin dashboard for ops and support teams — search and filter 100,000 users, invite with role and account assignment, suspend or delete in bulk, read a global audit log, and pull reports. Everything is client-side. MSW intercepts all fetch calls and responds from in-memory seed data. The network tab shows real requests because MSW operates at the Service Worker level.

How It Was Built

The user list has 100,000 records. @tanstack/react-virtual renders only the visible rows — the DOM never holds more than ~20 nodes regardless of the total count. Pagination runs on top of it: MSW handles offset and limit, and the total count from the response sizes the scroll container so the scrollbar feels correct. placeholderData: keepPreviousData keeps the current page visible while the next one loads so the layout never jumps.

Filters, search query, role, account, and status all live in useSearchParams. The URL is the source of truth — not component state, not Zustand. Refreshing restores the exact view, the back button navigates filter history, and sharing a link sends someone to the right filtered state.

Auth uses an adaptiveStorage object that routes Zustand's persist middleware to localStorage or sessionStorage based on whether "remember me" was checked — the decision is made at write time. ProtectedRoute reads isAuthenticated and redirects to /login if false.

Every form shares the same Zod schema for its TypeScript type, runtime validation, and error messages. Adding a field means updating one object.

Architecture

MSW (Service Worker)
  └─ intercepts fetch()
       └─ returns mock JSON from in-memory seed
            └─ API layer (src/api/)
                 └─ React Query hooks (src/hooks/)
                      └─ components via useQuery / useMutation
                           ├─ server state: cached by React Query
                           └─ UI state: sidebar, modals, theme in Zustand

Mutation path:
  Component calls mutateAsync()
    → API function fires fetch()
      → MSW intercepts, updates in-memory seed
        → React Query invalidates cache key
          → hook re-fetches, UI updates

Stack

ToolVersionRole
React19UI layer
TypeScript5Strict mode, zero any
Vite5Dev server + build
Tailwind CSSv4Styling, dark mode via class strategy
shadcn/uilatestAccessible UI primitives
TanStack Query5Server state, caching, pagination
Zustand4Auth state + UI state
MSW2API simulation at Service Worker level
React Virtual3Virtual scrolling for 100k row list
React Hook Form7Form state
Zod3Schema validation + TypeScript types
React Routerv6Nested routes + URL-as-state
PlaywrightlatestE2E tests
VitestlatestUnit + integration tests