create-blitzpack 0.1.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.
Files changed (259) hide show
  1. package/dist/index.js +452 -0
  2. package/package.json +57 -0
  3. package/template/.dockerignore +59 -0
  4. package/template/.github/workflows/ci.yml +157 -0
  5. package/template/.husky/pre-commit +1 -0
  6. package/template/.husky/pre-push +1 -0
  7. package/template/.lintstagedrc.cjs +4 -0
  8. package/template/.nvmrc +1 -0
  9. package/template/.prettierrc +9 -0
  10. package/template/.vscode/settings.json +13 -0
  11. package/template/CLAUDE.md +175 -0
  12. package/template/CONTRIBUTING.md +32 -0
  13. package/template/Dockerfile +90 -0
  14. package/template/GETTING_STARTED.md +35 -0
  15. package/template/LICENSE +21 -0
  16. package/template/README.md +116 -0
  17. package/template/apps/api/.dockerignore +51 -0
  18. package/template/apps/api/.env.local.example +62 -0
  19. package/template/apps/api/emails/account-deleted-email.tsx +69 -0
  20. package/template/apps/api/emails/components/email-layout.tsx +154 -0
  21. package/template/apps/api/emails/config.ts +22 -0
  22. package/template/apps/api/emails/password-changed-email.tsx +88 -0
  23. package/template/apps/api/emails/password-reset-email.tsx +86 -0
  24. package/template/apps/api/emails/verification-email.tsx +85 -0
  25. package/template/apps/api/emails/welcome-email.tsx +70 -0
  26. package/template/apps/api/package.json +84 -0
  27. package/template/apps/api/prisma/migrations/20251012111439_init/migration.sql +13 -0
  28. package/template/apps/api/prisma/migrations/20251018162629_add_better_auth_fields/migration.sql +67 -0
  29. package/template/apps/api/prisma/migrations/20251019142208_add_user_role_enum/migration.sql +5 -0
  30. package/template/apps/api/prisma/migrations/20251019182151_user_auth/migration.sql +7 -0
  31. package/template/apps/api/prisma/migrations/20251019211416_faster_session_lookup/migration.sql +2 -0
  32. package/template/apps/api/prisma/migrations/20251119124337_add_upload_model/migration.sql +26 -0
  33. package/template/apps/api/prisma/migrations/20251120071241_add_scope_to_account/migration.sql +2 -0
  34. package/template/apps/api/prisma/migrations/20251120072608_add_oauth_token_expiration_fields/migration.sql +10 -0
  35. package/template/apps/api/prisma/migrations/20251120144705_add_audit_logs/migration.sql +29 -0
  36. package/template/apps/api/prisma/migrations/20251127123614_remove_impersonated_by/migration.sql +8 -0
  37. package/template/apps/api/prisma/migrations/20251127125630_remove_audit_logs/migration.sql +11 -0
  38. package/template/apps/api/prisma/migrations/migration_lock.toml +3 -0
  39. package/template/apps/api/prisma/schema.prisma +116 -0
  40. package/template/apps/api/prisma/seed.ts +159 -0
  41. package/template/apps/api/prisma.config.ts +14 -0
  42. package/template/apps/api/src/app.ts +377 -0
  43. package/template/apps/api/src/common/logger.service.ts +227 -0
  44. package/template/apps/api/src/config/env.ts +60 -0
  45. package/template/apps/api/src/config/rate-limit.ts +29 -0
  46. package/template/apps/api/src/hooks/auth.ts +122 -0
  47. package/template/apps/api/src/plugins/auth.ts +198 -0
  48. package/template/apps/api/src/plugins/database.ts +45 -0
  49. package/template/apps/api/src/plugins/logger.ts +33 -0
  50. package/template/apps/api/src/plugins/multipart.ts +16 -0
  51. package/template/apps/api/src/plugins/scalar.ts +20 -0
  52. package/template/apps/api/src/plugins/schedule.ts +52 -0
  53. package/template/apps/api/src/plugins/services.ts +66 -0
  54. package/template/apps/api/src/plugins/swagger.ts +56 -0
  55. package/template/apps/api/src/routes/accounts.ts +91 -0
  56. package/template/apps/api/src/routes/admin-sessions.ts +92 -0
  57. package/template/apps/api/src/routes/metrics.ts +71 -0
  58. package/template/apps/api/src/routes/password.ts +46 -0
  59. package/template/apps/api/src/routes/sessions.ts +53 -0
  60. package/template/apps/api/src/routes/stats.ts +38 -0
  61. package/template/apps/api/src/routes/uploads-serve.ts +27 -0
  62. package/template/apps/api/src/routes/uploads.ts +154 -0
  63. package/template/apps/api/src/routes/users.ts +114 -0
  64. package/template/apps/api/src/routes/verification.ts +90 -0
  65. package/template/apps/api/src/server.ts +34 -0
  66. package/template/apps/api/src/services/accounts.service.ts +125 -0
  67. package/template/apps/api/src/services/authorization.service.ts +162 -0
  68. package/template/apps/api/src/services/email.service.ts +170 -0
  69. package/template/apps/api/src/services/file-storage.service.ts +267 -0
  70. package/template/apps/api/src/services/metrics.service.ts +175 -0
  71. package/template/apps/api/src/services/password.service.ts +56 -0
  72. package/template/apps/api/src/services/sessions.service.spec.ts +134 -0
  73. package/template/apps/api/src/services/sessions.service.ts +276 -0
  74. package/template/apps/api/src/services/stats.service.ts +273 -0
  75. package/template/apps/api/src/services/uploads.service.ts +163 -0
  76. package/template/apps/api/src/services/users.service.spec.ts +249 -0
  77. package/template/apps/api/src/services/users.service.ts +198 -0
  78. package/template/apps/api/src/utils/file-validation.ts +108 -0
  79. package/template/apps/api/start.sh +33 -0
  80. package/template/apps/api/test/helpers/fastify-app.ts +24 -0
  81. package/template/apps/api/test/helpers/mock-authorization.ts +16 -0
  82. package/template/apps/api/test/helpers/mock-logger.ts +28 -0
  83. package/template/apps/api/test/helpers/mock-prisma.ts +30 -0
  84. package/template/apps/api/test/helpers/test-db.ts +125 -0
  85. package/template/apps/api/test/integration/auth-flow.integration.spec.ts +449 -0
  86. package/template/apps/api/test/integration/password.integration.spec.ts +427 -0
  87. package/template/apps/api/test/integration/rate-limit.integration.spec.ts +51 -0
  88. package/template/apps/api/test/integration/sessions.integration.spec.ts +445 -0
  89. package/template/apps/api/test/integration/users.integration.spec.ts +211 -0
  90. package/template/apps/api/test/setup.ts +31 -0
  91. package/template/apps/api/tsconfig.json +26 -0
  92. package/template/apps/api/vitest.config.ts +35 -0
  93. package/template/apps/web/.env.local.example +11 -0
  94. package/template/apps/web/components.json +24 -0
  95. package/template/apps/web/next.config.ts +22 -0
  96. package/template/apps/web/package.json +56 -0
  97. package/template/apps/web/postcss.config.js +5 -0
  98. package/template/apps/web/public/apple-icon.png +0 -0
  99. package/template/apps/web/public/icon.png +0 -0
  100. package/template/apps/web/public/robots.txt +3 -0
  101. package/template/apps/web/src/app/(admin)/admin/layout.tsx +222 -0
  102. package/template/apps/web/src/app/(admin)/admin/page.tsx +157 -0
  103. package/template/apps/web/src/app/(admin)/admin/sessions/page.tsx +18 -0
  104. package/template/apps/web/src/app/(admin)/admin/users/page.tsx +20 -0
  105. package/template/apps/web/src/app/(auth)/forgot-password/page.tsx +177 -0
  106. package/template/apps/web/src/app/(auth)/login/page.tsx +159 -0
  107. package/template/apps/web/src/app/(auth)/reset-password/page.tsx +245 -0
  108. package/template/apps/web/src/app/(auth)/signup/page.tsx +153 -0
  109. package/template/apps/web/src/app/dashboard/change-password/page.tsx +255 -0
  110. package/template/apps/web/src/app/dashboard/page.tsx +296 -0
  111. package/template/apps/web/src/app/error.tsx +32 -0
  112. package/template/apps/web/src/app/examples/file-upload/page.tsx +200 -0
  113. package/template/apps/web/src/app/favicon.ico +0 -0
  114. package/template/apps/web/src/app/global-error.tsx +96 -0
  115. package/template/apps/web/src/app/globals.css +22 -0
  116. package/template/apps/web/src/app/icon.png +0 -0
  117. package/template/apps/web/src/app/layout.tsx +34 -0
  118. package/template/apps/web/src/app/not-found.tsx +28 -0
  119. package/template/apps/web/src/app/page.tsx +192 -0
  120. package/template/apps/web/src/components/admin/activity-feed.tsx +101 -0
  121. package/template/apps/web/src/components/admin/charts/auth-breakdown-chart.tsx +114 -0
  122. package/template/apps/web/src/components/admin/charts/chart-tooltip.tsx +124 -0
  123. package/template/apps/web/src/components/admin/charts/realtime-metrics-chart.tsx +511 -0
  124. package/template/apps/web/src/components/admin/charts/role-distribution-chart.tsx +102 -0
  125. package/template/apps/web/src/components/admin/charts/session-activity-chart.tsx +90 -0
  126. package/template/apps/web/src/components/admin/charts/user-growth-chart.tsx +108 -0
  127. package/template/apps/web/src/components/admin/health-indicator.tsx +175 -0
  128. package/template/apps/web/src/components/admin/refresh-control.tsx +90 -0
  129. package/template/apps/web/src/components/admin/session-revoke-all-dialog.tsx +79 -0
  130. package/template/apps/web/src/components/admin/session-revoke-dialog.tsx +74 -0
  131. package/template/apps/web/src/components/admin/sessions-management-table.tsx +372 -0
  132. package/template/apps/web/src/components/admin/stat-card.tsx +137 -0
  133. package/template/apps/web/src/components/admin/user-create-dialog.tsx +152 -0
  134. package/template/apps/web/src/components/admin/user-delete-dialog.tsx +73 -0
  135. package/template/apps/web/src/components/admin/user-edit-dialog.tsx +170 -0
  136. package/template/apps/web/src/components/admin/users-management-table.tsx +285 -0
  137. package/template/apps/web/src/components/auth/email-verification-banner.tsx +85 -0
  138. package/template/apps/web/src/components/auth/github-button.tsx +40 -0
  139. package/template/apps/web/src/components/auth/google-button.tsx +54 -0
  140. package/template/apps/web/src/components/auth/protected-route.tsx +66 -0
  141. package/template/apps/web/src/components/auth/redirect-if-authenticated.tsx +31 -0
  142. package/template/apps/web/src/components/auth/with-auth.tsx +30 -0
  143. package/template/apps/web/src/components/error/error-card.tsx +47 -0
  144. package/template/apps/web/src/components/error/forbidden.tsx +25 -0
  145. package/template/apps/web/src/components/landing/command-block.tsx +64 -0
  146. package/template/apps/web/src/components/landing/feature-card.tsx +60 -0
  147. package/template/apps/web/src/components/landing/included-feature-card.tsx +63 -0
  148. package/template/apps/web/src/components/landing/logo.tsx +41 -0
  149. package/template/apps/web/src/components/landing/tech-badge.tsx +11 -0
  150. package/template/apps/web/src/components/layout/auth-nav.tsx +58 -0
  151. package/template/apps/web/src/components/layout/footer.tsx +3 -0
  152. package/template/apps/web/src/config/landing-data.ts +152 -0
  153. package/template/apps/web/src/config/site.ts +5 -0
  154. package/template/apps/web/src/hooks/api/__tests__/use-users.test.tsx +181 -0
  155. package/template/apps/web/src/hooks/api/use-admin-sessions.ts +75 -0
  156. package/template/apps/web/src/hooks/api/use-admin-stats.ts +33 -0
  157. package/template/apps/web/src/hooks/api/use-sessions.ts +52 -0
  158. package/template/apps/web/src/hooks/api/use-uploads.ts +156 -0
  159. package/template/apps/web/src/hooks/api/use-users.ts +149 -0
  160. package/template/apps/web/src/hooks/use-mobile.ts +21 -0
  161. package/template/apps/web/src/hooks/use-realtime-metrics.ts +120 -0
  162. package/template/apps/web/src/lib/__tests__/utils.test.ts +29 -0
  163. package/template/apps/web/src/lib/api.ts +151 -0
  164. package/template/apps/web/src/lib/auth.ts +13 -0
  165. package/template/apps/web/src/lib/env.ts +52 -0
  166. package/template/apps/web/src/lib/form-utils.ts +11 -0
  167. package/template/apps/web/src/lib/utils.ts +1 -0
  168. package/template/apps/web/src/providers.tsx +34 -0
  169. package/template/apps/web/src/store/atoms.ts +15 -0
  170. package/template/apps/web/src/test/helpers/test-utils.tsx +44 -0
  171. package/template/apps/web/src/test/setup.ts +8 -0
  172. package/template/apps/web/tailwind.config.ts +5 -0
  173. package/template/apps/web/tsconfig.json +26 -0
  174. package/template/apps/web/vitest.config.ts +32 -0
  175. package/template/assets/logo-512.png +0 -0
  176. package/template/assets/logo.svg +4 -0
  177. package/template/docker-compose.prod.yml +66 -0
  178. package/template/docker-compose.yml +36 -0
  179. package/template/eslint.config.ts +119 -0
  180. package/template/package.json +77 -0
  181. package/template/packages/tailwind-config/package.json +9 -0
  182. package/template/packages/tailwind-config/theme.css +179 -0
  183. package/template/packages/types/package.json +29 -0
  184. package/template/packages/types/src/__tests__/schemas.test.ts +255 -0
  185. package/template/packages/types/src/api-response.ts +53 -0
  186. package/template/packages/types/src/health-check.ts +11 -0
  187. package/template/packages/types/src/pagination.ts +41 -0
  188. package/template/packages/types/src/role.ts +5 -0
  189. package/template/packages/types/src/session.ts +48 -0
  190. package/template/packages/types/src/stats.ts +113 -0
  191. package/template/packages/types/src/upload.ts +51 -0
  192. package/template/packages/types/src/user.ts +36 -0
  193. package/template/packages/types/tsconfig.json +5 -0
  194. package/template/packages/types/vitest.config.ts +21 -0
  195. package/template/packages/ui/components.json +21 -0
  196. package/template/packages/ui/package.json +108 -0
  197. package/template/packages/ui/src/__tests__/button.test.tsx +70 -0
  198. package/template/packages/ui/src/alert-dialog.tsx +141 -0
  199. package/template/packages/ui/src/alert.tsx +66 -0
  200. package/template/packages/ui/src/animated-theme-toggler.tsx +167 -0
  201. package/template/packages/ui/src/avatar.tsx +53 -0
  202. package/template/packages/ui/src/badge.tsx +36 -0
  203. package/template/packages/ui/src/button.tsx +84 -0
  204. package/template/packages/ui/src/card.tsx +92 -0
  205. package/template/packages/ui/src/checkbox.tsx +32 -0
  206. package/template/packages/ui/src/data-table/data-table-column-header.tsx +68 -0
  207. package/template/packages/ui/src/data-table/data-table-pagination.tsx +99 -0
  208. package/template/packages/ui/src/data-table/data-table-toolbar.tsx +55 -0
  209. package/template/packages/ui/src/data-table/data-table-view-options.tsx +63 -0
  210. package/template/packages/ui/src/data-table/data-table.tsx +167 -0
  211. package/template/packages/ui/src/dialog.tsx +143 -0
  212. package/template/packages/ui/src/dropdown-menu.tsx +257 -0
  213. package/template/packages/ui/src/empty-state.tsx +52 -0
  214. package/template/packages/ui/src/file-upload-input.tsx +202 -0
  215. package/template/packages/ui/src/form.tsx +168 -0
  216. package/template/packages/ui/src/hooks/use-mobile.ts +19 -0
  217. package/template/packages/ui/src/icons/brand-icons.tsx +16 -0
  218. package/template/packages/ui/src/input.tsx +21 -0
  219. package/template/packages/ui/src/label.tsx +24 -0
  220. package/template/packages/ui/src/lib/utils.ts +6 -0
  221. package/template/packages/ui/src/password-input.tsx +102 -0
  222. package/template/packages/ui/src/popover.tsx +48 -0
  223. package/template/packages/ui/src/radio-group.tsx +45 -0
  224. package/template/packages/ui/src/scroll-area.tsx +58 -0
  225. package/template/packages/ui/src/select.tsx +187 -0
  226. package/template/packages/ui/src/separator.tsx +28 -0
  227. package/template/packages/ui/src/sheet.tsx +139 -0
  228. package/template/packages/ui/src/sidebar.tsx +726 -0
  229. package/template/packages/ui/src/skeleton-variants.tsx +87 -0
  230. package/template/packages/ui/src/skeleton.tsx +13 -0
  231. package/template/packages/ui/src/slider.tsx +63 -0
  232. package/template/packages/ui/src/sonner.tsx +25 -0
  233. package/template/packages/ui/src/spinner.tsx +16 -0
  234. package/template/packages/ui/src/switch.tsx +31 -0
  235. package/template/packages/ui/src/table.tsx +116 -0
  236. package/template/packages/ui/src/tabs.tsx +66 -0
  237. package/template/packages/ui/src/textarea.tsx +18 -0
  238. package/template/packages/ui/src/tooltip.tsx +61 -0
  239. package/template/packages/ui/src/user-avatar.tsx +97 -0
  240. package/template/packages/ui/test-config.js +3 -0
  241. package/template/packages/ui/tsconfig.json +12 -0
  242. package/template/packages/ui/turbo.json +18 -0
  243. package/template/packages/ui/vitest.config.ts +17 -0
  244. package/template/packages/ui/vitest.setup.ts +1 -0
  245. package/template/packages/utils/package.json +23 -0
  246. package/template/packages/utils/src/__tests__/utils.test.ts +223 -0
  247. package/template/packages/utils/src/array.ts +18 -0
  248. package/template/packages/utils/src/async.ts +3 -0
  249. package/template/packages/utils/src/date.ts +77 -0
  250. package/template/packages/utils/src/errors.ts +73 -0
  251. package/template/packages/utils/src/number.ts +11 -0
  252. package/template/packages/utils/src/string.ts +13 -0
  253. package/template/packages/utils/tsconfig.json +5 -0
  254. package/template/packages/utils/vitest.config.ts +21 -0
  255. package/template/pnpm-workspace.yaml +4 -0
  256. package/template/tsconfig.base.json +32 -0
  257. package/template/turbo.json +133 -0
  258. package/template/vitest.shared.ts +26 -0
  259. package/template/vitest.workspace.ts +9 -0
@@ -0,0 +1,511 @@
1
+ 'use client';
2
+
3
+ import type { RealtimeMetricsPoint } from '@repo/packages-types/stats';
4
+ import { cn } from '@repo/packages-ui/lib/utils';
5
+ import { Skeleton } from '@repo/packages-ui/skeleton';
6
+ import { formatTime } from '@repo/packages-utils/date';
7
+ import { AnimatePresence, motion } from 'framer-motion';
8
+ import {
9
+ Activity,
10
+ AlertTriangle,
11
+ Cpu,
12
+ HardDrive,
13
+ Pause,
14
+ Timer,
15
+ Wifi,
16
+ WifiOff,
17
+ } from 'lucide-react';
18
+ import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
19
+ import {
20
+ Area,
21
+ AreaChart,
22
+ CartesianGrid,
23
+ ReferenceDot,
24
+ ResponsiveContainer,
25
+ Tooltip,
26
+ XAxis,
27
+ YAxis,
28
+ } from 'recharts';
29
+
30
+ import { useRealtimeMetrics } from '@/hooks/use-realtime-metrics';
31
+
32
+ type MetricType = 'memory' | 'cpu' | 'requests' | 'responseTime' | 'errorRate';
33
+
34
+ interface MetricConfig {
35
+ key: MetricType;
36
+ label: string;
37
+ color: string;
38
+ gradientId: string;
39
+ icon: typeof Cpu;
40
+ unit: string;
41
+ getValue: (point: RealtimeMetricsPoint) => number;
42
+ format: (value: number, point?: RealtimeMetricsPoint) => string;
43
+ }
44
+
45
+ const METRICS: MetricConfig[] = [
46
+ {
47
+ key: 'memory',
48
+ label: 'Memory',
49
+ color: '#8b5cf6',
50
+ gradientId: 'memoryGradient',
51
+ icon: HardDrive,
52
+ unit: '%',
53
+ getValue: (p) => p.memory.usedPercent,
54
+ format: (v, p) =>
55
+ p
56
+ ? `${v.toFixed(1)}% (${p.memory.heapUsedMB.toFixed(0)} MB)`
57
+ : `${v.toFixed(1)}%`,
58
+ },
59
+ {
60
+ key: 'cpu',
61
+ label: 'CPU',
62
+ color: '#f59e0b',
63
+ gradientId: 'cpuGradient',
64
+ icon: Cpu,
65
+ unit: '%',
66
+ getValue: (p) => p.cpu.percentage,
67
+ format: (v) => `${v.toFixed(1)}%`,
68
+ },
69
+ {
70
+ key: 'requests',
71
+ label: 'Req/s',
72
+ color: '#10b981',
73
+ gradientId: 'requestsGradient',
74
+ icon: Activity,
75
+ unit: '/s',
76
+ getValue: (p) => p.requests.perSecond,
77
+ format: (v) => `${v.toFixed(1)}/s`,
78
+ },
79
+ {
80
+ key: 'responseTime',
81
+ label: 'Latency',
82
+ color: '#3b82f6',
83
+ gradientId: 'responseTimeGradient',
84
+ icon: Timer,
85
+ unit: 'ms',
86
+ getValue: (p) => p.requests.avgResponseTimeMs,
87
+ format: (v) => `${v.toFixed(0)}ms`,
88
+ },
89
+ {
90
+ key: 'errorRate',
91
+ label: 'Error Rate',
92
+ color: '#ef4444',
93
+ gradientId: 'errorRateGradient',
94
+ icon: AlertTriangle,
95
+ unit: '%',
96
+ getValue: (p) => p.errors.rate,
97
+ format: (v) => `${v.toFixed(2)}%`,
98
+ },
99
+ ];
100
+
101
+ interface MetricTabProps {
102
+ config: MetricConfig;
103
+ isActive: boolean;
104
+ onClick: () => void;
105
+ }
106
+
107
+ function MetricTab({ config, isActive, onClick }: MetricTabProps) {
108
+ const Icon = config.icon;
109
+
110
+ return (
111
+ <button
112
+ onClick={onClick}
113
+ className={cn(
114
+ 'relative flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium transition-colors',
115
+ isActive
116
+ ? 'text-foreground'
117
+ : 'text-muted-foreground hover:text-foreground'
118
+ )}
119
+ >
120
+ <Icon className="size-3.5" />
121
+ <span>{config.label}</span>
122
+ {isActive && (
123
+ <motion.div
124
+ layoutId="activeMetricTab"
125
+ className="bg-foreground absolute inset-x-0 -bottom-px h-0.5"
126
+ transition={{ type: 'spring', stiffness: 500, damping: 30 }}
127
+ />
128
+ )}
129
+ </button>
130
+ );
131
+ }
132
+
133
+ interface CustomTooltipProps {
134
+ active?: boolean;
135
+ payload?: Array<{ value: number; payload: RealtimeMetricsPoint }>;
136
+ activeMetric: MetricConfig;
137
+ }
138
+
139
+ function CustomTooltip({ active, payload, activeMetric }: CustomTooltipProps) {
140
+ if (!active || !payload || payload.length === 0) return null;
141
+
142
+ const point = payload[0].payload;
143
+ const Icon = activeMetric.icon;
144
+
145
+ return (
146
+ <motion.div
147
+ initial={{ opacity: 0, y: 5 }}
148
+ animate={{ opacity: 1, y: 0 }}
149
+ className="rounded-lg border border-zinc-200 bg-white/95 px-3 py-2 shadow-lg backdrop-blur dark:border-zinc-700 dark:bg-zinc-800/95"
150
+ >
151
+ <p className="mb-1 text-[10px] text-zinc-500 dark:text-zinc-400">
152
+ {formatTime(point.timestamp)}
153
+ </p>
154
+ <div className="flex items-center gap-2">
155
+ <Icon className="size-4" style={{ color: activeMetric.color }} />
156
+ <span className="font-semibold" style={{ color: activeMetric.color }}>
157
+ {activeMetric.format(activeMetric.getValue(point), point)}
158
+ </span>
159
+ </div>
160
+ </motion.div>
161
+ );
162
+ }
163
+
164
+ export function RealtimeMetricsChart() {
165
+ const { data, isConnected, error, reconnect } = useRealtimeMetrics();
166
+ const [activeMetric, setActiveMetric] = useState<MetricType>('memory');
167
+ const [frozenData, setFrozenData] = useState<RealtimeMetricsPoint[] | null>(
168
+ null
169
+ );
170
+ const dataRef = useRef(data);
171
+
172
+ useLayoutEffect(() => {
173
+ dataRef.current = data;
174
+ }, [data]);
175
+
176
+ const handleMouseEnter = useCallback(() => {
177
+ const currentData = dataRef.current;
178
+ if (currentData.length > 0) {
179
+ setFrozenData(structuredClone(currentData));
180
+ }
181
+ }, []);
182
+
183
+ const handleMouseLeave = useCallback(() => {
184
+ setFrozenData(null);
185
+ }, []);
186
+
187
+ const isPaused = frozenData !== null;
188
+ const displayData = frozenData ?? data;
189
+
190
+ const metricConfig = useMemo(
191
+ () => METRICS.find((m) => m.key === activeMetric)!,
192
+ [activeMetric]
193
+ );
194
+
195
+ const chartData = useMemo(() => {
196
+ return displayData.map((point) => ({
197
+ ...point,
198
+ value: metricConfig.getValue(point),
199
+ }));
200
+ }, [displayData, metricConfig]);
201
+
202
+ const displayedLatestPoint = displayData[displayData.length - 1];
203
+
204
+ const maxValue = useMemo(() => {
205
+ if (chartData.length === 0) return 100;
206
+ const max = Math.max(...chartData.map((d) => d.value));
207
+ return Math.ceil(max * 1.2) || 100;
208
+ }, [chartData]);
209
+
210
+ const stats = useMemo(() => {
211
+ if (chartData.length === 0) return null;
212
+ const values = chartData.map((d) => d.value);
213
+ return {
214
+ min: Math.min(...values),
215
+ max: Math.max(...values),
216
+ avg: values.reduce((a, b) => a + b, 0) / values.length,
217
+ };
218
+ }, [chartData]);
219
+
220
+ return (
221
+ <div className="bg-card col-span-2 rounded-xl border p-5">
222
+ <div className="mb-4 flex items-center justify-between">
223
+ <div className="flex items-center gap-3">
224
+ <div className="relative">
225
+ <div
226
+ className={cn(
227
+ 'rounded-lg p-2',
228
+ isConnected ? 'bg-emerald-500/10' : 'bg-rose-500/10'
229
+ )}
230
+ >
231
+ {isConnected ? (
232
+ <Wifi className="size-5 text-emerald-500" />
233
+ ) : (
234
+ <WifiOff className="size-5 text-rose-500" />
235
+ )}
236
+ </div>
237
+ <AnimatePresence>
238
+ {isConnected && (
239
+ <motion.span
240
+ initial={{ scale: 0 }}
241
+ animate={{ scale: 1 }}
242
+ exit={{ scale: 0 }}
243
+ className="absolute -right-0.5 -top-0.5 flex size-2.5"
244
+ >
245
+ <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75" />
246
+ <span className="relative inline-flex size-2.5 rounded-full bg-emerald-500" />
247
+ </motion.span>
248
+ )}
249
+ </AnimatePresence>
250
+ </div>
251
+ <div>
252
+ <h3 className="text-sm font-semibold">Real-time Metrics</h3>
253
+ <p className="text-muted-foreground text-xs">
254
+ {isConnected ? (
255
+ 'Live system monitoring'
256
+ ) : (
257
+ <button
258
+ onClick={reconnect}
259
+ className="text-rose-500 hover:underline"
260
+ >
261
+ {error?.message || 'Disconnected. Click to reconnect'}
262
+ </button>
263
+ )}
264
+ </p>
265
+ </div>
266
+ </div>
267
+
268
+ {displayedLatestPoint && (
269
+ <div className="text-right">
270
+ <p className="text-muted-foreground text-[10px]">
271
+ {isPaused ? 'Paused at' : 'Last update'}
272
+ </p>
273
+ <p className="font-mono text-xs tabular-nums">
274
+ {formatTime(displayedLatestPoint.timestamp)}
275
+ </p>
276
+ </div>
277
+ )}
278
+ </div>
279
+
280
+ <div className="mb-4 flex items-center border-b">
281
+ {METRICS.map((metric) => (
282
+ <MetricTab
283
+ key={metric.key}
284
+ config={metric}
285
+ isActive={activeMetric === metric.key}
286
+ onClick={() => setActiveMetric(metric.key)}
287
+ />
288
+ ))}
289
+ <div className="ml-auto flex items-center gap-3 pb-1.5 text-xs">
290
+ {isPaused && (
291
+ <span className="flex items-center gap-1 text-amber-500">
292
+ <Pause className="size-3" />
293
+ Paused
294
+ </span>
295
+ )}
296
+ {displayedLatestPoint && (
297
+ <div className="flex items-center gap-1.5">
298
+ <span className="text-muted-foreground">
299
+ {metricConfig.label}:
300
+ </span>
301
+ <span className="font-semibold tabular-nums">
302
+ {metricConfig.format(
303
+ metricConfig.getValue(displayedLatestPoint),
304
+ displayedLatestPoint
305
+ )}
306
+ </span>
307
+ </div>
308
+ )}
309
+ </div>
310
+ </div>
311
+
312
+ <div
313
+ role="img"
314
+ aria-label={`Real-time ${metricConfig.label} chart`}
315
+ className="h-[280px]"
316
+ onMouseEnter={handleMouseEnter}
317
+ onMouseLeave={handleMouseLeave}
318
+ >
319
+ <ResponsiveContainer width="100%" height="100%">
320
+ <AreaChart
321
+ data={chartData}
322
+ margin={{ top: 10, right: 10, left: -10, bottom: 0 }}
323
+ >
324
+ <defs>
325
+ {METRICS.map((metric) => (
326
+ <linearGradient
327
+ key={metric.gradientId}
328
+ id={metric.gradientId}
329
+ x1="0"
330
+ y1="0"
331
+ x2="0"
332
+ y2="1"
333
+ >
334
+ <stop
335
+ offset="5%"
336
+ stopColor={metric.color}
337
+ stopOpacity={0.3}
338
+ />
339
+ <stop offset="95%" stopColor={metric.color} stopOpacity={0} />
340
+ </linearGradient>
341
+ ))}
342
+ </defs>
343
+ <CartesianGrid
344
+ strokeDasharray="3 3"
345
+ vertical={false}
346
+ className="stroke-zinc-200 dark:stroke-zinc-700"
347
+ />
348
+ <XAxis
349
+ dataKey="timestamp"
350
+ tickFormatter={formatTime}
351
+ tick={{ fontSize: 10 }}
352
+ tickLine={false}
353
+ axisLine={false}
354
+ interval="preserveStartEnd"
355
+ minTickGap={50}
356
+ className="text-zinc-500 dark:text-zinc-400"
357
+ />
358
+ <YAxis
359
+ domain={[0, maxValue]}
360
+ tick={{ fontSize: 10 }}
361
+ tickLine={false}
362
+ axisLine={false}
363
+ width={40}
364
+ tickFormatter={(v) =>
365
+ `${v}${metricConfig.unit === '%' ? '%' : ''}`
366
+ }
367
+ className="text-zinc-500 dark:text-zinc-400"
368
+ />
369
+ <Tooltip
370
+ content={<CustomTooltip activeMetric={metricConfig} />}
371
+ cursor={{
372
+ stroke: metricConfig.color,
373
+ strokeWidth: 1,
374
+ strokeDasharray: '5 5',
375
+ }}
376
+ />
377
+ <Area
378
+ type="monotone"
379
+ dataKey="value"
380
+ stroke={metricConfig.color}
381
+ strokeWidth={2}
382
+ fill={`url(#${metricConfig.gradientId})`}
383
+ isAnimationActive={false}
384
+ />
385
+ {displayedLatestPoint && (
386
+ <ReferenceDot
387
+ x={displayedLatestPoint.timestamp}
388
+ y={metricConfig.getValue(displayedLatestPoint)}
389
+ r={0}
390
+ shape={(props) => {
391
+ const { cx, cy } = props as { cx: number; cy: number };
392
+ return (
393
+ <g>
394
+ {!isPaused && (
395
+ <circle
396
+ cx={cx}
397
+ cy={cy}
398
+ r={4}
399
+ fill={metricConfig.color}
400
+ opacity={0.4}
401
+ >
402
+ <animate
403
+ attributeName="r"
404
+ from="4"
405
+ to="12"
406
+ dur="1.5s"
407
+ repeatCount="indefinite"
408
+ />
409
+ <animate
410
+ attributeName="opacity"
411
+ from="0.4"
412
+ to="0"
413
+ dur="1.5s"
414
+ repeatCount="indefinite"
415
+ />
416
+ </circle>
417
+ )}
418
+ <circle
419
+ cx={cx}
420
+ cy={cy}
421
+ r={4}
422
+ fill={metricConfig.color}
423
+ stroke="white"
424
+ strokeWidth={2}
425
+ />
426
+ </g>
427
+ );
428
+ }}
429
+ />
430
+ )}
431
+ </AreaChart>
432
+ </ResponsiveContainer>
433
+ </div>
434
+
435
+ <div className="mt-4 flex items-center justify-between border-t pt-3">
436
+ <div className="flex items-center gap-4 text-xs">
437
+ <div className="flex items-center gap-1.5">
438
+ <div
439
+ className="size-2 rounded-full"
440
+ style={{ backgroundColor: metricConfig.color }}
441
+ />
442
+ <span className="text-muted-foreground">{metricConfig.label}</span>
443
+ </div>
444
+ {stats && (
445
+ <div className="text-muted-foreground flex items-center gap-3">
446
+ <span>
447
+ Min:{' '}
448
+ <span className="text-foreground tabular-nums">
449
+ {metricConfig.format(stats.min)}
450
+ </span>
451
+ </span>
452
+ <span>
453
+ Avg:{' '}
454
+ <span className="text-foreground tabular-nums">
455
+ {metricConfig.format(stats.avg)}
456
+ </span>
457
+ </span>
458
+ <span>
459
+ Max:{' '}
460
+ <span className="text-foreground tabular-nums">
461
+ {metricConfig.format(stats.max)}
462
+ </span>
463
+ </span>
464
+ </div>
465
+ )}
466
+ </div>
467
+ <div className="flex items-center gap-1.5">
468
+ <span
469
+ className={cn(
470
+ 'size-2 rounded-full',
471
+ isConnected ? 'bg-emerald-500' : 'bg-rose-500'
472
+ )}
473
+ />
474
+ <span className="text-muted-foreground text-xs">
475
+ {isConnected ? 'Live' : 'Offline'}
476
+ </span>
477
+ </div>
478
+ </div>
479
+ </div>
480
+ );
481
+ }
482
+
483
+ export function RealtimeMetricsChartSkeleton() {
484
+ return (
485
+ <div className="bg-card col-span-2 rounded-xl border p-5">
486
+ <div className="mb-4 flex items-center justify-between">
487
+ <div className="flex items-center gap-3">
488
+ <Skeleton className="size-9 rounded-lg" />
489
+ <div className="space-y-1">
490
+ <Skeleton className="h-4 w-32" />
491
+ <Skeleton className="h-3 w-28" />
492
+ </div>
493
+ </div>
494
+ <div className="space-y-1 text-right">
495
+ <Skeleton className="ml-auto h-3 w-16" />
496
+ <Skeleton className="ml-auto h-3 w-20" />
497
+ </div>
498
+ </div>
499
+ <div className="mb-4 flex items-center gap-4 border-b pb-1.5">
500
+ {Array.from({ length: 5 }).map((_, i) => (
501
+ <Skeleton key={i} className="h-4 w-16" />
502
+ ))}
503
+ </div>
504
+ <Skeleton className="h-[280px] w-full rounded-lg" />
505
+ <div className="mt-4 flex items-center justify-between border-t pt-3">
506
+ <Skeleton className="h-3 w-40" />
507
+ <Skeleton className="h-3 w-16" />
508
+ </div>
509
+ </div>
510
+ );
511
+ }
@@ -0,0 +1,102 @@
1
+ 'use client';
2
+
3
+ import type { RoleDistributionItem } from '@repo/packages-types/stats';
4
+ import { Skeleton } from '@repo/packages-ui/skeleton';
5
+ import {
6
+ Cell,
7
+ Legend,
8
+ Pie,
9
+ PieChart,
10
+ ResponsiveContainer,
11
+ Tooltip,
12
+ } from 'recharts';
13
+
14
+ import { SimpleChartTooltip } from './chart-tooltip';
15
+
16
+ interface RoleDistributionChartProps {
17
+ data: RoleDistributionItem[];
18
+ }
19
+
20
+ const ROLE_COLORS: Record<string, string> = {
21
+ user: '#3b82f6',
22
+ admin: '#f59e0b',
23
+ super_admin: '#ef4444',
24
+ };
25
+
26
+ const ROLE_LABELS: Record<string, string> = {
27
+ user: 'Users',
28
+ admin: 'Admins',
29
+ super_admin: 'Super Admins',
30
+ };
31
+
32
+ export function RoleDistributionChart({ data }: RoleDistributionChartProps) {
33
+ const chartData = data.map((item) => ({
34
+ ...item,
35
+ name: ROLE_LABELS[item.role] || item.role,
36
+ color: ROLE_COLORS[item.role] || '#94a3b8',
37
+ }));
38
+
39
+ const total = data.reduce((sum, item) => sum + item.count, 0);
40
+
41
+ return (
42
+ <div className="bg-card rounded-xl border p-5">
43
+ <div className="mb-4">
44
+ <h3 className="text-sm font-semibold">Role Distribution</h3>
45
+ <p className="text-muted-foreground text-xs">
46
+ Users by permission level
47
+ </p>
48
+ </div>
49
+ <div className="h-[200px]">
50
+ <ResponsiveContainer width="100%" height="100%">
51
+ <PieChart>
52
+ <Pie
53
+ data={chartData}
54
+ cx="50%"
55
+ cy="50%"
56
+ innerRadius={50}
57
+ outerRadius={75}
58
+ paddingAngle={2}
59
+ dataKey="count"
60
+ nameKey="name"
61
+ >
62
+ {chartData.map((entry, index) => (
63
+ <Cell key={`cell-${index}`} fill={entry.color} />
64
+ ))}
65
+ </Pie>
66
+ <Tooltip
67
+ content={
68
+ <SimpleChartTooltip
69
+ valueFormatter={(value: number) => [
70
+ `${value} (${((value / total) * 100).toFixed(1)}%)`,
71
+ 'Count',
72
+ ]}
73
+ />
74
+ }
75
+ />
76
+ <Legend
77
+ verticalAlign="bottom"
78
+ height={36}
79
+ formatter={(value) => (
80
+ <span className="text-foreground text-xs">{value}</span>
81
+ )}
82
+ />
83
+ </PieChart>
84
+ </ResponsiveContainer>
85
+ </div>
86
+ </div>
87
+ );
88
+ }
89
+
90
+ export function RoleDistributionChartSkeleton() {
91
+ return (
92
+ <div className="bg-card rounded-xl border p-5">
93
+ <div className="mb-4 space-y-1">
94
+ <Skeleton className="h-4 w-28" />
95
+ <Skeleton className="h-3 w-36" />
96
+ </div>
97
+ <div className="flex h-[200px] items-center justify-center">
98
+ <Skeleton className="size-[150px] rounded-full" />
99
+ </div>
100
+ </div>
101
+ );
102
+ }
@@ -0,0 +1,90 @@
1
+ 'use client';
2
+
3
+ import type { SessionActivityPoint } from '@repo/packages-types/stats';
4
+ import { Skeleton } from '@repo/packages-ui/skeleton';
5
+ import {
6
+ Bar,
7
+ BarChart,
8
+ CartesianGrid,
9
+ ResponsiveContainer,
10
+ Tooltip,
11
+ XAxis,
12
+ YAxis,
13
+ } from 'recharts';
14
+
15
+ import { ChartTooltip } from './chart-tooltip';
16
+
17
+ interface SessionActivityChartProps {
18
+ data: SessionActivityPoint[];
19
+ }
20
+
21
+ function formatDate(dateStr: string) {
22
+ const date = new Date(dateStr);
23
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
24
+ }
25
+
26
+ export function SessionActivityChart({ data }: SessionActivityChartProps) {
27
+ return (
28
+ <div className="bg-card rounded-xl border p-5">
29
+ <div className="mb-4">
30
+ <h3 className="text-sm font-semibold">Session Activity</h3>
31
+ <p className="text-muted-foreground text-xs">
32
+ Login sessions created per day (30 days)
33
+ </p>
34
+ </div>
35
+ <div className="h-[240px]">
36
+ <ResponsiveContainer width="100%" height="100%">
37
+ <BarChart
38
+ data={data}
39
+ margin={{ top: 5, right: 5, left: -20, bottom: 0 }}
40
+ >
41
+ <CartesianGrid
42
+ strokeDasharray="3 3"
43
+ vertical={false}
44
+ className="stroke-zinc-200 dark:stroke-zinc-700"
45
+ />
46
+ <XAxis
47
+ dataKey="date"
48
+ tickFormatter={formatDate}
49
+ tick={{ fontSize: 11 }}
50
+ tickLine={false}
51
+ axisLine={false}
52
+ interval="preserveStartEnd"
53
+ className="text-zinc-500 dark:text-zinc-400"
54
+ />
55
+ <YAxis
56
+ tick={{ fontSize: 11 }}
57
+ tickLine={false}
58
+ axisLine={false}
59
+ allowDecimals={false}
60
+ className="text-zinc-500 dark:text-zinc-400"
61
+ />
62
+ <Tooltip
63
+ content={<ChartTooltip labelFormatter={formatDate} />}
64
+ cursor={{ fill: 'rgba(99, 102, 241, 0.08)' }}
65
+ />
66
+ <Bar
67
+ dataKey="count"
68
+ name="Sessions"
69
+ fill="#3b82f6"
70
+ radius={[4, 4, 0, 0]}
71
+ maxBarSize={32}
72
+ />
73
+ </BarChart>
74
+ </ResponsiveContainer>
75
+ </div>
76
+ </div>
77
+ );
78
+ }
79
+
80
+ export function SessionActivityChartSkeleton() {
81
+ return (
82
+ <div className="bg-card rounded-xl border p-5">
83
+ <div className="mb-4 space-y-1">
84
+ <Skeleton className="h-4 w-28" />
85
+ <Skeleton className="h-3 w-44" />
86
+ </div>
87
+ <Skeleton className="h-[240px] w-full" />
88
+ </div>
89
+ );
90
+ }