Revenue Analytics Dashboard with AI
Build and deploy a SaaS revenue dashboard using Claude AI and Next.js.
Introduction
30 Second Summary
Every business owner wants to see their numbers at a glance. Revenue, customers, churn, growth. A clean dashboard that tells the story of how a business is doing.
In this project, you will build and deploy a professional revenue analytics dashboard using Claude AI as your coding assistant. You will generate production-quality code, paste it into your editor, and ship a live web application that you can share with potential clients via URL.
What You'll Build
A fully responsive SaaS revenue analytics dashboard built with Next.js, shadcn/ui, and Recharts, deployed live on Vercel.
By the end of this project, you'll have:
- A deployed Next.js application on Vercel with a live shareable URL you can send to potential clients.
- A responsive dashboard layout with collapsible sidebar navigation, KPI stat cards, interactive line and bar charts, and a dark/light mode toggle.
- A repeatable AI-assisted workflow for using Claude.ai to generate production-quality React code, even without prior JavaScript experience.
- Secret Mission: Add a sortable, filterable recent transactions data table that takes your dashboard from visual to fully functional.
Are there any prerequisites?
You need Node.js (version 20.9.0 or higher), Git installed, a GitHub account, and a Claude.ai account (free tier works). No prior React or JavaScript experience is required.
Set Up Your Project
Every professional dashboard starts with a solid foundation. Your project needs a modern React framework, a component library for polished UI elements, and a charting library for data visualization.
In this step, you will scaffold a complete Next.js 16 project using a single command that pre-configures shadcn/ui and Tailwind CSS v4. Then you will install the remaining packages needed for charts and icons.
In this step, get ready to:
- Scaffold a Next.js 16 project with shadcn/ui and Tailwind CSS v4 pre-configured.
- Install Recharts for interactive charts and lucide-react for icons.
- Add the specific shadcn/ui components your dashboard needs and verify the dev server runs.
Create a new Next.js project with shadcn/ui
The shadcn/ui CLI can scaffold an entire Next.js project in one command. This gives you TypeScript, Tailwind CSS v4, and shadcn/ui all pre-configured with zero manual setup.
Next.js 16 requires Node.js version 20.9.0 or higher. Let's confirm your version before proceeding.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Check your Node.js version by running this command:
node --version
✔️ I see v20.9.0 or higher
You are all set. Your Node.js version meets the minimum requirement for Next.js 16.
ⓧ My version is lower than 20.9.0
You need to update Node.js to version 20.9.0 or higher.
- Visit nodejs.org and download the latest LTS version.
- Run the installer and follow the prompts.
- Close and reopen your terminal, then run node --version again to confirm the update.
- Move to your Desktop by running this command:
cd ~/Desktop
- Scaffold your new project by running this command:
npx shadcn@latest init -t next --name revenue-dashboard
What does this command do?
This single command does all of the following for you:
- npx shadcn@latest runs the latest version of the shadcn/ui CLI without installing it permanently.
- init -t next tells it to initialize a project using the Next.js template.
- --name revenue-dashboard names your project folder revenue-dashboard.
The result is a complete Next.js 16 project with TypeScript, Tailwind CSS v4, and shadcn/ui all wired up and ready to go.
What if I see prompts during setup?
The CLI may ask you to confirm style preferences or color themes. Accept the defaults by pressing Enter for each prompt.
- Confirm the project folder was created by running:
ls
You should see revenue-dashboard listed in the output.
Don't see the revenue-dashboard folder?
- Make sure you ran the command from your Desktop. Run `pwd` to check your current location.
- If you see a permission error, try running the command again. Sometimes `npx` needs a second attempt to download the package.
- If you see a Node.js version error, go back and update Node.js to version 20.9.0 or higher.
Help me troubleshoot the shadcn init command
Install Recharts and lucide-react
Your dashboard needs two more packages. Recharts is a React charting library that renders interactive line charts and bar charts. The lucide-react package provides the icons for your sidebar navigation and stat cards.
- Navigate into your project folder by running this command:
cd revenue-dashboard
- Install both packages by running this command:
npm i recharts lucide-react
What does this install?
- recharts provides React components like LineChart, BarChart, and ResponsiveContainer that you will use for data visualization.
- lucide-react provides hundreds of clean, consistent SVG icons used throughout the dashboard UI.
You should see output showing packages added. Look for a line like added X packages near the end of the output.
Seeing npm errors?
- If you see ENOENT or no such file or directory, you are not inside the revenue-dashboard folder. Run cd revenue-dashboard first.
- If you see network timeout errors, check your internet connection and try running the command again.
Help me fix my npm install error
Add shadcn/ui components and start the dev server
shadcn/ui works differently from most component libraries. Instead of importing from a package, you add components directly into your project as source files. This gives you full control to customize them later.
- Add the shadcn/ui components your dashboard needs by running this command:
npx shadcn@latest add card button separator avatar dropdown-menu sheet
What are these components for?
- card wraps your stat cards and chart sections in styled containers.
- button provides the theme toggle and navigation actions.
- separator, avatar, dropdown-menu, and sheet handle layout dividers, user profiles, menus, and mobile sidebar overlays.
You should see each component being added with a confirmation message for each one.
- Start your development server by running this command:
npm run dev
- Open your browser and go to http://localhost:3000.
You should see the default Next.js page. This confirms your project is scaffolded correctly and the dev server is running.
Page not loading at localhost:3000?
- Check your terminal for error messages. If you see port 3000 is already in use, close other applications using that port or check if the terminal shows an alternative port like localhost:3001.
- If you see a Module not found error, stop the server with Ctrl+C and re-run npm i to reinstall dependencies.
- Make sure you are running the command from inside the revenue-dashboard folder, not from your Desktop.
Help me get my dev server running
Keep the dev server running
Leave this terminal window open with the dev server running for the rest of the project. When you need to run other commands in later steps, open a new terminal tab or window.
Your project foundation is solid. You have a fully configured Next.js 16 project with shadcn/ui, Tailwind CSS v4, Recharts, and lucide-react all ready to use. Next up, you will build the dashboard layout with a sidebar and header.
Build the Dashboard Layout with Sidebar
Your project is scaffolded, but right now it just shows a default Next.js page. A professional dashboard needs structure: a persistent sidebar for navigation and a header bar with utility controls.
In this step, you will use Claude.ai to generate a responsive dashboard layout with a collapsible sidebar, a sticky header with a dark/light mode toggle, and a main content area. Then you will paste the generated code into your project files.
In this step, get ready to:
- Generate dashboard layout code using Claude.ai.
- Create sidebar and header components with navigation items and a theme toggle.
- Add dark mode support with next-themes and verify the layout at localhost:3000.
Ask Claude.ai for the dashboard layout code
Instead of writing every line yourself, you will describe what you need in Claude.ai and let it generate production-quality React code. This is the core workflow you will repeat throughout the project.
- Open Claude.ai in your browser and start a new conversation.
- Ask Claude to generate a responsive dashboard layout with the following requirements:
- A fixed sidebar on desktop (240px wide) with navigation items: Dashboard, Analytics, Customers, Revenue, Settings.
- A sticky header bar with a dark/light mode toggle button.
- A main content area that offsets from the sidebar on desktop.
- Use Next.js App Router, Tailwind CSS v4, shadcn/ui components, lucide-react for icons, and next-themes for dark mode.
Be specific with Claude
The more specific your prompt, the better the output. Mention the exact libraries, component names, and layout dimensions. Claude will generate code that you can paste directly into your files with minimal editing.
Claude will generate code for several components. In the next substeps, you will create the files and paste the code. The code below matches what Claude produces for these requirements.
Create the sidebar component
The sidebar is the fixed left panel that provides navigation across different dashboard sections. It uses lucide-react icons and Next.js routing to highlight the active page.
- Create a new file at src/components/sidebar.tsx by pasting the following code:
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
LayoutDashboard,
BarChart3,
Users,
DollarSign,
Settings,
} from "lucide-react";
import { cn } from "@/lib/utils";
// Map icon names to their components for dynamic rendering
const iconMap = {
LayoutDashboard,
BarChart3,
Users,
DollarSign,
Settings,
};
// Define all navigation items with labels, routes, and icon names
const navItems = [
{ label: "Dashboard", href: "/", icon: "LayoutDashboard" as const },
{ label: "Analytics", href: "/analytics", icon: "BarChart3" as const },
{ label: "Customers", href: "/customers", icon: "Users" as const },
{ label: "Revenue", href: "/revenue", icon: "DollarSign" as const },
{ label: "Settings", href: "/settings", icon: "Settings" as const },
];
export function Sidebar() {
// Get current path to highlight the active nav item
const pathname = usePathname();
return (
<aside className="hidden md:flex md:w-60 md:flex-col md:fixed md:inset-y-0 border-r bg-card">
<div className="flex h-14 items-center border-b px-6">
<span className="font-semibold text-lg">RevenueApp</span>
</div>
<nav className="flex-1 space-y-1 px-3 py-4">
{navItems.map((item) => {
const Icon = iconMap[item.icon];
const isActive = pathname === item.href;
return (
<Link
key={item.href}
href={item.href}
className={cn(
"flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
isActive
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:bg-muted hover:text-foreground"
)}
>
<Icon className="h-4 w-4" />
{item.label}
</Link>
);
})}
</nav>
</aside>
);
}
What does this code do?
This component creates a fixed sidebar that stays visible on medium-and-larger screens (hidden on mobile).
- The usePathname() hook from Next.js detects which page is active and highlights that nav item.
- The cn() utility from shadcn/ui merges Tailwind classes conditionally, so active items get a primary background color.
- Each nav item renders as a Next.js Link for client-side navigation without full page reloads.
- Save src/components/sidebar.tsx.
You will not see any change in the browser yet. The sidebar needs to be imported into your root layout, which you will create in the next substep.
✔️ Awesome, I've got everything!
Great. Make sure the file is saved before moving on.
ⓧ I'd like to double check the full code
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
LayoutDashboard,
BarChart3,
Users,
DollarSign,
Settings,
} from "lucide-react";
import { cn } from "@/lib/utils";
const iconMap = {
LayoutDashboard,
BarChart3,
Users,
DollarSign,
Settings,
};
const navItems = [
{ label: "Dashboard", href: "/", icon: "LayoutDashboard" as const },
{ label: "Analytics", href: "/analytics", icon: "BarChart3" as const },
{ label: "Customers", href: "/customers", icon: "Users" as const },
{ label: "Revenue", href: "/revenue", icon: "DollarSign" as const },
{ label: "Settings", href: "/settings", icon: "Settings" as const },
];
export function Sidebar() {
const pathname = usePathname();
return (
<aside className="hidden md:flex md:w-60 md:flex-col md:fixed md:inset-y-0 border-r bg-card">
<div className="flex h-14 items-center border-b px-6">
<span className="font-semibold text-lg">RevenueApp</span>
</div>
<nav className="flex-1 space-y-1 px-3 py-4">
{navItems.map((item) => {
const Icon = iconMap[item.icon];
const isActive = pathname === item.href;
return (
<Link
key={item.href}
href={item.href}
className={cn(
"flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
isActive
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:bg-muted hover:text-foreground"
)}
>
<Icon className="h-4 w-4" />
{item.label}
</Link>
);
})}
</nav>
</aside>
);
}
Install next-themes and create the supporting components
The header component uses a dark/light mode toggle powered by next-themes. You need to install this package before creating the header and theme provider.
- Install the next-themes package by running this command in your terminal:
npm i next-themes
- You should see next-themes added to your package.json dependencies.
Seeing an npm error?
Make sure your terminal is inside the revenue-dashboard folder. Run pwd to check your current directory.
If you see a peer dependency warning, that is safe to ignore. The install still succeeds.
Why is npm install failing for me?
Now create the theme provider component. This is a thin wrapper that makes the theme context available to all components in your app.
- Create a new file at src/components/theme-provider.tsx and paste the following code:
"use client";
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
// Wrapper component that passes all props to NextThemesProvider
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
What does this code do?
This component wraps the NextThemesProvider so you can use it inside your app layout. The "use client" directive is required because theme detection runs in the browser, not on the server.
- Save src/components/theme-provider.tsx.
Next, create the header component with the dark/light mode toggle button.
- Create a new file at src/components/header.tsx and paste the following code:
"use client";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { Button } from "@/components/ui/button";
export function Header() {
// Access current theme and setter from next-themes
const { theme, setTheme } = useTheme();
return (
<header className="sticky top-0 z-40 flex h-14 items-center border-b bg-background px-6">
<div className="flex-1">
<h1 className="text-lg font-semibold">Revenue Dashboard</h1>
</div>
{/* Toggle between dark and light themes */}
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
>
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</header>
);
}
What does this code do?
- The useTheme() hook reads and sets the current theme (dark or light).
- The Sun and Moon icons animate with Tailwind's dark: variant. When dark mode is active, the Sun rotates away and the Moon appears.
- The sr-only span provides an accessible label for screen readers.
- Save src/components/header.tsx.
✔️ Awesome, I've got everything!
Great. Both files are saved and ready to go.
ⓧ I'd like to double check the full code
Here is the complete header component:
"use client";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { Button } from "@/components/ui/button";
export function Header() {
const { theme, setTheme } = useTheme();
return (
<header className="sticky top-0 z-40 flex h-14 items-center border-b bg-background px-6">
<div className="flex-1">
<h1 className="text-lg font-semibold">Revenue Dashboard</h1>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
>
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</header>
);
}
Create the root layout and verify everything
The root layout is the shell that wraps every page in your app. It brings together the sidebar, header, theme provider, and main content area into a single cohesive structure.
- Open the existing file at src/app/layout.tsx and replace its entire contents with the following code:
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";
import { Sidebar } from "@/components/sidebar";
import { Header } from "@/components/header";
// Load the Inter font for clean, professional typography
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Revenue Dashboard",
description: "SMB Revenue Analytics Dashboard",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" suppressHydrationWarning>
<body className={inter.className}>
{/* ThemeProvider enables dark/light mode across all components */}
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<div className="flex min-h-screen">
<Sidebar />
{/* Main content area offset by sidebar width on desktop */}
<div className="flex-1 md:ml-60">
<Header />
<main className="p-6">{children}</main>
</div>
</div>
</ThemeProvider>
</body>
</html>
);
}
What does this code do?
- The ThemeProvider wraps everything so that dark/light mode propagates to all child components.
- The suppressHydrationWarning on the <html> tag prevents a React warning caused by next-themes injecting a class during hydration.
- The md:ml-60 class offsets the main content by 240px on desktop screens, matching the sidebar width.
- On mobile (below md breakpoint), the sidebar hides and the content takes the full width.
- Save src/app/layout.tsx.
- Open your browser and go to http://localhost:3000.
You should see a fixed sidebar on the left with the "RevenueApp" title and five navigation items (Dashboard, Analytics, Customers, Revenue, Settings). The header bar at the top shows "Revenue Dashboard" and a sun/moon icon button for toggling dark mode.
- Click the sun/moon toggle button in the top-right corner of the header.
- Verify the page switches between dark and light themes.
Don't see the sidebar or header?
- Check your terminal for errors. If you see Module not found: Can't resolve '@/components/theme-provider', confirm that src/components/theme-provider.tsx exists and is saved.
- If the sidebar is not visible, widen your browser window past 768px. The sidebar is hidden on mobile-width screens by design.
- If the dark mode toggle does nothing, confirm next-themes is listed in your package.json dependencies. Re-run npm i next-themes if needed.
Help me debug my dashboard layout not rendering correctly.
✔️ Awesome, I've got everything!
Great. Double check you have saved all four files before moving on.
ⓧ I'd like to double check the full code
Here is the complete root layout:
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";
import { Sidebar } from "@/components/sidebar";
import { Header } from "@/components/header";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Revenue Dashboard",
description: "SMB Revenue Analytics Dashboard",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" suppressHydrationWarning>
<body className={inter.className}>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<div className="flex min-h-screen">
<Sidebar />
<div className="flex-1 md:ml-60">
<Header />
<main className="p-6">{children}</main>
</div>
</div>
</ThemeProvider>
</body>
</html>
);
}
Your dashboard shell is in place. You have a professional sidebar, a sticky header, and working dark mode. Next up, you will fill the main content area with KPI stat cards that display real business metrics.
Create KPI Stat Cards
Your dashboard now has a professional layout with sidebar navigation and a dark mode toggle. The next piece is the most important part of any analytics dashboard: the numbers.
KPI (Key Performance Indicator) cards give business owners an instant snapshot of their most critical metrics. In this step, you will ask Claude to generate a reusable stat card component, set up mock business data, and wire everything together on your dashboard page.
In this step, get ready to:
- Create a reusable StatCard component with trend indicators.
- Set up a data file with realistic SMB revenue metrics.
- Build the dashboard page displaying 4 KPI cards in a responsive grid.
Ask Claude to generate a StatCard component
Each KPI card needs to display a title, a value, the percentage change, and a visual indicator showing whether the trend is positive or negative. This is a perfect task to hand to Claude.ai.
- Open Claude.ai in your browser.
- Ask Claude to generate a reusable StatCard component that accepts props for: title, value, change percentage, and trend direction (up or down). Tell it to use the shadcn/ui Card component, Tailwind CSS for styling, and lucide-react arrow icons colored green for positive and red for negative trends.
- Create the file src/components/stat-card.tsx and paste the following code into it:
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { ArrowUpRight, ArrowDownRight } from "lucide-react";
import { cn } from "@/lib/utils";
// Define the props each stat card accepts
interface StatCardProps {
title: string;
value: string;
change: string;
trend: "up" | "down";
}
export function StatCard({ title, value, change, trend }: StatCardProps) {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
{title}
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
<div className="flex items-center gap-1 mt-1">
{/* Show green up-arrow or red down-arrow based on trend */}
{trend === "up" ? (
<ArrowUpRight className="h-4 w-4 text-green-500" />
) : (
<ArrowDownRight className="h-4 w-4 text-red-500" />
)}
<span
className={cn(
"text-xs font-medium",
trend === "up" ? "text-green-500" : "text-red-500"
)}
>
{change}
</span>
<span className="text-xs text-muted-foreground">vs last month</span>
</div>
</CardContent>
</Card>
);
}
What does this code do?
This component is a reusable card template. Every time you use it, you pass in different data through props (title, value, change, trend) and it renders a styled card.
- The StatCardProps interface tells TypeScript exactly what data each card expects.
- The cn() utility from shadcn/ui conditionally applies green or red text color depending on whether the trend is up or down.
- ArrowUpRight and ArrowDownRight are icon components from lucide-react that give a visual cue about each metric's direction.
- Save src/components/stat-card.tsx.
- You won't see a change yet. The next substep creates the data that feeds into this component.
Seeing a red underline on the imports?
If your editor shows an error on the @/components/ui/card import, make sure you ran npx shadcn@latest add card in the previous step. That command creates the Card component files your import references.
help me fix import errors in my stat-card component
Create the mock data file
Your StatCard component is ready to display data, but it needs something to display. A centralized data file keeps your metrics in one place, making it easy to swap in real API data later.
- Ask Claude to generate realistic SMB revenue data with 4 metrics: Monthly Revenue ($48,500, up 12.5%), Active Customers (2,420, up 8.2%), Churn Rate (2.4%, down 0.5%), and Revenue Growth (+12.5%, up 3.1%). Also ask for 12 months of monthly revenue data and a revenue-by-source breakdown.
- Create the file src/lib/data.ts and paste the following code into it:
// KPI Metrics
export const metrics = [
{
title: "Monthly Revenue",
value: "$48,500",
change: "+12.5%",
trend: "up" as const,
},
{
title: "Active Customers",
value: "2,420",
change: "+8.2%",
trend: "up" as const,
},
{
title: "Churn Rate",
value: "2.4%",
change: "-0.5%",
trend: "down" as const,
},
{
title: "Revenue Growth",
value: "+12.5%",
change: "+3.1%",
trend: "up" as const,
},
];
// Monthly Revenue Data (12 months)
export const revenueData = [
{ month: "Jan", revenue: 32000 },
{ month: "Feb", revenue: 34500 },
{ month: "Mar", revenue: 36200 },
{ month: "Apr", revenue: 35800 },
{ month: "May", revenue: 38400 },
{ month: "Jun", revenue: 40100 },
{ month: "Jul", revenue: 41800 },
{ month: "Aug", revenue: 43200 },
{ month: "Sep", revenue: 44500 },
{ month: "Oct", revenue: 45900 },
{ month: "Nov", revenue: 47200 },
{ month: "Dec", revenue: 48500 },
];
// Revenue by Source
export const revenueBySource = [
{ source: "Subscriptions", amount: 28000 },
{ source: "One-time Sales", amount: 9500 },
{ source: "Consulting", amount: 7000 },
{ source: "Add-ons", amount: 4000 },
];
// Navigation items
export const navItems = [
{ label: "Dashboard", href: "/", icon: "LayoutDashboard" },
{ label: "Analytics", href: "/analytics", icon: "BarChart3" },
{ label: "Customers", href: "/customers", icon: "Users" },
{ label: "Revenue", href: "/revenue", icon: "DollarSign" },
{ label: "Settings", href: "/settings", icon: "Settings" },
];
What does this data represent?
- metrics holds the 4 KPI values your stat cards will display. The as const on each trend tells TypeScript the value is literally "up" or "down" (not just any string).
- revenueData and revenueBySource will power the charts you build in the next step. You are setting them up now so the data file is complete.
- navItems centralizes navigation labels and icons. Your sidebar could import these later instead of hardcoding its own list.
Why is churn rate's trend "down" but positive?
Churn rate measures how many customers leave. A decrease in churn (-0.5%) means fewer customers are leaving, which is good for the business. The trend arrow still shows red because the direction is "down." A real production dashboard might use green for "positive business outcome" instead, but this keeps the component logic simple.
- Save src/lib/data.ts.
- You won't see a change yet. The next substep creates the page that pulls this data into your stat cards.
Editor showing a TypeScript error in data.ts?
Make sure every object in the metrics array includes as const after the trend value. Without it, TypeScript treats the string as a generic string type instead of the literal "up" | "down" your StatCard component expects.
help me fix TypeScript errors in my data file
Build the dashboard page with stat cards
You have the component and the data. Now you need a page that brings them together in a responsive grid layout.
- Open src/app/page.tsx in your editor. This file may already exist with default Next.js content.
- Replace its entire contents with the following code:
import { StatCard } from "@/components/stat-card";
import { metrics } from "@/lib/data";
export default function DashboardPage() {
return (
<div className="space-y-6">
{/* Page heading */}
<div>
<h2 className="text-2xl font-bold tracking-tight">Dashboard</h2>
<p className="text-muted-foreground">
Your business performance at a glance.
</p>
</div>
{/* Responsive grid: 1 col on mobile, 2 on tablet, 4 on desktop */}
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
{metrics.map((metric) => (
<StatCard
key={metric.title}
title={metric.title}
value={metric.value}
change={metric.change}
trend={metric.trend}
/>
))}
</div>
</div>
);
}
What does this page do?
- It imports the metrics array from your data file and maps over it, rendering one StatCard per metric.
- The grid uses Tailwind's responsive classes: grid-cols-1 for phones, sm:grid-cols-2 for tablets, and lg:grid-cols-4 for desktops. This means the 4 cards stack, pair, or line up depending on screen width.
- The key={metric.title} prop gives React a stable identifier for each card so it can efficiently update the list if data changes.
- Save src/app/page.tsx.
- Refresh your browser at http://localhost:3000. You should see 4 stat cards in a row (on a desktop-width screen) showing Monthly Revenue, Active Customers, Churn Rate, and Revenue Growth, each with a colored arrow indicator.
Don't see the stat cards?
- Check your terminal for errors. A common issue is a typo in the import path. Make sure @/components/stat-card matches your file name exactly (case-sensitive).
- If you see a blank page, check that src/lib/data.ts exists and exports a metrics array. A missing export causes a silent failure.
- Make sure your development server is still running. If it stopped, run npm run dev again from your revenue-dashboard folder.
help me debug why my stat cards are not showing up
✔️ 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 is the full code for each file you created in this step.
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { ArrowUpRight, ArrowDownRight } from "lucide-react";
import { cn } from "@/lib/utils";
interface StatCardProps {
title: string;
value: string;
change: string;
trend: "up" | "down";
}
export function StatCard({ title, value, change, trend }: StatCardProps) {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
{title}
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
<div className="flex items-center gap-1 mt-1">
{trend === "up" ? (
<ArrowUpRight className="h-4 w-4 text-green-500" />
) : (
<ArrowDownRight className="h-4 w-4 text-red-500" />
)}
<span
className={cn(
"text-xs font-medium",
trend === "up" ? "text-green-500" : "text-red-500"
)}
>
{change}
</span>
<span className="text-xs text-muted-foreground">vs last month</span>
</div>
</CardContent>
</Card>
);
}
// KPI Metrics
export const metrics = [
{
title: "Monthly Revenue",
value: "$48,500",
change: "+12.5%",
trend: "up" as const,
},
{
title: "Active Customers",
value: "2,420",
change: "+8.2%",
trend: "up" as const,
},
{
title: "Churn Rate",
value: "2.4%",
change: "-0.5%",
trend: "down" as const,
},
{
title: "Revenue Growth",
value: "+12.5%",
change: "+3.1%",
trend: "up" as const,
},
];
// Monthly Revenue Data (12 months)
export const revenueData = [
{ month: "Jan", revenue: 32000 },
{ month: "Feb", revenue: 34500 },
{ month: "Mar", revenue: 36200 },
{ month: "Apr", revenue: 35800 },
{ month: "May", revenue: 38400 },
{ month: "Jun", revenue: 40100 },
{ month: "Jul", revenue: 41800 },
{ month: "Aug", revenue: 43200 },
{ month: "Sep", revenue: 44500 },
{ month: "Oct", revenue: 45900 },
{ month: "Nov", revenue: 47200 },
{ month: "Dec", revenue: 48500 },
];
// Revenue by Source
export const revenueBySource = [
{ source: "Subscriptions", amount: 28000 },
{ source: "One-time Sales", amount: 9500 },
{ source: "Consulting", amount: 7000 },
{ source: "Add-ons", amount: 4000 },
];
// Navigation items
export const navItems = [
{ label: "Dashboard", href: "/", icon: "LayoutDashboard" },
{ label: "Analytics", href: "/analytics", icon: "BarChart3" },
{ label: "Customers", href: "/customers", icon: "Users" },
{ label: "Revenue", href: "/revenue", icon: "DollarSign" },
{ label: "Settings", href: "/settings", icon: "Settings" },
];
import { StatCard } from "@/components/stat-card";
import { metrics } from "@/lib/data";
export default function DashboardPage() {
return (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold tracking-tight">Dashboard</h2>
<p className="text-muted-foreground">
Your business performance at a glance.
</p>
</div>
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
{metrics.map((metric) => (
<StatCard
key={metric.title}
title={metric.title}
value={metric.value}
change={metric.change}
trend={metric.trend}
/>
))}
</div>
</div>
);
}
Your dashboard now displays real KPI data at a glance. Next up, you will add interactive charts that visualize revenue trends over time and break down income by source.
Add Interactive Charts
Your stat cards give clients a quick snapshot of their business health. But numbers alone don't tell the full story.
Charts show trends and patterns that raw numbers hide. In this step, you'll use Recharts to add a revenue trend line chart and a revenue-by-source bar chart to your dashboard.
In this step, get ready to:
- Build a Revenue Trend line chart component with Recharts.
- Build a Revenue by Source bar chart component.
- Add both charts to your dashboard page in a responsive grid.
Create the revenue trend line chart
A line chart is the best way to show how revenue changes over time. Each point represents one month's total, and the connecting line reveals the overall trajectory.
- Open Claude.ai in your browser.
- Ask Claude to generate a Revenue Trend line chart component. Include these details in your prompt:
- You are using Next.js App Router, Tailwind CSS v4, shadcn/ui, and Recharts.
- The component should use LineChart wrapped in a ResponsiveContainer (100% width, 300px height).
- Include XAxis for months, YAxis for dollar amounts (formatted as $Xk), a Tooltip for hover details, and a CartesianGrid for readability.
- Wrap the chart in a shadcn/ui Card with the title "Revenue Trend".
- Import revenue data from @/lib/data (the revenueData array).
- The component must have "use client" at the top because Recharts uses browser APIs.
- Create a new file at src/components/revenue-chart.tsx and paste Claude's generated code into it.
- Make sure your code matches this:
"use client";
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from "recharts";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { revenueData } from "@/lib/data";
export function RevenueChart() {
return (
<Card>
<CardHeader>
<CardTitle>Revenue Trend</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={revenueData}>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis
dataKey="month"
className="text-xs"
tick={{ fill: "hsl(var(--muted-foreground))" }}
/>
<YAxis
className="text-xs"
tick={{ fill: "hsl(var(--muted-foreground))" }}
tickFormatter={(value) => `$${value / 1000}k`}
/>
<Tooltip
formatter={(value: number) => [`$${value.toLocaleString()}`, "Revenue"]}
/>
<Line
type="monotone"
dataKey="revenue"
stroke="hsl(var(--primary))"
strokeWidth={2}
dot={{ fill: "hsl(var(--primary))", r: 4 }}
/>
</LineChart>
</ResponsiveContainer>
</CardContent>
</Card>
);
}
What does this code do?
ResponsiveContainer makes the chart stretch to fill its parent's width while staying 300px tall. This keeps the chart responsive on any screen size.
The tickFormatter on YAxis converts raw numbers like 32000 into readable labels like $32k. The Tooltip formatter does the same thing for hover values, showing $32,000 instead of 32000.
- Save src/components/revenue-chart.tsx.
You won't see this chart on the page yet. The next substep adds both charts to your dashboard.
Create the revenue by source bar chart
A bar chart is ideal for comparing categories side by side. This one shows clients exactly where their revenue comes from.
- Ask Claude to generate a Revenue by Source bar chart component. Include the same stack details (Next.js App Router, Tailwind CSS v4, shadcn/ui, Recharts) and these specifics:
- Use BarChart with Bar inside a ResponsiveContainer (100% width, 300px height).
- Show income sources (Subscriptions, One-time Sales, Consulting, Add-ons).
- Import revenueBySource from @/lib/data.
- Wrap in a shadcn/ui Card with the title "Revenue by Source".
- Add rounded top corners on the bars for a polished look.
- Create a new file at src/components/source-chart.tsx and paste Claude's generated code into it.
- Make sure your code matches this:
"use client";
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from "recharts";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { revenueBySource } from "@/lib/data";
export function SourceChart() {
return (
<Card>
<CardHeader>
<CardTitle>Revenue by Source</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={revenueBySource}>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis
dataKey="source"
className="text-xs"
tick={{ fill: "hsl(var(--muted-foreground))" }}
/>
<YAxis
className="text-xs"
tick={{ fill: "hsl(var(--muted-foreground))" }}
tickFormatter={(value) => `$${value / 1000}k`}
/>
<Tooltip
formatter={(value: number) => [`$${value.toLocaleString()}`, "Amount"]}
/>
<Bar
dataKey="amount"
fill="hsl(var(--primary))"
radius={[4, 4, 0, 0]}
/>
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
);
}
What does this code do?
The BarChart works just like the LineChart but renders data as vertical bars instead of a connected line. Each bar represents one revenue source.
The radius={[4, 4, 0, 0]} rounds the top-left and top-right corners of each bar, giving them a softer, more modern look.
- Save src/components/source-chart.tsx.
Seeing red squiggles in your editor?
If your editor shows import errors for recharts, make sure you ran npm i recharts in Step 1. You can verify by checking your package.json for a recharts entry under dependencies.
Why isn't Recharts showing up in my imports?
Add both charts to your dashboard page
Both chart components are ready. Now you need to import them into your dashboard page and place them in a responsive grid below the stat cards.
- Open src/app/page.tsx.
- Find the existing imports at the top of the file:
import { StatCard } from "@/components/stat-card";
import { metrics } from "@/lib/data";
- Add the two chart imports between the StatCard import and the metrics import, so your imports look like this:
import { StatCard } from "@/components/stat-card";
import { RevenueChart } from "@/components/revenue-chart";
import { SourceChart } from "@/components/source-chart";
import { metrics } from "@/lib/data";
- Now find the closing </div> of the stat cards grid (the one after the metrics.map block).
- Add this chart grid directly below it:
<div className="grid gap-4 grid-cols-1 lg:grid-cols-2">
<RevenueChart />
<SourceChart />
</div>
What does this grid do?
The grid-cols-1 lg:grid-cols-2 classes make the charts stack vertically on mobile and sit side by side on larger screens. The space-y-6 on the parent container adds consistent spacing between the stat cards section and this charts section.
- Save src/app/page.tsx.
- Go to http://localhost:3000 in your browser. You should see both charts rendered below your stat cards.
- Hover over data points on the line chart. A tooltip should appear showing the exact revenue for that month.
- Hover over a bar in the bar chart. A tooltip should show the revenue amount for that source.
Charts not showing up?
Check that both chart files have "use client" as the very first line. Recharts uses browser APIs and won't render without this directive.
If you see a blank space where the charts should be, make sure your ResponsiveContainer has both width="100%" and height={300} set. Without an explicit height, the container collapses to zero.
Why aren't my Recharts components rendering in Next.js?
✔️ 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 is the complete src/app/page.tsx file with both chart imports and the chart grid:
import { StatCard } from "@/components/stat-card";
import { RevenueChart } from "@/components/revenue-chart";
import { SourceChart } from "@/components/source-chart";
import { metrics } from "@/lib/data";
export default function DashboardPage() {
return (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold tracking-tight">Dashboard</h2>
<p className="text-muted-foreground">
Your business performance at a glance.
</p>
</div>
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
{metrics.map((metric) => (
<StatCard
key={metric.title}
title={metric.title}
value={metric.value}
change={metric.change}
trend={metric.trend}
/>
))}
</div>
<div className="grid gap-4 grid-cols-1 lg:grid-cols-2">
<RevenueChart />
<SourceChart />
</div>
</div>
);
}
Here is the complete src/components/revenue-chart.tsx:
"use client";
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from "recharts";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { revenueData } from "@/lib/data";
export function RevenueChart() {
return (
<Card>
<CardHeader>
<CardTitle>Revenue Trend</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={revenueData}>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis
dataKey="month"
className="text-xs"
tick={{ fill: "hsl(var(--muted-foreground))" }}
/>
<YAxis
className="text-xs"
tick={{ fill: "hsl(var(--muted-foreground))" }}
tickFormatter={(value) => `$${value / 1000}k`}
/>
<Tooltip
formatter={(value: number) => [`$${value.toLocaleString()}`, "Revenue"]}
/>
<Line
type="monotone"
dataKey="revenue"
stroke="hsl(var(--primary))"
strokeWidth={2}
dot={{ fill: "hsl(var(--primary))", r: 4 }}
/>
</LineChart>
</ResponsiveContainer>
</CardContent>
</Card>
);
}
Here is the complete src/components/source-chart.tsx:
"use client";
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from "recharts";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { revenueBySource } from "@/lib/data";
export function SourceChart() {
return (
<Card>
<CardHeader>
<CardTitle>Revenue by Source</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={revenueBySource}>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis
dataKey="source"
className="text-xs"
tick={{ fill: "hsl(var(--muted-foreground))" }}
/>
<YAxis
className="text-xs"
tick={{ fill: "hsl(var(--muted-foreground))" }}
tickFormatter={(value) => `$${value / 1000}k`}
/>
<Tooltip
formatter={(value: number) => [`$${value.toLocaleString()}`, "Amount"]}
/>
<Bar
dataKey="amount"
fill="hsl(var(--primary))"
radius={[4, 4, 0, 0]}
/>
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
);
}
Why Recharts over other charting libraries?
Recharts renders charts as SVG elements, which means they integrate naturally with Tailwind CSS theming and respond to dark/light mode changes. Canvas-based alternatives like Chart.js render pixels, making them harder to style with CSS.
Recharts also follows React's declarative pattern. Each chart piece (axis, line, tooltip) is its own component you can configure with props.
Your dashboard now tells a complete data story. Stat cards for quick KPIs, a line chart for trends over time, and a bar chart for revenue composition. Next up, you'll deploy this live so anyone can access it via URL.
Deploy to Vercel
Your dashboard looks great locally, but a dashboard only you can see on your own computer is not very useful for impressing clients.
In this step, you will deploy your project to Vercel, which gives you a live public URL you can share with anyone.
In this step, get ready to:
- Push your code to GitHub.
- Deploy your dashboard to Vercel with a live URL.
- Verify everything works on the public internet.
Initialize Git and push to GitHub
Vercel pulls your code from a Git repository to build and host your app. You need to push your project to GitHub first.
- Make sure you are in the terminal inside your revenue-dashboard folder.
- Stop the development server by pressing Ctrl+C in the terminal.
- Initialize a Git repository and make your first commit by running these commands:
git init
git add .
git commit -m "Initial dashboard build"
What do these commands do?
- git init turns your folder into a Git repository that tracks changes.
- git add . stages all your files for the commit.
- git commit -m "..." saves a snapshot of your project with a descriptive message.
- You should see a summary of files committed in the terminal output.
Now you need to create a repository on GitHub and push your code to it.
- Open your browser and go to github.com/new.
- Enter revenue-dashboard as the Repository name.
- Leave it set to Public.
- Do NOT check any boxes for README, .gitignore, or license (your project already has these).
- Click Create repository.
GitHub shows you setup commands on the next page. Look for the section labeled "push an existing repository from the command line."
- Copy and run the commands GitHub shows you. They look like this:
git remote add origin [[YOUR_REPO_URL="https://github.com/YOUR_USERNAME/revenue-dashboard.git"]]
git branch -M main
git push -u origin main
What do these commands do?
- git remote add origin connects your local project to the GitHub repository.
- git branch -M main renames your default branch to main.
- git push -u origin main uploads your code to GitHub.
- Refresh your GitHub repository page in the browser.
- You should see all your project files listed (the src folder, package.json, etc.).
Push failed or asking for credentials?
- If Git asks for a username and password, GitHub no longer accepts passwords. You need a personal access token or SSH key. Follow the prompts GitHub shows to authenticate.
- If you see fatal: remote origin already exists, run git remote remove origin first, then try the commands again.
Help me push my code to GitHub
Deploy to Vercel
Vercel is the company that created Next.js. It automatically detects Next.js projects and handles all the build configuration for you.
- Open vercel.com in your browser.
- Click Sign Up.
- Select Continue with GitHub to create your free Vercel account using your GitHub login.
- Authorize Vercel to access your GitHub account when prompted.
Is Vercel free?
The Vercel Hobby plan is completely free. It includes 200 deployments per day and 100 projects. No credit card is required.
Once you are signed in, you can import your GitHub repository.
- Click Add New Project (or Add New... then Project).
- Find revenue-dashboard in the list of your GitHub repositories.
- Click Import next to it.
- On the Configure Project page, leave all settings as their defaults. Vercel auto-detects Next.js.
- Click Deploy.
Vercel now builds your project. This usually takes under 2 minutes. You will see a build log with progress updates.
Don't see your repository in the list?
- Click Adjust GitHub App Permissions at the bottom of the import page.
- Grant Vercel access to your revenue-dashboard repository (or all repositories).
- Return to Vercel and refresh the page.
Help me connect my GitHub repo to Vercel
Verify your live dashboard
Once the build finishes, Vercel shows a congratulations screen with a preview of your deployed site.
- Click the preview image or the Visit button to open your live dashboard.
- Your URL will look something like revenue-dashboard-yourname.vercel.app.
- Verify the sidebar navigation appears on the left.
- Verify the 4 stat cards display correctly with their trend arrows.
- Verify both charts (Revenue Trend and Revenue by Source) render with data.
- Click the dark mode toggle in the header to confirm it switches themes.
What is my dashboard URL?
You can find your live URL on your Vercel project dashboard page. Copy it and save it somewhere. This is the link you share with potential clients to showcase your work.
Every time you push new code to GitHub, Vercel automatically rebuilds and redeploys your site. No manual action needed.
Build failed on Vercel?
- Check the build log on Vercel for the specific error message. The most common issue is a TypeScript type error that did not appear locally.
- Make sure all your files are saved and committed. Run git status to check for uncommitted changes.
- If you find an error, fix it locally, then push the fix:
git add .
git commit -m "Fix build error"
git push
Help me fix my Vercel deployment error
Your revenue analytics dashboard is now live on the internet. Anyone with the link can see your work. Next up, an optional Secret Mission to take your dashboard even further.
Secret mission
Add a Recent Transactions Data Table
Your dashboard looks great with charts and KPI cards. But real dashboards display transaction-level data too. In this secret mission, you will add a sortable, filterable data table showing recent client transactions below your charts.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs on Vercel's free Hobby plan with no ongoing costs, so there is no financial reason to delete anything.
Resources you used:
- Local Next.js project folder (revenue-dashboard) on your Desktop.
- GitHub repository with your dashboard code.
- Vercel project and deployment (free Hobby plan).
- Node.js packages installed locally in node_modules.
- Claude.ai conversation history (no cleanup needed).
Keep everything running
No action needed. Choose this if you are still actively building or want to keep your live dashboard URL available to share with potential clients.
- Your Vercel deployment stays live at your public URL with no cost.
- Your GitHub repository remains available for future updates.
- Push any code changes to GitHub and Vercel automatically redeploys.
Pause - I'll come back to this later
Shut down running processes to free up memory. Your files and deployments remain intact for when you return.
- Stop the development server by pressing Ctrl+C in your terminal.
- Your Vercel deployment stays live with no action needed. There is no cost for idle Hobby projects.
- When you return, open the revenue-dashboard folder and run npm run dev to restart your local server.
Delete - I don't want to use this again
Remove all project resources and start fresh. This permanently deletes your local code, GitHub repository, and Vercel deployment.
- Delete the local project folder by dragging revenue-dashboard from your Desktop to the Trash (macOS) or Recycle Bin (Windows).
- Delete the GitHub repository by going to your repository page, clicking Settings, scrolling to the bottom, and clicking Delete this repository.
- Delete the Vercel project by opening your Vercel dashboard, selecting the project, going to Settings, scrolling to the bottom, and clicking Delete Project.
That's a Wrap!
That's a Wrap!
Nice work! You just built and deployed a professional SaaS revenue analytics dashboard using an AI-assisted workflow.
You now have a live portfolio piece at your Vercel URL that demonstrates to potential clients exactly what you can build for them.
Here's what you learned:
- Built a responsive dashboard layout with sidebar navigation using the modern SaaS stack: Next.js + shadcn/ui + Tailwind CSS.
- Created interactive data visualizations with Recharts line and bar charts, plus implemented dark/light mode theming.
- Learned a repeatable workflow for using Claude.ai to generate production-quality code and deployed a live application to Vercel that anyone can access via URL.
- Added a sortable, filterable data table for recent transactions to make the dashboard fully functional.
The same workflow you used here scales to any dashboard or web application a client might need.
Ready to quiz yourself?