Nepal IPO Intelligence Dashboard

Build a fintech dashboard for Nepal IPO data with Next.js and Prisma.

Introduction

30 Second Summary

Every year, thousands of Nepalese investors rush to apply for IPOs on the Nepal Stock Exchange. Understanding which companies are worth investing in requires sifting through financial reports, sector trends, and market data scattered across multiple sources.

In this project, you will build a Nepal IPO Intelligence dashboard using Next.js and Prisma. You will design financial data models, seed a database with demo data, and render market stats and interactive charts on a professional fintech interface.

What You'll Build

When you run the app, a polished dashboard loads showing live NEPSE market stats, upcoming IPO cards, sector performance breakdowns, and interactive Recharts charts you can hover over to explore trends.

By the end of this project, you'll have:

  • A live fintech dashboard where you can view NEPSE Index stats, market turnover, upcoming IPO cards, and sector performance at a glance.
  • Interactive financial charts that let you hover over NEPSE Index trends and compare average return on equity across sectors.
  • Company detail pages where you click into any company and review its IPO history, financial profile, and three years of key metrics.
  • Secret Mission: Add a persistent dark/light mode toggle that saves your theme preference across page refreshes.

Are there any prerequisites?

This project assumes intermediate web development experience. You should be comfortable with HTML, CSS, and JavaScript basics, and have some familiarity with React components.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters to you. You're creating the foundation of a Nepal-focused IPO research platform. It's a fintech dashboard that helps investors understand IPO data with accurate, explainable analysis.

Set Up Node.js, Next.js, and Prisma

Before you can build the Nepal IPO Intelligence dashboard, you need a working development environment. Node.js powers the JavaScript runtime that everything else depends on.

In this step, you will install Node.js, scaffold a new Next.js project with TypeScript and Tailwind CSS, and initialize Prisma with a SQLite database. By the end, a dev server will be running at http://localhost:3000.

In this step, get ready to:
  • Install Node.js v24 LTS and verify it is working.
  • Scaffold a Next.js 16 project and install Prisma and Recharts.
  • Start the dev server and confirm the starter page loads.
Install Node.js v24 LTS

Node.js is the JavaScript runtime that powers both the Next.js framework and the Prisma database tools. This project requires version 24 (LTS) or higher.

  • In VS Code, click Terminal in the top menu bar.
  • Click New Terminal.
  • Check if Node.js is already installed by running this command:
node -v

✔️ I see v24.x or higher

Node.js is ready. Confirm npm is also available by running this command:

npm -v

You should see a version number printed. You are all set to continue.

ⓧ I see an older version

Your current Node.js version is too old for this project. Next.js 16 requires Node.js 20.9.0 or higher, and this project targets v24 LTS for the latest features.

Why not just use the older version?

On macOS, your system may ship with an older Node.js that shadows newly installed versions. After installing v24, close and reopen your terminal so the new version is picked up. If node -v still shows the old version, use a version manager like nvm to switch.

  • Visit the Node.js download page.
  • Download the v24 LTS installer for your operating system.
  • Run the installer with default settings. It will replace your existing Node.js installation.
  • Close your VS Code terminal and open a new one by clicking Terminal then New Terminal.
  • Run node -v again. You should see v24.x.
  • Confirm npm works by running:
npm -v

You should see a version number printed.

ⓧ Command not found

Node.js is not installed yet. You need to download and install it.

  • Visit the Node.js download page.
  • Download the v24 LTS installer for your operating system.
  • Run the installer with default settings.
  • Close your VS Code terminal and open a new one by clicking Terminal then New Terminal.
  • Run node -v. You should see v24.x.
  • Confirm npm works by running:
npm -v

You should see a version number printed.

Scaffold the Next.js project and install dependencies

Next.js is the React framework that handles routing, server-side rendering, and the full-stack structure of the dashboard. Recharts is the charting library you will use for financial data visualizations later.

  • Move to your Desktop and create the Next.js project by running these commands:
cd ~/Desktop
npx create-next-app@latest nepal-ipo-intelligence --yes --use-npm

What do these flags do?

The --yes flag accepts the recommended defaults: TypeScript, Tailwind CSS, ESLint, App Router, and Turbopack. No interactive prompts will appear.

The --use-npm flag ensures npm is used as the package manager for the project.

npx install prompt

If npx asks "Need to install the following packages: create-next-app@latest. Ok to proceed?", type y and press Enter.

  • Confirm the project folder was created by running:
ls

You should see nepal-ipo-intelligence listed in the output.

Don't see the folder?

Make sure you ran cd ~/Desktop first. If you are in a different directory, the folder will be created there instead.

If you see a permission error, try running the command again. On Windows, make sure you are using a terminal with write access to the Desktop.

help me troubleshoot why my create-next-app command failed

  • Move into the project folder and install the additional dependencies by running these commands:
cd nepal-ipo-intelligence
npm install prisma --save-dev
npm install @prisma/client recharts react-is

What are these packages?

The prisma package is the development CLI for creating and running database migrations. The @prisma/client package is the runtime library your app uses to query the database.

The recharts package is the charting library, and react-is is a peer dependency that Recharts requires.

You should see terminal output confirming packages were added after each npm install command.

Seeing npm errors?

Check that you are inside the nepal-ipo-intelligence folder. Run pwd (macOS) or cd (Windows) to confirm your current directory. You should see a path ending in nepal-ipo-intelligence.

help me fix npm install errors in my Next.js project

  • Initialize Prisma with SQLite as the database provider by running this command:
npx prisma init --datasource-provider sqlite

What does this command do?

This creates a prisma/ directory with a schema.prisma file configured to use SQLite. It also creates a .env file with the database connection string pointing to a local dev.db file.

  • Confirm the Prisma files were created by running:
ls prisma/

You should see schema.prisma listed in the output.

✔️ Awesome, I've got everything!

Great. Your project has Prisma, Recharts, and all dependencies installed. Moving on.

ⓧ I'd like to double check the files

Open prisma/schema.prisma in VS Code. It should look like this:

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

Open the .env file in the project root. It should contain:

DATABASE_URL="file:./dev.db"

If both files match, you are good to continue.

Start the dev server and verify the starter page

Everything is installed. Time to start the Next.js dev server and confirm the full stack is working.

  • Open the project in a new VS Code window by running this command:
code .

If the code command is not found

In your current VS Code window, click File in the top menu bar, then Open Folder. Navigate to Desktop and select the nepal-ipo-intelligence folder.

A new VS Code window opens with the full project structure in the Explorer sidebar.

  • In the new VS Code window, open a terminal by clicking Terminal in the top menu bar, then New Terminal.
  • Before you run the dev server, what do you expect to see at http://localhost:3000?
  • Start the dev server by running this command:
npm run dev

What happens when you run this?

The npm run dev command starts the Next.js development server. It takes over the terminal and keeps running until you stop it with Ctrl+C. Leave it running for the rest of this project.

  • Open your browser and navigate to http://localhost:3000.

You should see the default Next.js starter page with the Next.js logo and links to documentation.

Don't see the starter page?

Make sure the terminal still shows the dev server running. If you see an error, check that you are in the nepal-ipo-intelligence folder by looking at the terminal prompt.

If port 3000 is already in use, Next.js will try 3001 instead. Check the terminal output for the actual URL.

help me troubleshoot my Next.js dev server not starting

Your development environment is ready. You have Node.js running the show, Next.js providing the framework, and Prisma wired up to SQLite. Next up, you will design the database schema and seed it with realistic Nepalese financial data.

Design the Database Schema and Seed Data

Your Next.js app is running, but the dashboard has no data to display. Every chart, every IPO card, every market stat needs structured data behind it.

In this step, you will design the core financial data models that power the entire platform. You will create Prisma models for companies, IPOs, sectors, financials, and market snapshots. Then you will populate the database with clearly-marked demo data modeled on real Nepalese market sectors like Banking, Hydropower, Insurance, and Manufacturing.

In this step, get ready to:
  • Define Prisma models for Sector, Company, IPO, Financial, and MarketData.
  • Create a seed script with demo data across four Nepalese market sectors.
  • Run the migration and verify the data in Prisma Studio.
Define the Prisma schema models

The prisma/schema.prisma file currently has only the generator and datasource blocks. You need to add five models that capture Nepal-specific financial data.

  • In VS Code, open prisma/schema.prisma.
  • Below the closing } of the datasource db block, add the Sector and Company models by pasting the following code:
model Sector {
  id          Int       @id @default(autoincrement())
  name        String    @unique
  description String?
  color       String?
  companies   Company[]
  createdAt   DateTime  @default(now())
  updatedAt   DateTime  @updatedAt
}

model Company {
  id             Int         @id @default(autoincrement())
  name           String
  symbol         String      @unique
  sectorId       Int
  sector         Sector      @relation(fields: [sectorId], references: [id])
  description    String?
  registeredDate String?
  status         String      @default("LISTED")
  dataConfidence String      @default("DEMO")
  source         String      @default("Demo data for development")
  ipos           IPO[]
  financials     Financial[]
  createdAt      DateTime    @default(now())
  updatedAt      DateTime    @updatedAt
}

What do these models do?

Sector groups companies by industry (Banking, Hydropower, etc.). Each sector has a color field for chart styling later.

Company stores each listed company with a unique symbol (like NABIL). The dataConfidence and source fields default to "DEMO" so demo data is never mistaken for real market data.

  • Save prisma/schema.prisma.
  • Verify the schema is valid so far by running this command:
npx prisma validate

You should see a message ending with "is valid" confirming no syntax errors.

Seeing a validation error?

Check that both model Sector and model Company are placed after the datasource db block, not inside it. Each model needs its own { and } braces.

help me fix my Prisma schema validation error

  • Add the IPO model below the Company model by pasting this code:
model IPO {
  id              Int      @id @default(autoincrement())
  companyId       Int
  company         Company  @relation(fields: [companyId], references: [id])
  ipoPrice        Float
  openDate        String
  closeDate       String
  openDateBS      String?
  closeDateBS     String?
  issueSize       Float
  issueManager    String?
  sebonApproval   String?
  status          String   @default("UPCOMING")
  dataConfidence  String   @default("DEMO")
  source          String   @default("Demo data for development")
  createdAt       DateTime @default(now())
  updatedAt       DateTime @updatedAt
}

What are the BS date fields?

The openDateBS and closeDateBS fields store Bikram Sambat (BS) calendar dates alongside Gregorian dates. Nepal uses both calendar systems, so storing both makes the platform usable for Nepalese investors.

sebonApproval tracks approval status from SEBON (Securities Board of Nepal), the regulatory body overseeing securities markets.

  • Save prisma/schema.prisma.
  • Add the Financial model below the IPO model by pasting this code:
model Financial {
  id             Int      @id @default(autoincrement())
  companyId      Int
  company        Company  @relation(fields: [companyId], references: [id])
  fiscalYear     String
  fiscalYearBS   String?
  revenue        Float
  netProfit      Float
  eps            Float
  roe            Float
  roce           Float
  debtToEquity   Float
  dataConfidence String   @default("DEMO")
  source         String   @default("Demo data for development")
  createdAt      DateTime @default(now())
  updatedAt      DateTime @updatedAt
}

What do these financial fields track?

Each Financial record captures one year of a company's performance. eps (earnings per share), roe (return on equity), roce (return on capital employed), and debtToEquity are key metrics investors use to evaluate company health.

  • Save prisma/schema.prisma.
  • Verify the schema again by running:
npx prisma validate

You should see the "is valid" confirmation again. Three models down, one to go.

  • Add the final model, MarketData, below the Financial model by pasting this code:
model MarketData {
  id             Int      @id @default(autoincrement())
  date           String
  dateBS         String?
  nepseIndex     Float
  turnover       Float
  volume         Int
  dataConfidence String   @default("DEMO")
  source         String   @default("Demo data for development")
  createdAt      DateTime @default(now())
  updatedAt      DateTime @updatedAt
}

What is MarketData?

MarketData stores daily NEPSE snapshots. Each record captures the index value, total market turnover (in NPR), and trading volume for a single day. This is a standalone model with no foreign keys because it represents the overall market, not individual companies.

  • Save prisma/schema.prisma.
  • Run the validation one more time to confirm the complete schema is correct:
npx prisma validate

You should see the "is valid" message. All five models are now defined.

✔️ Awesome, I've got everything!

Great. Make sure you have saved prisma/schema.prisma before moving on.

ⓧ I'd like to double check the full code

Your prisma/schema.prisma file should contain the generator, datasource, and all five models. Scroll through the file and confirm these blocks appear in order:

  • generator client block.
  • datasource db block with provider = "sqlite".
  • model Sector with companies Company[] relation.
  • model Company with sectorId, ipos, and financials relations.
  • model IPO with companyId relation and BS date fields.
  • model Financial with eps, roe, roce, debtToEquity fields.
  • model MarketData with nepseIndex, turnover, volume fields.
Create the seed script and configure the seed command

The schema defines the shape of the data. Now you need actual records to populate the database. You will create a TypeScript seed script that inserts demo data across four Nepalese market sectors.

  • First, install ts-node so Prisma can run the TypeScript seed file. Run this command:
npm install ts-node --save-dev
  • Open package.json in VS Code.
  • Add the following prisma key at the top level of the JSON object (after the "dependencies" block). This tells Prisma how to run the seed script:
"prisma": { "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts" }

Why the compiler-options flag?

Next.js 16 projects use ES modules by default. The ts-node runner needs CommonJS module resolution to execute the seed file outside of the Next.js build pipeline. The --compiler-options flag overrides the module setting just for this one command.

  • Save package.json.
  • Create the seed file at prisma/seed.ts and paste the following code to set up the sectors and companies:
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

async function main() {
  // Sectors
  const banking = await prisma.sector.create({ data: { name: 'Commercial Banks', description: 'Licensed commercial banks regulated by NRB', color: '#3B82F6' } })
  const hydro = await prisma.sector.create({ data: { name: 'Hydropower', description: 'Hydropower generation companies', color: '#10B981' } })
  const insurance = await prisma.sector.create({ data: { name: 'Life Insurance', description: 'Life insurance companies regulated by IB', color: '#8B5CF6' } })
  const manufacturing = await prisma.sector.create({ data: { name: 'Manufacturing and Processing', description: 'Manufacturing and processing industries', color: '#F59E0B' } })

  // Companies (8 total)
  const nabil = await prisma.company.create({ data: { name: 'Nabil Bank Limited', symbol: 'NABIL', sectorId: banking.id, description: 'One of the leading commercial banks in Nepal', status: 'LISTED', dataConfidence: 'DEMO', source: 'Demo data for development' } })
  const nic = await prisma.company.create({ data: { name: 'NIC Asia Bank Limited', symbol: 'NICA', sectorId: banking.id, description: 'A major commercial bank in Nepal', status: 'LISTED', dataConfidence: 'DEMO', source: 'Demo data for development' } })
  const upper = await prisma.company.create({ data: { name: 'Upper Tamakoshi Hydropower', symbol: 'UPPER', sectorId: hydro.id, description: 'Largest hydropower project in Nepal', status: 'LISTED', dataConfidence: 'DEMO', source: 'Demo data for development' } })
  const chilime = await prisma.company.create({ data: { name: 'Chilime Hydropower Company', symbol: 'CHCL', sectorId: hydro.id, description: 'Pioneer hydropower company in Nepal', status: 'LISTED', dataConfidence: 'DEMO', source: 'Demo data for development' } })
  const nli = await prisma.company.create({ data: { name: 'Nepal Life Insurance Company', symbol: 'NLIC', sectorId: insurance.id, description: 'Leading life insurance company in Nepal', status: 'LISTED', dataConfidence: 'DEMO', source: 'Demo data for development' } })
  const gli = await prisma.company.create({ data: { name: 'Gurans Life Insurance Company', symbol: 'GLICL', sectorId: insurance.id, description: 'Life insurance company in Nepal', status: 'UPCOMING', dataConfidence: 'DEMO', source: 'Demo data for development' } })
  const unilever = await prisma.company.create({ data: { name: 'Unilever Nepal Limited', symbol: 'UNL', sectorId: manufacturing.id, description: 'FMCG manufacturing company in Nepal', status: 'LISTED', dataConfidence: 'DEMO', source: 'Demo data for development' } })
  const himalayan = await prisma.company.create({ data: { name: 'Himalayan Distillery Limited', symbol: 'HDL', sectorId: manufacturing.id, description: 'Beverages manufacturing company in Nepal', status: 'LISTED', dataConfidence: 'DEMO', source: 'Demo data for development' } })

What does this code do?

The script creates 4 sectors matching real Nepalese market categories: Commercial Banks, Hydropower, Life Insurance, and Manufacturing and Processing. Each sector gets a hex color for chart styling.

Then it creates 8 companies across those sectors. Each company has a unique stock symbol and is linked to its sector through sectorId. Every record sets dataConfidence: 'DEMO' so the data is clearly marked as non-real.

  • Confirm the new file exists by running:
ls prisma/

You should see schema.prisma and seed.ts listed in the output.

  • Add the financial data below the companies in prisma/seed.ts. Paste this code after the last company variable (himalayan):

  // Financials (3 years each)
  const companies = [nabil, nic, upper, chilime, nli, gli, unilever, himalayan]
  const financialData = [
    [{ fiscalYear: '2021', revenue: 8500000000, netProfit: 2100000000, eps: 42.5, roe: 18.2, roce: 15.1, debtToEquity: 0.8 }, { fiscalYear: '2022', revenue: 9200000000, netProfit: 2400000000, eps: 48.6, roe: 19.5, roce: 16.3, debtToEquity: 0.75 }, { fiscalYear: '2023', revenue: 10100000000, netProfit: 2700000000, eps: 54.7, roe: 21.0, roce: 17.8, debtToEquity: 0.7 }],
    [{ fiscalYear: '2021', revenue: 7200000000, netProfit: 1800000000, eps: 36.2, roe: 16.5, roce: 13.8, debtToEquity: 0.9 }, { fiscalYear: '2022', revenue: 7900000000, netProfit: 2000000000, eps: 40.1, roe: 17.8, roce: 14.9, debtToEquity: 0.85 }, { fiscalYear: '2023', revenue: 8600000000, netProfit: 2200000000, eps: 44.2, roe: 19.0, roce: 16.0, debtToEquity: 0.8 }],
    [{ fiscalYear: '2021', revenue: 3200000000, netProfit: 1500000000, eps: 28.4, roe: 22.1, roce: 19.5, debtToEquity: 0.4 }, { fiscalYear: '2022', revenue: 3500000000, netProfit: 1700000000, eps: 32.2, roe: 24.0, roce: 21.2, debtToEquity: 0.38 }, { fiscalYear: '2023', revenue: 3800000000, netProfit: 1900000000, eps: 36.0, roe: 25.5, roce: 22.8, debtToEquity: 0.35 }],
    [{ fiscalYear: '2021', revenue: 1800000000, netProfit: 900000000, eps: 22.5, roe: 20.3, roce: 17.8, debtToEquity: 0.5 }, { fiscalYear: '2022', revenue: 2000000000, netProfit: 1000000000, eps: 25.0, roe: 21.8, roce: 19.0, debtToEquity: 0.48 }, { fiscalYear: '2023', revenue: 2200000000, netProfit: 1100000000, eps: 27.5, roe: 23.0, roce: 20.5, debtToEquity: 0.45 }],
    [{ fiscalYear: '2021', revenue: 4500000000, netProfit: 800000000, eps: 18.2, roe: 14.5, roce: 12.0, debtToEquity: 0.6 }, { fiscalYear: '2022', revenue: 5000000000, netProfit: 900000000, eps: 20.5, roe: 15.8, roce: 13.2, debtToEquity: 0.58 }, { fiscalYear: '2023', revenue: 5500000000, netProfit: 1000000000, eps: 22.8, roe: 17.0, roce: 14.5, debtToEquity: 0.55 }],
    [{ fiscalYear: '2021', revenue: 1200000000, netProfit: 200000000, eps: 8.5, roe: 10.2, roce: 8.5, debtToEquity: 0.7 }, { fiscalYear: '2022', revenue: 1400000000, netProfit: 250000000, eps: 10.6, roe: 11.5, roce: 9.8, debtToEquity: 0.68 }, { fiscalYear: '2023', revenue: 1600000000, netProfit: 300000000, eps: 12.7, roe: 12.8, roce: 11.0, debtToEquity: 0.65 }],
    [{ fiscalYear: '2021', revenue: 12000000000, netProfit: 1800000000, eps: 95.2, roe: 28.5, roce: 25.0, debtToEquity: 0.3 }, { fiscalYear: '2022', revenue: 13500000000, netProfit: 2000000000, eps: 105.8, roe: 30.2, roce: 26.8, debtToEquity: 0.28 }, { fiscalYear: '2023', revenue: 15000000000, netProfit: 2200000000, eps: 116.3, roe: 32.0, roce: 28.5, debtToEquity: 0.25 }],
    [{ fiscalYear: '2021', revenue: 3500000000, netProfit: 500000000, eps: 35.8, roe: 16.8, roce: 14.2, debtToEquity: 0.55 }, { fiscalYear: '2022', revenue: 3800000000, netProfit: 580000000, eps: 41.5, roe: 18.2, roce: 15.5, debtToEquity: 0.52 }, { fiscalYear: '2023', revenue: 4100000000, netProfit: 650000000, eps: 46.5, roe: 19.5, roce: 16.8, debtToEquity: 0.5 }]
  ]
  for (let i = 0; i < companies.length; i++) {
    for (const fd of financialData[i]) {
      await prisma.financial.create({ data: { companyId: companies[i].id, ...fd, dataConfidence: 'DEMO', source: 'Demo data for development' } })
    }
  }

What does this code do?

The financialData array holds 3 years (2021-2023) of financial metrics for each of the 8 companies. The nested loop creates one Financial record per year per company, giving you 24 financial records total.

Revenue and profit values are in NPR (Nepalese Rupees). The numbers are realistic in scale for Nepalese companies but are demo values.

  • Save prisma/seed.ts.
  • Add the IPO records, market data generation, and closing code below the financials loop. Paste this code at the end of prisma/seed.ts, right after the closing } of the financials for loop:

  // IPOs
  await prisma.iPO.create({ data: { companyId: gli.id, ipoPrice: 100, openDate: '2024-03-15', closeDate: '2024-03-19', openDateBS: '2080-12-02', closeDateBS: '2080-12-06', issueSize: 30000000, issueManager: 'NIBL Ace Capital', sebonApproval: 'Approved', status: 'UPCOMING', dataConfidence: 'DEMO', source: 'Demo data for development' } })
  await prisma.iPO.create({ data: { companyId: upper.id, ipoPrice: 100, openDate: '2021-09-01', closeDate: '2021-09-05', openDateBS: '2078-05-16', closeDateBS: '2078-05-20', issueSize: 500000000, issueManager: 'NIBL Ace Capital', sebonApproval: 'Approved', status: 'CLOSED', dataConfidence: 'DEMO', source: 'Demo data for development' } })
  await prisma.iPO.create({ data: { companyId: nabil.id, ipoPrice: 100, openDate: '1996-04-10', closeDate: '1996-04-14', openDateBS: '2052-12-28', closeDateBS: '2053-01-01', issueSize: 120000000, issueManager: 'NMB Capital', sebonApproval: 'Approved', status: 'CLOSED', dataConfidence: 'DEMO', source: 'Demo data for development' } })

  // Market Data (30 days of NEPSE snapshots)
  const baseIndex = 2150
  for (let i = 29; i >= 0; i--) {
    const date = new Date()
    date.setDate(date.getDate() - i)
    const variation = (Math.random() - 0.48) * 30
    await prisma.marketData.create({ data: { date: date.toISOString().split('T')[0], nepseIndex: parseFloat((baseIndex + variation * (30 - i) / 10).toFixed(2)), turnover: parseFloat((Math.random() * 3000000000 + 1000000000).toFixed(0)), volume: Math.floor(Math.random() * 5000000 + 1000000), dataConfidence: 'DEMO', source: 'Demo data for development' } })
  }

  console.log('Seed complete')
}

main().catch(e => { console.error(e); process.exit(1) }).finally(() => prisma.$disconnect())

What does this code do?

The IPO section creates 3 records: one upcoming IPO (Gurans Life Insurance), and two closed historical IPOs (Upper Tamakoshi and Nabil Bank). Each includes both Gregorian and BS dates.

The market data loop generates 30 days of NEPSE index snapshots with randomized but realistic variations around a base index of 2150. The main() call at the bottom runs the function and disconnects from the database when done.

  • Save prisma/seed.ts.

✔️ Awesome, I've got everything!

Great. Confirm prisma/seed.ts is saved before continuing.

ⓧ I'd like to double check the full code

Scroll through your prisma/seed.ts file and confirm these sections appear in order:

  • import { PrismaClient } from '@prisma/client' and const prisma = new PrismaClient() at the top.
  • async function main() opening.
  • 4 sector create calls (banking, hydro, insurance, manufacturing).
  • 8 company create calls (nabil through himalayan).
  • financialData array with 8 entries and a nested for loop.
  • 3 IPO create calls.
  • Market data loop generating 30 days of snapshots.
  • console.log('Seed complete') and the main().catch(...).finally(...) closing.
Run the migration and verify the data

The schema and seed script are ready. Now you need to apply the schema to the SQLite database and populate it with the demo data.

  • Run the database migration by executing this command:
npx prisma migrate dev --name init

What does this command do?

npx prisma migrate dev reads your schema, compares it to the current database state (empty), generates a SQL migration file, and applies it. The --name init flag labels this migration as "init". It also generates the Prisma Client so your code can query these models.

After running, you will see a new prisma/migrations/ folder and a prisma/dev.db SQLite database file.

You should see output confirming the migration was applied and the Prisma Client was generated.

Migration failed?

If you see a schema validation error, run npx prisma validate to pinpoint the exact line with the issue. Common causes: a missing comma, a mismatched relation field name, or a model placed inside the datasource block instead of after it.

help me fix my Prisma migration error

  • Seed the database with demo data by running:
npx prisma db seed

You should see Seed complete printed in the terminal. This confirms all 4 sectors, 8 companies, 24 financial records, 3 IPOs, and 30 market data snapshots were created.

Seed command not found or failing?

Make sure you added the "prisma" key to package.json and saved the file. The key should be at the top level of the JSON object, not nested inside "scripts".

If you see a TypeScript compilation error, confirm ts-node is installed by running npx ts-node --version. If it is not installed, run npm install ts-node --save-dev and try again.

help me fix my Prisma seed error

Before you open Prisma Studio, how many sectors do you expect to see in the Sector table?

  • Open Prisma Studio to visually inspect all the data by running:
npx prisma studio

Prisma Studio opens in your browser. You should see tables for Sector, Company, IPO, Financial, and MarketData. Click each table to confirm data is populated. You should see 4 sectors, 8 companies across Banking, Hydropower, Insurance, and Manufacturing.

Your database is fully structured and populated with realistic Nepalese market data. Next up, you will wire this data into a professional dashboard that brings these numbers to life.

Build the Dashboard Page with Live Data

In the previous step, you designed the database schema and seeded it with realistic demo data across Banking, Hydropower, Insurance, and Manufacturing sectors. You confirmed the data is in place through Prisma Studio.

The database is full of financial data, but the app still shows the default Next.js starter page. In this step, you will replace that starter page with the core dashboard, pulling data from the database via server components. This is the first visible proof that the full stack works end-to-end.

In this step, get ready to:
  • Create a Prisma client singleton and data-fetching functions that query the database.
  • Build reusable dashboard components for market stats, IPO cards, sector performance, and a financial disclaimer.
  • Wire up the main page as a server component that renders live data in a responsive grid.
Create the Prisma client and data-fetching functions

Every time Next.js hot-reloads your code during development, it re-imports your modules. Without a safeguard, each reload would create a new database connection and eventually exhaust the connection pool. A singleton pattern solves this by reusing one PrismaClient instance across reloads.

  • In the VS Code Explorer sidebar, click the New File icon.
  • Type lib/prisma.ts and press Enter. VS Code creates the lib/ directory automatically.
  • Paste the following code into lib/prisma.ts:
import { PrismaClient } from '@prisma/client'

// Attach PrismaClient to globalThis so hot reloads reuse the same instance
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }

export const prisma =
  globalForPrisma.prisma ||
  new PrismaClient({
    log: ['query'],
  })

// Only cache the instance outside production to prevent connection leaks
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma

What does this code do?

globalForPrisma casts globalThis so TypeScript lets you attach a prisma property to the global object.

The || check reuses an existing client if one is already cached. In production, a fresh client is created on each cold start since there are no hot reloads.

  • Save lib/prisma.ts.

Now you need functions that use this client to query the database. Each function maps to a section of the dashboard: market overview, upcoming IPOs, sector performance, and top companies.

  • Create another file by clicking the New File icon and typing lib/data.ts.
  • Paste the first two data-fetching functions into lib/data.ts:
import { prisma } from './prisma'

// Return the most recent NEPSE market snapshot
export async function getMarketOverview() {
  return prisma.marketData.findFirst({ orderBy: { date: 'desc' } })
}

// Return upcoming IPOs with their company and sector details
export async function getUpcomingIPOs() {
  return prisma.iPO.findMany({
    where: { status: 'UPCOMING' },
    include: { company: { include: { sector: true } } },
    orderBy: { openDate: 'asc' },
  })
}

What do these functions do?

getMarketOverview() fetches the single most recent MarketData record by sorting on date descending and taking the first result.

getUpcomingIPOs() filters for IPOs with status: 'UPCOMING' and uses Prisma's include to pull in the related company and sector data in a single query.

  • Continue adding the remaining two functions below the code you just pasted in lib/data.ts:
// Calculate average ROE per sector from each company's latest financials
export async function getSectorPerformance() {
  const sectors = await prisma.sector.findMany({
    include: { companies: { include: { financials: { orderBy: { fiscalYear: 'desc' }, take: 1 } } } },
  })
  return sectors.map(sector => ({
    ...sector,
    avgROE: sector.companies.length
      ? sector.companies.reduce((sum, c) => sum + (c.financials[0]?.roe ?? 0), 0) / sector.companies.length
      : 0,
  }))
}

// Return top 5 companies with their sector and latest financial record
export async function getTopCompanies() {
  return prisma.company.findMany({
    include: { sector: true, financials: { orderBy: { fiscalYear: 'desc' }, take: 1 } },
    take: 5,
  })
}

What do these functions do?

getSectorPerformance() is the most complex query. It fetches every sector with its companies and each company's latest financial record (take: 1). Then it calculates the average ROE across all companies in that sector.

getTopCompanies() returns 5 companies with their sector and latest financials, ready for a "top performers" widget.

  • Save lib/data.ts.
  • Check the terminal where your dev server is running. You should see no red error messages. The TypeScript compiler accepts both new files without issues.

Seeing a TypeScript error in the terminal?

  • Make sure lib/prisma.ts exports prisma (not a default export). The import in lib/data.ts uses a named import: import { prisma } from './prisma'.
  • Check that lib/data.ts references prisma.iPO (capital P and O) in getUpcomingIPOs. Prisma generates the model accessor from the schema model name IPO, which becomes iPO in camelCase.
  • If the terminal shows Cannot find module '@prisma/client', run npx prisma generate in a second terminal to regenerate the client.

Help me debug a TypeScript error in my Prisma data-fetching functions.

✔️ Awesome, I've got everything!

Great. Make sure both files are saved before moving on.

ⓧ I'd like to double check the full code

Scroll up and confirm your lib/data.ts has these four functions in order:

  • getMarketOverview() (returns latest market data).
  • getUpcomingIPOs() (filters by UPCOMING status with company and sector included).
  • getSectorPerformance() (fetches sectors, calculates average ROE).
  • getTopCompanies() (returns 5 companies with sector and financials).

All four should import from './prisma' at the top of the file.

Build the dashboard components

The dashboard needs four reusable components. Each one handles a single piece of the UI: market stats cards, IPO listing cards, sector performance rows, and a financial disclaimer banner.

  • Click the New File icon in the Explorer sidebar and type components/MarketStatsCard.tsx. VS Code creates the components/ directory for you.
  • Paste this code into components/MarketStatsCard.tsx:
// Props for the market stats display card
interface MarketStatsCardProps {
  label: string
  value: string
  change?: string
  positive?: boolean
}

export default function MarketStatsCard({ label, value, change, positive }: MarketStatsCardProps) {
  return (
    <div className="bg-white rounded-lg border border-gray-200 p-4 shadow-sm">
      <p className="text-sm text-gray-500">{label}</p>
      <p className="text-2xl font-bold text-gray-900 mt-1">{value}</p>
      {/* Show change indicator if a change value is provided */}
      {change && (
        <p className={`text-sm mt-1 ${positive ? 'text-green-600' : 'text-red-600'}`}>{change}</p>
      )}
    </div>
  )
}

What does this component do?

MarketStatsCard displays a single metric as a white card with a label, a large bold value, and an optional color-coded change indicator. The dashboard will use three of these side by side for NEPSE Index, Turnover, and Volume.

  • Save components/MarketStatsCard.tsx.
  • Create another file: click New File and type components/IPOCard.tsx.
  • Paste this code into components/IPOCard.tsx:
// Props for an individual IPO listing card
interface IPOCardProps {
  companyName: string
  sector: string
  ipoPrice: number
  openDate: string
  closeDate: string
  status: string
  issueSize: number
}

export default function IPOCard({ companyName, sector, ipoPrice, openDate, closeDate, status, issueSize }: IPOCardProps) {
  return (
    <div className="bg-white rounded-lg border border-gray-200 p-4 shadow-sm">
      <div className="flex justify-between items-start">
        <h3 className="font-semibold text-gray-900">{companyName}</h3>
        {/* Status badge showing UPCOMING, OPEN, or CLOSED */}
        <span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded-full">{status}</span>
      </div>
      <p className="text-sm text-gray-500 mt-1">{sector}</p>
      {/* Two-column grid of IPO details */}
      <div className="mt-3 grid grid-cols-2 gap-2 text-sm">
        <div><span className="text-gray-500">IPO Price:</span> <span className="font-medium">NPR {ipoPrice}</span></div>
        <div><span className="text-gray-500">Issue Size:</span> <span className="font-medium">NPR {issueSize.toLocaleString()}</span></div>
        <div><span className="text-gray-500">Open:</span> <span className="font-medium">{openDate}</span></div>
        <div><span className="text-gray-500">Close:</span> <span className="font-medium">{closeDate}</span></div>
      </div>
    </div>
  )
}

What does this component do?

IPOCard renders a card for a single IPO listing. It shows the company name and a colored status badge at the top, the sector below, and then a 2x2 grid with the IPO price in NPR, issue size, open date, and close date.

  • Save components/IPOCard.tsx.
  • Confirm both component files exist by running this in the terminal:
ls components/

You should see IPOCard.tsx and MarketStatsCard.tsx listed.

  • Create components/SectorRow.tsx by clicking New File and typing the path.
  • Paste this code into components/SectorRow.tsx:
// Props for a sector performance row
interface SectorRowProps {
  name: string
  avgROE: number
  color: string
}

export default function SectorRow({ name, avgROE, color }: SectorRowProps) {
  return (
    <div className="flex items-center justify-between py-2 border-b border-gray-100 last:border-0">
      <div className="flex items-center gap-2">
        {/* Color dot matching the sector's assigned color */}
        <span className="w-3 h-3 rounded-full" style={{ backgroundColor: color }} />
        <span className="text-sm text-gray-700">{name}</span>
      </div>
      <span className="text-sm font-medium text-green-600">{avgROE.toFixed(1)}% ROE</span>
    </div>
  )
}

What does this component do?

SectorRow displays one row in the sector performance sidebar. A small colored dot (matching the sector's color from the database) sits next to the sector name on the left, and the average ROE percentage appears on the right.

  • Save components/SectorRow.tsx.
  • Create components/DisclaimerBanner.tsx using the New File icon.
  • Paste this code into components/DisclaimerBanner.tsx:
// Financial disclaimer banner shown at the bottom of every page
export default function DisclaimerBanner() {
  return (
    <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 mt-6">
      <p className="text-xs text-yellow-800">
        <strong>Disclaimer:</strong> All data displayed is demo data for development purposes only and does not represent real financial information. This platform is not financial advice. Always consult a qualified financial advisor before making investment decisions.
      </p>
    </div>
  )
}

Why include a disclaimer?

This platform uses demo data modeled on real Nepalese market sectors, but every record is clearly marked with dataConfidence: 'DEMO' in the database. The banner reinforces this at the UI level so no one mistakes demo numbers for real financial data.

  • Save components/DisclaimerBanner.tsx.
  • Verify all four components exist by running:
ls components/

You should see DisclaimerBanner.tsx, IPOCard.tsx, MarketStatsCard.tsx, and SectorRow.tsx listed.

Missing a file?

Double-check that each file is inside the components/ directory, not the project root. If you accidentally created a file in the wrong location, drag it into components/ in the Explorer sidebar.

Help me fix a missing component file in my Next.js project.

Wire up the dashboard page with live data

Now you connect everything. The main page will be a server component that calls your data-fetching functions, then passes the results to the components you just built.

  • Open app/page.tsx in the Explorer sidebar.
  • Select all the existing content (Cmd+A on macOS or Ctrl+A on Windows) and delete it.
  • Paste the following code into the now-empty app/page.tsx:
import { getMarketOverview, getUpcomingIPOs, getSectorPerformance, getTopCompanies } from '@/lib/data'
import MarketStatsCard from '@/components/MarketStatsCard'
import IPOCard from '@/components/IPOCard'
import SectorRow from '@/components/SectorRow'
import DisclaimerBanner from '@/components/DisclaimerBanner'

export default async function Home() {
  // Fetch all dashboard data in parallel for faster loading
  const [market, upcomingIPOs, sectors, topCompanies] = await Promise.all([
    getMarketOverview(),
    getUpcomingIPOs(),
    getSectorPerformance(),
    getTopCompanies(),
  ])

  return (
    <main className="min-h-screen bg-gray-50">
      <header className="bg-white border-b border-gray-200 px-6 py-4">
        <h1 className="text-2xl font-bold text-gray-900">Nepal IPO Intelligence</h1>
        <p className="text-sm text-gray-500">NEPSE Market Dashboard</p>
      </header>
      <div className="max-w-7xl mx-auto px-6 py-6">
        {/* Market overview stats row */}
        <div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
          <MarketStatsCard label="NEPSE Index" value={market?.nepseIndex.toLocaleString() ?? 'N/A'} />
          <MarketStatsCard label="Turnover (NPR)" value={market ? `NPR ${(market.turnover / 1e9).toFixed(2)}B` : 'N/A'} />
          <MarketStatsCard label="Volume" value={market?.volume.toLocaleString() ?? 'N/A'} />
        </div>
  • Keep your cursor at the end of the code. Paste this directly below to complete the page:
        {/* IPO cards and sector performance sidebar */}
        <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
          <div className="lg:col-span-2">
            <h2 className="text-lg font-semibold text-gray-900 mb-3">Upcoming IPOs</h2>
            <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
              {upcomingIPOs.map(ipo => (
                <IPOCard key={ipo.id} companyName={ipo.company.name} sector={ipo.company.sector.name} ipoPrice={ipo.ipoPrice} openDate={ipo.openDate} closeDate={ipo.closeDate} status={ipo.status} issueSize={ipo.issueSize} />
              ))}
            </div>
          </div>
          <div>
            <h2 className="text-lg font-semibold text-gray-900 mb-3">Sector Performance</h2>
            <div className="bg-white rounded-lg border border-gray-200 p-4 shadow-sm">
              {sectors.map(s => <SectorRow key={s.id} name={s.name} avgROE={s.avgROE} color={s.color ?? '#6B7280'} />)}
            </div>
          </div>
        </div>
        <DisclaimerBanner />
      </div>
    </main>
  )
}

What does this page do?

This is a server component (it uses async directly in the function signature). Next.js runs it on the server, so the database queries never reach the browser.

Promise.all fires all four data queries in parallel instead of one by one. The page then renders a responsive grid: three MarketStatsCard components across the top, IPOCard listings in the center, SectorRow entries in a sidebar, and the DisclaimerBanner at the bottom.

Before you refresh the browser, what do you think you will see at http://localhost:3000? Will it still be the default Next.js starter page, or something new?

  • Save app/page.tsx.
  • Switch to your browser and refresh http://localhost:3000.

The default starter page is gone. In its place you should see a professional dashboard with "Nepal IPO Intelligence" as the header, three market stats cards showing the NEPSE Index, Turnover, and Volume, an upcoming IPO card for Gurans Life Insurance Company, four sector rows with colored dots, and a yellow disclaimer banner at the bottom.

Dashboard not loading or showing an error?

  • If you see a blank page or a compilation error, check the terminal for red error text. A common cause is a typo in an import path. Verify that @/lib/data and @/components/MarketStatsCard match your file locations exactly.
  • If you see "Module not found" for @/lib/data, make sure the lib/ folder is at the project root (inside nepal-ipo-intelligence/), not inside app/.
  • If market stats show "N/A" for all values, the database might be empty. Re-run the seed command:
npx prisma db seed

Help me fix my Nepal IPO dashboard that is not loading.

✔️ Awesome, I've got everything!

Make sure you have saved app/page.tsx and confirmed the dashboard renders in the browser.

ⓧ I'd like to double check the full code

Your app/page.tsx should contain these sections from top to bottom:

  • Five import lines: one from @/lib/data (four named imports) and four default component imports.
  • An async function Home() with a Promise.all call fetching market, upcomingIPOs, sectors, and topCompanies.
  • A <header> with "Nepal IPO Intelligence" and "NEPSE Market Dashboard".
  • A 3-column grid of MarketStatsCard components for Index, Turnover, and Volume.
  • A 3-column layout: left side with upcomingIPOs.map rendering IPOCard, right side with sectors.map rendering SectorRow.
  • A <DisclaimerBanner /> before the closing </div> tags.

Your dashboard now shows real data from the seeded database. Next up, you will add interactive financial charts that turn those numbers into visual trends investors can actually read.

Add Interactive Financial Charts

Your dashboard displays raw numbers for the NEPSE Index, turnover, and volume. Numbers alone tell you the current state, but they hide the trend. A rising index and a falling index can show the same number on different days.

Charts reveal patterns that tables cannot. In this step, you will build two interactive Recharts visualizations: a line chart for the NEPSE Index trend and a bar chart comparing sector ROE. Both pull live data from the seeded database.

In this step, get ready to:
  • Build a NEPSE Index trend line chart with hover tooltips.
  • Build a sector ROE comparison bar chart with color-coded bars.
  • Wire both charts into the dashboard with live database data.
Create the NEPSE Index trend chart component

Recharts components use browser APIs like the DOM and Canvas. In Next.js App Router, components are server components by default. Any component that uses browser APIs must be explicitly marked as a client component with the 'use client' directive at the top of the file.

  • Create a new folder for chart components by running this command in your terminal:
mkdir -p components/charts
  • Confirm the folder exists by running:
ls components/charts

You should see an empty directory with no output. The folder is ready for your chart files.

  • Create a new file at components/charts/NepseIndexChart.tsx and paste the following code for the imports, type definitions, and custom tooltip:
'use client'

import { ResponsiveContainer, LineChart, Line, XAxis, YAxis, Tooltip, CartesianGrid } from 'recharts'

// Shape of each data point in the chart
interface DataPoint {
  date: string
  nepseIndex: number
}

// Props accepted by the chart component
interface NepseIndexChartProps {
  data: DataPoint[]
}

// Custom tooltip shown when hovering over a data point
function CustomTooltip({ active, payload, label }: any) {
  if (active && payload && payload.length) {
    return (
      <div className="bg-white border border-gray-200 rounded p-2 shadow text-sm">
        <p className="text-gray-500">{label}</p>
        <p className="font-semibold text-blue-600">{payload[0].value.toLocaleString()}</p>
      </div>
    )
  }
  return null
}

What does this code do?

The 'use client' directive tells Next.js to render this component in the browser, not on the server. Recharts needs access to the DOM to draw SVG charts, so this directive is required.

The CustomTooltip function renders a styled popup when the learner hovers over a data point. It formats the index value with commas using toLocaleString() so large numbers like 2,450 are easy to read.

  • Below the CustomTooltip function in the same file, add the main chart component:
export default function NepseIndexChart({ data }: NepseIndexChartProps) {
  return (
    <ResponsiveContainer width="100%" height={250}>
      <LineChart data={data} margin={{ top: 5, right: 20, left: 10, bottom: 5 }}>
        <CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
        <XAxis dataKey="date" tick={{ fontSize: 11 }} tickFormatter={v => v.slice(5)} />
        <YAxis tick={{ fontSize: 11 }} domain={['auto', 'auto']} />
        <Tooltip content={<CustomTooltip />} />
        <Line type="monotone" dataKey="nepseIndex" stroke="#3B82F6" strokeWidth={2} dot={false} />
      </LineChart>
    </ResponsiveContainer>
  )
}

What does the chart component do?

ResponsiveContainer makes the chart resize to fit its parent container. This keeps the chart responsive across screen sizes.

The XAxis uses tickFormatter={v => v.slice(5)} to strip the year prefix from dates, showing only the month-day portion so labels stay compact. The Line component draws a smooth blue trend line with dot={false} to keep the chart clean.

  • Save components/charts/NepseIndexChart.tsx.
  • Confirm the file was created by running:
ls components/charts/

You should see NepseIndexChart.tsx listed in the output.

Don't see the file?

Make sure you created the components/charts/ directory with the mkdir -p command earlier. Verify the file is saved inside that folder, not in the components/ root.

help me find where my NepseIndexChart.tsx file was saved

Create the sector comparison chart component

The second chart compares average Return on Equity (ROE) across the four market sectors. A bar chart is the right shape for this because it makes side-by-side comparison immediate.

  • Create a new file at components/charts/SectorComparisonChart.tsx and paste the following imports, type definitions, and custom tooltip:
'use client'

import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, CartesianGrid, Cell } from 'recharts'

// Shape of each sector data point
interface SectorData {
  name: string
  color: string
  avgROE: number
}

// Props accepted by the chart component
interface SectorComparisonChartProps {
  data: SectorData[]
}

// Custom tooltip shown when hovering over a bar
function CustomTooltip({ active, payload }: any) {
  if (active && payload && payload.length) {
    return (
      <div className="bg-white border border-gray-200 rounded p-2 shadow text-sm">
        <p className="text-gray-700 font-medium">{payload[0].payload.name}</p>
        <p className="text-green-600">ROE: {payload[0].value}%</p>
      </div>
    )
  }
  return null
}

What does this code do?

The Cell import is unique to this chart. It lets you assign a different fill color to each bar individually, so Banking gets blue, Hydropower gets green, and so on. The colors come from the color field stored in each sector's database record.

The tooltip accesses the sector name through payload[0].payload.name because Recharts nests the original data object inside the payload entry.

  • Below the CustomTooltip function in the same file, add the main bar chart component:
export default function SectorComparisonChart({ data }: SectorComparisonChartProps) {
  return (
    <ResponsiveContainer width="100%" height={250}>
      <BarChart data={data} margin={{ top: 5, right: 20, left: 10, bottom: 5 }}>
        <CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
        <XAxis dataKey="name" tick={{ fontSize: 10 }} />
        <YAxis tick={{ fontSize: 11 }} unit="%" />
        <Tooltip content={<CustomTooltip />} />
        <Bar dataKey="avgROE">
          {data.map((entry, index) => <Cell key={index} fill={entry.color} />)}
        </Bar>
      </BarChart>
    </ResponsiveContainer>
  )
}

How do the color-coded bars work?

The Bar component renders all bars from the avgROE data key. Inside it, the data.map() call creates one Cell per sector, each with a fill color pulled from the sector record. This means each bar gets the same color you see in the Sector Performance sidebar.

  • Save components/charts/SectorComparisonChart.tsx.
  • Confirm both chart files exist by running:
ls components/charts/

You should see NepseIndexChart.tsx and SectorComparisonChart.tsx listed.

Wire chart data into the dashboard

The chart components are ready, but they need data passed as props. You need two new data-fetching functions in lib/data.ts and an updated app/page.tsx that fetches the data and renders the charts.

  • Open lib/data.ts and add these two new functions below your existing getTopCompanies() function:
export async function getNepseIndexHistory() {
  // Fetch all market data points ordered by date for the trend line
  return prisma.marketData.findMany({ orderBy: { date: 'asc' } })
}

export async function getSectorAverages() {
  // Calculate average ROE per sector from latest financials
  const sectors = await prisma.sector.findMany({
    include: { companies: { include: { financials: { orderBy: { fiscalYear: 'desc' }, take: 1 } } } },
  })
  return sectors.map(sector => ({
    name: sector.name,
    color: sector.color ?? '#6B7280',
    avgROE: sector.companies.length
      ? parseFloat((sector.companies.reduce((sum, c) => sum + (c.financials[0]?.roe ?? 0), 0) / sector.companies.length).toFixed(2))
      : 0,
  }))
}

What do these functions do?

getNepseIndexHistory() returns every MarketData record sorted by date ascending. This gives the line chart a time series to plot from earliest to latest.

getSectorAverages() pulls each sector's companies, grabs the most recent fiscal year's ROE for each company, and averages them. It returns the sector name, color, and average ROE so the bar chart can render color-coded bars.

  • Save lib/data.ts.

Now update app/page.tsx to import the new functions and chart components, fetch the chart data, and render the charts on the dashboard.

  • In app/page.tsx, find this import line at the top of the file:
import { getMarketOverview, getUpcomingIPOs, getSectorPerformance, getTopCompanies } from '@/lib/data'
  • Replace it with this updated import that adds the two new data functions:
import { getMarketOverview, getUpcomingIPOs, getSectorPerformance, getTopCompanies, getNepseIndexHistory, getSectorAverages } from '@/lib/data'
  • Below the existing DisclaimerBanner import line, add two new import lines for the chart components:
import NepseIndexChart from '@/components/charts/NepseIndexChart'
import SectorComparisonChart from '@/components/charts/SectorComparisonChart'
  • Find the data-fetching line inside the Home function:
  const [market, upcomingIPOs, sectors, topCompanies] = await Promise.all([
    getMarketOverview(),
    getUpcomingIPOs(),
    getSectorPerformance(),
    getTopCompanies(),
  ])
  • Replace it with this updated version that fetches chart data alongside the existing queries:
  const [market, upcomingIPOs, sectors, topCompanies, nepseHistory, sectorAverages] = await Promise.all([
    getMarketOverview(),
    getUpcomingIPOs(),
    getSectorPerformance(),
    getTopCompanies(),
    getNepseIndexHistory(),
    getSectorAverages(),
  ])

Why fetch everything in Promise.all?

Promise.all runs all six database queries in parallel instead of one after another. This keeps the page load fast because no query has to wait for a previous one to finish.

  • Find the closing </div> of the market stats grid (the one right after the three MarketStatsCard components). It looks like this:
        </div>
        <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
  • Replace that section with the following, which adds the charts grid between the market stats and the IPO/sector section:
        </div>
        <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
          <div className="bg-white rounded-lg border border-gray-200 p-4 shadow-sm">
            <h2 className="text-base font-semibold text-gray-900 mb-3">NEPSE Index Trend</h2>
            <NepseIndexChart data={nepseHistory} />
          </div>
          <div className="bg-white rounded-lg border border-gray-200 p-4 shadow-sm">
            <h2 className="text-base font-semibold text-gray-900 mb-3">Sector ROE Comparison</h2>
            <SectorComparisonChart data={sectorAverages} />
          </div>
        </div>
        <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">

What changed in the layout?

A new two-column grid sits between the market stats row and the IPO/sector section. Each chart lives inside a white card with a heading. The NepseIndexChart receives the full array of market data points as its data prop. The SectorComparisonChart receives the sector averages.

  • Save both lib/data.ts and app/page.tsx.

Before you refresh the browser, what do you expect to see below the market stats cards? Do you think the charts will show data from day one, or will they be empty?

  • Refresh your browser at http://localhost:3000.

You should see two new chart cards below the market stats. On the left, a blue line chart traces the NEPSE Index over 30 days of seeded data. On the right, a bar chart shows average ROE for each sector, with each bar in its sector color. Hover over any point or bar to see the tooltip with the exact value.

Charts not showing up?

  • Check that both chart files are saved inside components/charts/ (not components/ directly).
  • Make sure 'use client' is the very first line of each chart file, with no blank lines or comments above it.
  • Verify that the import paths in app/page.tsx match the folder structure: @/components/charts/NepseIndexChart and @/components/charts/SectorComparisonChart.
  • If you see a blank white card with a heading but no chart inside, check your terminal for errors. A common cause is a missing react-is package, which Recharts requires as a peer dependency.

my Recharts charts are not rendering on the dashboard

✔️ Awesome, I've got everything!

Great. Double check you have saved all three files: components/charts/NepseIndexChart.tsx, components/charts/SectorComparisonChart.tsx, and lib/data.ts.

ⓧ I'd like to double check the full code

Compare your files against the complete code below.

components/charts/NepseIndexChart.tsx:

'use client'

import { ResponsiveContainer, LineChart, Line, XAxis, YAxis, Tooltip, CartesianGrid } from 'recharts'

interface DataPoint {
  date: string
  nepseIndex: number
}

interface NepseIndexChartProps {
  data: DataPoint[]
}

function CustomTooltip({ active, payload, label }: any) {
  if (active && payload && payload.length) {
    return (
      <div className="bg-white border border-gray-200 rounded p-2 shadow text-sm">
        <p className="text-gray-500">{label}</p>
        <p className="font-semibold text-blue-600">{payload[0].value.toLocaleString()}</p>
      </div>
    )
  }
  return null
}
export default function NepseIndexChart({ data }: NepseIndexChartProps) {
  return (
    <ResponsiveContainer width="100%" height={250}>
      <LineChart data={data} margin={{ top: 5, right: 20, left: 10, bottom: 5 }}>
        <CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
        <XAxis dataKey="date" tick={{ fontSize: 11 }} tickFormatter={v => v.slice(5)} />
        <YAxis tick={{ fontSize: 11 }} domain={['auto', 'auto']} />
        <Tooltip content={<CustomTooltip />} />
        <Line type="monotone" dataKey="nepseIndex" stroke="#3B82F6" strokeWidth={2} dot={false} />
      </LineChart>
    </ResponsiveContainer>
  )
}

components/charts/SectorComparisonChart.tsx:

'use client'

import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, CartesianGrid, Cell } from 'recharts'

interface SectorData {
  name: string
  color: string
  avgROE: number
}

interface SectorComparisonChartProps {
  data: SectorData[]
}

function CustomTooltip({ active, payload }: any) {
  if (active && payload && payload.length) {
    return (
      <div className="bg-white border border-gray-200 rounded p-2 shadow text-sm">
        <p className="text-gray-700 font-medium">{payload[0].payload.name}</p>
        <p className="text-green-600">ROE: {payload[0].value}%</p>
      </div>
    )
  }
  return null
}
export default function SectorComparisonChart({ data }: SectorComparisonChartProps) {
  return (
    <ResponsiveContainer width="100%" height={250}>
      <BarChart data={data} margin={{ top: 5, right: 20, left: 10, bottom: 5 }}>
        <CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
        <XAxis dataKey="name" tick={{ fontSize: 10 }} />
        <YAxis tick={{ fontSize: 11 }} unit="%" />
        <Tooltip content={<CustomTooltip />} />
        <Bar dataKey="avgROE">
          {data.map((entry, index) => <Cell key={index} fill={entry.color} />)}
        </Bar>
      </BarChart>
    </ResponsiveContainer>
  )
}

lib/data.ts (full file):

import { prisma } from './prisma'

export async function getMarketOverview() {
  return prisma.marketData.findFirst({ orderBy: { date: 'desc' } })
}

export async function getUpcomingIPOs() {
  return prisma.iPO.findMany({
    where: { status: 'UPCOMING' },
    include: { company: { include: { sector: true } } },
    orderBy: { openDate: 'asc' },
  })
}

export async function getSectorPerformance() {
  const sectors = await prisma.sector.findMany({
    include: { companies: { include: { financials: { orderBy: { fiscalYear: 'desc' }, take: 1 } } } },
  })
  return sectors.map(sector => ({
    ...sector,
    avgROE: sector.companies.length
      ? sector.companies.reduce((sum, c) => sum + (c.financials[0]?.roe ?? 0), 0) / sector.companies.length
      : 0,
  }))
}
export async function getTopCompanies() {
  return prisma.company.findMany({
    include: { sector: true, financials: { orderBy: { fiscalYear: 'desc' }, take: 1 } },
    take: 5,
  })
}

export async function getNepseIndexHistory() {
  return prisma.marketData.findMany({ orderBy: { date: 'asc' } })
}

export async function getSectorAverages() {
  const sectors = await prisma.sector.findMany({
    include: { companies: { include: { financials: { orderBy: { fiscalYear: 'desc' }, take: 1 } } } },
  })
  return sectors.map(sector => ({
    name: sector.name,
    color: sector.color ?? '#6B7280',
    avgROE: sector.companies.length
      ? parseFloat((sector.companies.reduce((sum, c) => sum + (c.financials[0]?.roe ?? 0), 0) / sector.companies.length).toFixed(2))
      : 0,
  }))
}

app/page.tsx: scroll up and confirm each of the following sections is present, in order:

  • The six-function import from @/lib/data including getNepseIndexHistory and getSectorAverages.
  • Imports for NepseIndexChart and SectorComparisonChart from @/components/charts/.
  • Six items in the Promise.all destructuring, including nepseHistory and sectorAverages.
  • The two-column chart grid with NepseIndexChart and SectorComparisonChart between the market stats and the IPO section.

Your dashboard now shows real trends alongside raw numbers. Investors can see whether the NEPSE Index is climbing or dipping, and compare which sectors deliver the strongest returns. Next up, you will build the company list and detail pages so users can drill into individual companies from the dashboard.

Build the Company List and Detail Page

Your dashboard gives investors a broad market overview, but right now there is no way to dig into a specific company.

Investors need to browse all companies in one place and then drill into a full financial profile. In this step, you will build a company list page with a responsive data table and a company detail page showing IPO details, 3-year financial summaries, and data-confidence indicators.

In this step, get ready to:
  • Add data-fetching functions that query all companies and individual company profiles.
  • Build a company list page with a responsive table at /companies.
  • Create a company detail page with IPO data and financial summaries at /companies/[id].
Add the company data-fetching functions

Before you can render any company data, you need queries that pull it from the database. One function fetches every company with its latest financials for the list view. The other fetches a single company with its full history for the detail view.

  • In lib/data.ts, scroll to the bottom of the file after the getSectorAverages() function.
  • Add these two new data-fetching functions at the end of the file:
// Fetch all companies with their most recent financial record
export async function getAllCompanies() {
  return prisma.company.findMany({
    include: { sector: true, financials: { orderBy: { fiscalYear: 'desc' }, take: 1 } },
    orderBy: { name: 'asc' },
  })
}

// Fetch a single company with all financials and IPO history
export async function getCompanyById(id: number) {
  return prisma.company.findUnique({
    where: { id },
    include: {
      sector: true,
      financials: { orderBy: { fiscalYear: 'asc' } },
      ipos: { orderBy: { openDate: 'desc' } },
    },
  })
}

What do these functions do?

getAllCompanies() fetches every company alongside its sector and only the latest financial record (take: 1). This keeps the list view lightweight.

getCompanyById() fetches a single company with ALL its financial records sorted by year and all its IPO records. This powers the detail page's full profile.

  • Save lib/data.ts.
  • Check the terminal where npm run dev is running. You should see no errors in the output.
Build the company list page

The company list page lives at /companies and renders every company in a table. Each company name links to its detail page.

  • Create a new folder inside app/ called companies.
  • Inside app/companies/, create a new file called page.tsx.
  • Start by adding the page shell with the header, breadcrumb, and heading by pasting this code:
import Link from 'next/link'
import { getAllCompanies } from '@/lib/data'

export default async function CompaniesPage() {
  // Query all companies with their latest financial data
  const companies = await getAllCompanies()

  return (
    <main className="min-h-screen bg-gray-50">
      {/* Page header */}
      <header className="bg-white border-b border-gray-200 px-6 py-4">
        <h1 className="text-2xl font-bold text-gray-900">Nepal IPO Intelligence</h1>
        <p className="text-sm text-gray-500">NEPSE Market Dashboard</p>
      </header>
      <div className="max-w-7xl mx-auto px-6 py-6">
        {/* Breadcrumb navigation */}
        <div className="flex items-center gap-2 mb-4 text-sm text-gray-500">
          <Link href="/" className="hover:text-blue-600">Dashboard</Link>
          <span>/</span>
          <span className="text-gray-900">Companies</span>
        </div>
        <h2 className="text-xl font-semibold text-gray-900 mb-4">All Companies</h2>
      </div>
    </main>
  )
}

What does this code do?

This is a server component. It calls getAllCompanies() directly on the server before the page renders, so the database query never runs in the browser.

The breadcrumb uses Next.js Link components so navigating back to the dashboard is a fast client-side transition.

  • Save app/companies/page.tsx and open http://localhost:3000/companies in your browser.
  • You should see the "Nepal IPO Intelligence" header, a breadcrumb showing "Dashboard / Companies", and the "All Companies" heading.

The page structure is ready. Now add the data table that displays every company's key financial metrics.

  • In app/companies/page.tsx, find the <h2> line that says "All Companies".
  • Copy the following table code directly below that <h2> line:
        {/* Company data table */}
        <div className="bg-white rounded-lg border border-gray-200 shadow-sm overflow-hidden">
          <table className="w-full text-sm">
            <thead className="bg-gray-50 border-b border-gray-200">
              <tr>
                <th className="text-left px-4 py-3 font-medium text-gray-700">Company</th>
                <th className="text-left px-4 py-3 font-medium text-gray-700">Sector</th>
                <th className="text-right px-4 py-3 font-medium text-gray-700">Revenue (NPR)</th>
                <th className="text-right px-4 py-3 font-medium text-gray-700">Net Profit (NPR)</th>
                <th className="text-right px-4 py-3 font-medium text-gray-700">ROE (%)</th>
                <th className="text-right px-4 py-3 font-medium text-gray-700">EPS</th>
              </tr>
            </thead>
            <tbody>
              {/* Map each company to a table row with alternating colors */}
              {companies.map((company, idx) => {
                const latest = company.financials[0]
                return (
                  <tr key={company.id} className={idx % 2 === 0 ? 'bg-white hover:bg-blue-50' : 'bg-gray-50 hover:bg-blue-50'}>
                    <td className="px-4 py-3"><Link href={`/companies/${company.id}`} className="text-blue-600 hover:underline font-medium">{company.name}</Link></td>
                    <td className="px-4 py-3 text-gray-600">{company.sector.name}</td>
                    <td className="px-4 py-3 text-right">{latest ? `NPR ${(latest.revenue / 1e9).toFixed(2)}B` : 'N/A'}</td>
                    <td className="px-4 py-3 text-right">{latest ? `NPR ${(latest.netProfit / 1e9).toFixed(2)}B` : 'N/A'}</td>
                    <td className="px-4 py-3 text-right text-green-600">{latest ? `${latest.roe.toFixed(1)}%` : 'N/A'}</td>
                    <td className="px-4 py-3 text-right">{latest ? latest.eps.toFixed(2) : 'N/A'}</td>
                  </tr>
                )
              })}
            </tbody>
          </table>
        </div>

What does this table do?

The <thead> defines six columns: Company, Sector, Revenue, Net Profit, ROE, and EPS. Each row in <tbody> maps a company from the database.

Alternating row colours (bg-white and bg-gray-50) improve readability. The company name is a clickable link to /companies/[id] so investors can drill into any company.

  • Save app/companies/page.tsx and refresh http://localhost:3000/companies.
  • You should see a table with all 8 companies listed. Each row shows the company name in blue (clickable), its sector, revenue in NPR billions, net profit, ROE percentage in green, and EPS.

Table not showing any companies?

  • Check that the new table code is placed inside the <div className="max-w-7xl..."> wrapper, below the <h2> heading.
  • Make sure getAllCompanies is imported from @/lib/data at the top of the file.
  • Confirm your database is seeded by running npx prisma studio and checking the Company table has data.
  • Still stuck? Help me debug why my company list table is empty.

✔️ Awesome, I've got everything!

Make sure you have saved app/companies/page.tsx.

ⓧ I'd like to double check the full code

This file is built from two chunks. Scroll up and confirm both are present, in order:

  • The page shell: imports, CompaniesPage function, header, breadcrumb, and "All Companies" heading.
  • The data table: a <table> with 6 column headers and a <tbody> that maps companies to rows with alternating colours.
Create the company detail page

Clicking a company name in the table should take investors to a full profile. This page uses a dynamic route so one template serves every company based on its ID in the URL.

  • Inside app/companies/, create a new folder called [id] (including the square brackets).
  • Inside app/companies/[id]/, create a file called page.tsx.
  • Add the page shell with the header, breadcrumb, and data fetch by pasting this code:
import Link from 'next/link'
import { getCompanyById } from '@/lib/data'
import { notFound } from 'next/navigation'

export default async function CompanyDetailPage({ params }: { params: { id: string } }) {
  // Fetch the company by ID, including all financials and IPO data
  const company = await getCompanyById(parseInt(params.id))
  if (!company) notFound()

  return (
    <main className="min-h-screen bg-gray-50">
      <header className="bg-white border-b border-gray-200 px-6 py-4">
        <h1 className="text-2xl font-bold text-gray-900">Nepal IPO Intelligence</h1>
        <p className="text-sm text-gray-500">NEPSE Market Dashboard</p>
      </header>
      <div className="max-w-7xl mx-auto px-6 py-6">
        {/* Breadcrumb: Dashboard / Companies / Company Name */}
        <div className="flex items-center gap-2 mb-4 text-sm text-gray-500">
          <Link href="/" className="hover:text-blue-600">Dashboard</Link>
          <span>/</span>
          <Link href="/companies" className="hover:text-blue-600">Companies</Link>
          <span>/</span>
          <span className="text-gray-900">{company.name}</span>
        </div>
      </div>
    </main>
  )
}

What does this code do?

The [id] folder name tells Next.js this is a dynamic route. When a user visits /companies/3, the params.id value is "3".

If no company matches that ID, notFound() triggers Next.js's built-in 404 page. The breadcrumb includes the company's name so the investor always knows where they are.

  • Save app/companies/[id]/page.tsx and go to http://localhost:3000/companies/1 in your browser.
  • You should see the header and a breadcrumb showing "Dashboard / Companies / [company name]".

The shell is working. Now add the company overview card and the IPO details section.

  • In app/companies/[id]/page.tsx, find the closing </div> of the breadcrumb (the one right after {company.name}).
  • Paste the following code below that closing </div>:
        {/* Company overview card */}
        <div className="bg-white rounded-lg border border-gray-200 p-6 shadow-sm mb-6">
          <div className="flex justify-between items-start">
            <div>
              <h2 className="text-xl font-bold text-gray-900">{company.name}</h2>
              <p className="text-gray-500 mt-1">{company.sector.name} · {company.symbol}</p>
              <p className="text-gray-600 mt-2 text-sm">{company.description}</p>
            </div>
            <span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded-full">{company.status}</span>
          </div>
          <p className="text-xs text-gray-400 mt-3">Data confidence: {company.dataConfidence} · Source: {company.source}</p>
        </div>
        {/* IPO details section */}
        {company.ipos.length > 0 && (
          <div className="bg-white rounded-lg border border-gray-200 p-6 shadow-sm mb-6">
            <h3 className="text-base font-semibold text-gray-900 mb-3">IPO Details</h3>
            {company.ipos.map(ipo => (
              <div key={ipo.id} className="grid grid-cols-2 sm:grid-cols-3 gap-3 text-sm">
                <div><span className="text-gray-500">IPO Price:</span> <span className="font-medium">NPR {ipo.ipoPrice}</span></div>
                <div><span className="text-gray-500">Open Date:</span> <span className="font-medium">{ipo.openDate}</span></div>
                <div><span className="text-gray-500">Close Date:</span> <span className="font-medium">{ipo.closeDate}</span></div>
                <div><span className="text-gray-500">Issue Size:</span> <span className="font-medium">NPR {ipo.issueSize.toLocaleString()}</span></div>
                <div><span className="text-gray-500">Issue Manager:</span> <span className="font-medium">{ipo.issueManager ?? 'N/A'}</span></div>
                <div><span className="text-gray-500">SEBON Approval:</span> <span className="font-medium">{ipo.sebonApproval ?? 'N/A'}</span></div>
                <div><span className="text-gray-500">Status:</span> <span className="font-medium">{ipo.status}</span></div>
              </div>
            ))}
          </div>
        )}

What do these sections show?

The overview card displays the company's name, sector, stock symbol, description, and listing status. At the bottom, the dataConfidence and source fields make it clear this is demo data.

The IPO section only renders if the company has IPO records. It displays fields specific to the Nepalese market like the issue manager and SEBON approval status.

  • Save the file and refresh http://localhost:3000/companies/1.
  • You should see the company name, sector, symbol, and a blue status badge at the top. Below that, the IPO Details card shows the IPO price, dates, issue size, issue manager, and SEBON approval.

The last piece is the financial summary table showing each year's key metrics.

  • In app/companies/[id]/page.tsx, find the closing )} of the IPO details conditional block (the line that ends the {company.ipos.length > 0 && (...)} expression).
  • Paste the following financial summary table directly below it:
        {/* Financial summary table */}
        <div className="bg-white rounded-lg border border-gray-200 p-6 shadow-sm">
          <h3 className="text-base font-semibold text-gray-900 mb-3">Financial Summary</h3>
          <div className="overflow-x-auto"><table className="w-full text-sm">
              <thead className="bg-gray-50 border-b border-gray-200"><tr>
                  <th className="text-left px-3 py-2 font-medium text-gray-700">Fiscal Year</th>
                  <th className="text-right px-3 py-2 font-medium text-gray-700">Revenue (NPR)</th>
                  <th className="text-right px-3 py-2 font-medium text-gray-700">Net Profit (NPR)</th>
                  <th className="text-right px-3 py-2 font-medium text-gray-700">EPS</th>
                  <th className="text-right px-3 py-2 font-medium text-gray-700">ROE (%)</th>
                  <th className="text-right px-3 py-2 font-medium text-gray-700">ROCE (%)</th>
                  <th className="text-right px-3 py-2 font-medium text-gray-700">D/E</th>
                  <th className="text-right px-3 py-2 font-medium text-gray-700">Confidence</th>
                </tr></thead>
              <tbody>
                {company.financials.map((f, idx) => (
                  <tr key={f.id} className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
                    <td className="px-3 py-2">{f.fiscalYear}</td>
                    <td className="px-3 py-2 text-right">NPR {(f.revenue / 1e9).toFixed(2)}B</td>
                    <td className="px-3 py-2 text-right">NPR {(f.netProfit / 1e9).toFixed(2)}B</td>
                    <td className="px-3 py-2 text-right">{f.eps.toFixed(2)}</td>
                    <td className="px-3 py-2 text-right text-green-600">{f.roe.toFixed(1)}%</td>
                    <td className="px-3 py-2 text-right">{f.roce.toFixed(1)}%</td>
                    <td className="px-3 py-2 text-right">{f.debtToEquity.toFixed(2)}</td>
                    <td className="px-3 py-2 text-right text-xs text-gray-400">{f.dataConfidence}</td>
                  </tr>
                ))}
              </tbody>
            </table></div>
        </div>

What does this table display?

Each row shows one fiscal year's key metrics: revenue, net profit, EPS, ROE, ROCE, and debt-to-equity ratio. The last column shows the data confidence level so every data point is transparent about its source.

The overflow-x-auto wrapper makes the table scroll horizontally on smaller screens instead of breaking the layout.

  • Save app/companies/[id]/page.tsx and refresh http://localhost:3000/companies/1.
  • Below the IPO Details card, you should see a Financial Summary table with 3 years of data showing revenue, net profit, EPS, ROE, ROCE, and debt-to-equity for each year.

Financial table not showing any rows?

  • Verify that the financial table code is placed after the IPO section's closing )} and before the final </div> tags that close the page.
  • Check the terminal for any syntax errors. Mismatched JSX brackets are the most common cause.
  • Try visiting /companies/2 or /companies/3 to confirm the dynamic route works with different IDs.
  • Still stuck? Help me debug my company detail page at /companies/[id].

✔️ Awesome, I've got everything!

Make sure you have saved app/companies/[id]/page.tsx.

ⓧ I'd like to double check the full code

This file is built from three chunks. Scroll up and confirm all three are present inside the <div className="max-w-7xl..."> wrapper, in order:

  • The page shell: imports (Link, getCompanyById, notFound), data fetch, header, and breadcrumb.
  • Company overview card with name, sector, symbol, description, status badge, and data confidence line.
  • IPO details conditional block (company.ipos.length > 0) with a grid of IPO fields.
  • Financial summary table with 8 columns (Fiscal Year, Revenue, Net Profit, EPS, ROE, ROCE, D/E, Confidence) and mapped rows.
Connect the dashboard to the company pages

The company pages work, but there is no way to reach them from the main dashboard. You need a "Top Companies" section with clickable cards and a "View All Companies" link.

  • Open app/page.tsx.
  • At the very top of the file, find the first import line:
import { getMarketOverview, getUpcomingIPOs, getSectorPerformance, getTopCompanies, getNepseIndexHistory, getSectorAverages } from '@/lib/data'
  • Add this new import line directly above it:
import Link from 'next/link'
  • Now scroll down and find the <DisclaimerBanner /> line near the bottom of the file.
  • Add the following Top Companies section directly above <DisclaimerBanner />:
        {/* Top companies with links to detail pages */}
        <div className="mt-6">
          <h2 className="text-lg font-semibold text-gray-900 mb-3">Top Companies</h2>
          <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
            {topCompanies.map(c => (
              <Link key={c.id} href={`/companies/${c.id}`} className="bg-white rounded-lg border border-gray-200 p-4 shadow-sm hover:border-blue-300 transition-colors">
                <p className="font-medium text-blue-600">{c.name}</p>
                <p className="text-sm text-gray-500">{c.sector.name}</p>
              </Link>
            ))}
          </div>
          <Link href="/companies" className="inline-block mt-3 text-sm text-blue-600 hover:underline">View All Companies →</Link>
        </div>

What does this section do?

Each top company renders as a clickable card that links to its detail page at /companies/[id]. The hover:border-blue-300 class gives a subtle blue highlight on hover to signal the card is interactive.

The "View All Companies" link at the bottom navigates to the full /companies list page.

  • Save app/page.tsx.

Before you check the dashboard, take a moment to predict what you will see. What new section do you expect below the sector performance area?

  • Refresh http://localhost:3000 in your browser.

Scroll down past the sector performance area. You should see a "Top Companies" section with clickable cards showing each company's name and sector. Below the cards, a blue "View All Companies" link appears.

  • Click any company card. It should take you to that company's detail page at /companies/[id] with the full profile.
  • Click the "View All Companies" link. It should take you to /companies where the full table is displayed.
  • On the company detail page, click "Companies" in the breadcrumb. It should navigate back to the company list.

Links not working or showing a 404?

  • Confirm the import Link from 'next/link' line is at the top of app/page.tsx.
  • Verify the folder structure is app/companies/page.tsx and app/companies/[id]/page.tsx (the [id] folder MUST include square brackets).
  • Need help? Help me debug my Next.js dynamic route for company pages.

Your Nepal IPO Intelligence platform now has a complete navigation flow. Investors can browse the dashboard overview, explore the full company list, drill into any company's financial profile, and navigate back using breadcrumbs. Next up, you will tackle an optional Secret Mission to add a dark/light mode toggle.

Secret mission

Add Dark/Light Mode Toggle

Your dashboard works, but it only supports light mode. Traders and analysts often work in low-light environments where a bright white dashboard causes eye strain. Can you add a persistent dark/light mode toggle that switches the entire app between light and dark themes with one click, and remembers the preference across page refreshes?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally with a file-based SQLite database, so there are no ongoing costs. Decide whether to keep your project files, pause to come back later, or delete everything.

Resources you used:

  • nepal-ipo-intelligence project folder on your Desktop, containing all source code, the prisma/dev.db SQLite database, and installed packages in node_modules.
  • Node.js v24 LTS installed on your system.

Keep everything running

No action needed. Choose this if you plan to continue building on the Nepal IPO Intelligence platform.

  • Your project stays in the nepal-ipo-intelligence folder on your Desktop.
  • Run npm run dev from the project folder anytime to restart the dashboard.
  • Node.js remains installed on your system and is ready for future projects.

Pause - I'll come back to this later

Nothing to shut down. The SQLite database is a local file that does not run as a background service.

  • Close VS Code and any open terminal windows.
  • Your project folder and Node.js installation stay on your machine until you are ready to return.

Delete - I don't want to use this again

Remove all project files and start fresh.

macOS

  • Delete the project folder by running:
rm -rf ~/Desktop/nepal-ipo-intelligence

Windows

  • Delete the project folder by running this in your VS Code terminal:
Remove-Item -Recurse -Force ~\Desktop\nepal-ipo-intelligence

This removes all source code, the SQLite database, and installed packages in one step.

  • To uninstall Node.js on Windows, open Settings, select Apps, search for Node.js, and click Uninstall.
  • On macOS, if you installed Node.js using a package manager, remove it with the same tool. Otherwise, Node.js is safe to leave installed for future projects.

Nice Work!

Nice Work!

You just built a professional Nepal IPO Intelligence dashboard from scratch. The platform pulls live data from a structured database and renders it through interactive charts and detailed company profiles.

You've learned how to:

  • Design a Prisma database schema modeling Nepalese financial data across companies, IPOs, sectors, and market snapshots, then seed it with clearly-marked demo data spanning Banking, Hydropower, Insurance, and Manufacturing sectors.
  • Build a professional fintech dashboard using Next.js server components that query live data and render NEPSE market stats, upcoming IPO cards, and sector performance indicators.
  • Create interactive Recharts charts for NEPSE Index trends and sector comparison, plus company list and detail pages with 3-year financial summaries and full IPO profiles.
  • Secret Mission: Completed an optional Secret Mission to push your skills further.

Ready to quiz yourself?