← Dashboard
Zubeyr··Pagestableslayout

Orders 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-react from 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.

terminal.sh
pnpm add lucide-react

Usage

example.tsx
import { OrdersDashboard } from '@/components/dashboard/orders-dashboard/orders-dashboard'

export default function Demo() {
  return <OrdersDashboard />
}

Source code

orders-dashboard.tsx
'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>
  )
}
orders.tsx
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 },
]