Zubeyr··PagestableslayoutOrders Dashboard
An orders screen: sidebar with nested links, page actions, stat cards and an index table with views, search, filter chips, sorting and bulk selection.
Live demo ↗Installation
Run the shadcn CLI in a project that already has components.json. Using the @tomei tab? Register the namespace once first.
pnpm dlx shadcn@latest add https://tomei.zubeyr.dev/ui/r/orders-dashboard.json- →Installs
lucide-reactfrom npm. - →Pulls in
sidebar. - →Pulls in
button. - →Pulls in
badge. - →Pulls in
data-table. - →Pulls in
stat-card. - →Pulls in
date-range-picker. - →Files land in
components/dashboard/orders-dashboard.
Installing by hand? Add the dependencies, then copy the source code below.
pnpm add lucide-reactUsage
import { OrdersDashboard } from '@/components/dashboard/orders-dashboard/orders-dashboard'
export default function Demo() {
return <OrdersDashboard />
}Source code
'use client'
import { useMemo, useState } from 'react'
import {
BarChart3,
ChevronDown,
Home,
Megaphone,
Package,
Settings,
ShoppingBag,
Tag,
Users,
} from 'lucide-react'
import { Button } from '@/registry/dashboard/components/button/button'
import { DataTable } from '@/registry/dashboard/components/data-table/data-table'
import {
addDays,
DateRangePicker,
startOfDay,
type DatePreset,
type DateRange,
} from '@/registry/dashboard/components/date-range-picker/date-range-picker'
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarHeader,
SidebarItem,
} from '@/registry/dashboard/components/sidebar/sidebar'
import { StatCard } from '@/registry/dashboard/components/stat-card/stat-card'
import { columns, filters, orders, stats, views } from './orders'
const DAY = 86_400_000
const presets: DatePreset[] = [
{ label: 'All time', range: (t) => ({ start: new Date(2000, 0, 1), end: t }) },
{ label: 'Last 7 days', range: (t) => ({ start: addDays(t, -6), end: t }) },
{ label: 'Last 30 days', range: (t) => ({ start: addDays(t, -29), end: t }) },
{ label: 'Last 90 days', range: (t) => ({ start: addDays(t, -89), end: t }) },
]
export function OrdersDashboard() {
const [loading, setLoading] = useState(false)
const [range, setRange] = useState<DateRange>(() => presets[2].range(startOfDay(new Date())))
const [rangeLabel, setRangeLabel] = useState('Last 30 days')
const inRange = useMemo(() => {
const today = startOfDay(new Date()).getTime()
const newest = Math.round((today - range.end.getTime()) / DAY)
const oldest = Math.round((today - range.start.getTime()) / DAY)
return orders.filter((o) => o.daysAgo >= newest && o.daysAgo <= oldest)
}, [range])
const refresh = () =>
new Promise<void>((resolve) => {
setLoading(true)
setTimeout(() => {
setLoading(false)
resolve()
}, 900)
})
return (
<div className="flex h-svh overflow-hidden bg-background text-foreground">
<Sidebar className="max-lg:hidden">
<SidebarHeader
logo={<span className="size-8 rounded-lg bg-sidebar-primary-foreground" />}
title="Tomei"
subtitle="Store admin"
/>
<SidebarContent>
<SidebarGroup>
<SidebarItem icon={<Home />}>Home</SidebarItem>
<SidebarItem
icon={<ShoppingBag />}
active
submenu={
<>
<SidebarItem>Drafts</SidebarItem>
<SidebarItem>Shipping labels</SidebarItem>
<SidebarItem>Abandoned checkouts</SidebarItem>
</>
}
>
Orders
</SidebarItem>
<SidebarItem icon={<Package />}>Products</SidebarItem>
<SidebarItem icon={<Users />}>Customers</SidebarItem>
<SidebarItem icon={<BarChart3 />}>Analytics</SidebarItem>
</SidebarGroup>
<SidebarGroup title="Growth">
<SidebarItem icon={<Megaphone />}>Marketing</SidebarItem>
<SidebarItem icon={<Tag />}>Discounts</SidebarItem>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarItem icon={<Settings />}>Settings</SidebarItem>
</SidebarFooter>
</Sidebar>
<main className="min-w-0 flex-1 overflow-y-auto">
<div className="mx-auto flex max-w-[1200px] flex-col gap-4 p-6">
<div className="flex flex-wrap items-center justify-between gap-3">
<h1 className="text-xl font-semibold">Orders</h1>
<div className="flex flex-wrap gap-2">
<DateRangePicker
presets={presets}
defaultValue={range}
disableFuture
align="end"
onChange={(next, preset) => {
setRange(next)
setRangeLabel(preset ?? 'Custom range')
}}
/>
<Button>Export</Button>
<Button>
More actions <ChevronDown />
</Button>
<Button variant="primary">Create order</Button>
</div>
</div>
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 xl:grid-cols-5">
{stats.map((stat, i) => (
<StatCard
key={stat.label}
{...stat}
{...(i === 0 && { value: inRange.length.toLocaleString(), hint: rangeLabel })}
/>
))}
</div>
<DataTable
rows={inRange}
columns={columns}
getRowId={(o) => o.id}
views={views}
filters={filters}
searchText={(o) => `${o.id} ${o.customer}`}
searchPlaceholder="Search orders or customers"
defaultSort={{ key: 'order', direction: 'desc' }}
selectable
pageSize={10}
onRefresh={refresh}
loading={loading}
bulkActions={() => (
<>
<Button size="sm">Mark as fulfilled</Button>
<Button size="sm">Print labels</Button>
</>
)}
/>
</div>
</main>
</div>
)
}import { Badge } from '@/registry/dashboard/components/badge/badge'
import type { Column, Filter, View } from '@/registry/dashboard/components/data-table/data-table'
export type Order = {
id: string
placed: number
daysAgo: number
date: string
customer: string
total: number
payment: 'Paid' | 'Payment pending' | 'Refunded'
fulfillment: 'Unfulfilled' | 'Fulfilled' | 'On hold'
items: number
}
const names = [
'Ava Brooks',
'Leo Turner',
'Mia Patel',
'Noah Kim',
'Zoe Carter',
'Eli Morgan',
'Ivy Chen',
]
const payments: Order['payment'][] = ['Paid', 'Paid', 'Payment pending', 'Paid', 'Refunded']
const fulfillments: Order['fulfillment'][] = ['Unfulfilled', 'Fulfilled', 'On hold', 'Unfulfilled']
const daysAgo = (i: number) => (i < 4 ? 0 : (i - 3) * 3)
export const orders: Order[] = Array.from({ length: 40 }, (_, i) => ({
id: `#${2050 - i}`,
placed: 16 - i,
daysAgo: daysAgo(i),
date:
i < 4
? `Today at ${9 - i * 2}:${String(55 - i * 7).padStart(2, '0')} a.m.`
: `${daysAgo(i)} days ago`,
customer: names[i % names.length],
total: [98.99, 396.84, 54.2, 1210.5, 18][i % 5],
payment: payments[i % payments.length],
fulfillment: fulfillments[i % fulfillments.length],
items: (i % 3) + 1,
}))
const paymentTone = {
Paid: 'neutral',
'Payment pending': 'attention',
Refunded: 'critical',
} as const
const fulfillmentTone = {
Unfulfilled: 'warning',
Fulfilled: 'success',
'On hold': 'warning',
} as const
const currency = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' })
export const columns: Column<Order>[] = [
{
key: 'order',
header: 'Order',
sortValue: (o) => o.placed,
cell: (o) => <span className="font-semibold">{o.id}</span>,
},
{ key: 'date', header: 'Date', sortValue: (o) => o.placed, cell: (o) => o.date },
{ key: 'customer', header: 'Customer', sortValue: (o) => o.customer, cell: (o) => o.customer },
{
key: 'total',
header: 'Total',
align: 'right',
sortValue: (o) => o.total,
cell: (o) => currency.format(o.total),
},
{
key: 'payment',
header: 'Payment status',
cell: (o) => (
<Badge tone={paymentTone[o.payment]} indicator={o.payment === 'Paid' ? 'dot' : 'ring'}>
{o.payment}
</Badge>
),
},
{
key: 'fulfillment',
header: 'Fulfillment status',
cell: (o) => (
<Badge
tone={fulfillmentTone[o.fulfillment]}
indicator={o.fulfillment === 'Fulfilled' ? 'dot' : 'ring'}
>
{o.fulfillment}
</Badge>
),
},
{
key: 'items',
header: 'Items',
sortValue: (o) => o.items,
cell: (o) => `${o.items} ${o.items === 1 ? 'item' : 'items'}`,
},
]
export const views: View<Order>[] = [
{ label: 'Unfulfilled', filter: (o) => o.fulfillment === 'Unfulfilled' },
{ label: 'Unpaid', filter: (o) => o.payment === 'Payment pending' },
{ label: 'On hold', filter: (o) => o.fulfillment === 'On hold' },
{ label: 'Refunded', filter: (o) => o.payment === 'Refunded' },
]
export const filters: Filter<Order>[] = [
{
key: 'payment',
label: 'Payment status',
options: ['Paid', 'Payment pending', 'Refunded'],
value: (o) => o.payment,
},
{
key: 'fulfillment',
label: 'Fulfillment status',
options: ['Unfulfilled', 'Fulfilled', 'On hold'],
value: (o) => o.fulfillment,
},
]
const spark = (seed: number) =>
Array.from({ length: 14 }, (_, i) => Math.round(4 + 3 * Math.sin(i / 2 + seed) + i / 3))
export const stats = [
{ label: 'Orders', value: '1,271', hint: 'Last 30 days', change: 45, spark: spark(1) },
{ label: 'Ordered items', value: '31', hint: '24 the month before', change: 29 },
{
label: 'Returned items',
value: '6',
hint: 'Within 30 days of delivery',
info: 'Items marked as returned',
},
{ label: 'Fulfilled', value: '41', hint: 'Shipped or picked up', change: 11, spark: spark(3) },
{ label: 'Delivered', value: '22', hint: 'Confirmed by the carrier', change: 45 },
]