Skip to content
Rudra P. edited this page Feb 6, 2026 · 1 revision

Welcome to the OctaByte wiki!

OctaByte Financial Dashboard - Technical Documentation

Table of Contents

  1. Introduction and Project Overview
  2. Technology Stack Selection
  3. Project Architecture and Folder Structure
  4. Implementation Steps — In Order of Development
  5. Detailed File Breakdown
  6. UI Design and Styling Approach
  7. Performance Optimizations
  8. Error Handling Strategy
  9. Challenges and Solutions

1. Introduction and Project Overview

This document provides an in-depth explanation of the Dynamic Portfolio Dashboard project—a real-time stock portfolio tracking application built with modern web technologies. The application allows investors to monitor their holdings, view live market prices, track gains and losses, and analyze sector-wise allocation.

Business Requirements

The core requirements were:

  • Display portfolio holdings in a tabular format with columns: Stock Name, Purchase Price, Quantity, Investment, Portfolio Percentage, Exchange (NSE/BSE), Current Market Price (CMP), Present Value, Gain/Loss, P/E Ratio, and Latest Earnings (EPS).
  • Fetch live CMP data from Yahoo Finance.
  • Fetch P/E Ratio and EPS data from Google Finance.
  • Implement dynamic updates every 15 seconds without page refresh.
  • Color-code Gain/Loss values (green for profit, red for loss).
  • Group stocks by sector with sector-level summaries.
  • Build a responsive, visually appealing, production-grade UI.

The Challenge

Neither Yahoo Finance nor Google Finance provide official free APIs for retail developers. This meant we needed to use unofficial libraries or web scraping—techniques that can be fragile and require robust error handling and fallback mechanisms.


2. Technology Stack Selection

Frontend

Technology Purpose Why Chosen
Next.js 16 React framework with App Router Server-side rendering, API routes, excellent DX
TypeScript Static typing Catch errors at compile time, better IDE support
Tailwind CSS v4 Utility-first CSS Rapid styling, excellent RTL and dark mode support
TanStack React Query Data fetching and caching Automatic refetching, caching, error/retry handling
TanStack React Table Table rendering Headless, performant, full control over UI
Lucide React Icons Consistent, tree-shakeable icon library

Backend

Technology Purpose Why Chosen
Next.js API Routes Backend endpoints Unified codebase, no separate server needed
yahoo-finance2 Stock price fetching Well-maintained unofficial Yahoo Finance library
Cheerio HTML parsing for scraping Fast, jQuery-like DOM manipulation for Node.js
Axios HTTP requests Promise-based, interceptor support, timeout handling

3. Project Architecture and Folder Structure

The project follows the Next.js 13+ App Router conventions with a clean separation of concerns:

portfolio-dashboard/
├── src/
│   ├── app/                    # Next.js App Router
│   │   ├── api/               # API Routes (Backend)
│   │   │   └── portfolio/
│   │   │       └── route.ts   # Main portfolio endpoint
│   │   ├── layout.tsx         # Root layout with providers
│   │   ├── page.tsx           # Dashboard page
│   │   └── globals.css        # Global styles and theme
│   ├── components/            # React components
│   │   ├── ui/                # Reusable UI primitives (shadcn/ui)
│   │   ├── providers.tsx      # Context providers wrapper
│   │   ├── PortfolioTable.tsx # Main data table
│   │   └── SectorSummary.tsx  # Sector allocation cards
│   ├── hooks/                 # Custom React hooks
│   │   └── use-portfolio.ts   # Portfolio data fetching hook
│   ├── lib/                   # Utilities and services
│   │   ├── holdings.ts        # Static portfolio data
│   │   ├── utils.ts           # Utility functions
│   │   └── services/
│   │       └── stock-service.ts # External API integration
│   └── types/                 # TypeScript type definitions
│       └── portfolio.ts       # Portfolio-related types
├── public/                    # Static assets
├── package.json
└── tailwind.config.ts

Architectural Decisions

  1. Co-located API Routes: Backend logic lives alongside frontend code in the app/api directory. This simplifies deployment and reduces infrastructure complexity.

  2. Service Layer Pattern: External API calls are abstracted into StockService, making it easy to swap data sources or add caching layers.

  3. Custom Hooks: The usePortfolio hook encapsulates all data fetching logic, query configuration, and derived computations, keeping components clean.

  4. Component Composition: The UI is broken into focused components (PortfolioTable, SectorSummary) that receive data as props, enabling easy testing and reuse.


4. Implementation Steps — In Order of Development

This section documents the exact order in which we built the application, mimicking a real production development workflow.

Step 1: Project Scaffolding

We started with create-next-app to bootstrap the project:

npx create-next-app@latest portfolio-dashboard --typescript --tailwind --app --src-dir

This gave us a Next.js 16 project with TypeScript and Tailwind CSS pre-configured.

Step 2: Define the Data Model (Types)

Before writing any functional code, we defined our TypeScript interfaces in src/types/portfolio.ts:

export interface Stock {
  id: string;
  name: string;
  ticker: string;
  exchange: "NSE" | "BSE" | "N/A";
  sector: string;
  purchasePrice: number;
  quantity: number;
}

export interface LiveStockData {
  ticker: string;
  price: number;
  change: number;
  changePercent: number;
  peRatio?: number | null;
  earnings?: number | null;
  lastUpdated: string;
}

export interface PortfolioPosition extends Stock {
  livePrice: number;
  investmentValue: number;
  presentValue: number;
  gainLoss: number;
  gainLossPercent: number;
  portfolioWeight: number;
  peRatio: number | null;
  latestEarnings: number | null;
}

Why this approach?

  • Stock: Represents static data the user provides (what they bought, at what price, how many shares).
  • LiveStockData: Represents real-time data fetched from external APIs.
  • PortfolioPosition: The computed entity that merges static holdings with live data and calculates derived metrics.

This "composition over extension" pattern keeps concerns separated and makes the data flow explicit.

Step 3: Seed the Static Portfolio Data

We created src/lib/holdings.ts with the user's portfolio data:

export const PORTFOLIO_HOLDINGS: Stock[] = [
  { id: "1", name: "HDFC Bank", ticker: "HDFCBANK.NS", exchange: "NSE", sector: "Financial Sector", purchasePrice: 1490, quantity: 50 },
  // ... 25 more stocks across 6 sectors
];

Each stock includes a Yahoo Finance-compatible ticker symbol (e.g., HDFCBANK.NS for NSE, 511577.BO for BSE).

Step 4: Build the Stock Service — API Integration

This was the most critical and challenging piece. We created src/lib/services/stock-service.ts with two main methods:

4.1 getQuotes(tickers: string[])

This method:

  1. Checks the cache first: Before making any API calls, we look for cached data less than 15 seconds old.
  2. Fetches from Yahoo Finance: Uses the yahoo-finance2 library to get current prices.
  3. Scrapes Google Finance: For P/E and EPS data, we send an HTTP request to Google Finance and parse the HTML with Cheerio.
  4. Falls back to simulated data: If both APIs fail (rate limiting, network issues), we generate realistic mock data based on purchase price with ±2% variance.
  5. Guarantees a response: Even in worst-case scenarios, every ticker gets a data entry to prevent UI crashes.
const yahooFinanceInstance = new yahooFinance();
yahooResults = await yahooFinanceInstance.quote(tickersToFetch);

Important Note: The yahoo-finance2 library (v3+) requires instantiation with new yahooFinance(). Earlier versions used a default export. This caused initial runtime errors that we debugged and fixed.

4.2 scrapeGoogleFinance(ticker: string)

This method converts Yahoo ticker format to Google Finance format and scrapes the page:

// Convert: HDFCBANK.NS → NSE:HDFCBANK
if (ticker.endsWith('.NS')) {
    searchTicker = `NSE:${ticker.replace('.NS', '')}`;
}

We use Cheerio to parse the HTML and find specific labels like "P/E ratio" and "Earnings per share".

Step 5: Create the Portfolio API Endpoint

In src/app/api/portfolio/route.ts, we built the Next.js API route that:

  1. Loads static holdings from PORTFOLIO_HOLDINGS.

  2. Fetches live quotes via StockService.getQuotes().

  3. Calculates derived metrics:

    • Investment Value = Purchase Price × Quantity
    • Present Value = Current Price × Quantity
    • Gain/Loss = Present Value - Investment Value
    • Gain/Loss % = (Gain/Loss / Investment Value) × 100
    • Portfolio Weight = (Present Value / Total Portfolio Value) × 100
  4. Returns structured JSON with the portfolio array, total value, and timestamp.

We also set export const dynamic = 'force-dynamic' to disable Next.js static caching for this route.

Step 6: Build the Data Fetching Hook

The src/hooks/use-portfolio.ts hook wraps React Query:

export const usePortfolio = () => {
  const query = useQuery({
    queryKey: ['portfolio'],
    queryFn: fetchPortfolio,
    refetchInterval: 15000, // Auto-refresh every 15 seconds
    staleTime: 10000,
    retry: 3,
    retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
  });
  // ... derived values
};

This hook also provides:

  • totalGainLoss: Computed from current data.
  • totalGainLossPercent: Portfolio-wide return percentage.
  • sortedPortfolio: Pre-sorted by present value (descending).
  • errorMessage: Human-readable error messages for different failure types.

Step 7: Create the Portfolio Table Component

src/components/PortfolioTable.tsx is the heart of the UI. Key features:

  1. Memoized Row Component: Each TableRow is wrapped in React.memo to prevent unnecessary re-renders when sibling rows update.

  2. Client-Side Sorting: Users can click column headers to sort by any field. We maintain sortField and sortDirection in local state.

  3. Column Visibility Toggle: A dropdown menu allows users to show/hide columns like P/E ratio or EPS.

  4. Formatting Functions: Currency and percentage formatters are wrapped in useCallback to maintain referential equality.

  5. Visual Indicators:

    • Green/red icons for profit/loss.
    • TrendingUp/TrendingDown icons.
    • Percentage color-coding.

Step 8: Create the Sector Summary Component

src/components/SectorSummary.tsx aggregates data by sector:

const sectors = portfolio.reduce((acc, stock) => {
  if (!acc[stock.sector]) {
    acc[stock.sector] = { name: stock.sector, totalInvestment: 0, currentValue: 0, gainLoss: 0, count: 0 };
  }
  acc[stock.sector].totalInvestment += stock.investmentValue;
  acc[stock.sector].currentValue += stock.presentValue;
  acc[stock.sector].gainLoss += stock.gainLoss;
  acc[stock.sector].count += 1;
  return acc;
}, {});

Each sector card displays:

  • Sector name and stock count.
  • Total current value.
  • Gain/loss with color coding.
  • Allocation percentage with progress bar.
  • Animated entrance effects (staggered fade-in).

Step 9: Compose the Dashboard Page

src/app/page.tsx brings everything together:

  1. Header: Logo, theme toggle (dark/light), refresh button.
  2. Metrics Cards: Three cards showing Total Investment, Current Value, and Total P&L.
  3. Two-Column Layout:
    • Left (8 cols): Holdings table.
    • Right (4 cols): Sector allocation cards.
  4. Loading States: Pulse animations while data loads.
  5. Responsive Design: Stacks vertically on mobile.

Step 10: Configure Providers and Layout

src/components/providers.tsx wraps the app with:

  • QueryClientProvider for React Query.
  • ThemeProvider from next-themes for dark/light mode.
  • TooltipProvider for tooltip components.

src/app/layout.tsx imports the global CSS and wraps the page with providers.


5. Detailed File Breakdown

src/types/portfolio.ts

Purpose: Central type definitions ensuring type safety across the application.

Interfaces Defined:

  • Stock: Base holding data (static).
  • LiveStockData: Real-time API response shape.
  • PortfolioPosition: Merged and enriched data for display.
  • SectorSummary: Aggregated sector metrics.

Why It's Important: TypeScript interfaces catch data shape mismatches at compile time. If the API response changes, we'll get immediate errors rather than runtime bugs.


src/lib/holdings.ts

Purpose: Static data store for portfolio holdings.

Structure: Array of 26 stocks across 6 sectors (Financial, Tech, Consumer, Power, Pipe, Others).

Key Design Choice: Ticker symbols follow Yahoo Finance naming conventions (.NS for NSE, .BO for BSE).


src/lib/services/stock-service.ts

Purpose: Abstraction layer for external API integration.

Methods:

Method Purpose Fallback Strategy
getQuotes() Fetch live prices and financials Returns mock data on failure
scrapeGoogleFinance() Scrape P/E and EPS from Google Returns null values on failure

Caching Strategy:

const CACHE_DURATION = 15000; // 15 seconds
const quoteCache: Record<string, CacheEntry> = {};

We use an in-memory cache keyed by ticker symbol. Each entry stores the data and a timestamp. If a request comes in within 15 seconds, we serve cached data instead of hitting the API.

Error Resilience:

The service has three levels of fallback:

  1. Yahoo succeeds, Google fails: Use Yahoo's P/E data if available.
  2. Both fail: Generate simulated data with random variance.
  3. Individual ticker fails: Still return data for other tickers.

src/app/api/portfolio/route.ts

Purpose: Next.js API route serving enriched portfolio data.

Request Flow:

Client Request → route.ts → StockService → Yahoo/Google → Process → JSON Response

Key Calculations:

const gainLoss = presentValue - investmentValue;
const gainLossPercent = (gainLoss / investmentValue) * 100;
const portfolioWeight = (presentValue / totalPortfolioValue) * 100;

Response Shape:

{
  "portfolio": [...],
  "totalValue": 1543060.50,
  "totalInvestment": 1500000.00,
  "lastUpdated": "2026-02-06T15:30:00.000Z"
}

src/hooks/use-portfolio.ts

Purpose: Custom hook encapsulating data fetching logic.

React Query Configuration:

Option Value Purpose
refetchInterval 15000ms Poll for updates every 15 seconds
staleTime 10000ms Consider data fresh for 10 seconds
gcTime 300000ms Keep cached data for 5 minutes
retry 3 Retry failed requests 3 times
retryDelay Exponential 1s, 2s, 4s backoff

Derived Values:

const totalGainLoss = useMemo(() => {
  return query.data.totalValue - query.data.totalInvestment;
}, [query.data]);

Using useMemo prevents recalculation on every render.


src/components/PortfolioTable.tsx

Purpose: Renders the main holdings table with sorting and column visibility.

Component Structure:

PortfolioTable (memo)
├── View Settings Dropdown
└── Table
    ├── Header Row with SortButtons
    └── Body (mapped TableRows)
        └── TableRow (memo) — Individual stock row

Performance Optimizations:

  1. Memoized Parent and Child: Both PortfolioTable and TableRow use React.memo.
  2. Stable Callbacks: handleSort, formatCurrency, formatPercent use useCallback.
  3. Memoized Sorted Data: The sortedData array is computed only when data, sortField, or sortDirection change.

Visual Features:

  • Gradient icons indicating profit/loss.
  • Micro-interaction on hover (background color change).
  • Responsive columns (hide on mobile if needed).

src/components/SectorSummary.tsx

Purpose: Displays sector allocation with visual cards.

Data Aggregation:

Uses Array.reduce() to group stocks by sector and sum their values.

Card Features:

  • Animated entrance (staggered fade-in using CSS animation-delay).
  • Color-coded borders (green for profit, red for loss).
  • Allocation progress bar.
  • Blurred background gradient for depth.

src/app/page.tsx

Purpose: Main dashboard page orchestrating all components.

Layout Sections:

  1. Header (sticky):

    • Logo image.
    • Theme toggle button (Sun/Moon icons).
    • Sync Data button (triggers manual refetch).
  2. Metrics Grid (3 columns):

    • Total Investment.
    • Current Value.
    • Total Gain/Loss (with percentage badge).
  3. Content Grid (12 columns):

    • Holdings table (8 cols on desktop).
    • Sector summary (4 cols on desktop).

Responsive Behavior:

  • Mobile: Single column layout.
  • Tablet: 2-column grid for metrics.
  • Desktop: Full 12-column grid.

src/app/globals.css

Purpose: Global styles, theme variables, and custom utility classes.

Key Sections:

  1. @theme Block: Defines CSS custom properties for colors using OKLCH color space:
--color-primary: oklch(55% 0.25 285);
  1. Dark Mode Overrides: .dark class modifies all color variables.

  2. Glassmorphism Classes:

    • .glass-panel: Heavy blur with border.
    • .glass-card: Lighter blur with gradient.
  3. Neon Border Animations:

    • .neon-border-profit: Animated green border for profit cards.
    • .neon-border-loss: Animated red border for loss cards.
  4. Text Glow Effects:

    • .text-glow-profit: Green text shadow in dark mode.
    • .text-glow-loss: Red text shadow in dark mode.

6. UI Design and Styling Approach

Design Philosophy

We aimed for a premium, modern fintech aesthetic with:

  • Dark mode as the default (matches trader preferences).
  • Glassmorphism for depth without clutter.
  • Subtle animations for perceived performance.
  • Green/red color psychology for profit/loss.

Color System

We used the OKLCH color space for perceptually uniform colors:

--color-profit: #10b981;  /* Emerald 500 */
--color-loss: #ef4444;     /* Red 500 */

Animation Patterns

  1. Fade-In-Up: Cards slide up and fade in on load.
  2. Border Travel: A glowing dot travels around profit/loss card borders.
  3. Hover Lift: Cards rise slightly on hover.
  4. Loading Pulse: Skeleton screens pulse while loading.

Responsive Design

We used Tailwind's responsive prefixes:

<div className="grid grid-cols-1 md:grid-cols-3 gap-6">

7. Performance Optimizations

1. React Query Caching

React Query caches API responses and deduplicates requests. If multiple components call usePortfolio(), only one network request is made.

2. Component Memoization

const TableRow = memo(function TableRow({ stock, ... }) { ... });
export const PortfolioTable = memo(function PortfolioTable({ data }) { ... });

This prevents re-renders when parent state changes but props remain equal.

3. useMemo for Expensive Computations

const sortedData = useMemo(() => {
  return [...data].sort((a, b) => { ... });
}, [data, sortField, sortDirection]);

4. useCallback for Stable References

const formatCurrency = useCallback((val: number) => {
  return new Intl.NumberFormat("en-IN", { ... }).format(val);
}, []);

5. Server-Side Caching

The StockService caches API responses for 15 seconds, reducing external API calls.

6. Next.js API Route Optimizations

export const dynamic = 'force-dynamic';

This ensures fresh data on every request while letting Next.js handle edge caching if deployed to Vercel.


8. Error Handling Strategy

API Layer

try {
  const yahooResults = await yahooFinanceInstance.quote(tickers);
} catch (e) {
  // Fallback to mock data
}

Errors are caught and never propagate to crash the app.

Hook Layer

const errorMessage = useMemo(() => {
  if (query.error instanceof AxiosError) {
    if (query.error.code === 'ECONNABORTED') return 'Request timed out. Retrying...';
    // ...
  }
  return 'An unexpected error occurred';
}, [query.error]);

Human-readable error messages are derived from error types.

UI Layer

{!data && (
  <div className="animate-pulse" /> // Loading skeleton
)}

Loading and error states are visually distinct.


9. Challenges and Solutions

Challenge 1: Yahoo Finance API Changes

Problem: The yahoo-finance2 library updated its API, requiring new yahooFinance() instead of direct method calls.

Solution: Updated the import and instantiation pattern.

Challenge 2: Google Finance Scraping Fragility

Problem: Google Finance HTML structure changes frequently, breaking scrapers.

Solution: Implemented multiple fallback strategies:

  1. Try Yahoo's P/E data first.
  2. Fall back to mock data if scraping fails.
  3. Never let the dashboard show "0" values.

Challenge 3: Rate Limiting

Problem: Too many API requests can get blocked.

Solution:

  • In-memory cache with 15-second TTL.
  • React Query's refetchInterval limits frontend polling.
  • Batch requests where possible.

Challenge 4: TypeScript Type Safety

Problem: External API responses have loose typing.

Solution: Cast responses to any[] at the boundary, then immediately transform into well-typed internal structures.


image

Clone this wiki locally