Dashboard

An admin screen for a store. Switch the time period, and the stat cards and chart update to that period’s data.

Dashboard

Key points

  1. 1

    Calculate the numbers from the raw data

    Totals like revenue and order count come from adding up the daily data with reduce. Switch the period and they stay correct — you just calculate again.

  2. 2

    Let a library draw the chart

    Recharts draws the bars, the axes, and the numbers that appear on hover. You just tell it which data to use and which fields to draw.

  3. 3

    Show up or down with color and symbols

    Compared to the previous period, a green ▲ means up and a red ▼ means down. You can tell good from bad at a glance, without reading the numbers.

Tools used

Click to see the comparison in Part 2

Edit the code

Edit the code and the preview on the right updates.

import { useState } from "react"
import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"
import { Button } from "./components/ui/button"

// Daily sales and orders (in a real app, these come from a server). Today is treated as 10/4
const weekDays = [
  { day: "9/28", sales: 1010, orders: 38 },
  { day: "9/29", sales: 850, orders: 33 },
  { day: "9/30", sales: 940, orders: 36 },
  { day: "10/1", sales: 1170, orders: 44 },
  { day: "10/2", sales: 650, orders: 27 },
  { day: "10/3", sales: 1430, orders: 61 },
  { day: "10/4", sales: 1570, orders: 64 },
]

// Last 30 days (9/5 to 10/4) = 23 days from 9/5 to 9/27 + the 7 days above (so the same day has the same numbers)
// The 23 days are made with a fixed formula (demo only; no need to remember it)
const monthDays = [
  ...Array.from({ length: 23 }, (_, i) => ({
    day: `9/${i + 5}`,
    sales: 730 + ((i * 37) % 11) * 80 + (i % 7 <= 1 ? 470 : 0),
    orders: 30 + ((i * 37) % 11) * 3 + (i % 7 <= 1 ? 18 : 0),
  })),
  ...weekDays,
]

// Data for each period. previous holds the numbers for the period before (to compare up or down)
const periods = {
  week: { label: "Last 7 days", days: weekDays, members: 48, previous: { sales: 6800, orders: 286, average: 24, members: 40 } },
  month: { label: "Last 30 days", days: monthDays, members: 186, previous: { sales: 33000, orders: 1390, average: 24, members: 201 } },
}

// Recent orders
const recentOrders = [
  { id: "A-1052", name: "Emily Johnson", amount: 85, status: "Shipped" },
  { id: "A-1051", name: "Michael Brown", amount: 30, status: "Preparing" },
  { id: "A-1050", name: "Sarah Davis", amount: 155, status: "Shipped" },
  { id: "A-1049", name: "David Wilson", amount: 20, status: "Preparing" },
  { id: "A-1048", name: "Olivia Moore", amount: 50, status: "Shipped" },
]

// Money format (1010 → $1,010)
function yen(value) {
  return "$" + value.toLocaleString()
}

// Percent change from the previous period (negative when it went down). e.g. now 120, before 100 → 20
function changePercent(value, previous) {
  return Math.round(((value - previous) / previous) * 100)
}

export default function App() {
  // ① The selected period ("week" = last 7 days, "month" = last 30 days), kept in state
  //const period = "week"
  const [period, setPeriod] = useState("week")

  const current = periods[period] // Data for the selected period

  // ② Totals for the period
  //const sales = 1144000 // Sum of the daily sales (d.sales)
  const sales = current.days.reduce((sum, d) => sum + d.sales, 0)
  //const orders = 303 // Sum of the daily orders (d.orders)
  const orders = current.days.reduce((sum, d) => sum + d.orders, 0)
  const average = Math.round(sales / orders) // Average order value = sales ÷ orders

  // Data for the 4 stat cards (label = name, text = the big number, change = percent change)
  const stats = [
    { label: "Sales", text: yen(sales), change: changePercent(sales, current.previous.sales) },
    { label: "Orders", text: orders.toLocaleString(), change: changePercent(orders, current.previous.orders) },
    { label: "Avg. order", text: yen(average), change: changePercent(average, current.previous.average) },
    { label: "New members", text: String(current.members), change: changePercent(current.members, current.previous.members) },
  ]

  return (
    <div className="mx-auto my-6 flex w-full max-w-4xl flex-col gap-6 px-4 text-foreground">
      {/* Heading and period buttons */}
      <div className="flex flex-wrap items-center justify-between gap-3">
        <h1 className="text-xl font-semibold">Dashboard</h1>
        <div className="flex gap-2">
          {/* ③ Change the period on click. The selected one is filled (default), the other is outline */}
          <Button size="sm" variant={period === "week" ? "default" : "outline"} onClick={() => setPeriod("week")}>
            Last 7 days
          </Button>
          <Button size="sm" variant={period === "month" ? "default" : "outline"} onClick={() => setPeriod("month")}>
            Last 30 days
          </Button>
        </div>
      </div>

      {/* ④ Stat cards: list the 4 stats with map */}

      <div className="grid grid-cols-2 gap-3 md:grid-cols-4">
        {stats.map((stat) => (
          <div key={stat.label} className="rounded-xl border bg-card p-4 text-card-foreground">
            <p className="text-xs text-muted-foreground">{stat.label}</p>
            <p className="mt-1 text-xl font-semibold tabular-nums">{stat.text}</p>
            {/* ⑤ Green ▲ if it went up (change is 0 or more), red ▼ if it went down */}
            {/* <p className="mt-1 text-xs text-emerald-600"> */}
            <p className={stat.change >= 0 ? "mt-1 text-xs text-emerald-600" : "mt-1 text-xs text-red-600"}>
              {stat.change >= 0 ? "▲" : "▼"} {Math.abs(stat.change)}%
              <span className="text-muted-foreground"> vs previous period</span>
            </p>
          </div>
        ))}
      </div>


      {/* Sales chart (Recharts draws the list passed to data as bars) */}
      <div className="rounded-xl border bg-card p-4 text-card-foreground">
        <p className="text-sm font-medium">Sales over time</p>
        <div className="mt-4 h-56">
          <ResponsiveContainer width="100%" height="100%">
            <BarChart data={current.days}>
              <CartesianGrid vertical={false} stroke="var(--border)" />
              <XAxis dataKey="day" tickLine={false} axisLine={false} fontSize={12} />
              <YAxis tickLine={false} axisLine={false} fontSize={12} width={40} tickFormatter={(v) => "$" + v / 1000 + "k"} />
              <Tooltip formatter={(v) => [yen(v), "Sales"]} cursor={{ fill: "var(--muted)" }} />
              <Bar dataKey="sales" fill="var(--primary)" radius={[4, 4, 0, 0]} />
            </BarChart>
          </ResponsiveContainer>
        </div>
      </div>

      {/* Recent orders */}
      <div className="rounded-xl border bg-card p-4 text-card-foreground">
        <p className="text-sm font-medium">Recent orders</p>
        <ul className="mt-3 divide-y text-sm">
          {recentOrders.map((order) => (
            <li key={order.id} className="flex items-center gap-3 py-2.5">
              <span className="w-16 text-muted-foreground tabular-nums">{order.id}</span>
              <span className="flex-1">{order.name}</span>
              <span className="tabular-nums">{yen(order.amount)}</span>
              <span
                // ⑥ Green if "Shipped", otherwise (Preparing) yellow
                className={order.status === "Shipped" ? "w-16 rounded-full bg-emerald-100 py-0.5 text-center text-xs text-emerald-700" : "w-16 rounded-full bg-amber-100 py-0.5 text-center text-xs text-amber-700"}
              >
                {order.status}
              </span>
            </li>
          ))}
        </ul>
      </div>
    </div >
  )
}