@squadbase/vantage-template 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -0
- package/base-template/.squadbase/skills/vantage-add-feature/SKILL.md +162 -0
- package/base-template/.squadbase/skills/vantage-app/SKILL.md +120 -0
- package/base-template/.squadbase/skills/vantage-pitfalls/SKILL.md +137 -0
- package/base-template/AGENTS.md +339 -0
- package/base-template/_404.tsx +18 -0
- package/base-template/_error.tsx +14 -0
- package/base-template/_gitignore +5 -0
- package/base-template/_layout.tsx +60 -0
- package/base-template/components/user-card.tsx +132 -0
- package/base-template/index.tsx +59 -0
- package/base-template/package.json +27 -0
- package/base-template/server/api/health.ts +12 -0
- package/base-template/squadbase.yml +5 -0
- package/base-template/styles.css +18 -0
- package/base-template/tsconfig.json +28 -0
- package/chart-presets/berry.css +16 -0
- package/chart-presets/blue.css +16 -0
- package/chart-presets/forest.css +16 -0
- package/chart-presets/mono.css +16 -0
- package/chart-presets/ocean.css +16 -0
- package/chart-presets/sunset.css +16 -0
- package/dist/index.js +1157 -0
- package/package.json +55 -0
- package/ui-templates/CLAUDE.md +200 -0
- package/ui-templates/blank/manifest.json +8 -0
- package/ui-templates/blank/pages/index.tsx +33 -0
- package/ui-templates/blank-ja/manifest.json +8 -0
- package/ui-templates/blank-ja/pages/index.tsx +33 -0
- package/ui-templates/chart-grid/lib/mock-data.ts +139 -0
- package/ui-templates/chart-grid/lib/types.ts +31 -0
- package/ui-templates/chart-grid/manifest.json +10 -0
- package/ui-templates/chart-grid/pages/index.tsx +331 -0
- package/ui-templates/chart-grid/preview-square.png +0 -0
- package/ui-templates/chart-grid/preview-wide.png +0 -0
- package/ui-templates/chart-grid-ja/lib/mock-data.ts +139 -0
- package/ui-templates/chart-grid-ja/lib/types.ts +31 -0
- package/ui-templates/chart-grid-ja/manifest.json +10 -0
- package/ui-templates/chart-grid-ja/pages/index.tsx +331 -0
- package/ui-templates/chart-grid-ja/preview-square.png +0 -0
- package/ui-templates/chart-grid-ja/preview-wide.png +0 -0
- package/ui-templates/funnel/components/chart-helpers.ts +11 -0
- package/ui-templates/funnel/components/stage-table.tsx +63 -0
- package/ui-templates/funnel/lib/mock-data.ts +22 -0
- package/ui-templates/funnel/lib/types.ts +11 -0
- package/ui-templates/funnel/manifest.json +12 -0
- package/ui-templates/funnel/pages/index.tsx +252 -0
- package/ui-templates/funnel/preview-square.png +0 -0
- package/ui-templates/funnel/preview-wide.png +0 -0
- package/ui-templates/funnel-ja/components/chart-helpers.ts +11 -0
- package/ui-templates/funnel-ja/components/stage-table.tsx +63 -0
- package/ui-templates/funnel-ja/lib/mock-data.ts +22 -0
- package/ui-templates/funnel-ja/lib/types.ts +11 -0
- package/ui-templates/funnel-ja/manifest.json +12 -0
- package/ui-templates/funnel-ja/pages/index.tsx +251 -0
- package/ui-templates/funnel-ja/preview-square.png +0 -0
- package/ui-templates/funnel-ja/preview-wide.png +0 -0
- package/ui-templates/kpi-chart-advanced/components/campaign-table.tsx +69 -0
- package/ui-templates/kpi-chart-advanced/lib/mock-data.ts +109 -0
- package/ui-templates/kpi-chart-advanced/lib/types.ts +38 -0
- package/ui-templates/kpi-chart-advanced/manifest.json +11 -0
- package/ui-templates/kpi-chart-advanced/pages/index.tsx +339 -0
- package/ui-templates/kpi-chart-advanced/preview-square.png +0 -0
- package/ui-templates/kpi-chart-advanced/preview-wide.png +0 -0
- package/ui-templates/kpi-chart-advanced-ja/components/campaign-table.tsx +69 -0
- package/ui-templates/kpi-chart-advanced-ja/lib/mock-data.ts +109 -0
- package/ui-templates/kpi-chart-advanced-ja/lib/types.ts +38 -0
- package/ui-templates/kpi-chart-advanced-ja/manifest.json +11 -0
- package/ui-templates/kpi-chart-advanced-ja/pages/index.tsx +333 -0
- package/ui-templates/kpi-chart-advanced-ja/preview-square.png +0 -0
- package/ui-templates/kpi-chart-advanced-ja/preview-wide.png +0 -0
- package/ui-templates/kpi-chart-simple/components/top-items-table.tsx +73 -0
- package/ui-templates/kpi-chart-simple/lib/types.ts +44 -0
- package/ui-templates/kpi-chart-simple/manifest.json +11 -0
- package/ui-templates/kpi-chart-simple/pages/index.tsx +200 -0
- package/ui-templates/kpi-chart-simple/preview-square.png +0 -0
- package/ui-templates/kpi-chart-simple/preview-wide.png +0 -0
- package/ui-templates/kpi-chart-simple/server/kpi-summary.ts +141 -0
- package/ui-templates/kpi-chart-simple-ja/components/top-items-table.tsx +73 -0
- package/ui-templates/kpi-chart-simple-ja/lib/types.ts +44 -0
- package/ui-templates/kpi-chart-simple-ja/manifest.json +11 -0
- package/ui-templates/kpi-chart-simple-ja/pages/index.tsx +200 -0
- package/ui-templates/kpi-chart-simple-ja/preview-square.png +0 -0
- package/ui-templates/kpi-chart-simple-ja/preview-wide.png +0 -0
- package/ui-templates/kpi-chart-simple-ja/server/kpi-summary.ts +141 -0
- package/ui-templates/table-focus/components/detail-table.tsx +88 -0
- package/ui-templates/table-focus/lib/mock-data.ts +69 -0
- package/ui-templates/table-focus/lib/types.ts +24 -0
- package/ui-templates/table-focus/manifest.json +11 -0
- package/ui-templates/table-focus/pages/index.tsx +180 -0
- package/ui-templates/table-focus/preview-square.png +0 -0
- package/ui-templates/table-focus/preview-wide.png +0 -0
- package/ui-templates/table-focus-ja/components/detail-table.tsx +95 -0
- package/ui-templates/table-focus-ja/lib/mock-data.ts +69 -0
- package/ui-templates/table-focus-ja/lib/types.ts +24 -0
- package/ui-templates/table-focus-ja/manifest.json +11 -0
- package/ui-templates/table-focus-ja/pages/index.tsx +180 -0
- package/ui-templates/table-focus-ja/preview-square.png +0 -0
- package/ui-templates/table-focus-ja/preview-wide.png +0 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
KpiItem,
|
|
3
|
+
ComparisonPoint,
|
|
4
|
+
BreakdownSlice,
|
|
5
|
+
TrendPoint,
|
|
6
|
+
CampaignRow,
|
|
7
|
+
} from "./types"
|
|
8
|
+
|
|
9
|
+
export const trendSeries: TrendPoint[] = [
|
|
10
|
+
{ date: "2024-03-02", value: 25537 },
|
|
11
|
+
{ date: "2024-03-03", value: 25443 },
|
|
12
|
+
{ date: "2024-03-04", value: 31199 },
|
|
13
|
+
{ date: "2024-03-05", value: 27676 },
|
|
14
|
+
{ date: "2024-03-06", value: 31689 },
|
|
15
|
+
{ date: "2024-03-07", value: 31329 },
|
|
16
|
+
{ date: "2024-03-08", value: 29802 },
|
|
17
|
+
{ date: "2024-03-09", value: 24254 },
|
|
18
|
+
{ date: "2024-03-10", value: 23112 },
|
|
19
|
+
{ date: "2024-03-11", value: 32913 },
|
|
20
|
+
{ date: "2024-03-12", value: 29638 },
|
|
21
|
+
{ date: "2024-03-13", value: 28547 },
|
|
22
|
+
{ date: "2024-03-14", value: 33511 },
|
|
23
|
+
{ date: "2024-03-15", value: 30842 },
|
|
24
|
+
{ date: "2024-03-16", value: 26136 },
|
|
25
|
+
{ date: "2024-03-17", value: 29594 },
|
|
26
|
+
{ date: "2024-03-18", value: 34866 },
|
|
27
|
+
{ date: "2024-03-19", value: 30377 },
|
|
28
|
+
{ date: "2024-03-20", value: 31164 },
|
|
29
|
+
{ date: "2024-03-21", value: 32209 },
|
|
30
|
+
{ date: "2024-03-22", value: 36270 },
|
|
31
|
+
{ date: "2024-03-23", value: 28720 },
|
|
32
|
+
{ date: "2024-03-24", value: 26871 },
|
|
33
|
+
{ date: "2024-03-25", value: 32817 },
|
|
34
|
+
{ date: "2024-03-26", value: 36762 },
|
|
35
|
+
{ date: "2024-03-27", value: 34853 },
|
|
36
|
+
{ date: "2024-03-28", value: 34516 },
|
|
37
|
+
{ date: "2024-03-29", value: 38290 },
|
|
38
|
+
{ date: "2024-03-30", value: 28654 },
|
|
39
|
+
{ date: "2024-03-31", value: 33368 },
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
export const comparisonSeries: ComparisonPoint[] = [
|
|
43
|
+
{ period: "2023-10", current: 308820, previous: 285196, growth: 8.28 },
|
|
44
|
+
{ period: "2023-11", current: 360871, previous: 300245, growth: 20.19 },
|
|
45
|
+
{ period: "2023-12", current: 369038, previous: 344274, growth: 7.19 },
|
|
46
|
+
{ period: "2024-01", current: 402455, previous: 338646, growth: 18.84 },
|
|
47
|
+
{ period: "2024-02", current: 443741, previous: 396096, growth: 12.03 },
|
|
48
|
+
{ period: "2024-03", current: 445883, previous: 387812, growth: 14.97 },
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
export const breakdownSlices: BreakdownSlice[] = [
|
|
52
|
+
{ segment: "セグメント1", value: 412_000 },
|
|
53
|
+
{ segment: "セグメント2", value: 318_000 },
|
|
54
|
+
{ segment: "セグメント3", value: 287_000 },
|
|
55
|
+
{ segment: "セグメント4", value: 164_000 },
|
|
56
|
+
{ segment: "セグメント5", value: 132_000 },
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
export const headerKpis: [KpiItem, KpiItem, KpiItem, KpiItem, KpiItem] = [
|
|
60
|
+
{
|
|
61
|
+
id: "revenue",
|
|
62
|
+
value: "¥104万",
|
|
63
|
+
change: 14.2,
|
|
64
|
+
changeLabel: "",
|
|
65
|
+
positiveIsGood: true,
|
|
66
|
+
sparklineData: [29802, 23112, 29638, 33511, 26136, 34866, 31164, 36270, 26871, 36762, 34516, 28654],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "users",
|
|
70
|
+
value: "82,400",
|
|
71
|
+
change: 9.8,
|
|
72
|
+
changeLabel: "",
|
|
73
|
+
positiveIsGood: true,
|
|
74
|
+
sparklineData: [4967, 3852, 4940, 5585, 4356, 5811, 5194, 6045, 4479, 6127, 5753, 4776],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "sessions",
|
|
78
|
+
value: "132,900",
|
|
79
|
+
change: 6.5,
|
|
80
|
+
changeLabel: "",
|
|
81
|
+
positiveIsGood: true,
|
|
82
|
+
sparklineData: [7451, 5778, 7410, 8378, 6534, 8717, 7791, 9068, 6718, 9191, 8629, 7164],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "conversion",
|
|
86
|
+
value: "4.18%",
|
|
87
|
+
change: -0.4,
|
|
88
|
+
changeLabel: "",
|
|
89
|
+
positiveIsGood: true,
|
|
90
|
+
sparklineData: [4.8, 4.66, 4.79, 4.87, 4.72, 4.9, 4.82, 4.93, 4.74, 4.94, 4.89, 4.77],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "aov",
|
|
94
|
+
value: "¥12.62",
|
|
95
|
+
change: 5.1,
|
|
96
|
+
changeLabel: "",
|
|
97
|
+
positiveIsGood: true,
|
|
98
|
+
sparklineData: [1064.36, 825.43, 1058.5, 1196.82, 933.43, 1245.21, 1113, 1295.36, 959.68, 1312.93, 1232.71, 1023.36],
|
|
99
|
+
},
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
export const campaignRows: CampaignRow[] = [
|
|
103
|
+
{ id: "c-01", name: "項目1", channel: "グループA", spend: 48_200, conversions: 612, roi: 4.8 },
|
|
104
|
+
{ id: "c-02", name: "項目2", channel: "グループB", spend: 34_600, conversions: 421, roi: 3.4 },
|
|
105
|
+
{ id: "c-03", name: "項目3", channel: "グループC", spend: 12_800, conversions: 308, roi: 6.9 },
|
|
106
|
+
{ id: "c-04", name: "項目4", channel: "グループD", spend: 22_100, conversions: 254, roi: 4.1 },
|
|
107
|
+
{ id: "c-05", name: "項目5", channel: "グループE", spend: 18_400, conversions: 196, roi: 3.2 },
|
|
108
|
+
{ id: "c-06", name: "項目6", channel: "グループB", spend: 14_300, conversions: 124, roi: 2.5 },
|
|
109
|
+
]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface KpiItem {
|
|
2
|
+
id: "revenue" | "users" | "conversion" | "aov" | "sessions"
|
|
3
|
+
value: string
|
|
4
|
+
change: number
|
|
5
|
+
changeLabel: string
|
|
6
|
+
positiveIsGood: boolean
|
|
7
|
+
sparklineData: number[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ComparisonPoint {
|
|
11
|
+
period: string
|
|
12
|
+
current: number
|
|
13
|
+
previous: number
|
|
14
|
+
growth: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface BreakdownSlice {
|
|
18
|
+
segment: string
|
|
19
|
+
value: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface TrendPoint {
|
|
23
|
+
date: string
|
|
24
|
+
value: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CampaignRow {
|
|
28
|
+
id: string
|
|
29
|
+
name: string
|
|
30
|
+
channel: string
|
|
31
|
+
spend: number
|
|
32
|
+
conversions: number
|
|
33
|
+
roi: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DashboardFilters {
|
|
37
|
+
dateRange: { from: Date | undefined; to: Date | undefined }
|
|
38
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kpi-chart-advanced-ja",
|
|
3
|
+
"description": "[日本語] KPI + チャート (アドバンスト) — ヒーローKPI (大1+小4の2×2) ・棒+折れ線比較チャート・ドーナツ・日次トレンド・明細テーブルを搭載したKPI主役のダッシュボード構成",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"files": [
|
|
6
|
+
{ "src": "pages/index.tsx", "dest": "index.tsx", "action": "replace" },
|
|
7
|
+
{ "src": "components/campaign-table.tsx", "dest": "components/kpi-chart-advanced/campaign-table.tsx", "action": "add" },
|
|
8
|
+
{ "src": "lib/mock-data.ts", "dest": "components/kpi-chart-advanced/mock-data.ts", "action": "add", "relabel": false },
|
|
9
|
+
{ "src": "lib/types.ts", "dest": "components/kpi-chart-advanced/types.ts", "action": "add", "relabel": false }
|
|
10
|
+
]
|
|
11
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { useState } from "react"
|
|
2
|
+
|
|
3
|
+
import { subDays } from "date-fns"
|
|
4
|
+
import {
|
|
5
|
+
Activity,
|
|
6
|
+
DollarSign,
|
|
7
|
+
MousePointer,
|
|
8
|
+
PieChart,
|
|
9
|
+
ShoppingCart,
|
|
10
|
+
TrendingUp,
|
|
11
|
+
Users,
|
|
12
|
+
} from "lucide-react"
|
|
13
|
+
|
|
14
|
+
import { definePage } from "@squadbase/vantage"
|
|
15
|
+
import {
|
|
16
|
+
DashboardCard,
|
|
17
|
+
DashboardCardAction,
|
|
18
|
+
DashboardCardContent,
|
|
19
|
+
DashboardCardHeader,
|
|
20
|
+
DashboardCardPreset,
|
|
21
|
+
DashboardCardTitle,
|
|
22
|
+
DateRangePicker,
|
|
23
|
+
EChart,
|
|
24
|
+
PageShell,
|
|
25
|
+
PageShellContent,
|
|
26
|
+
PageShellDescription,
|
|
27
|
+
PageShellHeader,
|
|
28
|
+
PageShellHeaderEnd,
|
|
29
|
+
PageShellHeading,
|
|
30
|
+
PageShellSummary,
|
|
31
|
+
PageShellSummaryCard,
|
|
32
|
+
PageShellTitle,
|
|
33
|
+
Placeholder,
|
|
34
|
+
Sparkline,
|
|
35
|
+
type EChartsOption,
|
|
36
|
+
} from "@squadbase/vantage/components"
|
|
37
|
+
|
|
38
|
+
import { CampaignTable } from "./components/kpi-chart-advanced/campaign-table"
|
|
39
|
+
import {
|
|
40
|
+
breakdownSlices,
|
|
41
|
+
campaignRows,
|
|
42
|
+
comparisonSeries,
|
|
43
|
+
headerKpis,
|
|
44
|
+
trendSeries,
|
|
45
|
+
} from "./components/kpi-chart-advanced/mock-data"
|
|
46
|
+
import type {
|
|
47
|
+
BreakdownSlice,
|
|
48
|
+
ComparisonPoint,
|
|
49
|
+
DashboardFilters,
|
|
50
|
+
TrendPoint,
|
|
51
|
+
} from "./components/kpi-chart-advanced/types"
|
|
52
|
+
|
|
53
|
+
export const page = definePage({
|
|
54
|
+
title: "KPI + チャート (アドバンスト)",
|
|
55
|
+
description:
|
|
56
|
+
"ヒーローKPI (大1+小4の2×2) ・棒+折れ線比較チャート・ドーナツ・日次トレンド・明細テーブルを搭載したKPI主役のダッシュボード構成",
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// ── chart helpers ───────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
function getBaseGrid() {
|
|
62
|
+
return { left: "3%", right: "4%", bottom: "12%", containLabel: true }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getDualAxisGrid() {
|
|
66
|
+
return { left: "3%", right: "6%", bottom: "12%", containLabel: true }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function formatNumber(n: number): string {
|
|
70
|
+
const sign = n < 0 ? "-" : ""
|
|
71
|
+
const abs = Math.abs(n)
|
|
72
|
+
if (abs >= 100_000_000) return `${sign}${(abs / 100_000_000).toFixed(1)}億`
|
|
73
|
+
if (abs >= 10_000) return `${sign}${(abs / 10_000).toFixed(0)}万`
|
|
74
|
+
return n.toLocaleString("ja-JP")
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── chart options ────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
function comparisonOption(data: ComparisonPoint[]): EChartsOption {
|
|
80
|
+
return {
|
|
81
|
+
tooltip: { trigger: "axis" },
|
|
82
|
+
legend: { bottom: 0 },
|
|
83
|
+
grid: getDualAxisGrid(),
|
|
84
|
+
xAxis: {
|
|
85
|
+
type: "category",
|
|
86
|
+
data: data.map((d) => d.period),
|
|
87
|
+
},
|
|
88
|
+
yAxis: [
|
|
89
|
+
{
|
|
90
|
+
type: "value",
|
|
91
|
+
axisLabel: { formatter: (v: number) => formatNumber(v) },
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
type: "value",
|
|
95
|
+
axisLabel: { formatter: (v: number) => `${v.toFixed(0)}%` },
|
|
96
|
+
splitLine: { show: false },
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
series: [
|
|
100
|
+
{
|
|
101
|
+
name: "系列A",
|
|
102
|
+
type: "bar",
|
|
103
|
+
data: data.map((d) => d.current),
|
|
104
|
+
barMaxWidth: 28,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: "系列B",
|
|
108
|
+
type: "bar",
|
|
109
|
+
data: data.map((d) => d.previous),
|
|
110
|
+
barMaxWidth: 28,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "系列C",
|
|
114
|
+
type: "line",
|
|
115
|
+
yAxisIndex: 1,
|
|
116
|
+
smooth: true,
|
|
117
|
+
data: data.map((d) => d.growth),
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function breakdownOption(data: BreakdownSlice[]): EChartsOption {
|
|
124
|
+
return {
|
|
125
|
+
tooltip: { trigger: "item" },
|
|
126
|
+
legend: { bottom: 0 },
|
|
127
|
+
series: [
|
|
128
|
+
{
|
|
129
|
+
type: "pie",
|
|
130
|
+
radius: ["52%", "78%"],
|
|
131
|
+
avoidLabelOverlap: true,
|
|
132
|
+
itemStyle: { borderRadius: 6, borderColor: "transparent", borderWidth: 2 },
|
|
133
|
+
label: { show: false },
|
|
134
|
+
data: data.map((d) => ({ name: d.segment, value: d.value })),
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function trendOption(data: TrendPoint[]): EChartsOption {
|
|
141
|
+
return {
|
|
142
|
+
tooltip: { trigger: "axis" },
|
|
143
|
+
legend: { bottom: 0 },
|
|
144
|
+
grid: getBaseGrid(),
|
|
145
|
+
xAxis: {
|
|
146
|
+
type: "category",
|
|
147
|
+
data: data.map((d) => d.date),
|
|
148
|
+
boundaryGap: false,
|
|
149
|
+
axisLabel: { formatter: (v: string) => v.slice(5) },
|
|
150
|
+
},
|
|
151
|
+
yAxis: {
|
|
152
|
+
type: "value",
|
|
153
|
+
axisLabel: { formatter: (v: number) => formatNumber(v) },
|
|
154
|
+
},
|
|
155
|
+
series: [
|
|
156
|
+
{
|
|
157
|
+
name: "系列A",
|
|
158
|
+
type: "line",
|
|
159
|
+
smooth: true,
|
|
160
|
+
showSymbol: false,
|
|
161
|
+
data: data.map((d) => d.value),
|
|
162
|
+
areaStyle: { opacity: 0.18 },
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── page ─────────────────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
const today = new Date()
|
|
171
|
+
const initialFilters: DashboardFilters = {
|
|
172
|
+
dateRange: {
|
|
173
|
+
from: subDays(today, 29),
|
|
174
|
+
to: today,
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const smallKpiCards = [
|
|
179
|
+
{ label: "指標2", icon: Users, kpi: headerKpis[1] },
|
|
180
|
+
{ label: "指標3", icon: MousePointer, kpi: headerKpis[2] },
|
|
181
|
+
{ label: "指標4", icon: Activity, kpi: headerKpis[3] },
|
|
182
|
+
{ label: "指標5", icon: ShoppingCart, kpi: headerKpis[4] },
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
export default function HomePage() {
|
|
186
|
+
const [filters, setFilters] = useState<DashboardFilters>(initialFilters)
|
|
187
|
+
const heroKpi = headerKpis[0]
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<PageShell>
|
|
191
|
+
<PageShellHeader>
|
|
192
|
+
<PageShellHeading>
|
|
193
|
+
<PageShellTitle>[テンプレート] パフォーマンスダッシュボード</PageShellTitle>
|
|
194
|
+
<PageShellDescription>
|
|
195
|
+
選択期間の主要KPI・比較・詳細。
|
|
196
|
+
</PageShellDescription>
|
|
197
|
+
</PageShellHeading>
|
|
198
|
+
<PageShellHeaderEnd>
|
|
199
|
+
<DateRangePicker
|
|
200
|
+
value={filters.dateRange}
|
|
201
|
+
onChange={(range) =>
|
|
202
|
+
setFilters((prev) => ({ ...prev, dateRange: range }))
|
|
203
|
+
}
|
|
204
|
+
maxDate={today}
|
|
205
|
+
/>
|
|
206
|
+
</PageShellHeaderEnd>
|
|
207
|
+
<PageShellSummary>
|
|
208
|
+
<PageShellSummaryCard accent="accent">
|
|
209
|
+
<TrendingUp />
|
|
210
|
+
<div className="min-w-0 flex-1 space-y-1">
|
|
211
|
+
<p className="text-sm font-semibold">成長</p>
|
|
212
|
+
<p className="text-xs text-muted-foreground">
|
|
213
|
+
全体のパフォーマンスは前期比で上昇傾向です。
|
|
214
|
+
</p>
|
|
215
|
+
</div>
|
|
216
|
+
</PageShellSummaryCard>
|
|
217
|
+
<PageShellSummaryCard accent="default">
|
|
218
|
+
<PieChart />
|
|
219
|
+
<div className="min-w-0 flex-1 space-y-1">
|
|
220
|
+
<p className="text-sm font-semibold">構成</p>
|
|
221
|
+
<p className="text-xs text-muted-foreground">
|
|
222
|
+
単一セグメントが全体で最大のシェアを占めています。
|
|
223
|
+
</p>
|
|
224
|
+
</div>
|
|
225
|
+
</PageShellSummaryCard>
|
|
226
|
+
<PageShellSummaryCard accent="accent">
|
|
227
|
+
<Activity />
|
|
228
|
+
<div className="min-w-0 flex-1 space-y-1">
|
|
229
|
+
<p className="text-sm font-semibold">勢い</p>
|
|
230
|
+
<p className="text-xs text-muted-foreground">
|
|
231
|
+
直近の動きはトレンドの好転を示しています。
|
|
232
|
+
</p>
|
|
233
|
+
</div>
|
|
234
|
+
</PageShellSummaryCard>
|
|
235
|
+
</PageShellSummary>
|
|
236
|
+
</PageShellHeader>
|
|
237
|
+
|
|
238
|
+
<PageShellContent className="space-y-6">
|
|
239
|
+
<div className="grid gap-4 lg:grid-cols-3">
|
|
240
|
+
<DashboardCard className="h-full">
|
|
241
|
+
<DashboardCardHeader>
|
|
242
|
+
<DashboardCardTitle className="text-muted-foreground">
|
|
243
|
+
指標1
|
|
244
|
+
</DashboardCardTitle>
|
|
245
|
+
<DashboardCardAction>
|
|
246
|
+
<DollarSign className="size-5 text-muted-foreground" />
|
|
247
|
+
</DashboardCardAction>
|
|
248
|
+
</DashboardCardHeader>
|
|
249
|
+
<DashboardCardContent className="flex flex-1 flex-col gap-5">
|
|
250
|
+
<div>
|
|
251
|
+
<Placeholder className="text-4xl font-bold">
|
|
252
|
+
{heroKpi.value}
|
|
253
|
+
</Placeholder>
|
|
254
|
+
<div className="mt-3">
|
|
255
|
+
<Placeholder className="text-sm font-medium">
|
|
256
|
+
{heroKpi.change >= 0
|
|
257
|
+
? `+${heroKpi.change}%`
|
|
258
|
+
: `${heroKpi.change}%`}
|
|
259
|
+
</Placeholder>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
<Sparkline
|
|
263
|
+
data={heroKpi.sparklineData.map((v) => ({ value: v }))}
|
|
264
|
+
height={88}
|
|
265
|
+
area
|
|
266
|
+
/>
|
|
267
|
+
<div className="grid grid-cols-2 gap-3 border-t pt-3">
|
|
268
|
+
<div className="space-y-1.5">
|
|
269
|
+
<p className="text-xs text-muted-foreground">指標A</p>
|
|
270
|
+
<Placeholder className="text-sm font-medium">¥92万</Placeholder>
|
|
271
|
+
</div>
|
|
272
|
+
<div className="space-y-1.5">
|
|
273
|
+
<p className="text-xs text-muted-foreground">指標B</p>
|
|
274
|
+
<Placeholder className="text-sm font-medium">+8.4%</Placeholder>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
</DashboardCardContent>
|
|
278
|
+
</DashboardCard>
|
|
279
|
+
|
|
280
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:col-span-2">
|
|
281
|
+
{smallKpiCards.map(({ label, icon: Icon, kpi }) => (
|
|
282
|
+
<DashboardCard key={kpi.id}>
|
|
283
|
+
<DashboardCardHeader>
|
|
284
|
+
<DashboardCardTitle className="text-muted-foreground">
|
|
285
|
+
{label}
|
|
286
|
+
</DashboardCardTitle>
|
|
287
|
+
<DashboardCardAction>
|
|
288
|
+
<Icon className="size-4 text-muted-foreground" />
|
|
289
|
+
</DashboardCardAction>
|
|
290
|
+
</DashboardCardHeader>
|
|
291
|
+
<DashboardCardContent>
|
|
292
|
+
<Placeholder className="text-2xl font-bold">
|
|
293
|
+
{kpi.value}
|
|
294
|
+
</Placeholder>
|
|
295
|
+
<div className="mt-2">
|
|
296
|
+
<Placeholder className="text-sm font-medium">
|
|
297
|
+
{kpi.change >= 0 ? `+${kpi.change}%` : `${kpi.change}%`}
|
|
298
|
+
</Placeholder>
|
|
299
|
+
</div>
|
|
300
|
+
<Sparkline
|
|
301
|
+
data={kpi.sparklineData.map((v) => ({ value: v }))}
|
|
302
|
+
height={32}
|
|
303
|
+
area
|
|
304
|
+
className="mt-3"
|
|
305
|
+
/>
|
|
306
|
+
</DashboardCardContent>
|
|
307
|
+
</DashboardCard>
|
|
308
|
+
))}
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
<div className="grid gap-4 lg:grid-cols-3">
|
|
313
|
+
<div className="lg:col-span-2">
|
|
314
|
+
<DashboardCardPreset title="比較" description="今期 vs 前期">
|
|
315
|
+
<EChart option={comparisonOption(comparisonSeries)} height="320px" />
|
|
316
|
+
</DashboardCardPreset>
|
|
317
|
+
</div>
|
|
318
|
+
<DashboardCardPreset title="内訳" description="セグメント別シェア">
|
|
319
|
+
<EChart option={breakdownOption(breakdownSlices)} height="320px" />
|
|
320
|
+
</DashboardCardPreset>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
<DashboardCardPreset title="トレンド" description="選択期間の値">
|
|
324
|
+
<EChart option={trendOption(trendSeries)} height="280px" />
|
|
325
|
+
</DashboardCardPreset>
|
|
326
|
+
|
|
327
|
+
<DashboardCardPreset title="キャンペーン" description="キャンペーン別パフォーマンス">
|
|
328
|
+
<CampaignTable data={campaignRows} />
|
|
329
|
+
</DashboardCardPreset>
|
|
330
|
+
</PageShellContent>
|
|
331
|
+
</PageShell>
|
|
332
|
+
)
|
|
333
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useMemo } from "react"
|
|
2
|
+
import { DataTablePreset, Placeholder } from "@squadbase/vantage/components"
|
|
3
|
+
import type { ColumnDef } from "@squadbase/vantage/components"
|
|
4
|
+
|
|
5
|
+
import type { TopItemRow } from "../../lib/kpi-chart-simple/types"
|
|
6
|
+
|
|
7
|
+
function formatCurrency(n: number, opts: { short?: boolean } = {}): string {
|
|
8
|
+
const sign = n < 0 ? "-" : ""
|
|
9
|
+
const abs = Math.abs(n)
|
|
10
|
+
if (opts.short) {
|
|
11
|
+
if (abs >= 1_000_000) return `${sign}$${(abs / 1_000_000).toFixed(2)}M`
|
|
12
|
+
if (abs >= 1_000) return `${sign}$${(abs / 1_000).toFixed(1)}K`
|
|
13
|
+
return `${sign}$${abs.toLocaleString("en-US")}`
|
|
14
|
+
}
|
|
15
|
+
return `${sign}$${Math.round(abs).toLocaleString("en-US")}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function formatPercent(value: number, digits = 1): string {
|
|
19
|
+
return `${value.toFixed(digits)}%`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface TopItemsTableProps {
|
|
23
|
+
data: TopItemRow[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function TopItemsTable({ data }: TopItemsTableProps) {
|
|
27
|
+
const columns = useMemo<ColumnDef<TopItemRow>[]>(
|
|
28
|
+
() => [
|
|
29
|
+
{
|
|
30
|
+
accessorKey: "name",
|
|
31
|
+
header: "Column 1",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
accessorKey: "category",
|
|
35
|
+
header: "Column 2",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
accessorKey: "revenue",
|
|
39
|
+
header: "Column 3",
|
|
40
|
+
cell: ({ row }) => (
|
|
41
|
+
<div className="flex justify-end">
|
|
42
|
+
<Placeholder>
|
|
43
|
+
{formatCurrency(row.original.revenue, { short: true })}
|
|
44
|
+
</Placeholder>
|
|
45
|
+
</div>
|
|
46
|
+
),
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
accessorKey: "units",
|
|
50
|
+
header: "Column 4",
|
|
51
|
+
cell: ({ row }) => (
|
|
52
|
+
<div className="flex justify-end">
|
|
53
|
+
<Placeholder>
|
|
54
|
+
{row.original.units.toLocaleString("en-US")}
|
|
55
|
+
</Placeholder>
|
|
56
|
+
</div>
|
|
57
|
+
),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
accessorKey: "share",
|
|
61
|
+
header: "Column 5",
|
|
62
|
+
cell: ({ row }) => (
|
|
63
|
+
<div className="flex justify-end">
|
|
64
|
+
<Placeholder>{formatPercent(row.original.share * 100)}</Placeholder>
|
|
65
|
+
</div>
|
|
66
|
+
),
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
[],
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return <DataTablePreset columns={columns} data={data} enableSorting />
|
|
73
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared between the page and `server/api/kpi-summary.ts`.
|
|
3
|
+
*
|
|
4
|
+
* These live in `lib/` rather than `components/` because the client may never
|
|
5
|
+
* import from `server/` (`vantage check` reports `CLIENT_IMPORTS_SERVER`), so
|
|
6
|
+
* anything both sides need has to sit in neutral ground.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type RangeKey = "7d" | "30d" | "90d"
|
|
10
|
+
|
|
11
|
+
export type KpiId = "total-revenue" | "active-users" | "conversion-rate" | "aov"
|
|
12
|
+
|
|
13
|
+
/** Raw numbers only — labels and formatting belong to the page. */
|
|
14
|
+
export interface KpiItem {
|
|
15
|
+
id: KpiId
|
|
16
|
+
value: number
|
|
17
|
+
/** Percent change against the preceding window of the same length. */
|
|
18
|
+
change: number
|
|
19
|
+
sparklineData: number[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface TrendPoint {
|
|
23
|
+
date: string
|
|
24
|
+
revenue: number
|
|
25
|
+
orders: number
|
|
26
|
+
visitors: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface TopItemRow {
|
|
30
|
+
id: string
|
|
31
|
+
name: string
|
|
32
|
+
category: string
|
|
33
|
+
revenue: number
|
|
34
|
+
units: number
|
|
35
|
+
share: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The payload `GET /api/kpi-summary` returns. */
|
|
39
|
+
export interface KpiSummary {
|
|
40
|
+
range: RangeKey
|
|
41
|
+
kpis: KpiItem[]
|
|
42
|
+
trend: TrendPoint[]
|
|
43
|
+
topItems: TopItemRow[]
|
|
44
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kpi-chart-simple",
|
|
3
|
+
"description": "KPI + Chart (Simple) — 4 KPI cards, a single trend line, and a ranked-items table, served by server/api and filtered through the URL. The minimal headline-metrics dashboard layout.",
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"files": [
|
|
6
|
+
{ "src": "pages/index.tsx", "dest": "index.tsx", "action": "replace" },
|
|
7
|
+
{ "src": "components/top-items-table.tsx", "dest": "components/kpi-chart-simple/top-items-table.tsx", "action": "add" },
|
|
8
|
+
{ "src": "lib/types.ts", "dest": "lib/kpi-chart-simple/types.ts", "action": "add", "relabel": false },
|
|
9
|
+
{ "src": "server/kpi-summary.ts", "dest": "server/api/kpi-summary.ts", "action": "add", "relabel": false }
|
|
10
|
+
]
|
|
11
|
+
}
|