- Learn
- Guided Projects
- Build a Dashboard
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