Shopify Sales Dashboard
Build a live Shopify sales dashboard with revenue charts and order tables — step by step in 15 minutes.
Build a Shopify Sales Dashboard in 15 Minutes
In this tutorial, you'll build a real-time sales dashboard that shows revenue trends, recent orders, and top products — all pulling live data from the merchant's connected Shopify store.
What you'll build:
- Revenue summary cards (today, this week, this month)
- Recent orders table with status badges
- Top products by revenue
- Auto-refresh every 60 seconds
Skills covered: reeve.data.query, CSS variables for theming, React state management
Time: ~15 minutes
1. Scaffold the Project
npx create-reeve-app shopify-dashboard --template react-app
cd shopify-dashboard
npm installInstall a lightweight charting library:
npm install recharts2. Update the Manifest
Open reeve-manifest.json and declare the data.shopify permission:
{
"id": "com.yourcompany.shopify-dashboard",
"name": "Shopify Sales Dashboard",
"version": "1.0.0",
"description": "Real-time revenue, orders, and product analytics for your Shopify store.",
"author": {
"name": "Your Name",
"email": "you@yourcompany.com"
},
"category": "analytics",
"tags": ["shopify", "revenue", "dashboard", "analytics"],
"permissions": ["data.shopify"],
"entrypoint": "index.html",
"icon": "public/icon.png"
}3. Build the Dashboard Component
Replace src/App.tsx with the full dashboard:
import { useEffect, useState, useCallback } from 'react';
import { reeve } from '@reeve/app-sdk';
import {
AreaChart,
Area,
XAxis,
YAxis,
Tooltip,
ResponsiveContainer,
} from 'recharts';
import './App.css';
interface Order {
id: string;
name: string;
total_price: string;
financial_status: string;
fulfillment_status: string | null;
created_at: string;
customer: { first_name: string; last_name: string } | null;
}
interface Product {
id: string;
title: string;
revenue: number;
units_sold: number;
}
interface RevenuePoint {
date: string;
revenue: number;
}
interface DashboardData {
todayRevenue: number;
weekRevenue: number;
monthRevenue: number;
orderCount: number;
recentOrders: Order[];
topProducts: Product[];
revenueChart: RevenuePoint[];
}
function formatCurrency(amount: number) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
}).format(amount);
}
function formatDate(iso: string) {
return new Date(iso).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
});
}
function StatusBadge({ status }: { status: string }) {
const colors: Record<string, string> = {
paid: 'var(--reeve-success)',
pending: 'var(--reeve-warning)',
refunded: 'var(--reeve-danger)',
fulfilled: 'var(--reeve-accent)',
unfulfilled: 'var(--reeve-text-muted)',
};
return (
<span
style={{
color: colors[status] ?? 'var(--reeve-text-muted)',
fontSize: '0.75rem',
fontWeight: 600,
textTransform: 'capitalize',
}}
>
{status}
</span>
);
}
export default function App() {
const [data, setData] = useState<DashboardData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
const loadData = useCallback(async () => {
try {
// Fetch recent orders (last 30 days)
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const [ordersResult, productsResult] = await Promise.all([
reeve.data.query({
source: 'shopify',
type: 'orders',
filters: {
created_at_min: thirtyDaysAgo.toISOString(),
status: 'any',
},
limit: 250,
fields: [
'id', 'name', 'total_price', 'financial_status',
'fulfillment_status', 'created_at', 'customer', 'line_items',
],
}),
reeve.data.query({
source: 'shopify',
type: 'products',
limit: 10,
fields: ['id', 'title', 'revenue', 'units_sold'],
}),
]);
const orders = ordersResult.data as Order[];
// Calculate revenue metrics
const now = new Date();
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const weekStart = new Date(now);
weekStart.setDate(now.getDate() - 7);
let todayRevenue = 0;
let weekRevenue = 0;
let monthRevenue = 0;
// Build revenue chart (last 14 days)
const chartMap: Record<string, number> = {};
for (let i = 13; i >= 0; i--) {
const d = new Date();
d.setDate(d.getDate() - i);
const key = d.toISOString().split('T')[0];
chartMap[key] = 0;
}
orders.forEach((order) => {
const amount = parseFloat(order.total_price);
const orderDate = new Date(order.created_at);
monthRevenue += amount;
if (orderDate >= weekStart) weekRevenue += amount;
if (orderDate >= todayStart) todayRevenue += amount;
const key = order.created_at.split('T')[0];
if (key in chartMap) {
chartMap[key] += amount;
}
});
const revenueChart: RevenuePoint[] = Object.entries(chartMap).map(
([date, revenue]) => ({
date: formatDate(date),
revenue: Math.round(revenue),
})
);
setData({
todayRevenue,
weekRevenue,
monthRevenue,
orderCount: orders.length,
recentOrders: orders.slice(0, 8),
topProducts: (productsResult.data as Product[]).slice(0, 5),
revenueChart,
});
setLastUpdated(new Date());
setError(null);
} catch (err) {
setError('Failed to load Shopify data. Make sure Shopify is connected.');
console.error(err);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
reeve.init().then(loadData);
// Auto-refresh every 60 seconds
const interval = setInterval(loadData, 60_000);
return () => clearInterval(interval);
}, [loadData]);
if (loading) {
return (
<div className="loading-screen">
<div className="spinner" />
<p>Loading dashboard...</p>
</div>
);
}
if (error) {
return (
<div className="error-screen">
<p>⚠️ {error}</p>
<button onClick={loadData}>Retry</button>
</div>
);
}
return (
<div className="dashboard">
{/* Header */}
<div className="dashboard-header">
<h1>Sales Dashboard</h1>
{lastUpdated && (
<span className="last-updated">
Updated {lastUpdated.toLocaleTimeString()}
</span>
)}
</div>
{/* Revenue cards */}
<div className="stat-grid">
<StatCard label="Today" value={formatCurrency(data!.todayRevenue)} />
<StatCard label="Last 7 days" value={formatCurrency(data!.weekRevenue)} />
<StatCard label="Last 30 days" value={formatCurrency(data!.monthRevenue)} />
<StatCard label="Orders (30d)" value={data!.orderCount.toString()} />
</div>
{/* Revenue chart */}
<div className="card">
<h2>Revenue (14 days)</h2>
<ResponsiveContainer width="100%" height={180}>
<AreaChart data={data!.revenueChart}>
<defs>
<linearGradient id="revenueGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--reeve-accent)" stopOpacity={0.3} />
<stop offset="95%" stopColor="var(--reeve-accent)" stopOpacity={0} />
</linearGradient>
</defs>
<XAxis
dataKey="date"
tick={{ fontSize: 11, fill: 'var(--reeve-text-muted)' }}
axisLine={false}
tickLine={false}
/>
<YAxis
tick={{ fontSize: 11, fill: 'var(--reeve-text-muted)' }}
axisLine={false}
tickLine={false}
tickFormatter={(v) => `$${v}`}
/>
<Tooltip
contentStyle={{
background: 'var(--reeve-surface)',
border: '1px solid var(--reeve-border)',
borderRadius: 'var(--reeve-radius)',
color: 'var(--reeve-text)',
}}
formatter={(v: number) => [formatCurrency(v), 'Revenue']}
/>
<Area
type="monotone"
dataKey="revenue"
stroke="var(--reeve-accent)"
strokeWidth={2}
fill="url(#revenueGrad)"
/>
</AreaChart>
</ResponsiveContainer>
</div>
<div className="two-col">
{/* Recent orders */}
<div className="card">
<h2>Recent Orders</h2>
<table className="orders-table">
<thead>
<tr>
<th>Order</th>
<th>Customer</th>
<th>Status</th>
<th>Total</th>
</tr>
</thead>
<tbody>
{data!.recentOrders.map((order) => (
<tr key={order.id}>
<td className="order-name">{order.name}</td>
<td>
{order.customer
? `${order.customer.first_name} ${order.customer.last_name}`
: 'Guest'}
</td>
<td>
<StatusBadge status={order.financial_status} />
</td>
<td>${parseFloat(order.total_price).toFixed(2)}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Top products */}
<div className="card">
<h2>Top Products</h2>
{data!.topProducts.map((product, i) => (
<div key={product.id} className="product-row">
<span className="product-rank">#{i + 1}</span>
<span className="product-name">{product.title}</span>
<span className="product-revenue">
{formatCurrency(product.revenue)}
</span>
</div>
))}
</div>
</div>
</div>
);
}
function StatCard({ label, value }: { label: string; value: string }) {
return (
<div className="stat-card">
<div className="stat-label">{label}</div>
<div className="stat-value">{value}</div>
</div>
);
}4. Add the Styles
Replace src/App.css:
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--reeve-font);
background: var(--reeve-bg);
color: var(--reeve-text);
font-size: 14px;
}
.dashboard {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.dashboard-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.dashboard-header h1 {
font-size: 1.4rem;
font-weight: 700;
}
.last-updated {
font-size: 0.75rem;
color: var(--reeve-text-muted);
}
.stat-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 20px;
}
.stat-card {
background: var(--reeve-surface);
border: 1px solid var(--reeve-border);
border-radius: var(--reeve-radius);
padding: 16px;
}
.stat-label {
font-size: 0.75rem;
color: var(--reeve-text-muted);
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.stat-value {
font-size: 1.5rem;
font-weight: 700;
color: var(--reeve-text);
}
.card {
background: var(--reeve-surface);
border: 1px solid var(--reeve-border);
border-radius: var(--reeve-radius);
padding: 20px;
margin-bottom: 20px;
}
.card h2 {
font-size: 0.9rem;
font-weight: 600;
margin-bottom: 16px;
color: var(--reeve-text);
}
.two-col {
display: grid;
grid-template-columns: 3fr 2fr;
gap: 16px;
}
.orders-table {
width: 100%;
border-collapse: collapse;
}
.orders-table th {
text-align: left;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--reeve-text-muted);
padding: 0 8px 10px 0;
border-bottom: 1px solid var(--reeve-border);
}
.orders-table td {
padding: 10px 8px 10px 0;
border-bottom: 1px solid var(--reeve-border);
font-size: 0.85rem;
}
.orders-table tr:last-child td {
border-bottom: none;
}
.order-name {
font-family: monospace;
color: var(--reeve-accent);
}
.product-row {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 0;
border-bottom: 1px solid var(--reeve-border);
}
.product-row:last-child {
border-bottom: none;
}
.product-rank {
font-size: 0.75rem;
color: var(--reeve-text-muted);
width: 24px;
}
.product-name {
flex: 1;
font-size: 0.875rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.product-revenue {
font-weight: 600;
font-size: 0.875rem;
}
.loading-screen,
.error-screen {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
gap: 12px;
color: var(--reeve-text-muted);
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid var(--reeve-border);
border-top-color: var(--reeve-accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
button {
background: var(--reeve-accent);
color: white;
border: none;
border-radius: var(--reeve-radius);
padding: 8px 16px;
cursor: pointer;
font-size: 0.875rem;
}
button:hover {
background: var(--reeve-accent-hover);
}
@media (max-width: 768px) {
.stat-grid {
grid-template-columns: repeat(2, 1fr);
}
.two-col {
grid-template-columns: 1fr;
}
}5. Test It
npm run devOpen the Cockpit → Settings → Apps → Install from URL → http://localhost:5173. Navigate to your app in the sidebar. You'll see live Shopify data populating the dashboard.
Try switching the Cockpit to light mode — the CSS variables update automatically and the dashboard themes correctly.
If Shopify isn't connected for the test account, the dashboard shows an error. Connect Shopify first in Connectors → Shopify.
6. What's Next?
Extend the dashboard with:
- Date picker — let the merchant choose a custom range
- Export button — download orders as CSV using
reeve.storage - AI insights — add a "Summarize trends" button using
reeve.ai.complete() - Alerts — subscribe to
shopify:order_createdevents for a live order feed
See the SDK Reference for all available APIs.