Student Performance Analytics Dashboard

Build a responsive SaaS-style dashboard with charts using Next.js and Tailwind.

Introduction

30 Second Summary

Think about school report cards. Teachers and administrators deal with hundreds of students, each with scores across multiple subjects. A well-designed dashboard turns that raw data into instant insights like pass rates, top performers, and subject trends.

In this project, you will build a student performance analytics dashboard using Next.js, React, and Tailwind CSS that displays KPI metrics, an interactive bar chart, and a sortable data table in a polished dark-mode-ready interface.

What You'll Build

A modern SaaS-style student performance dashboard with a fixed sidebar, KPI cards, a responsive bar chart, and a sortable results table, all powered by mock student data and styled with dark mode support.

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

  • A Next.js 16 App Router project with TypeScript and a persistent dashboard layout featuring a sidebar and header.
  • Reusable KPI card components and an interactive Recharts bar chart displaying subject-wise average scores.
  • A dark/light mode toggle that persists the user's preference and a sortable student results table with click-to-sort column headers.
  • Secret Mission: A student search filter and a grade distribution pie chart to extend the dashboard further.

Are there any prerequisites?

You only need a basic understanding of HTML, CSS, and JavaScript. No prior React or Next.js experience is required.

Before We Start

Before We Start...

Set Up the Next.js Project

Building a polished analytics dashboard requires a modern framework that handles routing, server rendering, and styling out of the box.

In this step, you'll scaffold a Next.js 16 project with TypeScript and Tailwind CSS, install a charting library, and confirm everything runs locally.

In this step, get ready to:
  • Scaffold a Next.js 16 project with TypeScript, Tailwind CSS, and App Router.
  • Install Recharts for data visualization.
  • Start the dev server and verify the default page loads.
Run the create-next-app scaffolder

Next.js provides an official scaffolding tool called create-next-app that generates a project with all the configuration pre-wired.

  • Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Scaffold your Next.js project by running this command:
npx create-next-app@latest student-dashboard --src-dir

What does this command do?

The --src-dir flag tells the scaffolder to place your application code inside a src/ folder, keeping it separate from configuration files at the root.

Because you passed a flag, the CLI skips all interactive prompts and uses the recommended defaults for everything else: TypeScript, ESLint, Tailwind CSS, and App Router.

  • If npx asks for permission to install the package, type y and press Enter to confirm.
  • Wait for the scaffolder to finish installing dependencies. This may take a minute.
  • Navigate into your new project folder by running:
cd student-dashboard
  • Confirm the project was created by running:
ls

You should see folders like src, public, and node_modules along with files like package.json and tsconfig.json.

  • Open the project in VS Code by pressing Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Select the student-dashboard folder on your Desktop, then click Open.

Seeing a permission error or command not found?

Make sure Node.js v20.17.0 or higher is installed. Run node --version in your terminal to check.

If the scaffolder hangs or fails, try closing your terminal, reopening it, and running the command again from the Desktop.

Help me debug a create-next-app error

Install Recharts and react-is

Recharts is a charting library built with React components. It lets you create bar charts, pie charts, and more using the same declarative pattern you use for the rest of your UI.

  • Open the integrated terminal in VS Code by pressing Ctrl+` (backtick key, same on macOS and Windows).
  • Install Recharts and its peer dependency by running:
npm install recharts react-is

Why react-is?

Recharts depends on react-is to detect React element types at runtime. Installing it explicitly avoids peer dependency warnings.

  • Confirm the install succeeded by checking the terminal output. You should see a line mentioning added packages with no errors.

Install failed or showing errors?

Make sure your terminal is inside the student-dashboard folder. Run pwd to check your current directory.

Help me fix an npm install error for recharts

Start the dev server and prepare project files

With dependencies installed, let's start the development server to confirm the project runs. Then we will clean up the scaffolded demo content so the project is ready for our dashboard code.

  • Start the development server by running:
npm run dev
  • Open your browser and go to http://localhost:3000.

You should see the default Next.js welcome page with styled text and the Next.js logo. The styling confirms Tailwind CSS is working correctly.

Page not loading at localhost:3000?

Check that your terminal still shows the dev server running. If you see an error, stop the server with Ctrl+C and run npm run dev again.

Help me debug why localhost:3000 is not loading

Now that the project runs, let's replace the scaffolded demo content with clean starting files for our dashboard.

  • In the VS Code Explorer sidebar, expand src/app and click on globals.css to open it.
  • Select all the content with Cmd+A (macOS) or Ctrl+A (Windows) and replace it with the following code:
@import "tailwindcss";

@custom-variant dark (&:where(.dark, .dark *));

What does this CSS do?

The @import "tailwindcss" line loads all of Tailwind's utility classes. This replaces the old @tailwind directives from earlier versions of Tailwind.

The @custom-variant dark line tells Tailwind to activate dark mode styles when a .dark class is present on a parent element. This lets us toggle dark mode with JavaScript instead of relying on the system preference.

  • Save globals.css with Cmd+S (macOS) or Ctrl+S (Windows).
  • Open src/app/layout.tsx from the Explorer sidebar.
  • Select all and replace the content with:
import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
  title: "Student Performance Dashboard",
  description: "Smart student result and performance analysis system",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

What does this layout do?

This is the root layout. Every page in your app renders inside it. The metadata export sets the browser tab title and description for search engines.

We removed the default font imports and extra wrapper classes to start with a clean slate. The children prop is where each page's content gets injected.

  • Save layout.tsx.
  • Open src/app/page.tsx from the Explorer sidebar.
  • Select all and replace the content with:
import { redirect } from "next/navigation";

export default function Home() {
  redirect("/dashboard");
}

What does this page do?

When someone visits the root URL (/), this page immediately redirects them to /dashboard. This way the dashboard becomes the landing page for your app.

  • Save page.tsx.

If you refresh your browser, you will see a 404 page. That is expected because the /dashboard route does not exist yet. You will create it in the next step.

✔️ Awesome, I've got everything!

Great. Double check you have saved all three files before moving on.

ⓧ I'd like to double check the full code

Here are all three files you modified. Make sure yours match exactly.

@import "tailwindcss";

@custom-variant dark (&:where(.dark, .dark *));
import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
  title: "Student Performance Dashboard",
  description: "Smart student result and performance analysis system",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}
import { redirect } from "next/navigation";

export default function Home() {
  redirect("/dashboard");
}

Your project skeleton is ready with all the tools you need. Next up, you will build the dashboard layout with a sidebar navigation and a dark mode toggle.

Build the Dashboard Layout with Sidebar

Your development environment is ready and the server is running. Now it's time to build the visual shell that every page in your dashboard will share.

In this step, you'll use the Next.js App Router's nested layout system to create a persistent sidebar, a header with dark mode toggle, and a main content area. This layout pattern is the same architecture used in production SaaS dashboards.

In this step, get ready to:
  • Build a Sidebar component with navigation links and the app title.
  • Build a Header component with a dark mode toggle that persists across reloads.
  • Wire both components into a dashboard layout with a scrollable content area.
Create the Sidebar component

The sidebar is the fixed left panel that houses your app's navigation. It stays visible on every page, giving users quick access to different sections of the dashboard.

  • In VS Code, create a new folder called components inside the src folder.
  • Inside src/components, create a new file called Sidebar.tsx.
  • Add the following code to Sidebar.tsx:
export default function Sidebar() {
  // Navigation links for the dashboard sections
  const navItems = [
    { label: "Overview", href: "/dashboard" },
    { label: "Students", href: "/dashboard" },
    { label: "Reports", href: "/dashboard" },
  ];

  return (
    <aside className="flex w-64 flex-col bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700">
      <div className="flex h-16 items-center justify-center border-b border-gray-200 dark:border-gray-700">
        <h1 className="text-xl font-bold text-indigo-600 dark:text-indigo-400">
          EduAnalytics
        </h1>
      </div>
      <nav className="flex-1 p-4 space-y-2">
        {navItems.map((item) => (
          <a
            key={item.label}
            href={item.href}
            className="flex items-center rounded-lg px-4 py-2 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
          >
            {item.label}
          </a>
        ))}
      </nav>
    </aside>
  );
}

What does this code do?

The navItems array defines the three navigation links your sidebar displays. All links point to /dashboard for now. You can expand these to real routes later.

The aside element is fixed at 256px wide (w-64) and uses flexbox to stack the title area and navigation vertically. Every colour class includes a dark: variant so the sidebar adapts when dark mode is active.

  • Save Sidebar.tsx.

You won't see the sidebar in the browser yet because it isn't imported into a layout. The next substep builds the Header, and then we'll wire both together.

Build the Header with dark mode toggle

The Header sits at the top of the content area and houses the dark mode toggle button. Dark mode works by adding a dark class to the html element and saving the preference to localStorage.

How does dark mode work in Tailwind v4?

Your globals.css file already contains @custom-variant dark (&:where(.dark, .dark *)); from the project setup. This tells Tailwind CSS to activate any dark: class when the html element has a dark class.

The Header component toggles this class on and off, and stores the user's choice in the browser's localStorage so it persists across page reloads.

  • Inside src/components, create a new file called Header.tsx.
  • Add the following code to Header.tsx:
"use client";

import { useState, useEffect } from "react";

export default function Header() {
  const [darkMode, setDarkMode] = useState(false);

  // On first load, check localStorage or system preference
  useEffect(() => {
    const stored = localStorage.getItem("theme");
    if (
      stored === "dark" ||
      (!stored && window.matchMedia("(prefers-color-scheme: dark)").matches)
    ) {
      setDarkMode(true);
      document.documentElement.classList.add("dark");
    }
  }, []);

  // Toggle dark mode and persist the choice
  function toggleDarkMode() {
    const newMode = !darkMode;
    setDarkMode(newMode);
    if (newMode) {
      document.documentElement.classList.add("dark");
      localStorage.setItem("theme", "dark");
    } else {
      document.documentElement.classList.remove("dark");
      localStorage.setItem("theme", "light");
    }
  }

  return (
    <header className="flex h-16 items-center justify-between border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-6">
      <h2 className="text-lg font-semibold text-gray-800 dark:text-gray-100">
        Dashboard
      </h2>
      <button
        onClick={toggleDarkMode}
        className="rounded-lg border border-gray-300 dark:border-gray-600 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
      >
        {darkMode ? "Light Mode" : "Dark Mode"}
      </button>
    </header>
  );
}

What does this code do?

The "use client" directive at the top tells Next.js this component runs in the browser. This is required because useState, useEffect, and localStorage are all browser APIs.

The useEffect runs once on page load. It checks if the user previously chose dark mode, or if their system prefers dark colours. If either is true, it adds the dark class immediately.

The toggleDarkMode function flips the state, updates the class on the html element, and writes the new preference to localStorage.

  • Save Header.tsx.

You won't see the Header yet either. The final substep wires both components into a shared layout.

Wire the layout together

The App Router uses layout.tsx files to wrap every page in a route segment with shared UI. Your dashboard layout imports the Sidebar and Header, then renders page content in a scrollable main area.

  • Inside the src/app folder, create a new folder called dashboard.
  • Inside src/app/dashboard, create a new file called layout.tsx.
  • Add the following code to layout.tsx:
import Sidebar from "@/components/Sidebar";
import Header from "@/components/Header";

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex h-screen bg-gray-100 dark:bg-gray-900">
      <Sidebar />
      <div className="flex flex-1 flex-col overflow-hidden">
        <Header />
        <main className="flex-1 overflow-y-auto p-6">{children}</main>
      </div>
    </div>
  );
}

What does this code do?

The outermost div uses flex h-screen to fill the entire viewport and lay out children side by side. The Sidebar sits on the left at a fixed 256px width.

The second div takes the remaining space (flex-1) and stacks the Header on top with the main content below. The overflow-y-auto on main means page content scrolls independently while the sidebar and header stay fixed.

  • Save layout.tsx.
  • Switch to your browser. The page should automatically reload.
  • You should see the EduAnalytics title in the sidebar on the left, three navigation links below it, and the Dashboard heading with a Dark Mode button in the header.
  • Click the Dark Mode button. The entire page should switch to dark colours.
  • Refresh the page. Dark mode should persist because the preference is saved in localStorage.

What about the content area?

The main content area may appear empty or show a not-found message. This is expected because you haven't created the dashboard page file yet. You'll build that in the next step when you add KPI cards.

Sidebar not showing up?

Make sure the dashboard folder is inside src/app (not src/components). The App Router only recognises layouts placed in the app directory tree.

Check that your import paths use @/components/Sidebar and @/components/Header. The @/ prefix maps to the src folder in Next.js projects.

Help me debug my dashboard layout.

✔️ Awesome, I've got everything!

Great. Double check you've saved all three files before moving on.

ⓧ I'd like to double check the full code

Here are all three files you created in this step.

export default function Sidebar() {
  const navItems = [
    { label: "Overview", href: "/dashboard" },
    { label: "Students", href: "/dashboard" },
    { label: "Reports", href: "/dashboard" },
  ];

  return (
    <aside className="flex w-64 flex-col bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700">
      <div className="flex h-16 items-center justify-center border-b border-gray-200 dark:border-gray-700">
        <h1 className="text-xl font-bold text-indigo-600 dark:text-indigo-400">
          EduAnalytics
        </h1>
      </div>
      <nav className="flex-1 p-4 space-y-2">
        {navItems.map((item) => (
          <a
            key={item.label}
            href={item.href}
            className="flex items-center rounded-lg px-4 py-2 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
          >
            {item.label}
          </a>
        ))}
      </nav>
    </aside>
  );
}
"use client";

import { useState, useEffect } from "react";

export default function Header() {
  const [darkMode, setDarkMode] = useState(false);

  useEffect(() => {
    const stored = localStorage.getItem("theme");
    if (
      stored === "dark" ||
      (!stored && window.matchMedia("(prefers-color-scheme: dark)").matches)
    ) {
      setDarkMode(true);
      document.documentElement.classList.add("dark");
    }
  }, []);

  function toggleDarkMode() {
    const newMode = !darkMode;
    setDarkMode(newMode);
    if (newMode) {
      document.documentElement.classList.add("dark");
      localStorage.setItem("theme", "dark");
    } else {
      document.documentElement.classList.remove("dark");
      localStorage.setItem("theme", "light");
    }
  }

  return (
    <header className="flex h-16 items-center justify-between border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-6">
      <h2 className="text-lg font-semibold text-gray-800 dark:text-gray-100">
        Dashboard
      </h2>
      <button
        onClick={toggleDarkMode}
        className="rounded-lg border border-gray-300 dark:border-gray-600 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
      >
        {darkMode ? "Light Mode" : "Dark Mode"}
      </button>
    </header>
  );
}
import Sidebar from "@/components/Sidebar";
import Header from "@/components/Header";

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex h-screen bg-gray-100 dark:bg-gray-900">
      <Sidebar />
      <div className="flex flex-1 flex-col overflow-hidden">
        <Header />
        <main className="flex-1 overflow-y-auto p-6">{children}</main>
      </div>
    </div>
  );
}

Your dashboard shell is in place. Next up, you'll fill that empty content area with KPI metric cards that calculate student performance data in real time.

Create the KPI Metric Cards

Your dashboard layout is in place with a sidebar and header that persist across pages. Now it's time to fill the main content area with useful data.

KPI cards are the first thing users see on any analytics dashboard. They surface the most important metrics at a glance. In this step, you'll create mock student data, build a reusable card component, and compute key metrics to display in a responsive grid.

In this step, get ready to:
  • Create a mock data file with TypeScript interfaces for student records.
  • Build a reusable KpiCard component styled with Tailwind CSS dark mode variants.
  • Calculate and render four KPI metrics in a responsive grid on the dashboard page.
Create the mock student data file

Before you can display metrics, you need data to compute them from. A mock data file lets you build and test the UI without needing a real database.

  • In VS Code's Explorer sidebar, right-click the src folder and select New Folder.
  • Name the folder data.
  • Right-click the new data folder and select New File.
  • Name the file students.ts.
  • Add the following code to define the Student interface and populate the mock data:
export interface Student {
  id: number;
  name: string;
  subject: string;
  score: number;
  grade: string;
  status: "Pass" | "Fail";
}

export const students: Student[] = [
  { id: 1, name: "Alice Johnson", subject: "Math", score: 92, grade: "A", status: "Pass" },
  { id: 2, name: "Bob Smith", subject: "Science", score: 78, grade: "B", status: "Pass" },
  { id: 3, name: "Charlie Brown", subject: "English", score: 65, grade: "C", status: "Pass" },
  { id: 4, name: "Diana Ross", subject: "History", score: 88, grade: "A", status: "Pass" },
  { id: 5, name: "Ethan Hunt", subject: "Math", score: 45, grade: "F", status: "Fail" },
  { id: 6, name: "Fiona Green", subject: "Science", score: 91, grade: "A", status: "Pass" },
  { id: 7, name: "George White", subject: "English", score: 72, grade: "B", status: "Pass" },
  { id: 8, name: "Hannah Lee", subject: "History", score: 58, grade: "C", status: "Pass" },
  { id: 9, name: "Ian Clark", subject: "Math", score: 83, grade: "B", status: "Pass" },
  { id: 10, name: "Julia Adams", subject: "Science", score: 47, grade: "F", status: "Fail" },
  { id: 11, name: "Kevin Brown", subject: "English", score: 89, grade: "A", status: "Pass" },
  { id: 12, name: "Laura Wilson", subject: "History", score: 34, grade: "F", status: "Fail" },
  { id: 13, name: "Mike Davis", subject: "Math", score: 76, grade: "B", status: "Pass" },
  { id: 14, name: "Nina Patel", subject: "Science", score: 95, grade: "A", status: "Pass" },
  { id: 15, name: "Oscar Martinez", subject: "English", score: 52, grade: "C", status: "Pass" },
  { id: 16, name: "Paula King", subject: "History", score: 81, grade: "B", status: "Pass" },
];

What does this code do?

The Student interface defines the exact shape of each student record. TypeScript uses this to catch errors if you try to access a property that doesn't exist.

The status field uses a union type ("Pass" | "Fail") which means it can only be one of those two strings. The 16 student records cover four subjects so you can compute meaningful averages later.

  • Save src/data/students.ts.
  • Confirm the file appears in the Explorer sidebar under src/data/students.ts.

Seeing red squiggly lines in the file?

  • Check that the file extension is .ts (not .tsx or .js). This file has no JSX so it only needs .ts.
  • Ensure every object in the array has all six fields: id, name, subject, score, grade, and status.

Help me fix TypeScript errors in my students.ts data file.

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file before moving on.

ⓧ I'd like to double check the full code

export interface Student {
  id: number;
  name: string;
  subject: string;
  score: number;
  grade: string;
  status: "Pass" | "Fail";
}

export const students: Student[] = [
  { id: 1, name: "Alice Johnson", subject: "Math", score: 92, grade: "A", status: "Pass" },
  { id: 2, name: "Bob Smith", subject: "Science", score: 78, grade: "B", status: "Pass" },
  { id: 3, name: "Charlie Brown", subject: "English", score: 65, grade: "C", status: "Pass" },
  { id: 4, name: "Diana Ross", subject: "History", score: 88, grade: "A", status: "Pass" },
  { id: 5, name: "Ethan Hunt", subject: "Math", score: 45, grade: "F", status: "Fail" },
  { id: 6, name: "Fiona Green", subject: "Science", score: 91, grade: "A", status: "Pass" },
  { id: 7, name: "George White", subject: "English", score: 72, grade: "B", status: "Pass" },
  { id: 8, name: "Hannah Lee", subject: "History", score: 58, grade: "C", status: "Pass" },
  { id: 9, name: "Ian Clark", subject: "Math", score: 83, grade: "B", status: "Pass" },
  { id: 10, name: "Julia Adams", subject: "Science", score: 47, grade: "F", status: "Fail" },
  { id: 11, name: "Kevin Brown", subject: "English", score: 89, grade: "A", status: "Pass" },
  { id: 12, name: "Laura Wilson", subject: "History", score: 34, grade: "F", status: "Fail" },
  { id: 13, name: "Mike Davis", subject: "Math", score: 76, grade: "B", status: "Pass" },
  { id: 14, name: "Nina Patel", subject: "Science", score: 95, grade: "A", status: "Pass" },
  { id: 15, name: "Oscar Martinez", subject: "English", score: 52, grade: "C", status: "Pass" },
  { id: 16, name: "Paula King", subject: "History", score: 81, grade: "B", status: "Pass" },
];
Build the KpiCard component

A reusable component means you write the styling once and render it multiple times with different data. Each card will accept a title, value, and description as props.

  • In the Explorer sidebar, right-click the src/components folder and select New File.
  • Name it KpiCard.tsx.
  • Add the following code to create the KPI card component:
interface KpiCardProps {
  title: string;
  value: string;
  description: string;
}

export default function KpiCard({ title, value, description }: KpiCardProps) {
  return (
    <div className="rounded-xl bg-white dark:bg-gray-800 p-6 shadow-sm border border-gray-200 dark:border-gray-700">
      <p className="text-sm font-medium text-gray-500 dark:text-gray-400">
        {title}
      </p>
      <p className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">
        {value}
      </p>
      <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
        {description}
      </p>
    </div>
  );
}

What does this code do?

  • The KpiCardProps interface tells TypeScript exactly which props this component accepts.
  • Every Tailwind CSS class has a dark: variant so the card looks correct in both light and dark mode.
  • The value is styled at text-3xl font-bold to make the most important metric jump out visually.
  • Save src/components/KpiCard.tsx.

You won't see a change yet because nothing renders this component. The next chunk connects it to the dashboard page.

✔️ Awesome, I've got everything!

Great. Double check you've saved the file before moving on.

ⓧ I'd like to double check the full code

interface KpiCardProps {
  title: string;
  value: string;
  description: string;
}

export default function KpiCard({ title, value, description }: KpiCardProps) {
  return (
    <div className="rounded-xl bg-white dark:bg-gray-800 p-6 shadow-sm border border-gray-200 dark:border-gray-700">
      <p className="text-sm font-medium text-gray-500 dark:text-gray-400">
        {title}
      </p>
      <p className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">
        {value}
      </p>
      <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
        {description}
      </p>
    </div>
  );
}
Calculate KPIs and render the dashboard page

With data and a card component ready, you can now create the dashboard page that computes metrics and renders four cards in a responsive grid.

  • In the Explorer sidebar, right-click the src/app/dashboard folder and select New File.
  • Name it page.tsx.
  • Add the following code to import the data and compute the KPI values:
import { students } from "@/data/students";
import KpiCard from "@/components/KpiCard";

// Calculate key metrics from the student data
function calculateKpis() {
  const totalStudents = students.length;
  const averageScore = Math.round(
    students.reduce((sum, s) => sum + s.score, 0) / totalStudents
  );
  const passRate = Math.round(
    (students.filter((s) => s.status === "Pass").length / totalStudents) * 100
  );
  const topPerformer = students.reduce((top, s) =>
    s.score > top.score ? s : top
  );

  return { totalStudents, averageScore, passRate, topPerformer };
}

What does this code do?

  • totalStudents counts how many records exist in the array.
  • averageScore sums all scores and divides by the student count, rounded to a whole number.
  • passRate filters for students whose status is "Pass" and expresses the result as a percentage.
  • topPerformer finds the student with the highest score using reduce.
  • Below the calculateKpis function, add the page component that renders the KPI cards in a responsive grid:
export default function DashboardPage() {
  const { totalStudents, averageScore, passRate, topPerformer } =
    calculateKpis();

  return (
    <div className="space-y-6">
      <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
        <KpiCard
          title="Total Students"
          value={totalStudents.toString()}
          description="Enrolled this semester"
        />
        <KpiCard
          title="Average Score"
          value={`${averageScore}%`}
          description="Across all subjects"
        />
        <KpiCard
          title="Pass Rate"
          value={`${passRate}%`}
          description="Score above 50%"
        />
        <KpiCard
          title="Top Performer"
          value={topPerformer.name}
          description={`${topPerformer.score}% in ${topPerformer.subject}`}
        />
      </div>
    </div>
  );
}

What's happening here?

  • The grid classes create a responsive layout. It starts as 1 column on mobile, expands to 2 columns at the sm breakpoint, and becomes 4 columns at the lg breakpoint.
  • Each KpiCard receives computed values as props. The component doesn't know how to calculate anything. It just displays what it's given.
  • This is a server component (no "use client" directive). The Next.js App Router renders it on the server by default, which is fine because it only reads static data.
  • Save src/app/dashboard/page.tsx.
  • Switch back to your browser at http://localhost:3000. You should see four KPI cards displayed in a row showing Total Students (16), Average Score, Pass Rate, and Top Performer.
  • Click the Dark Mode toggle in the header. The cards should switch to a dark background with light text.
  • Resize your browser window narrower to see the grid collapse from 4 columns to 2 and then to 1.

Cards not showing up?

  • Make sure the file is named exactly page.tsx inside the src/app/dashboard folder. The App Router uses this exact filename to identify page content.
  • Check that the import path @/data/students matches the folder you created. The @/ alias points to src/.
  • If you see a compilation error in the terminal, check that both files (students.ts and KpiCard.tsx) are saved without errors.

Help me debug why my KPI cards aren't rendering on the dashboard page.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file.

ⓧ I'd like to double check the full code

import { students } from "@/data/students";
import KpiCard from "@/components/KpiCard";

function calculateKpis() {
  const totalStudents = students.length;
  const averageScore = Math.round(
    students.reduce((sum, s) => sum + s.score, 0) / totalStudents
  );
  const passRate = Math.round(
    (students.filter((s) => s.status === "Pass").length / totalStudents) * 100
  );
  const topPerformer = students.reduce((top, s) =>
    s.score > top.score ? s : top
  );

  return { totalStudents, averageScore, passRate, topPerformer };
}

export default function DashboardPage() {
  const { totalStudents, averageScore, passRate, topPerformer } =
    calculateKpis();

  return (
    <div className="space-y-6">
      <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
        <KpiCard
          title="Total Students"
          value={totalStudents.toString()}
          description="Enrolled this semester"
        />
        <KpiCard
          title="Average Score"
          value={`${averageScore}%`}
          description="Across all subjects"
        />
        <KpiCard
          title="Pass Rate"
          value={`${passRate}%`}
          description="Score above 50%"
        />
        <KpiCard
          title="Top Performer"
          value={topPerformer.name}
          description={`${topPerformer.score}% in ${topPerformer.subject}`}
        />
      </div>
    </div>
  );
}

Your dashboard now surfaces key metrics at a glance. Next up, you'll add an interactive bar chart below these cards to visualize how students are performing across different subjects.

Add the Subject Performance Bar Chart

Your KPI cards give a quick overview of the class, but they don't reveal which subjects are pulling students up or dragging them down.

In this step, you'll build an interactive bar chart that visualizes average scores per subject using Recharts. This makes it easy to spot underperforming subjects at a glance.

In this step, get ready to:
  • Compute subject-wise averages from the mock data.
  • Build a responsive bar chart component with Recharts.
  • Render the chart below the KPI cards on the dashboard page.
Compute subject-wise averages from the mock data

Before you can chart anything, you need to crunch the raw student records into per-subject averages. You'll add a helper function that groups scores by subject and returns a simple array of objects ready for the chart.

  • In src/app/dashboard/page.tsx, find the closing brace of the calculateKpis function. Add the following function directly below it:
function calculateSubjectAverages() {
  // Group all student scores by their subject
  const subjectMap: Record<string, { total: number; count: number }> = {};

  students.forEach((s) => {
    if (!subjectMap[s.subject]) {
      subjectMap[s.subject] = { total: 0, count: 0 };
    }
    subjectMap[s.subject].total += s.score;
    subjectMap[s.subject].count += 1;
  });

  // Convert the grouped data into an array of { subject, average } objects
  return Object.entries(subjectMap).map(([subject, data]) => ({
    subject,
    average: Math.round(data.total / data.count),
  }));
}

What does this code do?

This function loops through every student record and tallies up scores per subject. It then divides each subject's total by its count to get a rounded average.

The output is a clean array like [{ subject: "Math", average: 74 }, ...] that Recharts can consume directly as chart data.

  • Save src/app/dashboard/page.tsx.
  • You won't see a change in the browser yet. The next chunk uses this function to power the chart.
Create the SubjectChart client component

Recharts uses browser APIs like SVG rendering and ResizeObserver. In Next.js App Router, that means the chart component must be marked as a client component with the "use client" directive.

  • Create a new file at src/components/SubjectChart.tsx and add the following code:
"use client";

import {
  BarChart,
  Bar,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  ResponsiveContainer,
} from "recharts";

// Define the shape of data this component expects
interface SubjectChartProps {
  data: { subject: string; average: number }[];
}

export default function SubjectChart({ data }: SubjectChartProps) {
  return (
    <div className="rounded-xl bg-white dark:bg-gray-800 p-6 shadow-sm border border-gray-200 dark:border-gray-700">
      <h3 className="mb-4 text-lg font-semibold text-gray-800 dark:text-gray-100">
        Subject-wise Average Scores
      </h3>
      {/* ResponsiveContainer makes the chart resize with its parent */}
      <ResponsiveContainer width="100%" height={300}>
        <BarChart data={data}>
          <CartesianGrid strokeDasharray="3 3" stroke="#374151" />
          <XAxis dataKey="subject" stroke="#6b7280" />
          <YAxis stroke="#6b7280" domain={[0, 100]} />
          <Tooltip />
          <Bar dataKey="average" fill="#6366f1" radius={[4, 4, 0, 0]} />
        </BarChart>
      </ResponsiveContainer>
    </div>
  );
}

What does this code do?

  • "use client" tells Next.js to render this component in the browser, which Recharts needs for SVG and resize detection.
  • ResponsiveContainer wraps the chart so it stretches to fill its parent's width and stays at a fixed height of 300px.
  • CartesianGrid draws dashed grid lines for readability.
  • XAxis maps subject names along the bottom. YAxis shows score values from 0 to 100.
  • Tooltip displays the exact average when the learner hovers over a bar.
  • The radius prop rounds the top corners of each bar for a polished look.
  • Save src/components/SubjectChart.tsx.
  • You won't see the chart yet because nothing imports this component. The next chunk connects it to the dashboard page.

Why "use client" for Recharts?

Next.js App Router renders components on the server by default. Recharts needs access to the browser's DOM and window object to measure SVG dimensions and handle mouse events like tooltips. The "use client" directive opts this single component into client-side rendering while the rest of your app stays server-rendered.

Render the chart on the dashboard page

Now connect the data function and chart component to the dashboard page so the chart appears below your KPI cards.

  • In src/app/dashboard/page.tsx, find the imports at the top of the file. Add this import below the existing KpiCard import:
import SubjectChart from "@/components/SubjectChart";
  • Inside the DashboardPage function, find this line:
  const { totalStudents, averageScore, passRate, topPerformer } =
    calculateKpis();
  • Add this line directly below it to call your new function:
  const subjectAverages = calculateSubjectAverages();
  • Finally, in the JSX return, find the closing </div> of the KPI grid (the one right after the last KpiCard). Add the chart component below it, but still inside the outer <div className="space-y-6">:
      <SubjectChart data={subjectAverages} />

What's happening here?

You're passing the computed subject averages as a prop to SubjectChart. The space-y-6 class on the parent div automatically adds vertical spacing between the KPI grid and the chart.

  • Save src/app/dashboard/page.tsx.
  • Switch to your browser. You should see a bar chart below the KPI cards with four bars (Math, Science, English, History). Hover over any bar to see its exact average score in a tooltip.

Don't see the chart?

  • Check that SubjectChart.tsx starts with "use client"; on line 1. Without this, Next.js tries to render Recharts on the server and fails silently.
  • Make sure the import path is @/components/SubjectChart (capital S, capital C). File paths are case-sensitive.
  • If you see a blank white box, confirm that recharts and react-is are installed. Run npm install recharts react-is again if needed.

Still stuck? Why isn't my Recharts bar chart rendering on the dashboard?

✔️ Awesome, I've got everything!

Great. Double check you've saved both files.

ⓧ I'd like to double check the full code

Here is the full src/components/SubjectChart.tsx:

"use client";

import {
  BarChart,
  Bar,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  ResponsiveContainer,
} from "recharts";

interface SubjectChartProps {
  data: { subject: string; average: number }[];
}

export default function SubjectChart({ data }: SubjectChartProps) {
  return (
    <div className="rounded-xl bg-white dark:bg-gray-800 p-6 shadow-sm border border-gray-200 dark:border-gray-700">
      <h3 className="mb-4 text-lg font-semibold text-gray-800 dark:text-gray-100">
        Subject-wise Average Scores
      </h3>
      <ResponsiveContainer width="100%" height={300}>
        <BarChart data={data}>
          <CartesianGrid strokeDasharray="3 3" stroke="#374151" />
          <XAxis dataKey="subject" stroke="#6b7280" />
          <YAxis stroke="#6b7280" domain={[0, 100]} />
          <Tooltip />
          <Bar dataKey="average" fill="#6366f1" radius={[4, 4, 0, 0]} />
        </BarChart>
      </ResponsiveContainer>
    </div>
  );
}

Here is the full src/app/dashboard/page.tsx:

import { students } from "@/data/students";
import KpiCard from "@/components/KpiCard";
import SubjectChart from "@/components/SubjectChart";

function calculateKpis() {
  const totalStudents = students.length;
  const averageScore = Math.round(
    students.reduce((sum, s) => sum + s.score, 0) / totalStudents
  );
  const passRate = Math.round(
    (students.filter((s) => s.status === "Pass").length / totalStudents) * 100
  );
  const topPerformer = students.reduce((top, s) =>
    s.score > top.score ? s : top
  );

  return { totalStudents, averageScore, passRate, topPerformer };
}

function calculateSubjectAverages() {
  const subjectMap: Record<string, { total: number; count: number }> = {};

  students.forEach((s) => {
    if (!subjectMap[s.subject]) {
      subjectMap[s.subject] = { total: 0, count: 0 };
    }
    subjectMap[s.subject].total += s.score;
    subjectMap[s.subject].count += 1;
  });

  return Object.entries(subjectMap).map(([subject, data]) => ({
    subject,
    average: Math.round(data.total / data.count),
  }));
}

export default function DashboardPage() {
  const { totalStudents, averageScore, passRate, topPerformer } =
    calculateKpis();
  const subjectAverages = calculateSubjectAverages();

  return (
    <div className="space-y-6">
      <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
        <KpiCard
          title="Total Students"
          value={totalStudents.toString()}
          description="Enrolled this semester"
        />
        <KpiCard
          title="Average Score"
          value={`${averageScore}%`}
          description="Across all subjects"
        />
        <KpiCard
          title="Pass Rate"
          value={`${passRate}%`}
          description="Score above 50%"
        />
        <KpiCard
          title="Top Performer"
          value={topPerformer.name}
          description={`${topPerformer.score}% in ${topPerformer.subject}`}
        />
      </div>

      <SubjectChart data={subjectAverages} />
    </div>
  );
}

Your dashboard now surfaces subject-level performance at a glance. Next up, you'll build a sortable student results table so individual records are just as easy to explore.

Build the Student Results Table

Your dashboard has KPI cards that show the big picture and a chart that breaks down subject performance.

Now you need a way to see individual student records. A sortable data table lets users drill into specific students, compare scores, and quickly find who needs attention.

In this step, get ready to:
  • Create a ResultsTable client component with sortable column headers.
  • Add sorting logic that toggles between ascending and descending order.
  • Wire the table into the dashboard page and verify styling in both themes.
Create the ResultsTable client component

Tables with clickable column headers need browser interactivity. That means this component requires the "use client" directive, just like your chart component.

  • Create a new file at src/components/ResultsTable.tsx and paste the following code:
"use client";

import { useState } from "react";
import { Student } from "@/data/students";

interface ResultsTableProps {
  students: Student[];
}

type SortKey = "name" | "subject" | "score" | "grade";

export default function ResultsTable({ students }: ResultsTableProps) {
  const [sortKey, setSortKey] = useState<SortKey>("name");
  const [sortAsc, setSortAsc] = useState(true);

  function handleSort(key: SortKey) {
    if (sortKey === key) {
      setSortAsc(!sortAsc);
    } else {
      setSortKey(key);
      setSortAsc(true);
    }
  }

  const sorted = [...students].sort((a, b) => {
    const aVal = a[sortKey];
    const bVal = b[sortKey];
    if (typeof aVal === "number" && typeof bVal === "number") {
      return sortAsc ? aVal - bVal : bVal - aVal;
    }
    return sortAsc
      ? String(aVal).localeCompare(String(bVal))
      : String(bVal).localeCompare(String(aVal));
  });

  const columns: { key: SortKey; label: string }[] = [
    { key: "name", label: "Name" },
    { key: "subject", label: "Subject" },
    { key: "score", label: "Score" },
    { key: "grade", label: "Grade" },
  ];

  return (
    <div className="rounded-xl bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
      <div className="p-6 border-b border-gray-200 dark:border-gray-700">
        <h3 className="text-lg font-semibold text-gray-800 dark:text-gray-100">
          Student Results
        </h3>
      </div>
      <div className="overflow-x-auto">
        <table className="w-full text-left text-sm">
          <thead className="bg-gray-50 dark:bg-gray-700">
            <tr>
              {columns.map((col) => (
                <th
                  key={col.key}
                  onClick={() => handleSort(col.key)}
                  className="cursor-pointer px-6 py-3 font-medium text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white select-none"
                >
                  {col.label}
                  {sortKey === col.key && (
                    <span className="ml-1">{sortAsc ? "\u2191" : "\u2193"}</span>
                  )}
                </th>
              ))}
              <th className="px-6 py-3 font-medium text-gray-600 dark:text-gray-300">
                Status
              </th>
            </tr>
          </thead>
          <tbody className="divide-y divide-gray-200 dark:divide-gray-700">
            {sorted.map((student) => (
              <tr
                key={student.id}
                className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
              >
                <td className="px-6 py-4 text-gray-900 dark:text-gray-100">
                  {student.name}
                </td>
                <td className="px-6 py-4 text-gray-600 dark:text-gray-300">
                  {student.subject}
                </td>
                <td className="px-6 py-4 text-gray-600 dark:text-gray-300">
                  {student.score}%
                </td>
                <td className="px-6 py-4 text-gray-600 dark:text-gray-300">
                  {student.grade}
                </td>
                <td className="px-6 py-4">
                  <span
                    className={`inline-flex rounded-full px-2 py-1 text-xs font-medium ${
                      student.status === "Pass"
                        ? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
                        : "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
                    }`}
                  >
                    {student.status}
                  </span>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

What does this code do?

This component renders a full HTML table with five visible columns: Name, Subject, Score, Grade, and Status.

  • The columns array defines the four sortable headers. Status is not sortable because it derives from the score.
  • Each <th> has an onClick handler that calls handleSort to change the active sort column.
  • The status badge uses conditional Tailwind classes to show green for Pass and red for Fail.
  • Save ResultsTable.tsx.

You will not see anything new in the browser yet. The component needs to be imported and rendered on the dashboard page first.

Understand the sorting logic

The sorting behaviour lives entirely inside the component using two pieces of React state.

How does the sorting work?

  • sortKey tracks which column is active (defaults to "name").
  • sortAsc tracks whether the order is ascending or descending.
  • When a user clicks a column that is already active, handleSort flips the direction. Clicking a different column resets to ascending.
  • The sorted array creates a copy of the students with [...students] so the original prop is never mutated. It uses localeCompare for strings and subtraction for numbers.
  • The arrow indicator (\u2191 or \u2193) only renders next to the currently active column header.
Render the table on the dashboard page

The ResultsTable is ready. Now you need to import it into the dashboard page and pass the student data as a prop.

  • In src/app/dashboard/page.tsx, find the import lines at the top of the file:
import { students } from "@/data/students";
import KpiCard from "@/components/KpiCard";
import SubjectChart from "@/components/SubjectChart";
  • Add the ResultsTable import below the SubjectChart import, so the imports now look like this:
import { students } from "@/data/students";
import KpiCard from "@/components/KpiCard";
import SubjectChart from "@/components/SubjectChart";
import ResultsTable from "@/components/ResultsTable";
  • Scroll down to the DashboardPage function's return statement. Find this line:
      <SubjectChart data={subjectAverages} />
    </div>
  • Add the ResultsTable component between <SubjectChart /> and the closing </div>, so it looks like this:
      <SubjectChart data={subjectAverages} />

      <ResultsTable students={students} />
    </div>

What does this change do?

The students array from your mock data file is passed directly as a prop. The ResultsTable handles all rendering and sorting internally.

  • Save page.tsx and refresh your browser.
  • You should see a table below the bar chart showing all 16 student records with columns for Name, Subject, Score, Grade, and Status.
  • Click the Score column header. The rows should sort by score in ascending order, and an arrow indicator should appear.
  • Click Score again to reverse to descending order.
  • Toggle dark mode using the button in the header. The table background, text, and hover states should switch to their dark variants.

Table not showing up?

  • Check that you saved both ResultsTable.tsx and page.tsx. The import in page.tsx must match the exact file name.
  • Confirm the import path is @/components/ResultsTable. A typo in the path causes a module-not-found error in the terminal.
  • If you see a hydration error, make sure "use client" is the very first line of ResultsTable.tsx (before any imports).

Help me debug my ResultsTable component not rendering on the dashboard.

✔️ Awesome, I've got everything!

Great. Double check you have saved both files before moving on.

ⓧ I'd like to double check the full code

Here is the full src/components/ResultsTable.tsx:

"use client";

import { useState } from "react";
import { Student } from "@/data/students";

interface ResultsTableProps {
  students: Student[];
}

type SortKey = "name" | "subject" | "score" | "grade";

export default function ResultsTable({ students }: ResultsTableProps) {
  const [sortKey, setSortKey] = useState<SortKey>("name");
  const [sortAsc, setSortAsc] = useState(true);

  function handleSort(key: SortKey) {
    if (sortKey === key) {
      setSortAsc(!sortAsc);
    } else {
      setSortKey(key);
      setSortAsc(true);
    }
  }

  const sorted = [...students].sort((a, b) => {
    const aVal = a[sortKey];
    const bVal = b[sortKey];
    if (typeof aVal === "number" && typeof bVal === "number") {
      return sortAsc ? aVal - bVal : bVal - aVal;
    }
    return sortAsc
      ? String(aVal).localeCompare(String(bVal))
      : String(bVal).localeCompare(String(aVal));
  });

  const columns: { key: SortKey; label: string }[] = [
    { key: "name", label: "Name" },
    { key: "subject", label: "Subject" },
    { key: "score", label: "Score" },
    { key: "grade", label: "Grade" },
  ];

  return (
    <div className="rounded-xl bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
      <div className="p-6 border-b border-gray-200 dark:border-gray-700">
        <h3 className="text-lg font-semibold text-gray-800 dark:text-gray-100">
          Student Results
        </h3>
      </div>
      <div className="overflow-x-auto">
        <table className="w-full text-left text-sm">
          <thead className="bg-gray-50 dark:bg-gray-700">
            <tr>
              {columns.map((col) => (
                <th
                  key={col.key}
                  onClick={() => handleSort(col.key)}
                  className="cursor-pointer px-6 py-3 font-medium text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white select-none"
                >
                  {col.label}
                  {sortKey === col.key && (
                    <span className="ml-1">{sortAsc ? "\u2191" : "\u2193"}</span>
                  )}
                </th>
              ))}
              <th className="px-6 py-3 font-medium text-gray-600 dark:text-gray-300">
                Status
              </th>
            </tr>
          </thead>
          <tbody className="divide-y divide-gray-200 dark:divide-gray-700">
            {sorted.map((student) => (
              <tr
                key={student.id}
                className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
              >
                <td className="px-6 py-4 text-gray-900 dark:text-gray-100">
                  {student.name}
                </td>
                <td className="px-6 py-4 text-gray-600 dark:text-gray-300">
                  {student.subject}
                </td>
                <td className="px-6 py-4 text-gray-600 dark:text-gray-300">
                  {student.score}%
                </td>
                <td className="px-6 py-4 text-gray-600 dark:text-gray-300">
                  {student.grade}
                </td>
                <td className="px-6 py-4">
                  <span
                    className={`inline-flex rounded-full px-2 py-1 text-xs font-medium ${
                      student.status === "Pass"
                        ? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
                        : "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
                    }`}
                  >
                    {student.status}
                  </span>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

Here is the full src/app/dashboard/page.tsx:

import { students } from "@/data/students";
import KpiCard from "@/components/KpiCard";
import SubjectChart from "@/components/SubjectChart";
import ResultsTable from "@/components/ResultsTable";

function calculateKpis() {
  const totalStudents = students.length;
  const averageScore = Math.round(
    students.reduce((sum, s) => sum + s.score, 0) / totalStudents
  );
  const passRate = Math.round(
    (students.filter((s) => s.status === "Pass").length / totalStudents) * 100
  );
  const topPerformer = students.reduce((top, s) =>
    s.score > top.score ? s : top
  );

  return { totalStudents, averageScore, passRate, topPerformer };
}

function calculateSubjectAverages() {
  const subjectMap: Record<string, { total: number; count: number }> = {};

  students.forEach((s) => {
    if (!subjectMap[s.subject]) {
      subjectMap[s.subject] = { total: 0, count: 0 };
    }
    subjectMap[s.subject].total += s.score;
    subjectMap[s.subject].count += 1;
  });

  return Object.entries(subjectMap).map(([subject, data]) => ({
    subject,
    average: Math.round(data.total / data.count),
  }));
}

export default function DashboardPage() {
  const { totalStudents, averageScore, passRate, topPerformer } =
    calculateKpis();
  const subjectAverages = calculateSubjectAverages();

  return (
    <div className="space-y-6">
      <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
        <KpiCard
          title="Total Students"
          value={totalStudents.toString()}
          description="Enrolled this semester"
        />
        <KpiCard
          title="Average Score"
          value={`${averageScore}%`}
          description="Across all subjects"
        />
        <KpiCard
          title="Pass Rate"
          value={`${passRate}%`}
          description="Score above 50%"
        />
        <KpiCard
          title="Top Performer"
          value={topPerformer.name}
          description={`${topPerformer.score}% in ${topPerformer.subject}`}
        />
      </div>

      <SubjectChart data={subjectAverages} />

      <ResultsTable students={students} />
    </div>
  );
}

Your dashboard now has all the core pieces in place. You have KPI cards for the high-level view, a bar chart for subject trends, and a sortable table for drilling into individual records. This is the exact layout pattern you will see in production SaaS analytics tools.

Secret mission

Add Search Filter and Grade Distribution Pie Chart

Your dashboard shows data, but users need to find specific students quickly. In this mission, you'll add a real-time search filter to the results table and build a grade distribution pie chart that sits alongside the bar chart in a responsive layout.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs or cloud resources to worry about. Decide whether to keep your files for future development, pause the server to free up memory, or delete everything entirely.

Resources you used:

  • Next.js project folder on your Desktop (includes source files and node_modules).
  • Development server process running in your terminal.

Keep everything running

No action needed. Choose this if you are still actively building or plan to continue working on the dashboard right away.

  • Your development server stays running at http://localhost:3000.
  • Your project files remain on your Desktop, ready for editing.

Pause - I'll come back to this later

Stop the development server to free up memory and terminal space. Your project files stay on your Desktop so you can pick up where you left off.

  • Switch to the terminal where your dev server is running.
  • Press Ctrl+C to stop the server.
  • To restart later, open your terminal, navigate back into the project folder, and run:
npm run dev

Delete - I don't want to use this again

Remove all project files and stop the development server. This frees up disk space and cleans your Desktop completely.

  • Switch to the terminal where your dev server is running.
  • Press Ctrl+C to stop the server.
  • Delete the project folder by running these commands:
cd ~/Desktop
rm -rf [[PROJECT_FOLDER="your project folder name"]]

On Windows, use this command instead:

cd ~\Desktop
rmdir /s /q [[PROJECT_FOLDER="your project folder name"]]
  • Clear the dark mode preference from your browser by opening DevTools (F12), navigating to the Application tab, selecting Local Storage, and deleting the theme entry for localhost:3000.

Nice Work!

Nice Work!

You just built a production-style student performance analytics dashboard from scratch using Next.js 16, Tailwind CSS, and Recharts.

This dashboard is the UI foundation for your smart student result and performance analysis system.

You've learned how to:

  • Build a persistent dashboard layout using Next.js App Router nested layouts with a fixed sidebar, header, and scrollable content area.
  • Style a full application with Tailwind CSS v4 utility classes and implement a dark/light mode toggle that persists preferences with localStorage.
  • Create interactive data visualizations with Recharts and build a sortable data table with click-to-sort column headers.
  • Secret Mission: Added real-time search filtering and a grade distribution pie chart to extend the dashboard.

Ready to quiz yourself?