I’ve been running nexo-ta.com (nexo-ta.com) since 2021 as a side project — a privacy-first tool for analyzing Nexo transaction exports. It started as a simple React app and grew organically over the years. The tech debt finally caught up, so I rebuilt the entire thing from scratch.

Why rebuild?
The old stack was showing its age:
- Create React App — officially dead, no longer maintained
- Redux with 4 slices — massive overkill for what’s essentially “parse CSV, show charts”
- 15+ CSS Module files — scattered styling with no consistency
- gridjs — poor React integration, required raw HTML for links
- Zero tests — not a single unit or E2E test
The new stack
| Before | After |
|---|---|
| Create React App | Vite 8 |
| React 18 | React 19 |
| Redux (4 slices) | Zustand (1 store) |
| CSS Modules | Tailwind CSS v4 |
| gridjs | TanStack Table v8 |
| phosphor-react | Lucide React |
| No tests | 101 tests |
The entire state management went from 4 Redux slices + a store config + typed hooks file down to a single Zustand store. One file. The loadCSV action parses the CSV and calculates all balances/statistics in one call.
New features
Beyond the tech migration, I added several things I’d been wanting for a while:
- Dark mode — system default with manual toggle, including adaptive logo/favicon
- Date range filtering — 1M/3M/6M/1Y/All buttons on all charts
- Transaction type filter — dropdown to filter by Interest, Exchange, Withdrawal, etc.
- Portfolio metrics — Net Invested, Interest Earned, Unrealized P/L at a glance
- 1-week change — quick pulse check on the dashboard
- Demo mode — “Try Demo” button with realistic sample data
- Mobile responsive — collapsible sidebar for smaller screens

Testing
Going from zero tests to 101 felt good. The test pyramid:
- 84 unit tests (Vitest) — CSV parser, balance calculator, TX linkage for 19+ blockchains, formatting, localStorage, date filtering
- 17 E2E tests (Playwright) — full demo flow, page navigation, search, pagination, dark mode persistence, save/restore
The demo CSV generator (generate_demo.py) uses a fixed random seed, so the test data is deterministic and reproducible.
CI/CD
Three chained GitHub Actions workflows:
- CI — lint, typecheck, unit tests across Node 20/22/24, production build, E2E
- Deploy — FTP upload (only fires after CI passes on main)
- Release — creates a GitHub release from
CHANGELOG.md(only after Deploy succeeds)
Main branch is protected — all 9 CI checks must pass before merging.
What I learned
- Zustand is criminally underrated. The migration from Redux felt like removing a splint.
- Tailwind CSS v4’s
@custom-variantfor class-based dark mode is clean but not obvious if you’re coming from v3’s config file approach. - Recharts tooltips need
itemStyleandlabelStylein addition tocontentStyleto properly theme for dark mode. The docs don’t make this obvious. - The Nexo CSV export doesn’t distinguish between interest earned in-kind vs. earned as NEXO tokens. Both show up as
Input: NEXO, Output: NEXO. This means the “Earn in NEXO” breakdown can’t be accurately split per-asset.
The rebuild was also supported by AI tooling, mainly Claude Code for implementation assistance and faster iteration, while I handled architecture, review, and final decisions.
The source code is on GitHub (github.com) and the app is live at nexo-ta.com (nexo-ta.com).