Intermediate50 min1 prerequisite

Create a data dashboard with charts, metrics, and real-time updates using AI tools.

Build a Dashboard

Build an analytics dashboard with charts, KPI cards, and data tables to visualize and manage data.

Project Overview

What We're Building

Terminal
Dashboard Features:
├── KPI Cards (metrics at a glance)
├── Charts
   ├── Line chart (trends)
   ├── Bar chart (comparisons)
   └── Pie chart (distribution)
├── Data Tables (detailed data)
├── Filters (date range, category)
└── Real-time updates

Tech Stack

  • Frontend: Next.js + Tailwind + shadcn/ui
  • Charts: Recharts
  • Backend: Supabase
  • Deployment: Vercel

Phase 1: Project Setup

Create Project

Terminal
npx create-next-app@latest dashboard --typescript --tailwind --eslint
cd dashboard

# Add shadcn/ui
npx shadcn@latest init

# Add components
npx shadcn@latest add card table tabs select

# Add Recharts
npm install recharts

AI Prompt for Dashboard Layout

Terminal
"Create a dashboard layout with Next.js and Tailwind.
Include:
1. Sidebar navigation (Dashboard, Analytics, Settings)
2. Header with user menu
3. Main content area
4. Responsive design (sidebar collapses on mobile)
Use shadcn/ui components."

Phase 2: Database Setup

Create Tables

Terminal
-- Products table
CREATE TABLE products (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  category TEXT NOT NULL,
  price DECIMAL(10,2) NOT NULL,
  stock INTEGER DEFAULT 0,
  created_at TIMESTAMPTZ DEFAULT now()
);

-- Orders table
CREATE TABLE orders (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  product_id UUID REFERENCES products(id),
  quantity INTEGER NOT NULL,
  total DECIMAL(10,2) NOT NULL,
  status TEXT DEFAULT 'pending',
  created_at TIMESTAMPTZ DEFAULT now()
);

-- Enable RLS
ALTER TABLE products ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

-- Authenticated users can read
CREATE POLICY "Authenticated can read products"
ON products FOR SELECT TO authenticated USING (true);

CREATE POLICY "Authenticated can read orders"
ON orders FOR SELECT TO authenticated USING (true);

Seed Sample Data

Terminal
-- Sample products
INSERT INTO products (name, category, price, stock) VALUES
  ('Widget A', 'Electronics', 99.99, 150),
  ('Widget B', 'Electronics', 149.99, 75),
  ('Gadget X', 'Accessories', 29.99, 300),
  ('Gadget Y', 'Accessories', 49.99, 200),
  ('Device Z', 'Electronics', 299.99, 50);

-- Sample orders (generate many)
INSERT INTO orders (product_id, quantity, total, status, created_at)
SELECT
  (SELECT id FROM products ORDER BY random() LIMIT 1),
  floor(random() * 5 + 1)::int,
  (random() * 500 + 50)::decimal(10,2),
  (ARRAY['pending', 'completed', 'shipped'])[floor(random() * 3 + 1)],
  now() - (random() * 30 || ' days')::interval
FROM generate_series(1, 100);

Phase 3: KPI Cards

AI Prompt for KPIs

Terminal
"Create KPI card components for a dashboard.
Show 4 metrics:
1. Total Revenue (with percentage change)
2. Orders Today
3. Active Products
4. Conversion Rate

Each card should have:
- Icon
- Metric value
- Label
- Trend indicator (up/down arrow with color)"

KPI Card Component

Terminal
// components/KPICard.tsx
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { ArrowUpIcon, ArrowDownIcon } from 'lucide-react'
import { cn } from '@/lib/utils'

interface KPICardProps {
  title: string
  value: string | number
  change?: number
  icon: React.ReactNode
}

export function KPICard({ title, value, change, icon }: KPICardProps) {
  const isPositive = change && change > 0

  return (
    <Card>
      <CardHeader className="flex flex-row items-center justify-between pb-2">
        <CardTitle className="text-sm font-medium text-muted-foreground">
          {title}
        </CardTitle>
        {icon}
      </CardHeader>
      <CardContent>
        <div className="text-2xl font-bold">{value}</div>
        {change !== undefined && (
          <p className={cn(
            "text-xs flex items-center mt-1",
            isPositive ? "text-green-600" : "text-red-600"
          )}>
            {isPositive ? (
              <ArrowUpIcon className="h-3 w-3 mr-1" />
            ) : (
              <ArrowDownIcon className="h-3 w-3 mr-1" />
            )}
            {Math.abs(change)}% from last month
          </p>
        )}
      </CardContent>
    </Card>
  )
}

Fetch KPI Data

Terminal
// app/dashboard/page.tsx
import { createClient } from '@/lib/supabase/server'
import { KPICard } from '@/components/KPICard'
import { DollarSign, ShoppingCart, Package, TrendingUp } from 'lucide-react'

async function getKPIs() {
  const supabase = await createClient()

  const { data: revenueData } = await supabase
    .from('orders')
    .select('total')
    .eq('status', 'completed')

  const { data: ordersToday } = await supabase
    .from('orders')
    .select('id')
    .gte('created_at', new Date().toISOString().split('T')[0])

  const { count: productCount } = await supabase
    .from('products')
    .select('*', { count: 'exact', head: true })

  const totalRevenue = revenueData?.reduce((sum, o) => sum + Number(o.total), 0) || 0

  return {
    revenue: totalRevenue,
    ordersToday: ordersToday?.length || 0,
    products: productCount || 0,
    conversionRate: 3.2
  }
}

export default async function DashboardPage() {
  const kpis = await getKPIs()

  return (
    <div className="p-6">
      <h1 className="text-3xl font-bold mb-6">Dashboard</h1>

      <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4 mb-8">
        <KPICard
          title="Total Revenue"
          value={`$${kpis.revenue.toLocaleString()}`}
          change={12.5}
          icon={<DollarSign className="h-4 w-4 text-muted-foreground" />}
        />
        <KPICard
          title="Orders Today"
          value={kpis.ordersToday}
          change={8.2}
          icon={<ShoppingCart className="h-4 w-4 text-muted-foreground" />}
        />
        <KPICard
          title="Active Products"
          value={kpis.products}
          change={-2.4}
          icon={<Package className="h-4 w-4 text-muted-foreground" />}
        />
        <KPICard
          title="Conversion Rate"
          value={`${kpis.conversionRate}%`}
          change={4.1}
          icon={<TrendingUp className="h-4 w-4 text-muted-foreground" />}
        />
      </div>
    </div>
  )
}

Phase 4: Charts

AI Prompt for Charts

Terminal
"Create chart components using Recharts for the dashboard.
Include:
1. Line chart for revenue over time (last 30 days)
2. Bar chart for orders by category
3. Pie chart for order status distribution

Use shadcn/ui Card as wrapper.
Make charts responsive."

Revenue Line Chart

Terminal
// components/RevenueChart.tsx
'use client'

import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'

interface RevenueChartProps {
  data: { date: string; revenue: number }[]
}

export function RevenueChart({ data }: RevenueChartProps) {
  return (
    <Card>
      <CardHeader>
        <CardTitle>Revenue Over Time</CardTitle>
      </CardHeader>
      <CardContent>
        <ResponsiveContainer width="100%" height={300}>
          <LineChart data={data}>
            <CartesianGrid strokeDasharray="3 3" />
            <XAxis
              dataKey="date"
              tick={{ fontSize: 12 }}
              tickFormatter={(value) => new Date(value).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
            />
            <YAxis
              tick={{ fontSize: 12 }}
              tickFormatter={(value) => `$${value}`}
            />
            <Tooltip
              formatter={(value: number) => [`$${value.toFixed(2)}`, 'Revenue']}
            />
            <Line
              type="monotone"
              dataKey="revenue"
              stroke="#8884d8"
              strokeWidth={2}
              dot={false}
            />
          </LineChart>
        </ResponsiveContainer>
      </CardContent>
    </Card>
  )
}

Orders Bar Chart

Terminal
// components/OrdersChart.tsx
'use client'

import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'

interface OrdersChartProps {
  data: { category: string; orders: number }[]
}

export function OrdersChart({ data }: OrdersChartProps) {
  return (
    <Card>
      <CardHeader>
        <CardTitle>Orders by Category</CardTitle>
      </CardHeader>
      <CardContent>
        <ResponsiveContainer width="100%" height={300}>
          <BarChart data={data}>
            <CartesianGrid strokeDasharray="3 3" />
            <XAxis dataKey="category" tick={{ fontSize: 12 }} />
            <YAxis tick={{ fontSize: 12 }} />
            <Tooltip />
            <Bar dataKey="orders" fill="#82ca9d" />
          </BarChart>
        </ResponsiveContainer>
      </CardContent>
    </Card>
  )
}

Status Pie Chart

Terminal
// components/StatusChart.tsx
'use client'

import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from 'recharts'

const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042']

interface StatusChartProps {
  data: { name: string; value: number }[]
}

export function StatusChart({ data }: StatusChartProps) {
  return (
    <Card>
      <CardHeader>
        <CardTitle>Order Status</CardTitle>
      </CardHeader>
      <CardContent>
        <ResponsiveContainer width="100%" height={300}>
          <PieChart>
            <Pie
              data={data}
              cx="50%"
              cy="50%"
              innerRadius={60}
              outerRadius={80}
              paddingAngle={5}
              dataKey="value"
            >
              {data.map((entry, index) => (
                <Cell key={entry.name} fill={COLORS[index % COLORS.length]} />
              ))}
            </Pie>
            <Tooltip />
            <Legend />
          </PieChart>
        </ResponsiveContainer>
      </CardContent>
    </Card>
  )
}

Phase 5: Data Table

AI Prompt for Table

Terminal
"Create a data table component for recent orders.
Show columns: Order ID, Product, Quantity, Total, Status, Date
Include:
- Sorting by clicking column headers
- Status badge with colors
- Pagination
Use shadcn/ui Table component."

Orders Table

Terminal
// components/OrdersTable.tsx
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table'
import { Badge } from '@/components/ui/badge'

const statusColors = {
  pending: 'bg-yellow-100 text-yellow-800',
  completed: 'bg-green-100 text-green-800',
  shipped: 'bg-blue-100 text-blue-800',
}

interface Order {
  id: string
  product: { name: string }
  quantity: number
  total: number
  status: string
  created_at: string
}

export function OrdersTable({ orders }: { orders: Order[] }) {
  return (
    <div className="rounded-md border">
      <Table>
        <TableHeader>
          <TableRow>
            <TableHead>Order ID</TableHead>
            <TableHead>Product</TableHead>
            <TableHead>Quantity</TableHead>
            <TableHead>Total</TableHead>
            <TableHead>Status</TableHead>
            <TableHead>Date</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {orders.map((order) => (
            <TableRow key={order.id}>
              <TableCell className="font-mono text-sm">
                {order.id.slice(0, 8)}
              </TableCell>
              <TableCell>{order.product.name}</TableCell>
              <TableCell>{order.quantity}</TableCell>
              <TableCell>${order.total.toFixed(2)}</TableCell>
              <TableCell>
                <Badge className={statusColors[order.status as keyof typeof statusColors]}>
                  {order.status}
                </Badge>
              </TableCell>
              <TableCell>
                {new Date(order.created_at).toLocaleDateString()}
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </div>
  )
}

Phase 6: Real-Time Updates

Supabase Realtime

Terminal
// components/RealtimeOrders.tsx
'use client'

import { useEffect, useState } from 'react'
import { createClient } from '@/lib/supabase/client'
import { OrdersTable } from './OrdersTable'

export function RealtimeOrders({ initialOrders }: { initialOrders: Order[] }) {
  const [orders, setOrders] = useState(initialOrders)
  const supabase = createClient()

  useEffect(() => {
    const channel = supabase
      .channel('orders-changes')
      .on(
        'postgres_changes',
        { event: '*', schema: 'public', table: 'orders' },
        (payload) => {
          if (payload.eventType === 'INSERT') {
            setOrders(prev => [payload.new as Order, ...prev])
          }
          if (payload.eventType === 'UPDATE') {
            setOrders(prev =>
              prev.map(o => o.id === payload.new.id ? payload.new as Order : o)
            )
          }
        }
      )
      .subscribe()

    return () => {
      supabase.removeChannel(channel)
    }
  }, [supabase])

  return <OrdersTable orders={orders} />
}

Verification Checklist

  • KPI cards display correct metrics
  • Line chart shows revenue trend
  • Bar chart shows category distribution
  • Pie chart shows status breakdown
  • Data table displays orders
  • Charts are responsive
  • Real-time updates work
  • Authentication protects dashboard

Extensions

Once working, try adding:

  • Date range picker for filtering
  • Export to CSV
  • Dark mode charts
  • More detailed analytics
  • Custom dashboard widgets

Summary

You built an analytics dashboard with:

  • KPI cards with trend indicators
  • Multiple chart types (line, bar, pie)
  • Data tables with pagination
  • Real-time updates via Supabase

Next Steps

Build a SaaS landing page with marketing components.

Mark this lesson as complete to track your progress