-
Notifications
You must be signed in to change notification settings - Fork 0
Home
Welcome to the OctaByte wiki!
- Introduction and Project Overview
- Technology Stack Selection
- Project Architecture and Folder Structure
- Implementation Steps — In Order of Development
- Detailed File Breakdown
- UI Design and Styling Approach
- Performance Optimizations
- Error Handling Strategy
- Challenges and Solutions
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.
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.
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.
| 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 |
| 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 |
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
-
Co-located API Routes: Backend logic lives alongside frontend code in the
app/apidirectory. This simplifies deployment and reduces infrastructure complexity. -
Service Layer Pattern: External API calls are abstracted into
StockService, making it easy to swap data sources or add caching layers. -
Custom Hooks: The
usePortfoliohook encapsulates all data fetching logic, query configuration, and derived computations, keeping components clean. -
Component Composition: The UI is broken into focused components (
PortfolioTable,SectorSummary) that receive data as props, enabling easy testing and reuse.
This section documents the exact order in which we built the application, mimicking a real production development workflow.
We started with create-next-app to bootstrap the project:
npx create-next-app@latest portfolio-dashboard --typescript --tailwind --app --src-dirThis gave us a Next.js 16 project with TypeScript and Tailwind CSS pre-configured.
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.
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).
This was the most critical and challenging piece. We created src/lib/services/stock-service.ts with two main methods:
This method:
- Checks the cache first: Before making any API calls, we look for cached data less than 15 seconds old.
-
Fetches from Yahoo Finance: Uses the
yahoo-finance2library to get current prices. - Scrapes Google Finance: For P/E and EPS data, we send an HTTP request to Google Finance and parse the HTML with Cheerio.
- 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.
- 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.
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".
In src/app/api/portfolio/route.ts, we built the Next.js API route that:
-
Loads static holdings from
PORTFOLIO_HOLDINGS. -
Fetches live quotes via
StockService.getQuotes(). -
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
-
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.
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.
src/components/PortfolioTable.tsx is the heart of the UI. Key features:
-
Memoized Row Component: Each
TableRowis wrapped inReact.memoto prevent unnecessary re-renders when sibling rows update. -
Client-Side Sorting: Users can click column headers to sort by any field. We maintain
sortFieldandsortDirectionin local state. -
Column Visibility Toggle: A dropdown menu allows users to show/hide columns like P/E ratio or EPS.
-
Formatting Functions: Currency and percentage formatters are wrapped in
useCallbackto maintain referential equality. -
Visual Indicators:
- Green/red icons for profit/loss.
- TrendingUp/TrendingDown icons.
- Percentage color-coding.
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).
src/app/page.tsx brings everything together:
- Header: Logo, theme toggle (dark/light), refresh button.
- Metrics Cards: Three cards showing Total Investment, Current Value, and Total P&L.
-
Two-Column Layout:
- Left (8 cols): Holdings table.
- Right (4 cols): Sector allocation cards.
- Loading States: Pulse animations while data loads.
- Responsive Design: Stacks vertically on mobile.
src/components/providers.tsx wraps the app with:
-
QueryClientProviderfor React Query. -
ThemeProviderfrom next-themes for dark/light mode. -
TooltipProviderfor tooltip components.
src/app/layout.tsx imports the global CSS and wraps the page with providers.
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.
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).
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:
- Yahoo succeeds, Google fails: Use Yahoo's P/E data if available.
- Both fail: Generate simulated data with random variance.
- Individual ticker fails: Still return data for other tickers.
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"
}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.
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:
-
Memoized Parent and Child: Both
PortfolioTableandTableRowuseReact.memo. -
Stable Callbacks:
handleSort,formatCurrency,formatPercentuseuseCallback. -
Memoized Sorted Data: The
sortedDataarray is computed only whendata,sortField, orsortDirectionchange.
Visual Features:
- Gradient icons indicating profit/loss.
- Micro-interaction on hover (background color change).
- Responsive columns (hide on mobile if needed).
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.
Purpose: Main dashboard page orchestrating all components.
Layout Sections:
-
Header (sticky):
- Logo image.
- Theme toggle button (Sun/Moon icons).
- Sync Data button (triggers manual refetch).
-
Metrics Grid (3 columns):
- Total Investment.
- Current Value.
- Total Gain/Loss (with percentage badge).
-
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.
Purpose: Global styles, theme variables, and custom utility classes.
Key Sections:
- @theme Block: Defines CSS custom properties for colors using OKLCH color space:
--color-primary: oklch(55% 0.25 285);-
Dark Mode Overrides:
.darkclass modifies all color variables. -
Glassmorphism Classes:
-
.glass-panel: Heavy blur with border. -
.glass-card: Lighter blur with gradient.
-
-
Neon Border Animations:
-
.neon-border-profit: Animated green border for profit cards. -
.neon-border-loss: Animated red border for loss cards.
-
-
Text Glow Effects:
-
.text-glow-profit: Green text shadow in dark mode. -
.text-glow-loss: Red text shadow in dark mode.
-
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.
We used the OKLCH color space for perceptually uniform colors:
--color-profit: #10b981; /* Emerald 500 */
--color-loss: #ef4444; /* Red 500 */- Fade-In-Up: Cards slide up and fade in on load.
- Border Travel: A glowing dot travels around profit/loss card borders.
- Hover Lift: Cards rise slightly on hover.
- Loading Pulse: Skeleton screens pulse while loading.
We used Tailwind's responsive prefixes:
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">React Query caches API responses and deduplicates requests. If multiple components call usePortfolio(), only one network request is made.
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.
const sortedData = useMemo(() => {
return [...data].sort((a, b) => { ... });
}, [data, sortField, sortDirection]);const formatCurrency = useCallback((val: number) => {
return new Intl.NumberFormat("en-IN", { ... }).format(val);
}, []);The StockService caches API responses for 15 seconds, reducing external API calls.
export const dynamic = 'force-dynamic';This ensures fresh data on every request while letting Next.js handle edge caching if deployed to Vercel.
try {
const yahooResults = await yahooFinanceInstance.quote(tickers);
} catch (e) {
// Fallback to mock data
}Errors are caught and never propagate to crash the app.
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.
{!data && (
<div className="animate-pulse" /> // Loading skeleton
)}Loading and error states are visually distinct.
Problem: The yahoo-finance2 library updated its API, requiring new yahooFinance() instead of direct method calls.
Solution: Updated the import and instantiation pattern.
Problem: Google Finance HTML structure changes frequently, breaking scrapers.
Solution: Implemented multiple fallback strategies:
- Try Yahoo's P/E data first.
- Fall back to mock data if scraping fails.
- Never let the dashboard show "0" values.
Problem: Too many API requests can get blocked.
Solution:
- In-memory cache with 15-second TTL.
- React Query's
refetchIntervallimits frontend polling. - Batch requests where possible.
Problem: External API responses have loose typing.
Solution: Cast responses to any[] at the boundary, then immediately transform into well-typed internal structures.