astryx-dracula 0.1.3 → 0.2.0
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/BRAND.md +1 -1
- package/README.md +2 -0
- package/USAGE.md +13 -0
- package/astryx-dracula.js +4 -4
- package/astryx.integration.mjs +3 -0
- package/package.json +4 -2
- package/templates/ai-chat-landing.template.mjs +6 -0
- package/templates/ai-chat-landing.tsx +476 -0
- package/templates/ai-chat.template.mjs +6 -0
- package/templates/ai-chat.tsx +762 -0
- package/templates/blank.template.mjs +6 -0
- package/templates/blank.tsx +16 -0
- package/templates/centered-hero.template.mjs +6 -0
- package/templates/centered-hero.tsx +188 -0
- package/templates/classic-gallery.template.mjs +6 -0
- package/templates/classic-gallery.tsx +181 -0
- package/templates/contact-form.template.mjs +6 -0
- package/templates/contact-form.tsx +292 -0
- package/templates/dashboard-portfolio.template.mjs +6 -0
- package/templates/dashboard-portfolio.tsx +800 -0
- package/templates/dashboard.template.mjs +6 -0
- package/templates/dashboard.tsx +774 -0
- package/templates/detail-page.template.mjs +6 -0
- package/templates/detail-page.tsx +695 -0
- package/templates/documentation-design.template.mjs +6 -0
- package/templates/documentation-design.tsx +751 -0
- package/templates/documentation-technical.template.mjs +6 -0
- package/templates/documentation-technical.tsx +292 -0
- package/templates/documentation.template.mjs +6 -0
- package/templates/documentation.tsx +347 -0
- package/templates/editor.template.mjs +6 -0
- package/templates/editor.tsx +959 -0
- package/templates/file-explorer.template.mjs +6 -0
- package/templates/file-explorer.tsx +577 -0
- package/templates/form-two-column.template.mjs +6 -0
- package/templates/form-two-column.tsx +314 -0
- package/templates/gallery-hero.template.mjs +6 -0
- package/templates/gallery-hero.tsx +287 -0
- package/templates/ide.template.mjs +6 -0
- package/templates/ide.tsx +533 -0
- package/templates/incident-console.template.mjs +6 -0
- package/templates/incident-console.tsx +584 -0
- package/templates/kanban-board.template.mjs +6 -0
- package/templates/kanban-board.tsx +746 -0
- package/templates/library.template.mjs +6 -0
- package/templates/library.tsx +489 -0
- package/templates/login-card.template.mjs +6 -0
- package/templates/login-card.tsx +200 -0
- package/templates/login-split.template.mjs +6 -0
- package/templates/login-split.tsx +318 -0
- package/templates/login-sso.template.mjs +6 -0
- package/templates/login-sso.tsx +277 -0
- package/templates/login.template.mjs +6 -0
- package/templates/login.tsx +98 -0
- package/templates/messaging-shell.template.mjs +6 -0
- package/templates/messaging-shell.tsx +675 -0
- package/templates/mixed-gallery.template.mjs +6 -0
- package/templates/mixed-gallery.tsx +190 -0
- package/templates/payment-form.template.mjs +6 -0
- package/templates/payment-form.tsx +990 -0
- package/templates/product-detail.template.mjs +6 -0
- package/templates/product-detail.tsx +366 -0
- package/templates/product-gallery.template.mjs +6 -0
- package/templates/product-gallery.tsx +173 -0
- package/templates/settings-dialog.template.mjs +6 -0
- package/templates/settings-dialog.tsx +920 -0
- package/templates/settings-sidebar.template.mjs +6 -0
- package/templates/settings-sidebar.tsx +952 -0
- package/templates/settings.template.mjs +6 -0
- package/templates/settings.tsx +235 -0
- package/templates/shell-nav.template.mjs +6 -0
- package/templates/shell-nav.tsx +306 -0
- package/templates/shell-side-nav.template.mjs +6 -0
- package/templates/shell-side-nav.tsx +238 -0
- package/templates/shell-top-nav.template.mjs +6 -0
- package/templates/shell-top-nav.tsx +243 -0
- package/templates/side-gallery.template.mjs +6 -0
- package/templates/side-gallery.tsx +179 -0
- package/templates/table-grouped.template.mjs +6 -0
- package/templates/table-grouped.tsx +1174 -0
- package/templates/table-page-chart.template.mjs +6 -0
- package/templates/table-page-chart.tsx +693 -0
- package/templates/table-page-heatmap-status.template.mjs +6 -0
- package/templates/table-page-heatmap-status.tsx +547 -0
- package/templates/table-page-shoe-store-heatmap.template.mjs +6 -0
- package/templates/table-page-shoe-store-heatmap.tsx +1038 -0
- package/templates/table-page.template.mjs +6 -0
- package/templates/table-page.tsx +445 -0
- package/templates/table.template.mjs +6 -0
- package/templates/table.tsx +84 -0
- package/templates/theme-showcase.template.mjs +6 -0
- package/templates/theme-showcase.tsx +1282 -0
- package/theme.css +4 -4
- package/tokens.css +4 -4
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {VStack, HStack, Layout, LayoutContent} from '@astryxdesign/core/Layout';
|
|
4
|
+
import {Text, Heading} from '@astryxdesign/core/Text';
|
|
5
|
+
import {Card} from '@astryxdesign/core/Card';
|
|
6
|
+
import {Button} from '@astryxdesign/core/Button';
|
|
7
|
+
import {ProgressBar} from '@astryxdesign/core/ProgressBar';
|
|
8
|
+
import {Grid} from '@astryxdesign/core/Grid';
|
|
9
|
+
import {Table, proportional, pixel} from '@astryxdesign/core/Table';
|
|
10
|
+
import type {TableColumn} from '@astryxdesign/core/Table';
|
|
11
|
+
import {Divider} from '@astryxdesign/core/Divider';
|
|
12
|
+
import {Link} from '@astryxdesign/core/Link';
|
|
13
|
+
import {Icon} from '@astryxdesign/core/Icon';
|
|
14
|
+
|
|
15
|
+
// ============= ICONS =============
|
|
16
|
+
|
|
17
|
+
import {RefreshCw, ArrowUp, ArrowDown, Square} from 'lucide-react';
|
|
18
|
+
|
|
19
|
+
// ============= DATA =============
|
|
20
|
+
|
|
21
|
+
// Active users chart data (96 points over 24h at 15-min intervals: Apr 1 14:00 → Apr 2 14:00)
|
|
22
|
+
// Each point has an index (0–95) for even spacing, plus a label for display
|
|
23
|
+
const activeUsersData = [
|
|
24
|
+
// Apr 1 14:00 — mid-afternoon, strong work hours
|
|
25
|
+
{hour: 0, label: 'Apr 1 14:00', allUsers: 116, desktop: 79, mobile: 37},
|
|
26
|
+
{hour: 1, label: 'Apr 1 14:15', allUsers: 118, desktop: 80, mobile: 38},
|
|
27
|
+
{hour: 2, label: 'Apr 1 14:30', allUsers: 117, desktop: 79, mobile: 38},
|
|
28
|
+
{hour: 3, label: 'Apr 1 14:45', allUsers: 119, desktop: 81, mobile: 38},
|
|
29
|
+
// Apr 1 15:00 — afternoon lull
|
|
30
|
+
{hour: 4, label: 'Apr 1 15:00', allUsers: 116, desktop: 79, mobile: 37},
|
|
31
|
+
{hour: 5, label: 'Apr 1 15:15', allUsers: 113, desktop: 76, mobile: 37},
|
|
32
|
+
{hour: 6, label: 'Apr 1 15:30', allUsers: 109, desktop: 72, mobile: 37},
|
|
33
|
+
{hour: 7, label: 'Apr 1 15:45', allUsers: 111, desktop: 74, mobile: 37},
|
|
34
|
+
// Apr 1 16:00 — late afternoon, some leaving early
|
|
35
|
+
{hour: 8, label: 'Apr 1 16:00', allUsers: 110, desktop: 72, mobile: 38},
|
|
36
|
+
{hour: 9, label: 'Apr 1 16:15', allUsers: 107, desktop: 69, mobile: 38},
|
|
37
|
+
{hour: 10, label: 'Apr 1 16:30', allUsers: 104, desktop: 66, mobile: 38},
|
|
38
|
+
{hour: 11, label: 'Apr 1 16:45', allUsers: 101, desktop: 62, mobile: 39},
|
|
39
|
+
// Apr 1 17:00 — 5PM exodus, desktop drops fast, mobile bumps
|
|
40
|
+
{hour: 12, label: 'Apr 1 17:00', allUsers: 100, desktop: 57, mobile: 43},
|
|
41
|
+
{hour: 13, label: 'Apr 1 17:15', allUsers: 99, desktop: 54, mobile: 45},
|
|
42
|
+
{hour: 14, label: 'Apr 1 17:30', allUsers: 97, desktop: 50, mobile: 47},
|
|
43
|
+
{hour: 15, label: 'Apr 1 17:45', allUsers: 95, desktop: 46, mobile: 49},
|
|
44
|
+
// Apr 1 18:00 — commute, mobile overtakes desktop
|
|
45
|
+
{hour: 16, label: 'Apr 1 18:00', allUsers: 93, desktop: 41, mobile: 52},
|
|
46
|
+
{hour: 17, label: 'Apr 1 18:15', allUsers: 91, desktop: 39, mobile: 52},
|
|
47
|
+
{hour: 18, label: 'Apr 1 18:30', allUsers: 89, desktop: 37, mobile: 52},
|
|
48
|
+
{hour: 19, label: 'Apr 1 18:45', allUsers: 87, desktop: 36, mobile: 51},
|
|
49
|
+
// Apr 1 19:00 — dinner, decline slowing
|
|
50
|
+
{hour: 20, label: 'Apr 1 19:00', allUsers: 85, desktop: 34, mobile: 51},
|
|
51
|
+
{hour: 21, label: 'Apr 1 19:15', allUsers: 83, desktop: 32, mobile: 51},
|
|
52
|
+
{hour: 22, label: 'Apr 1 19:30', allUsers: 80, desktop: 30, mobile: 50},
|
|
53
|
+
{hour: 23, label: 'Apr 1 19:45', allUsers: 77, desktop: 28, mobile: 49},
|
|
54
|
+
// Apr 1 20:00 — couch browsing, mobile plateau
|
|
55
|
+
{hour: 24, label: 'Apr 1 20:00', allUsers: 75, desktop: 26, mobile: 49},
|
|
56
|
+
{hour: 25, label: 'Apr 1 20:15', allUsers: 76, desktop: 26, mobile: 50},
|
|
57
|
+
{hour: 26, label: 'Apr 1 20:30', allUsers: 76, desktop: 27, mobile: 49},
|
|
58
|
+
{hour: 27, label: 'Apr 1 20:45', allUsers: 75, desktop: 27, mobile: 48},
|
|
59
|
+
// Apr 1 21:00 — winding down, mobile dropping off
|
|
60
|
+
{hour: 28, label: 'Apr 1 21:00', allUsers: 73, desktop: 27, mobile: 46},
|
|
61
|
+
{hour: 29, label: 'Apr 1 21:15', allUsers: 71, desktop: 27, mobile: 44},
|
|
62
|
+
{hour: 30, label: 'Apr 1 21:30', allUsers: 69, desktop: 27, mobile: 42},
|
|
63
|
+
{hour: 31, label: 'Apr 1 21:45', allUsers: 67, desktop: 28, mobile: 39},
|
|
64
|
+
// Apr 1 22:00 — bedtime wave, mobile drops, desktop holds
|
|
65
|
+
{hour: 32, label: 'Apr 1 22:00', allUsers: 65, desktop: 29, mobile: 36},
|
|
66
|
+
{hour: 33, label: 'Apr 1 22:15', allUsers: 63, desktop: 29, mobile: 34},
|
|
67
|
+
{hour: 34, label: 'Apr 1 22:30', allUsers: 61, desktop: 30, mobile: 31},
|
|
68
|
+
{hour: 35, label: 'Apr 1 22:45', allUsers: 60, desktop: 31, mobile: 29},
|
|
69
|
+
// Apr 1 23:00 — desktop overtakes as local users sleep, other TZs active
|
|
70
|
+
{hour: 36, label: 'Apr 1 23:00', allUsers: 59, desktop: 33, mobile: 26},
|
|
71
|
+
{hour: 37, label: 'Apr 1 23:15', allUsers: 58, desktop: 34, mobile: 24},
|
|
72
|
+
{hour: 38, label: 'Apr 1 23:30', allUsers: 57, desktop: 35, mobile: 22},
|
|
73
|
+
{hour: 39, label: 'Apr 1 23:45', allUsers: 56, desktop: 36, mobile: 20},
|
|
74
|
+
// Apr 2 00:00 — EMEA morning starts, desktop dominant
|
|
75
|
+
{hour: 40, label: 'Apr 2 00:00', allUsers: 56, desktop: 38, mobile: 18},
|
|
76
|
+
{hour: 41, label: 'Apr 2 00:15', allUsers: 56, desktop: 39, mobile: 17},
|
|
77
|
+
{hour: 42, label: 'Apr 2 00:30', allUsers: 57, desktop: 40, mobile: 17},
|
|
78
|
+
{hour: 43, label: 'Apr 2 00:45', allUsers: 56, desktop: 40, mobile: 16},
|
|
79
|
+
// Apr 2 01:00 — EMEA working, plateau
|
|
80
|
+
{hour: 44, label: 'Apr 2 01:00', allUsers: 56, desktop: 41, mobile: 15},
|
|
81
|
+
{hour: 45, label: 'Apr 2 01:15', allUsers: 55, desktop: 41, mobile: 14},
|
|
82
|
+
{hour: 46, label: 'Apr 2 01:30', allUsers: 55, desktop: 41, mobile: 14},
|
|
83
|
+
{hour: 47, label: 'Apr 2 01:45', allUsers: 54, desktop: 40, mobile: 14},
|
|
84
|
+
// Apr 2 02:00 — EMEA mid-morning, holding steady
|
|
85
|
+
{hour: 48, label: 'Apr 2 02:00', allUsers: 54, desktop: 40, mobile: 14},
|
|
86
|
+
{hour: 49, label: 'Apr 2 02:15', allUsers: 53, desktop: 39, mobile: 14},
|
|
87
|
+
{hour: 50, label: 'Apr 2 02:30', allUsers: 53, desktop: 39, mobile: 14},
|
|
88
|
+
{hour: 51, label: 'Apr 2 02:45', allUsers: 52, desktop: 38, mobile: 14},
|
|
89
|
+
// Apr 2 03:00 — EMEA lunch gap, slight dip
|
|
90
|
+
{hour: 52, label: 'Apr 2 03:00', allUsers: 51, desktop: 37, mobile: 14},
|
|
91
|
+
{hour: 53, label: 'Apr 2 03:15', allUsers: 50, desktop: 36, mobile: 14},
|
|
92
|
+
{hour: 54, label: 'Apr 2 03:30', allUsers: 49, desktop: 35, mobile: 14},
|
|
93
|
+
{hour: 55, label: 'Apr 2 03:45', allUsers: 49, desktop: 35, mobile: 14},
|
|
94
|
+
// Apr 2 04:00 — EMEA afternoon, floor
|
|
95
|
+
{hour: 56, label: 'Apr 2 04:00', allUsers: 48, desktop: 34, mobile: 14},
|
|
96
|
+
{hour: 57, label: 'Apr 2 04:15', allUsers: 48, desktop: 33, mobile: 15},
|
|
97
|
+
{hour: 58, label: 'Apr 2 04:30', allUsers: 49, desktop: 33, mobile: 16},
|
|
98
|
+
{hour: 59, label: 'Apr 2 04:45', allUsers: 49, desktop: 32, mobile: 17},
|
|
99
|
+
// Apr 2 05:00 — early risers checking phones, mobile climbing
|
|
100
|
+
{hour: 60, label: 'Apr 2 05:00', allUsers: 50, desktop: 31, mobile: 19},
|
|
101
|
+
{hour: 61, label: 'Apr 2 05:15', allUsers: 51, desktop: 30, mobile: 21},
|
|
102
|
+
{hour: 62, label: 'Apr 2 05:30', allUsers: 52, desktop: 29, mobile: 23},
|
|
103
|
+
{hour: 63, label: 'Apr 2 05:45', allUsers: 54, desktop: 28, mobile: 26},
|
|
104
|
+
// Apr 2 06:00 — alarms going off, mobile surging
|
|
105
|
+
{hour: 64, label: 'Apr 2 06:00', allUsers: 56, desktop: 27, mobile: 29},
|
|
106
|
+
{hour: 65, label: 'Apr 2 06:15', allUsers: 58, desktop: 26, mobile: 32},
|
|
107
|
+
{hour: 66, label: 'Apr 2 06:30', allUsers: 61, desktop: 26, mobile: 35},
|
|
108
|
+
{hour: 67, label: 'Apr 2 06:45', allUsers: 64, desktop: 27, mobile: 37},
|
|
109
|
+
// Apr 2 07:00 — commute, mobile peaks, desktop starting
|
|
110
|
+
{hour: 68, label: 'Apr 2 07:00', allUsers: 67, desktop: 28, mobile: 39},
|
|
111
|
+
{hour: 69, label: 'Apr 2 07:15', allUsers: 70, desktop: 30, mobile: 40},
|
|
112
|
+
{hour: 70, label: 'Apr 2 07:30', allUsers: 73, desktop: 33, mobile: 40},
|
|
113
|
+
{hour: 71, label: 'Apr 2 07:45', allUsers: 76, desktop: 37, mobile: 39},
|
|
114
|
+
// Apr 2 08:00 — arriving at desks, desktop ramping
|
|
115
|
+
{hour: 72, label: 'Apr 2 08:00', allUsers: 80, desktop: 43, mobile: 37},
|
|
116
|
+
{hour: 73, label: 'Apr 2 08:15', allUsers: 85, desktop: 49, mobile: 36},
|
|
117
|
+
{hour: 74, label: 'Apr 2 08:30', allUsers: 90, desktop: 55, mobile: 35},
|
|
118
|
+
{hour: 75, label: 'Apr 2 08:45', allUsers: 95, desktop: 61, mobile: 34},
|
|
119
|
+
// Apr 2 09:00 — work day, desktop dominant
|
|
120
|
+
{hour: 76, label: 'Apr 2 09:00', allUsers: 99, desktop: 66, mobile: 33},
|
|
121
|
+
{hour: 77, label: 'Apr 2 09:15', allUsers: 102, desktop: 69, mobile: 33},
|
|
122
|
+
{hour: 78, label: 'Apr 2 09:30', allUsers: 104, desktop: 72, mobile: 32},
|
|
123
|
+
{hour: 79, label: 'Apr 2 09:45', allUsers: 104, desktop: 72, mobile: 32},
|
|
124
|
+
// Apr 2 10:00 — coffee break stall, then climbing
|
|
125
|
+
{hour: 80, label: 'Apr 2 10:00', allUsers: 106, desktop: 74, mobile: 32},
|
|
126
|
+
{hour: 81, label: 'Apr 2 10:15', allUsers: 109, desktop: 76, mobile: 33},
|
|
127
|
+
{hour: 82, label: 'Apr 2 10:30', allUsers: 112, desktop: 78, mobile: 34},
|
|
128
|
+
{hour: 83, label: 'Apr 2 10:45', allUsers: 114, desktop: 80, mobile: 34},
|
|
129
|
+
// Apr 2 11:00 — approaching peak
|
|
130
|
+
{hour: 84, label: 'Apr 2 11:00', allUsers: 116, desktop: 81, mobile: 35},
|
|
131
|
+
{hour: 85, label: 'Apr 2 11:15', allUsers: 117, desktop: 81, mobile: 36},
|
|
132
|
+
{hour: 86, label: 'Apr 2 11:30', allUsers: 119, desktop: 83, mobile: 36},
|
|
133
|
+
{hour: 87, label: 'Apr 2 11:45', allUsers: 119, desktop: 82, mobile: 37},
|
|
134
|
+
// Apr 2 12:00 — lunch dip, mobile ticks up
|
|
135
|
+
{hour: 88, label: 'Apr 2 12:00', allUsers: 120, desktop: 83, mobile: 37},
|
|
136
|
+
{hour: 89, label: 'Apr 2 12:15', allUsers: 115, desktop: 78, mobile: 37},
|
|
137
|
+
{hour: 90, label: 'Apr 2 12:30', allUsers: 111, desktop: 74, mobile: 37},
|
|
138
|
+
{hour: 91, label: 'Apr 2 12:45', allUsers: 110, desktop: 73, mobile: 37},
|
|
139
|
+
// Apr 2 13:00 — returning from lunch
|
|
140
|
+
{hour: 92, label: 'Apr 2 13:00', allUsers: 113, desktop: 76, mobile: 37},
|
|
141
|
+
{hour: 93, label: 'Apr 2 13:15', allUsers: 116, desktop: 79, mobile: 37},
|
|
142
|
+
{hour: 94, label: 'Apr 2 13:30', allUsers: 118, desktop: 81, mobile: 37},
|
|
143
|
+
{hour: 95, label: 'Apr 2 14:00', allUsers: 120, desktop: 83, mobile: 37},
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
// Metric cards
|
|
147
|
+
const metrics = [
|
|
148
|
+
{
|
|
149
|
+
label: 'Moonlit visitors',
|
|
150
|
+
value: '27.3 k',
|
|
151
|
+
change: '+18.2%',
|
|
152
|
+
positive: true,
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
label: 'Midnight page views',
|
|
156
|
+
value: '48.2 k',
|
|
157
|
+
change: '+12.5%',
|
|
158
|
+
positive: true,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
label: 'Avg. night visit',
|
|
162
|
+
value: '4.5 min',
|
|
163
|
+
change: '-14.3%',
|
|
164
|
+
positive: false,
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
label: 'Sunrise bounce rate',
|
|
168
|
+
value: '42.3%',
|
|
169
|
+
change: '-8.7%',
|
|
170
|
+
positive: true,
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
// Sparkline data for each metric card (30 days, weekends at indices 5-6, 12-13, 19-20, 26-27)
|
|
175
|
+
const sparklines = [
|
|
176
|
+
// Monthly Visitors: +18.2% — declining first 2 weeks, hits bottom around day 14, then sharp recovery
|
|
177
|
+
// prettier-ignore
|
|
178
|
+
[48, 46, 44, 42, 40, 18, 16, 38, 36, 34, 32, 30, 12, 10, 28, 26, 28, 32, 36, 14, 12, 40, 44, 48, 52, 56, 28, 24, 58, 62],
|
|
179
|
+
// Monthly Page Views: +12.5% — flat/choppy first 3 weeks, then kicks up sharply in final week
|
|
180
|
+
// prettier-ignore
|
|
181
|
+
[36, 38, 35, 37, 36, 14, 12, 38, 36, 34, 37, 35, 12, 10, 36, 34, 36, 35, 38, 14, 12, 40, 44, 50, 54, 56, 26, 22, 58, 60],
|
|
182
|
+
// Avg. Session: -14.3% — strong start, holds through week 2, then clear drop-off week 3-4
|
|
183
|
+
// prettier-ignore
|
|
184
|
+
[58, 56, 60, 58, 62, 30, 26, 60, 58, 62, 60, 58, 28, 24, 56, 54, 50, 46, 42, 18, 14, 38, 36, 34, 32, 30, 10, 8, 28, 26],
|
|
185
|
+
// Bounce Rate: -8.7% — high and volatile first half, starts dropping around day 16, steady decline
|
|
186
|
+
// prettier-ignore
|
|
187
|
+
[52, 56, 50, 54, 58, 62, 60, 54, 52, 56, 50, 54, 60, 58, 50, 48, 46, 44, 40, 46, 44, 38, 36, 34, 36, 32, 38, 36, 30, 28],
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
// Demographics
|
|
191
|
+
const regionData = [
|
|
192
|
+
{
|
|
193
|
+
label: 'NORAM',
|
|
194
|
+
value: 38,
|
|
195
|
+
color: 'var(--dracula-comment)',
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
label: 'EMEA',
|
|
199
|
+
value: 28,
|
|
200
|
+
color: 'var(--dracula-orange)',
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
label: 'APAC',
|
|
204
|
+
value: 22,
|
|
205
|
+
color: 'var(--dracula-green)',
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
label: 'LATAM',
|
|
209
|
+
value: 8,
|
|
210
|
+
color: 'var(--dracula-purple)',
|
|
211
|
+
},
|
|
212
|
+
{label: 'Other', value: 4, color: 'var(--dracula-selection)'},
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
const roleData = [
|
|
216
|
+
{
|
|
217
|
+
label: 'Engineer',
|
|
218
|
+
value: 45,
|
|
219
|
+
color: 'var(--dracula-comment)',
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
label: 'Manager',
|
|
223
|
+
value: 20,
|
|
224
|
+
color: 'var(--dracula-orange)',
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
label: 'Designer',
|
|
228
|
+
value: 15,
|
|
229
|
+
color: 'var(--dracula-green)',
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
label: 'Data Scientist',
|
|
233
|
+
value: 12,
|
|
234
|
+
color: 'var(--dracula-purple)',
|
|
235
|
+
},
|
|
236
|
+
{label: 'Other', value: 8, color: 'var(--dracula-selection)'},
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
// Engagement — Top pages
|
|
240
|
+
interface PageRow extends Record<string, unknown> {
|
|
241
|
+
id: string;
|
|
242
|
+
page: string;
|
|
243
|
+
views: number;
|
|
244
|
+
newUsers: string;
|
|
245
|
+
avgTime: string;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const topPagesData: PageRow[] = [
|
|
249
|
+
{
|
|
250
|
+
id: '1',
|
|
251
|
+
page: '/home',
|
|
252
|
+
views: 8420,
|
|
253
|
+
newUsers: '62.3%',
|
|
254
|
+
avgTime: '3:42',
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
id: '2',
|
|
258
|
+
page: '/products',
|
|
259
|
+
views: 6150,
|
|
260
|
+
newUsers: '45.1%',
|
|
261
|
+
avgTime: '4:15',
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
id: '3',
|
|
265
|
+
page: '/pricing',
|
|
266
|
+
views: 4830,
|
|
267
|
+
newUsers: '38.7%',
|
|
268
|
+
avgTime: '2:58',
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
id: '4',
|
|
272
|
+
page: '/blog',
|
|
273
|
+
views: 3920,
|
|
274
|
+
newUsers: '71.4%',
|
|
275
|
+
avgTime: '5:30',
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
id: '5',
|
|
279
|
+
page: '/docs',
|
|
280
|
+
views: 3410,
|
|
281
|
+
newUsers: '29.8%',
|
|
282
|
+
avgTime: '6:12',
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
id: '6',
|
|
286
|
+
page: '/about',
|
|
287
|
+
views: 2980,
|
|
288
|
+
newUsers: '55.6%',
|
|
289
|
+
avgTime: '2:15',
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
id: '7',
|
|
293
|
+
page: '/contact',
|
|
294
|
+
views: 2540,
|
|
295
|
+
newUsers: '48.2%',
|
|
296
|
+
avgTime: '1:48',
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
id: '8',
|
|
300
|
+
page: '/changelog',
|
|
301
|
+
views: 2210,
|
|
302
|
+
newUsers: '22.1%',
|
|
303
|
+
avgTime: '4:55',
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
id: '9',
|
|
307
|
+
page: '/support',
|
|
308
|
+
views: 1870,
|
|
309
|
+
newUsers: '59.3%',
|
|
310
|
+
avgTime: '3:22',
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
id: '10',
|
|
314
|
+
page: '/careers',
|
|
315
|
+
views: 1520,
|
|
316
|
+
newUsers: '83.1%',
|
|
317
|
+
avgTime: '2:34',
|
|
318
|
+
},
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
const topPagesMaxViews = Math.max(...topPagesData.map(d => d.views));
|
|
322
|
+
|
|
323
|
+
const topPagesColumns: TableColumn<PageRow>[] = [
|
|
324
|
+
{key: 'page', header: 'Page', width: pixel(160)},
|
|
325
|
+
{
|
|
326
|
+
key: 'views',
|
|
327
|
+
header: 'Views',
|
|
328
|
+
width: proportional(1),
|
|
329
|
+
renderCell: (item: PageRow) => (
|
|
330
|
+
<VStack gap={1}>
|
|
331
|
+
<ProgressBar
|
|
332
|
+
value={item.views}
|
|
333
|
+
max={topPagesMaxViews}
|
|
334
|
+
label={`${item.page} views`}
|
|
335
|
+
isLabelHidden
|
|
336
|
+
/>
|
|
337
|
+
<Text type="supporting" hasTabularNumbers>
|
|
338
|
+
{item.views.toLocaleString()}
|
|
339
|
+
</Text>
|
|
340
|
+
</VStack>
|
|
341
|
+
),
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
key: 'newUsers',
|
|
345
|
+
header: 'New Users',
|
|
346
|
+
width: pixel(120),
|
|
347
|
+
renderCell: (item: PageRow) => (
|
|
348
|
+
<Text hasTabularNumbers>{item.newUsers}</Text>
|
|
349
|
+
),
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
key: 'avgTime',
|
|
353
|
+
header: 'Avg. Time',
|
|
354
|
+
width: pixel(120),
|
|
355
|
+
renderCell: (item: PageRow) => (
|
|
356
|
+
<Text hasTabularNumbers>{item.avgTime}</Text>
|
|
357
|
+
),
|
|
358
|
+
},
|
|
359
|
+
];
|
|
360
|
+
|
|
361
|
+
// Engagement — Top events
|
|
362
|
+
interface EventRow extends Record<string, unknown> {
|
|
363
|
+
id: string;
|
|
364
|
+
event: string;
|
|
365
|
+
count: number;
|
|
366
|
+
users: number;
|
|
367
|
+
newUsers: number;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const topEventsData: EventRow[] = [
|
|
371
|
+
{id: '1', event: 'page_view', count: 18420, users: 12300, newUsers: 4920},
|
|
372
|
+
{id: '2', event: 'session_start', count: 14850, users: 9870, newUsers: 3950},
|
|
373
|
+
{id: '3', event: 'first_visit', count: 8230, users: 8230, newUsers: 8230},
|
|
374
|
+
{id: '4', event: 'user_engagement', count: 6120, users: 4510, newUsers: 1580},
|
|
375
|
+
{id: '5', event: 'click', count: 3540, users: 2680, newUsers: 940},
|
|
376
|
+
{id: '6', event: 'scroll', count: 2910, users: 2140, newUsers: 750},
|
|
377
|
+
{id: '7', event: 'form_submit', count: 1870, users: 1350, newUsers: 540},
|
|
378
|
+
{id: '8', event: 'video_play', count: 1240, users: 980, newUsers: 390},
|
|
379
|
+
{id: '9', event: 'search', count: 960, users: 720, newUsers: 290},
|
|
380
|
+
{id: '10', event: 'share', count: 580, users: 410, newUsers: 160},
|
|
381
|
+
];
|
|
382
|
+
|
|
383
|
+
const topEventsMaxCount = Math.max(...topEventsData.map(d => d.count));
|
|
384
|
+
|
|
385
|
+
const topEventsColumns: TableColumn<EventRow>[] = [
|
|
386
|
+
{key: 'event', header: 'Event', width: pixel(160)},
|
|
387
|
+
{
|
|
388
|
+
key: 'count',
|
|
389
|
+
header: 'Count',
|
|
390
|
+
width: proportional(1),
|
|
391
|
+
renderCell: (item: EventRow) => (
|
|
392
|
+
<VStack gap={1}>
|
|
393
|
+
<ProgressBar
|
|
394
|
+
value={item.count}
|
|
395
|
+
max={topEventsMaxCount}
|
|
396
|
+
label={`${item.count}`}
|
|
397
|
+
isLabelHidden
|
|
398
|
+
/>
|
|
399
|
+
<Text type="supporting" hasTabularNumbers>
|
|
400
|
+
{item.count.toLocaleString()}
|
|
401
|
+
</Text>
|
|
402
|
+
</VStack>
|
|
403
|
+
),
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
key: 'users',
|
|
407
|
+
header: 'Users',
|
|
408
|
+
width: pixel(120),
|
|
409
|
+
renderCell: (item: EventRow) => (
|
|
410
|
+
<Text hasTabularNumbers>{item.users.toLocaleString()}</Text>
|
|
411
|
+
),
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
key: 'newUsers',
|
|
415
|
+
header: 'New Users',
|
|
416
|
+
width: pixel(120),
|
|
417
|
+
renderCell: (item: EventRow) => (
|
|
418
|
+
<Text hasTabularNumbers>{item.newUsers.toLocaleString()}</Text>
|
|
419
|
+
),
|
|
420
|
+
},
|
|
421
|
+
];
|
|
422
|
+
|
|
423
|
+
// ============= CHART COMPONENTS =============
|
|
424
|
+
|
|
425
|
+
// Chart series colors: desktop glows orange, mobile purple
|
|
426
|
+
const chartColors = {
|
|
427
|
+
desktop: 'var(--dracula-orange)',
|
|
428
|
+
mobile: 'var(--dracula-purple)',
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
function ChartLegendItem({color, label}: {color: string; label: string}) {
|
|
432
|
+
return (
|
|
433
|
+
<HStack gap={2} vAlign="center">
|
|
434
|
+
<Icon icon={Square} size="xsm" style={{color}} />
|
|
435
|
+
<Text type="supporting" color="secondary">
|
|
436
|
+
{label}
|
|
437
|
+
</Text>
|
|
438
|
+
</HStack>
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function ActiveUsersChart() {
|
|
443
|
+
// Downsample the 96 quarter-hour points to one bar per hour, stacking
|
|
444
|
+
// mobile atop desktop so each bar total is all users
|
|
445
|
+
const bars = activeUsersData.filter((_, i) => i % 4 === 0);
|
|
446
|
+
const max = 130;
|
|
447
|
+
const tickHours = [0, 32, 64, 92];
|
|
448
|
+
// SVG type scales up with the viewBox, so unit sizes stay small
|
|
449
|
+
return (
|
|
450
|
+
<VStack gap={3}>
|
|
451
|
+
<Card
|
|
452
|
+
padding={3}
|
|
453
|
+
style={{
|
|
454
|
+
backgroundColor: 'var(--color-background)',
|
|
455
|
+
border: 'var(--border-width) solid var(--color-separator)',
|
|
456
|
+
}}>
|
|
457
|
+
<svg
|
|
458
|
+
viewBox="0 0 540 180"
|
|
459
|
+
width="100%"
|
|
460
|
+
role="img"
|
|
461
|
+
aria-label="Hourly active users, desktop and mobile stacked">
|
|
462
|
+
<line x1="20" y1="30" x2="520" y2="30" stroke="var(--color-separator)" strokeDasharray="3 3" opacity={0.5} />
|
|
463
|
+
<line x1="20" y1="80" x2="520" y2="80" stroke="var(--color-separator)" strokeDasharray="3 3" opacity={0.5} />
|
|
464
|
+
<line x1="20" y1="130" x2="520" y2="130" stroke="var(--color-separator)" />
|
|
465
|
+
{bars.map((d, i) => {
|
|
466
|
+
const hAll = (d.allUsers / max) * 110;
|
|
467
|
+
const hDesktop = (d.desktop / max) * 110;
|
|
468
|
+
const x = 32 + i * 21;
|
|
469
|
+
return (
|
|
470
|
+
<g key={d.hour}>
|
|
471
|
+
<rect
|
|
472
|
+
x={x}
|
|
473
|
+
y={130 - hAll}
|
|
474
|
+
width={14}
|
|
475
|
+
height={hAll - hDesktop}
|
|
476
|
+
rx={4}
|
|
477
|
+
fill={chartColors.mobile}
|
|
478
|
+
/>
|
|
479
|
+
<rect
|
|
480
|
+
x={x}
|
|
481
|
+
y={130 - hDesktop}
|
|
482
|
+
width={14}
|
|
483
|
+
height={hDesktop}
|
|
484
|
+
rx={4}
|
|
485
|
+
fill={chartColors.desktop}
|
|
486
|
+
/>
|
|
487
|
+
{(i % 6 === 0 || i === bars.length - 1) && (
|
|
488
|
+
<text
|
|
489
|
+
x={x + 7}
|
|
490
|
+
y={120 - hAll}
|
|
491
|
+
textAnchor="middle"
|
|
492
|
+
fontSize={5.5}
|
|
493
|
+
fill="var(--color-text-highlight)"
|
|
494
|
+
fontFamily="var(--font-family-mono)">
|
|
495
|
+
{d.allUsers}
|
|
496
|
+
</text>
|
|
497
|
+
)}
|
|
498
|
+
{tickHours.includes(d.hour) && (
|
|
499
|
+
<text
|
|
500
|
+
x={x + 7}
|
|
501
|
+
y={150}
|
|
502
|
+
textAnchor="middle"
|
|
503
|
+
fontSize={5.5}
|
|
504
|
+
fill="var(--color-text-paragraph)"
|
|
505
|
+
fontFamily="var(--font-family-mono)">
|
|
506
|
+
{d.label}
|
|
507
|
+
</text>
|
|
508
|
+
)}
|
|
509
|
+
</g>
|
|
510
|
+
);
|
|
511
|
+
})}
|
|
512
|
+
</svg>
|
|
513
|
+
</Card>
|
|
514
|
+
<Text type="supporting" color="secondary">
|
|
515
|
+
Hourly intervals · trailing 24 hours
|
|
516
|
+
</Text>
|
|
517
|
+
<HStack gap={6} vAlign="center">
|
|
518
|
+
<ChartLegendItem color={chartColors.desktop} label="Desktop" />
|
|
519
|
+
<ChartLegendItem color={chartColors.mobile} label="Mobile" />
|
|
520
|
+
</HStack>
|
|
521
|
+
</VStack>
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function Sparkline({data, label}: {data: number[]; label: string}) {
|
|
526
|
+
const max = Math.max(...data);
|
|
527
|
+
return (
|
|
528
|
+
<svg
|
|
529
|
+
viewBox="0 0 300 40"
|
|
530
|
+
width="100%"
|
|
531
|
+
height={40}
|
|
532
|
+
role="img"
|
|
533
|
+
aria-label={`${label} thirty-day trend`}>
|
|
534
|
+
{data.map((v, i) => {
|
|
535
|
+
const h = Math.max(3, (v / max) * 32);
|
|
536
|
+
return (
|
|
537
|
+
<rect
|
|
538
|
+
key={i}
|
|
539
|
+
x={i * 10}
|
|
540
|
+
y={36 - h}
|
|
541
|
+
width={7}
|
|
542
|
+
height={h}
|
|
543
|
+
rx={4}
|
|
544
|
+
fill="var(--dracula-purple)"
|
|
545
|
+
/>
|
|
546
|
+
);
|
|
547
|
+
})}
|
|
548
|
+
</svg>
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ============= CARD COMPONENTS =============
|
|
553
|
+
|
|
554
|
+
function MetricCard({
|
|
555
|
+
label,
|
|
556
|
+
value,
|
|
557
|
+
change,
|
|
558
|
+
positive,
|
|
559
|
+
sparkline,
|
|
560
|
+
}: {
|
|
561
|
+
label: string;
|
|
562
|
+
value: string;
|
|
563
|
+
change: string;
|
|
564
|
+
positive: boolean;
|
|
565
|
+
sparkline: number[];
|
|
566
|
+
}) {
|
|
567
|
+
return (
|
|
568
|
+
<Card>
|
|
569
|
+
<VStack gap={2}>
|
|
570
|
+
<Heading level={4}>{label}</Heading>
|
|
571
|
+
<HStack gap={2} vAlign="center">
|
|
572
|
+
<Heading level={2}>{value}</Heading>
|
|
573
|
+
<HStack gap={1} vAlign="center">
|
|
574
|
+
<Icon
|
|
575
|
+
icon={change.trim().startsWith('-') ? ArrowDown : ArrowUp}
|
|
576
|
+
size="xsm"
|
|
577
|
+
color={positive ? 'success' : 'error'}
|
|
578
|
+
/>
|
|
579
|
+
<Text type="body" color="secondary" hasTabularNumbers>
|
|
580
|
+
{change}
|
|
581
|
+
</Text>
|
|
582
|
+
</HStack>
|
|
583
|
+
</HStack>
|
|
584
|
+
<Text type="supporting" color="secondary">
|
|
585
|
+
Last 30 days vs. Previous
|
|
586
|
+
</Text>
|
|
587
|
+
<Sparkline data={sparkline} label={label} />
|
|
588
|
+
</VStack>
|
|
589
|
+
</Card>
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function StackedBarCard({
|
|
594
|
+
title,
|
|
595
|
+
data,
|
|
596
|
+
}: {
|
|
597
|
+
title: string;
|
|
598
|
+
data: Array<{label: string; value: number; color: string}>;
|
|
599
|
+
}) {
|
|
600
|
+
const total = data.reduce((sum, d) => sum + d.value, 0);
|
|
601
|
+
const clipId = `stacked-${title.toLowerCase().replace(/[^a-z]+/g, '-')}`;
|
|
602
|
+
let offset = 0;
|
|
603
|
+
const segments = data.map(d => {
|
|
604
|
+
const x = (offset / total) * 540;
|
|
605
|
+
offset += d.value;
|
|
606
|
+
return {...d, x, width: (d.value / total) * 540};
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
return (
|
|
610
|
+
<Card>
|
|
611
|
+
<VStack gap={4}>
|
|
612
|
+
<Heading level={4}>{title}</Heading>
|
|
613
|
+
<svg
|
|
614
|
+
viewBox="0 0 540 24"
|
|
615
|
+
width="100%"
|
|
616
|
+
height={24}
|
|
617
|
+
role="img"
|
|
618
|
+
aria-label={`${title} distribution strip`}>
|
|
619
|
+
<defs>
|
|
620
|
+
<clipPath id={clipId}>
|
|
621
|
+
<rect width={540} height={24} rx={4} />
|
|
622
|
+
</clipPath>
|
|
623
|
+
</defs>
|
|
624
|
+
<g clipPath={`url(#${clipId})`}>
|
|
625
|
+
{segments.map(s => (
|
|
626
|
+
<rect
|
|
627
|
+
key={s.label}
|
|
628
|
+
x={s.x}
|
|
629
|
+
y={0}
|
|
630
|
+
width={s.width}
|
|
631
|
+
height={24}
|
|
632
|
+
fill={s.color}
|
|
633
|
+
/>
|
|
634
|
+
))}
|
|
635
|
+
</g>
|
|
636
|
+
</svg>
|
|
637
|
+
{/* Legend */}
|
|
638
|
+
<HStack gap={4} wrap="wrap">
|
|
639
|
+
{data.map(d => (
|
|
640
|
+
<VStack key={d.label} gap={0}>
|
|
641
|
+
<HStack gap={2} vAlign="center">
|
|
642
|
+
<Icon icon={Square} size="xsm" style={{color: d.color}} />
|
|
643
|
+
<Text type="supporting">{d.label}</Text>
|
|
644
|
+
</HStack>
|
|
645
|
+
<Text type="supporting" color="secondary" hasTabularNumbers>
|
|
646
|
+
{((d.value / total) * 100).toFixed(0)}%
|
|
647
|
+
</Text>
|
|
648
|
+
</VStack>
|
|
649
|
+
))}
|
|
650
|
+
</HStack>
|
|
651
|
+
</VStack>
|
|
652
|
+
</Card>
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// ============= TABLE COMPONENTS =============
|
|
657
|
+
|
|
658
|
+
function TableCard<T extends {id: string}>({
|
|
659
|
+
title,
|
|
660
|
+
linkLabel,
|
|
661
|
+
linkHref,
|
|
662
|
+
data,
|
|
663
|
+
columns,
|
|
664
|
+
}: {
|
|
665
|
+
title: string;
|
|
666
|
+
linkLabel: string;
|
|
667
|
+
linkHref: string;
|
|
668
|
+
data: T[];
|
|
669
|
+
columns: TableColumn<T>[];
|
|
670
|
+
}) {
|
|
671
|
+
return (
|
|
672
|
+
<Card>
|
|
673
|
+
<VStack gap={6}>
|
|
674
|
+
<HStack hAlign="between" vAlign="center">
|
|
675
|
+
<Heading level={4}>{title}</Heading>
|
|
676
|
+
<Link href={linkHref}>{linkLabel}</Link>
|
|
677
|
+
</HStack>
|
|
678
|
+
<Table<T>
|
|
679
|
+
data={data}
|
|
680
|
+
columns={columns}
|
|
681
|
+
idKey="id"
|
|
682
|
+
density="compact"
|
|
683
|
+
dividers="rows"
|
|
684
|
+
hasHover
|
|
685
|
+
/>
|
|
686
|
+
</VStack>
|
|
687
|
+
</Card>
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// ============= SIDENAV =============
|
|
692
|
+
|
|
693
|
+
// ============= MAIN COMPONENT =============
|
|
694
|
+
|
|
695
|
+
export default function DashboardTemplate() {
|
|
696
|
+
return (
|
|
697
|
+
<Layout
|
|
698
|
+
height="fill"
|
|
699
|
+
content={
|
|
700
|
+
<LayoutContent padding={6}>
|
|
701
|
+
<VStack gap={6}>
|
|
702
|
+
{/* Active Users Chart */}
|
|
703
|
+
<VStack gap={6}>
|
|
704
|
+
<HStack hAlign="between" vAlign="center">
|
|
705
|
+
<Heading level={3}>Awake after dark</Heading>
|
|
706
|
+
<Button
|
|
707
|
+
label="Reload"
|
|
708
|
+
variant="secondary"
|
|
709
|
+
size="md"
|
|
710
|
+
icon={<Icon icon={RefreshCw} size="sm" />}
|
|
711
|
+
/>
|
|
712
|
+
</HStack>
|
|
713
|
+
<ActiveUsersChart />
|
|
714
|
+
</VStack>
|
|
715
|
+
|
|
716
|
+
{/* Metric Cards */}
|
|
717
|
+
<Grid columns={{minWidth: 320, repeat: 'fit'}} gap={4}>
|
|
718
|
+
{[0, 2].map(start => (
|
|
719
|
+
<Grid
|
|
720
|
+
key={start}
|
|
721
|
+
columns={{minWidth: 240, repeat: 'fit'}}
|
|
722
|
+
gap={4}>
|
|
723
|
+
{metrics.slice(start, start + 2).map((m, i) => (
|
|
724
|
+
<MetricCard
|
|
725
|
+
key={m.label}
|
|
726
|
+
{...m}
|
|
727
|
+
sparkline={sparklines[start + i]}
|
|
728
|
+
/>
|
|
729
|
+
))}
|
|
730
|
+
</Grid>
|
|
731
|
+
))}
|
|
732
|
+
</Grid>
|
|
733
|
+
|
|
734
|
+
<Divider />
|
|
735
|
+
|
|
736
|
+
{/* Demographics */}
|
|
737
|
+
<HStack hAlign="between" vAlign="center">
|
|
738
|
+
<Heading level={3}>Night denizens</Heading>
|
|
739
|
+
<Button label="View more" variant="secondary" size="md" />
|
|
740
|
+
</HStack>
|
|
741
|
+
<Grid columns={{minWidth: 320, repeat: 'fit'}} gap={4}>
|
|
742
|
+
<StackedBarCard title="Territory" data={regionData} />
|
|
743
|
+
<StackedBarCard title="Role" data={roleData} />
|
|
744
|
+
</Grid>
|
|
745
|
+
|
|
746
|
+
<Divider />
|
|
747
|
+
|
|
748
|
+
{/* Engagement */}
|
|
749
|
+
<HStack hAlign="between" vAlign="center">
|
|
750
|
+
<Heading level={3}>Engagement</Heading>
|
|
751
|
+
<Button label="View more" variant="secondary" size="md" />
|
|
752
|
+
</HStack>
|
|
753
|
+
<Grid columns={{minWidth: 320, repeat: 'fit'}} gap={4}>
|
|
754
|
+
<TableCard
|
|
755
|
+
title="Top pages"
|
|
756
|
+
linkLabel="All pages"
|
|
757
|
+
linkHref="#"
|
|
758
|
+
data={topPagesData}
|
|
759
|
+
columns={topPagesColumns}
|
|
760
|
+
/>
|
|
761
|
+
<TableCard
|
|
762
|
+
title="Top events"
|
|
763
|
+
linkLabel="All events"
|
|
764
|
+
linkHref="#"
|
|
765
|
+
data={topEventsData}
|
|
766
|
+
columns={topEventsColumns}
|
|
767
|
+
/>
|
|
768
|
+
</Grid>
|
|
769
|
+
</VStack>
|
|
770
|
+
</LayoutContent>
|
|
771
|
+
}
|
|
772
|
+
/>
|
|
773
|
+
);
|
|
774
|
+
}
|