ダッシュボード
お店の管理画面です。期間を切り替えると、数字のカードとグラフが、その期間のデータに変わります。
ダッシュボード
ここがポイント
- 1
数字は、元のデータから計算する
売上や注文数の合計は、日ごとのデータを reduce で足して出す。期間を切り替えても、計算し直すだけで合う
- 2
グラフはライブラリにまかせる
棒・目盛り・マウスを乗せたときの数字は、Recharts が描く。こちらは「どのデータの、どの項目を描くか」を渡すだけ
- 3
増えたか減ったかを、色と記号で見せる
前の期間とくらべて、増えたら緑の ▲、減ったら赤の ▼。数字を読まなくても、ひと目で良し悪しがわかる
使った道具
押すと、第2部のくらべるページへ
コードを書き換えてみる
コードを書き換えると、右の見た目が変わります
import { useState } from "react" import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts" import { Button } from "./components/ui/button" // 日ごとの売上と注文数(本物では、サーバーから取ってくる)。今日は 10/4 として作っている const weekDays = [ { day: "9/28", sales: 152000, orders: 38 }, { day: "9/29", sales: 128000, orders: 33 }, { day: "9/30", sales: 141000, orders: 36 }, { day: "10/1", sales: 176000, orders: 44 }, { day: "10/2", sales: 98000, orders: 27 }, { day: "10/3", sales: 214000, orders: 61 }, { day: "10/4", sales: 235000, orders: 64 }, ] // 過去30日分(9/5〜10/4)= 9/5〜9/27 の23日分 + 上の過去7日分(同じ日は、同じ数字になるように) // 23日分は、決まった式で作る(デモ用。中身は覚えなくてよい) const monthDays = [ ...Array.from({ length: 23 }, (_, i) => ({ day: `9/${i + 5}`, sales: 110000 + ((i * 37) % 11) * 12000 + (i % 7 <= 1 ? 70000 : 0), orders: 30 + ((i * 37) % 11) * 3 + (i % 7 <= 1 ? 18 : 0), })), ...weekDays, ] // 期間ごとのデータ。previous は「ひとつ前の期間」の数字(増えたか減ったかを比べる) const periods = { week: { label: "過去7日間", days: weekDays, members: 48, previous: { sales: 1020000, orders: 286, average: 3566, members: 40 } }, month: { label: "過去30日間", days: monthDays, members: 186, previous: { sales: 4980000, orders: 1390, average: 3583, members: 201 } }, } // 最近の注文 const recentOrders = [ { id: "A-1052", name: "佐藤 花子", amount: 12800, status: "発送済み" }, { id: "A-1051", name: "鈴木 一郎", amount: 4460, status: "準備中" }, { id: "A-1050", name: "高橋 美咲", amount: 23280, status: "発送済み" }, { id: "A-1049", name: "田中 健", amount: 2980, status: "準備中" }, { id: "A-1048", name: "伊藤 さくら", amount: 7480, status: "発送済み" }, ] // 金額の書き方(152000 → ¥152,000) function yen(value) { return "¥" + value.toLocaleString() } // 前の期間とくらべて、何%増えたか(減ったときはマイナス)。例:今 120・前 100 → 20 function changePercent(value, previous) { return Math.round(((value - previous) / previous) * 100) } export default function App() { // ① 今選んでいる期間("week" = 過去7日間、"month" = 過去30日間)。今は決まった値。ここを state にする //const period = "week" const [period, setPeriod] = useState("week") const current = periods[period] // 選んでいる期間のデータ // ② 期間の合計 //const sales = 1144000 // 今は決まった値。ここに:日ごとの売上(d.sales)を全部足した金額 const sales = current.days.reduce((sum, d) => sum + d.sales, 0) //const orders = 303 // 今は決まった値。ここに:日ごとの注文数(d.orders)を全部足した数 const orders = current.days.reduce((sum, d) => sum + d.orders, 0) const average = Math.round(sales / orders) // 客単価 = 売上 ÷ 注文数 // 数字のカード4つ分のデータ(label = 名前、text = 大きく出す文字、change = 何%増えたか) const stats = [ { label: "売上", text: yen(sales), change: changePercent(sales, current.previous.sales) }, { label: "注文数", text: orders.toLocaleString() + "件", change: changePercent(orders, current.previous.orders) }, { label: "客単価", text: yen(average), change: changePercent(average, current.previous.average) }, { label: "新規会員", text: 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"> {/* 見出しと期間のボタン */} <div className="flex flex-wrap items-center justify-between gap-3"> <h1 className="text-xl font-semibold">ダッシュボード</h1> <div className="flex gap-2"> {/* ③ ここに:押したら期間を変える。選んでいる方は黒(default)、それ以外は白(outline) */} <Button size="sm" variant={period === "week" ? "default" : "outline"} onClick={() => setPeriod("week")}> 過去7日間 </Button> <Button size="sm" variant={period === "month" ? "default" : "outline"} onClick={() => setPeriod("month")}> 過去30日間 </Button> </div> </div> {/* ④ 数字のカード ここに:stats を map で4つ並べる(今は1つだけ、決まった文字) */} <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> {/* ⑤ ここに:増えた(change が 0 以上)なら緑の ▲、減ったなら赤の ▼ */} {/* <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"> 前の期間とくらべて</span> </p> </div> ))} </div> {/* 売上のグラフ(Recharts。data に渡した一覧を、棒にして描く) */} <div className="rounded-xl border bg-card p-4 text-card-foreground"> <p className="text-sm font-medium">売上の推移</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 / 10000 + "万"} /> <Tooltip formatter={(v) => [yen(v), "売上"]} cursor={{ fill: "var(--muted)" }} /> <Bar dataKey="sales" fill="var(--primary)" radius={[4, 4, 0, 0]} /> </BarChart> </ResponsiveContainer> </div> </div> {/* 最近の注文 */} <div className="rounded-xl border bg-card p-4 text-card-foreground"> <p className="text-sm font-medium">最近の注文</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 // ⑥ ここに:「発送済み」なら緑、それ以外(準備中)なら黄色 className={order.status === "発送済み" ? "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 > ) }