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,108 @@
1
+ 'use client';
2
+
3
+ import type { UserGrowthPoint } from '@repo/packages-types/stats';
4
+ import { Skeleton } from '@repo/packages-ui/skeleton';
5
+ import {
6
+ Area,
7
+ AreaChart,
8
+ CartesianGrid,
9
+ ResponsiveContainer,
10
+ Tooltip,
11
+ XAxis,
12
+ YAxis,
13
+ } from 'recharts';
14
+
15
+ import { ChartTooltip } from './chart-tooltip';
16
+
17
+ interface UserGrowthChartProps {
18
+ data: UserGrowthPoint[];
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 UserGrowthChart({ data }: UserGrowthChartProps) {
27
+ return (
28
+ <div className="bg-card rounded-xl border p-5">
29
+ <div className="mb-4">
30
+ <h3 className="text-sm font-semibold">User Growth</h3>
31
+ <p className="text-muted-foreground text-xs">
32
+ New signups and cumulative users (30 days)
33
+ </p>
34
+ </div>
35
+ <div className="h-[240px]">
36
+ <ResponsiveContainer width="100%" height="100%">
37
+ <AreaChart
38
+ data={data}
39
+ margin={{ top: 5, right: 5, left: -20, bottom: 0 }}
40
+ >
41
+ <defs>
42
+ <linearGradient id="colorCumulative" x1="0" y1="0" x2="0" y2="1">
43
+ <stop offset="5%" stopColor="#6366f1" stopOpacity={0.3} />
44
+ <stop offset="95%" stopColor="#6366f1" stopOpacity={0} />
45
+ </linearGradient>
46
+ <linearGradient id="colorNew" x1="0" y1="0" x2="0" y2="1">
47
+ <stop offset="5%" stopColor="#10b981" stopOpacity={0.3} />
48
+ <stop offset="95%" stopColor="#10b981" stopOpacity={0} />
49
+ </linearGradient>
50
+ </defs>
51
+ <CartesianGrid
52
+ strokeDasharray="3 3"
53
+ vertical={false}
54
+ className="stroke-zinc-200 dark:stroke-zinc-700"
55
+ />
56
+ <XAxis
57
+ dataKey="date"
58
+ tickFormatter={formatDate}
59
+ tick={{ fontSize: 11 }}
60
+ tickLine={false}
61
+ axisLine={false}
62
+ interval="preserveStartEnd"
63
+ className="text-zinc-500 dark:text-zinc-400"
64
+ />
65
+ <YAxis
66
+ tick={{ fontSize: 11 }}
67
+ tickLine={false}
68
+ axisLine={false}
69
+ className="text-zinc-500 dark:text-zinc-400"
70
+ />
71
+ <Tooltip
72
+ content={<ChartTooltip labelFormatter={formatDate} />}
73
+ cursor={{ stroke: 'rgba(99, 102, 241, 0.3)', strokeWidth: 1 }}
74
+ />
75
+ <Area
76
+ type="monotone"
77
+ dataKey="cumulative"
78
+ name="Total Users"
79
+ stroke="#6366f1"
80
+ strokeWidth={2}
81
+ fill="url(#colorCumulative)"
82
+ />
83
+ <Area
84
+ type="monotone"
85
+ dataKey="count"
86
+ name="New Users"
87
+ stroke="#10b981"
88
+ strokeWidth={2}
89
+ fill="url(#colorNew)"
90
+ />
91
+ </AreaChart>
92
+ </ResponsiveContainer>
93
+ </div>
94
+ </div>
95
+ );
96
+ }
97
+
98
+ export function UserGrowthChartSkeleton() {
99
+ return (
100
+ <div className="bg-card rounded-xl border p-5">
101
+ <div className="mb-4 space-y-1">
102
+ <Skeleton className="h-4 w-24" />
103
+ <Skeleton className="h-3 w-48" />
104
+ </div>
105
+ <Skeleton className="h-[240px] w-full" />
106
+ </div>
107
+ );
108
+ }
@@ -0,0 +1,175 @@
1
+ 'use client';
2
+
3
+ import type { SystemHealth } from '@repo/packages-types/stats';
4
+ import { cn } from '@repo/packages-ui/lib/utils';
5
+ import { Skeleton } from '@repo/packages-ui/skeleton';
6
+ import { motion } from 'framer-motion';
7
+ import { Activity, Clock, Database } from 'lucide-react';
8
+
9
+ interface HealthIndicatorProps {
10
+ health: SystemHealth;
11
+ }
12
+
13
+ function formatUptime(seconds: number) {
14
+ const days = Math.floor(seconds / 86400);
15
+ const hours = Math.floor((seconds % 86400) / 3600);
16
+ const mins = Math.floor((seconds % 3600) / 60);
17
+
18
+ if (days > 0) return `${days}d ${hours}h`;
19
+ if (hours > 0) return `${hours}h ${mins}m`;
20
+ return `${mins}m`;
21
+ }
22
+
23
+ const STATUS_CONFIG = {
24
+ connected: {
25
+ label: 'Healthy',
26
+ color: 'bg-emerald-500',
27
+ textColor: 'text-emerald-600 dark:text-emerald-400',
28
+ bgColor: 'bg-emerald-500/10',
29
+ },
30
+ degraded: {
31
+ label: 'Degraded',
32
+ color: 'bg-amber-500',
33
+ textColor: 'text-amber-600 dark:text-amber-400',
34
+ bgColor: 'bg-amber-500/10',
35
+ },
36
+ disconnected: {
37
+ label: 'Offline',
38
+ color: 'bg-rose-500',
39
+ textColor: 'text-rose-600 dark:text-rose-400',
40
+ bgColor: 'bg-rose-500/10',
41
+ },
42
+ };
43
+
44
+ function PulseDot({
45
+ status,
46
+ }: {
47
+ status: 'connected' | 'degraded' | 'disconnected';
48
+ }) {
49
+ const config = STATUS_CONFIG[status];
50
+
51
+ return (
52
+ <span className="relative flex size-3">
53
+ <motion.span
54
+ className={cn(
55
+ 'absolute inline-flex h-full w-full rounded-full opacity-75',
56
+ config.color
57
+ )}
58
+ animate={{ scale: [1, 1.5, 1], opacity: [0.75, 0, 0.75] }}
59
+ transition={{ duration: 2, repeat: Infinity, ease: 'easeInOut' }}
60
+ />
61
+ <span
62
+ className={cn('relative inline-flex size-3 rounded-full', config.color)}
63
+ />
64
+ </span>
65
+ );
66
+ }
67
+
68
+ export function HealthIndicator({ health }: HealthIndicatorProps) {
69
+ const dbStatus = STATUS_CONFIG[health.database];
70
+
71
+ return (
72
+ <div className="bg-card rounded-xl border p-5">
73
+ <div className="mb-4 flex items-center justify-between">
74
+ <div>
75
+ <h3 className="text-sm font-semibold">System Health</h3>
76
+ <p className="text-muted-foreground text-xs">Real-time status</p>
77
+ </div>
78
+ <div
79
+ className={cn(
80
+ 'flex items-center gap-2 rounded-full px-3 py-1',
81
+ dbStatus.bgColor
82
+ )}
83
+ >
84
+ <PulseDot status={health.database} />
85
+ <span className={cn('text-xs font-medium', dbStatus.textColor)}>
86
+ {dbStatus.label}
87
+ </span>
88
+ </div>
89
+ </div>
90
+
91
+ <div className="space-y-4">
92
+ <div className="flex items-center justify-between rounded-lg border p-3">
93
+ <div className="flex items-center gap-3">
94
+ <div className="rounded-lg bg-blue-500/10 p-2">
95
+ <Database className="size-4 text-blue-500" />
96
+ </div>
97
+ <div>
98
+ <p className="text-sm font-medium">Database</p>
99
+ <p className="text-muted-foreground text-xs">PostgreSQL</p>
100
+ </div>
101
+ </div>
102
+ <div className="text-right">
103
+ <p className={cn('text-sm font-medium', dbStatus.textColor)}>
104
+ {dbStatus.label}
105
+ </p>
106
+ {health.dbLatencyMs !== undefined && (
107
+ <p className="text-muted-foreground text-xs">
108
+ {health.dbLatencyMs}ms latency
109
+ </p>
110
+ )}
111
+ </div>
112
+ </div>
113
+
114
+ <div className="flex items-center justify-between rounded-lg border p-3">
115
+ <div className="flex items-center gap-3">
116
+ <div className="rounded-lg bg-violet-500/10 p-2">
117
+ <Clock className="size-4 text-violet-500" />
118
+ </div>
119
+ <div>
120
+ <p className="text-sm font-medium">Uptime</p>
121
+ <p className="text-muted-foreground text-xs">API Server</p>
122
+ </div>
123
+ </div>
124
+ <p className="text-sm font-medium">{formatUptime(health.uptime)}</p>
125
+ </div>
126
+
127
+ <div className="flex items-center justify-between rounded-lg border p-3">
128
+ <div className="flex items-center gap-3">
129
+ <div className="rounded-lg bg-emerald-500/10 p-2">
130
+ <Activity className="size-4 text-emerald-500" />
131
+ </div>
132
+ <div>
133
+ <p className="text-sm font-medium">Last Check</p>
134
+ <p className="text-muted-foreground text-xs">Health check</p>
135
+ </div>
136
+ </div>
137
+ <p className="text-muted-foreground text-xs">
138
+ {new Date(health.lastChecked).toLocaleTimeString()}
139
+ </p>
140
+ </div>
141
+ </div>
142
+ </div>
143
+ );
144
+ }
145
+
146
+ export function HealthIndicatorSkeleton() {
147
+ return (
148
+ <div className="bg-card rounded-xl border p-5">
149
+ <div className="mb-4 flex items-center justify-between">
150
+ <div className="space-y-1">
151
+ <Skeleton className="h-4 w-24" />
152
+ <Skeleton className="h-3 w-20" />
153
+ </div>
154
+ <Skeleton className="h-6 w-20 rounded-full" />
155
+ </div>
156
+ <div className="space-y-4">
157
+ {Array.from({ length: 3 }).map((_, i) => (
158
+ <div
159
+ key={i}
160
+ className="flex items-center justify-between rounded-lg border p-3"
161
+ >
162
+ <div className="flex items-center gap-3">
163
+ <Skeleton className="size-8 rounded-lg" />
164
+ <div className="space-y-1">
165
+ <Skeleton className="h-3 w-16" />
166
+ <Skeleton className="h-2 w-12" />
167
+ </div>
168
+ </div>
169
+ <Skeleton className="h-4 w-14" />
170
+ </div>
171
+ ))}
172
+ </div>
173
+ </div>
174
+ );
175
+ }
@@ -0,0 +1,90 @@
1
+ 'use client';
2
+
3
+ import { Button } from '@repo/packages-ui/button';
4
+ import {
5
+ Select,
6
+ SelectContent,
7
+ SelectItem,
8
+ SelectTrigger,
9
+ SelectValue,
10
+ } from '@repo/packages-ui/select';
11
+ import { motion } from 'framer-motion';
12
+ import { RefreshCw } from 'lucide-react';
13
+
14
+ interface RefreshControlProps {
15
+ interval: number | null;
16
+ onIntervalChange: (interval: number | null) => void;
17
+ onRefresh: () => void;
18
+ isRefreshing: boolean;
19
+ lastUpdated: Date | null;
20
+ }
21
+
22
+ const INTERVALS = [
23
+ { value: '5000', label: '5s' },
24
+ { value: '15000', label: '15s' },
25
+ { value: '30000', label: '30s' },
26
+ { value: '60000', label: '1m' },
27
+ { value: 'off', label: 'Off' },
28
+ ];
29
+
30
+ export function RefreshControl({
31
+ interval,
32
+ onIntervalChange,
33
+ onRefresh,
34
+ isRefreshing,
35
+ lastUpdated,
36
+ }: RefreshControlProps) {
37
+ const handleIntervalChange = (value: string) => {
38
+ if (value === 'off') {
39
+ onIntervalChange(null);
40
+ } else {
41
+ onIntervalChange(parseInt(value, 10));
42
+ }
43
+ };
44
+
45
+ return (
46
+ <div className="flex items-center gap-3">
47
+ {lastUpdated && (
48
+ <span className="text-muted-foreground text-xs">
49
+ Updated {lastUpdated.toLocaleTimeString()}
50
+ </span>
51
+ )}
52
+ <div className="flex items-center gap-2">
53
+ <Select
54
+ value={interval?.toString() ?? 'off'}
55
+ onValueChange={handleIntervalChange}
56
+ >
57
+ <SelectTrigger className="h-8 w-[80px] text-xs">
58
+ <SelectValue placeholder="Auto" />
59
+ </SelectTrigger>
60
+ <SelectContent>
61
+ {INTERVALS.map((opt) => (
62
+ <SelectItem key={opt.value} value={opt.value} className="text-xs">
63
+ {opt.label}
64
+ </SelectItem>
65
+ ))}
66
+ </SelectContent>
67
+ </Select>
68
+ <Button
69
+ variant="outline"
70
+ size="sm"
71
+ onClick={onRefresh}
72
+ disabled={isRefreshing}
73
+ className="h-8 gap-1.5 px-2.5"
74
+ >
75
+ <motion.div
76
+ animate={isRefreshing ? { rotate: 360 } : { rotate: 0 }}
77
+ transition={
78
+ isRefreshing
79
+ ? { duration: 1, repeat: Infinity, ease: 'linear' }
80
+ : { duration: 0 }
81
+ }
82
+ >
83
+ <RefreshCw className="size-3.5" />
84
+ </motion.div>
85
+ <span className="text-xs">Refresh</span>
86
+ </Button>
87
+ </div>
88
+ </div>
89
+ );
90
+ }
@@ -0,0 +1,79 @@
1
+ 'use client';
2
+
3
+ import {
4
+ AlertDialog,
5
+ AlertDialogAction,
6
+ AlertDialogCancel,
7
+ AlertDialogContent,
8
+ AlertDialogDescription,
9
+ AlertDialogFooter,
10
+ AlertDialogHeader,
11
+ AlertDialogTitle,
12
+ AlertDialogTrigger,
13
+ } from '@repo/packages-ui/alert-dialog';
14
+ import { Button } from '@repo/packages-ui/button';
15
+ import { LogOut } from 'lucide-react';
16
+ import * as React from 'react';
17
+ import { toast } from 'sonner';
18
+
19
+ import { useRevokeUserSessions } from '@/hooks/api/use-admin-sessions';
20
+
21
+ interface SessionRevokeAllDialogProps {
22
+ userId: string;
23
+ userName: string;
24
+ userEmail: string;
25
+ }
26
+
27
+ export function SessionRevokeAllDialog({
28
+ userId,
29
+ userName,
30
+ userEmail,
31
+ }: SessionRevokeAllDialogProps) {
32
+ const [open, setOpen] = React.useState(false);
33
+ const { mutate: revokeUserSessions, isPending } = useRevokeUserSessions();
34
+
35
+ const handleRevoke = () => {
36
+ revokeUserSessions(userId, {
37
+ onSuccess: () => {
38
+ toast.success('All sessions revoked successfully');
39
+ setOpen(false);
40
+ },
41
+ onError: (error) => {
42
+ toast.error(error.message || 'Failed to revoke sessions');
43
+ },
44
+ });
45
+ };
46
+
47
+ return (
48
+ <AlertDialog open={open} onOpenChange={setOpen}>
49
+ <AlertDialogTrigger asChild>
50
+ <Button variant="outline" size="sm" className="text-destructive">
51
+ <LogOut className="mr-2 h-4 w-4" />
52
+ Revoke All
53
+ </Button>
54
+ </AlertDialogTrigger>
55
+ <AlertDialogContent>
56
+ <AlertDialogHeader>
57
+ <AlertDialogTitle>
58
+ Revoke all sessions for this user?
59
+ </AlertDialogTitle>
60
+ <AlertDialogDescription>
61
+ This will immediately log out{' '}
62
+ <span className="font-semibold">{userName || userEmail}</span> from
63
+ all devices. They will need to sign in again on each device.
64
+ </AlertDialogDescription>
65
+ </AlertDialogHeader>
66
+ <AlertDialogFooter>
67
+ <AlertDialogCancel disabled={isPending}>Cancel</AlertDialogCancel>
68
+ <AlertDialogAction
69
+ onClick={handleRevoke}
70
+ disabled={isPending}
71
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
72
+ >
73
+ {isPending ? 'Revoking...' : 'Revoke All Sessions'}
74
+ </AlertDialogAction>
75
+ </AlertDialogFooter>
76
+ </AlertDialogContent>
77
+ </AlertDialog>
78
+ );
79
+ }
@@ -0,0 +1,74 @@
1
+ 'use client';
2
+
3
+ import type { AdminSession } from '@repo/packages-types/session';
4
+ import {
5
+ AlertDialog,
6
+ AlertDialogAction,
7
+ AlertDialogCancel,
8
+ AlertDialogContent,
9
+ AlertDialogDescription,
10
+ AlertDialogFooter,
11
+ AlertDialogHeader,
12
+ AlertDialogTitle,
13
+ AlertDialogTrigger,
14
+ } from '@repo/packages-ui/alert-dialog';
15
+ import { Button } from '@repo/packages-ui/button';
16
+ import { LogOut } from 'lucide-react';
17
+ import * as React from 'react';
18
+ import { toast } from 'sonner';
19
+
20
+ import { useRevokeSession } from '@/hooks/api/use-admin-sessions';
21
+
22
+ interface SessionRevokeDialogProps {
23
+ session: AdminSession;
24
+ }
25
+
26
+ export function SessionRevokeDialog({ session }: SessionRevokeDialogProps) {
27
+ const [open, setOpen] = React.useState(false);
28
+ const { mutate: revokeSession, isPending } = useRevokeSession();
29
+
30
+ const handleRevoke = () => {
31
+ revokeSession(session.id, {
32
+ onSuccess: () => {
33
+ toast.success('Session revoked successfully');
34
+ setOpen(false);
35
+ },
36
+ onError: (error) => {
37
+ toast.error(error.message || 'Failed to revoke session');
38
+ },
39
+ });
40
+ };
41
+
42
+ return (
43
+ <AlertDialog open={open} onOpenChange={setOpen}>
44
+ <AlertDialogTrigger asChild>
45
+ <Button variant="ghost" size="sm" className="text-destructive">
46
+ <LogOut className="mr-2 h-4 w-4" />
47
+ Revoke
48
+ </Button>
49
+ </AlertDialogTrigger>
50
+ <AlertDialogContent>
51
+ <AlertDialogHeader>
52
+ <AlertDialogTitle>Revoke this session?</AlertDialogTitle>
53
+ <AlertDialogDescription>
54
+ This will immediately log out{' '}
55
+ <span className="font-semibold">
56
+ {session.user.name || session.user.email}
57
+ </span>{' '}
58
+ from this device. They will need to sign in again to continue.
59
+ </AlertDialogDescription>
60
+ </AlertDialogHeader>
61
+ <AlertDialogFooter>
62
+ <AlertDialogCancel disabled={isPending}>Cancel</AlertDialogCancel>
63
+ <AlertDialogAction
64
+ onClick={handleRevoke}
65
+ disabled={isPending}
66
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
67
+ >
68
+ {isPending ? 'Revoking...' : 'Revoke Session'}
69
+ </AlertDialogAction>
70
+ </AlertDialogFooter>
71
+ </AlertDialogContent>
72
+ </AlertDialog>
73
+ );
74
+ }