A professional-grade React application built for the KoinX Frontend Internship Assessment — designed to help crypto investors optimise their tax liability through real-time loss-harvesting simulations.
🔗 GitHub → | 🛠️ Built with React 18, TypeScript, Tailwind CSS v4
| Feature | Detail |
|---|---|
| Real-Time Tax Math | STCG & LTCG recalculated instantly via a custom useTaxLogic hook as you select/deselect assets |
| "You're going to save ₹X" | Savings banner appears only when pre-harvesting realised gains > post-harvesting gains — per spec |
| Amount to Sell column | Populated with the asset's full holding when a row is selected; shows a read-only badge with a 🔒 icon |
| Scientific notation safety | formatCryptoValue handles crypto dust values like 1.42e-14 that .toFixed() would silently zero |
| Select All / Deselect All | Header checkbox with indeterminate state (some but not all selected) |
| Column sorting | Click any header to sort by STCG, LTCG, Price, or Holdings — default is STCG ascending (worst losses first) |
| Shimmer loading skeletons | Replaces missing data during the API delay instead of layout shift |
| Toast notifications | Per-asset feedback when added/removed, collapsed to a summary for Select All |
| Keyboard navigation | Every table row is focusable; Space/Enter toggles selection |
| Dark theme | Full dark UI with glass-morphism cards, tabular-num font stack, and consistent colour tokens |
- Framework: React 18 · Vite
- Language: TypeScript (strict mode)
- Styling: Tailwind CSS v4 (
@themecustom tokens, notailwind.configat runtime) - Icons: Lucide React
- Utilities:
clsxfor conditional class management - No state management library — derived state via
useMemoprevents data desync entirely
Instead of maintaining two separate state objects for "Pre" and "Post" harvesting, useTaxLogic derives the post-harvest view from the pre-harvest base + the current selection set. This means there's a single source of truth — the selection map — and no risk of the two cards ever going out of sync.
All tax calculations live in src/hooks/useTaxLogic.ts and its exported helpers (getNetGain, getRealisedGains). This makes the financial logic independently testable without any React dependency. A vitest unit test can import and call it directly.
Standard .toFixed(8) silently turns 1.42e-14 into "0.00000000", hiding a non-zero balance. The formatter uses toLocaleString for the normal range and falls back to toExponential for sub-1e-6 dust values.
HoldingsTable is split into TableHeader, TableRow, and TableSkeleton sub-components. Each can be rendered and tested in isolation. The parent component is a coordinator — it only manages sort state and passes callbacks down.
The data-fetch effect uses a didCancel boolean (not AbortController — YAGNI for a mock API) to ignore stale responses if the component unmounts mid-fetch. This prevents the setState on unmounted component warning in React strict mode.
# Install dependencies
npm install
# Start dev server
npm run dev
# Open http://localhost:5173Requires Node.js ≥ 18.
src/
├── components/
│ ├── Dashboard.tsx # Main page — wires hooks to UI
│ ├── GainsSummaryCard.tsx # Pre / After Harvesting cards
│ ├── HarvestBanner.tsx # "You're going to save ₹X" banner
│ ├── Toast.tsx # Notification stack
│ └── HoldingsTable/
│ ├── index.tsx # Table coordinator + sort state
│ ├── TableHeader.tsx # Sticky thead with sortable columns
│ ├── TableRow.tsx # Individual asset row + Amount to Sell cell
│ └── TableSkeleton.tsx # Shimmer loading rows
├── hooks/
│ ├── useTaxLogic.ts # Pure tax calculation hook
│ ├── useHoldings.ts # Data fetch + selection state
│ └── useToast.ts # Lightweight toast queue
├── services/
│ └── portfolioApi.ts # Mock API with simulated network delay
├── data/
│ └── mockData.ts # Realistic INR-denominated portfolio data
├── types/
│ └── index.ts # Domain types: HoldingAsset, CapitalGainsReport
└── utils/
└── formatters.ts # Currency, crypto, and date formatters
- Unit tests via
vitest+@testing-library/react— the pure hooks are already structured for this - Persistence —
localStorageeffect inuseHoldingsto survive page refresh - WebSocket mock — simulates live price updates to make the demo more dynamic
- Export to CSV — download the harvesting plan as a spreadsheet for a CA
Built by Smit Prajapati · KoinX Frontend Internship Assessment · April 2026
