create-blitzpack 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/dist/index.js +35 -77
  2. package/package.json +5 -6
  3. package/template/.dockerignore +0 -59
  4. package/template/.github/workflows/ci.yml +0 -157
  5. package/template/.husky/pre-commit +0 -1
  6. package/template/.husky/pre-push +0 -1
  7. package/template/.lintstagedrc.cjs +0 -4
  8. package/template/.nvmrc +0 -1
  9. package/template/.prettierrc +0 -9
  10. package/template/.vscode/settings.json +0 -13
  11. package/template/CLAUDE.md +0 -175
  12. package/template/CONTRIBUTING.md +0 -32
  13. package/template/Dockerfile +0 -90
  14. package/template/GETTING_STARTED.md +0 -35
  15. package/template/LICENSE +0 -21
  16. package/template/README.md +0 -116
  17. package/template/apps/api/.dockerignore +0 -51
  18. package/template/apps/api/.env.local.example +0 -62
  19. package/template/apps/api/emails/account-deleted-email.tsx +0 -69
  20. package/template/apps/api/emails/components/email-layout.tsx +0 -154
  21. package/template/apps/api/emails/config.ts +0 -22
  22. package/template/apps/api/emails/password-changed-email.tsx +0 -88
  23. package/template/apps/api/emails/password-reset-email.tsx +0 -86
  24. package/template/apps/api/emails/verification-email.tsx +0 -85
  25. package/template/apps/api/emails/welcome-email.tsx +0 -70
  26. package/template/apps/api/package.json +0 -84
  27. package/template/apps/api/prisma/migrations/20251012111439_init/migration.sql +0 -13
  28. package/template/apps/api/prisma/migrations/20251018162629_add_better_auth_fields/migration.sql +0 -67
  29. package/template/apps/api/prisma/migrations/20251019142208_add_user_role_enum/migration.sql +0 -5
  30. package/template/apps/api/prisma/migrations/20251019182151_user_auth/migration.sql +0 -7
  31. package/template/apps/api/prisma/migrations/20251019211416_faster_session_lookup/migration.sql +0 -2
  32. package/template/apps/api/prisma/migrations/20251119124337_add_upload_model/migration.sql +0 -26
  33. package/template/apps/api/prisma/migrations/20251120071241_add_scope_to_account/migration.sql +0 -2
  34. package/template/apps/api/prisma/migrations/20251120072608_add_oauth_token_expiration_fields/migration.sql +0 -10
  35. package/template/apps/api/prisma/migrations/20251120144705_add_audit_logs/migration.sql +0 -29
  36. package/template/apps/api/prisma/migrations/20251127123614_remove_impersonated_by/migration.sql +0 -8
  37. package/template/apps/api/prisma/migrations/20251127125630_remove_audit_logs/migration.sql +0 -11
  38. package/template/apps/api/prisma/migrations/migration_lock.toml +0 -3
  39. package/template/apps/api/prisma/schema.prisma +0 -116
  40. package/template/apps/api/prisma/seed.ts +0 -159
  41. package/template/apps/api/prisma.config.ts +0 -14
  42. package/template/apps/api/src/app.ts +0 -377
  43. package/template/apps/api/src/common/logger.service.ts +0 -227
  44. package/template/apps/api/src/config/env.ts +0 -60
  45. package/template/apps/api/src/config/rate-limit.ts +0 -29
  46. package/template/apps/api/src/hooks/auth.ts +0 -122
  47. package/template/apps/api/src/plugins/auth.ts +0 -198
  48. package/template/apps/api/src/plugins/database.ts +0 -45
  49. package/template/apps/api/src/plugins/logger.ts +0 -33
  50. package/template/apps/api/src/plugins/multipart.ts +0 -16
  51. package/template/apps/api/src/plugins/scalar.ts +0 -20
  52. package/template/apps/api/src/plugins/schedule.ts +0 -52
  53. package/template/apps/api/src/plugins/services.ts +0 -66
  54. package/template/apps/api/src/plugins/swagger.ts +0 -56
  55. package/template/apps/api/src/routes/accounts.ts +0 -91
  56. package/template/apps/api/src/routes/admin-sessions.ts +0 -92
  57. package/template/apps/api/src/routes/metrics.ts +0 -71
  58. package/template/apps/api/src/routes/password.ts +0 -46
  59. package/template/apps/api/src/routes/sessions.ts +0 -53
  60. package/template/apps/api/src/routes/stats.ts +0 -38
  61. package/template/apps/api/src/routes/uploads-serve.ts +0 -27
  62. package/template/apps/api/src/routes/uploads.ts +0 -154
  63. package/template/apps/api/src/routes/users.ts +0 -114
  64. package/template/apps/api/src/routes/verification.ts +0 -90
  65. package/template/apps/api/src/server.ts +0 -34
  66. package/template/apps/api/src/services/accounts.service.ts +0 -125
  67. package/template/apps/api/src/services/authorization.service.ts +0 -162
  68. package/template/apps/api/src/services/email.service.ts +0 -170
  69. package/template/apps/api/src/services/file-storage.service.ts +0 -267
  70. package/template/apps/api/src/services/metrics.service.ts +0 -175
  71. package/template/apps/api/src/services/password.service.ts +0 -56
  72. package/template/apps/api/src/services/sessions.service.spec.ts +0 -134
  73. package/template/apps/api/src/services/sessions.service.ts +0 -276
  74. package/template/apps/api/src/services/stats.service.ts +0 -273
  75. package/template/apps/api/src/services/uploads.service.ts +0 -163
  76. package/template/apps/api/src/services/users.service.spec.ts +0 -249
  77. package/template/apps/api/src/services/users.service.ts +0 -198
  78. package/template/apps/api/src/utils/file-validation.ts +0 -108
  79. package/template/apps/api/start.sh +0 -33
  80. package/template/apps/api/test/helpers/fastify-app.ts +0 -24
  81. package/template/apps/api/test/helpers/mock-authorization.ts +0 -16
  82. package/template/apps/api/test/helpers/mock-logger.ts +0 -28
  83. package/template/apps/api/test/helpers/mock-prisma.ts +0 -30
  84. package/template/apps/api/test/helpers/test-db.ts +0 -125
  85. package/template/apps/api/test/integration/auth-flow.integration.spec.ts +0 -449
  86. package/template/apps/api/test/integration/password.integration.spec.ts +0 -427
  87. package/template/apps/api/test/integration/rate-limit.integration.spec.ts +0 -51
  88. package/template/apps/api/test/integration/sessions.integration.spec.ts +0 -445
  89. package/template/apps/api/test/integration/users.integration.spec.ts +0 -211
  90. package/template/apps/api/test/setup.ts +0 -31
  91. package/template/apps/api/tsconfig.json +0 -26
  92. package/template/apps/api/vitest.config.ts +0 -35
  93. package/template/apps/web/.env.local.example +0 -11
  94. package/template/apps/web/components.json +0 -24
  95. package/template/apps/web/next.config.ts +0 -22
  96. package/template/apps/web/package.json +0 -56
  97. package/template/apps/web/postcss.config.js +0 -5
  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 +0 -3
  101. package/template/apps/web/src/app/(admin)/admin/layout.tsx +0 -222
  102. package/template/apps/web/src/app/(admin)/admin/page.tsx +0 -157
  103. package/template/apps/web/src/app/(admin)/admin/sessions/page.tsx +0 -18
  104. package/template/apps/web/src/app/(admin)/admin/users/page.tsx +0 -20
  105. package/template/apps/web/src/app/(auth)/forgot-password/page.tsx +0 -177
  106. package/template/apps/web/src/app/(auth)/login/page.tsx +0 -159
  107. package/template/apps/web/src/app/(auth)/reset-password/page.tsx +0 -245
  108. package/template/apps/web/src/app/(auth)/signup/page.tsx +0 -153
  109. package/template/apps/web/src/app/dashboard/change-password/page.tsx +0 -255
  110. package/template/apps/web/src/app/dashboard/page.tsx +0 -296
  111. package/template/apps/web/src/app/error.tsx +0 -32
  112. package/template/apps/web/src/app/examples/file-upload/page.tsx +0 -200
  113. package/template/apps/web/src/app/favicon.ico +0 -0
  114. package/template/apps/web/src/app/global-error.tsx +0 -96
  115. package/template/apps/web/src/app/globals.css +0 -22
  116. package/template/apps/web/src/app/icon.png +0 -0
  117. package/template/apps/web/src/app/layout.tsx +0 -34
  118. package/template/apps/web/src/app/not-found.tsx +0 -28
  119. package/template/apps/web/src/app/page.tsx +0 -192
  120. package/template/apps/web/src/components/admin/activity-feed.tsx +0 -101
  121. package/template/apps/web/src/components/admin/charts/auth-breakdown-chart.tsx +0 -114
  122. package/template/apps/web/src/components/admin/charts/chart-tooltip.tsx +0 -124
  123. package/template/apps/web/src/components/admin/charts/realtime-metrics-chart.tsx +0 -511
  124. package/template/apps/web/src/components/admin/charts/role-distribution-chart.tsx +0 -102
  125. package/template/apps/web/src/components/admin/charts/session-activity-chart.tsx +0 -90
  126. package/template/apps/web/src/components/admin/charts/user-growth-chart.tsx +0 -108
  127. package/template/apps/web/src/components/admin/health-indicator.tsx +0 -175
  128. package/template/apps/web/src/components/admin/refresh-control.tsx +0 -90
  129. package/template/apps/web/src/components/admin/session-revoke-all-dialog.tsx +0 -79
  130. package/template/apps/web/src/components/admin/session-revoke-dialog.tsx +0 -74
  131. package/template/apps/web/src/components/admin/sessions-management-table.tsx +0 -372
  132. package/template/apps/web/src/components/admin/stat-card.tsx +0 -137
  133. package/template/apps/web/src/components/admin/user-create-dialog.tsx +0 -152
  134. package/template/apps/web/src/components/admin/user-delete-dialog.tsx +0 -73
  135. package/template/apps/web/src/components/admin/user-edit-dialog.tsx +0 -170
  136. package/template/apps/web/src/components/admin/users-management-table.tsx +0 -285
  137. package/template/apps/web/src/components/auth/email-verification-banner.tsx +0 -85
  138. package/template/apps/web/src/components/auth/github-button.tsx +0 -40
  139. package/template/apps/web/src/components/auth/google-button.tsx +0 -54
  140. package/template/apps/web/src/components/auth/protected-route.tsx +0 -66
  141. package/template/apps/web/src/components/auth/redirect-if-authenticated.tsx +0 -31
  142. package/template/apps/web/src/components/auth/with-auth.tsx +0 -30
  143. package/template/apps/web/src/components/error/error-card.tsx +0 -47
  144. package/template/apps/web/src/components/error/forbidden.tsx +0 -25
  145. package/template/apps/web/src/components/landing/command-block.tsx +0 -64
  146. package/template/apps/web/src/components/landing/feature-card.tsx +0 -60
  147. package/template/apps/web/src/components/landing/included-feature-card.tsx +0 -63
  148. package/template/apps/web/src/components/landing/logo.tsx +0 -41
  149. package/template/apps/web/src/components/landing/tech-badge.tsx +0 -11
  150. package/template/apps/web/src/components/layout/auth-nav.tsx +0 -58
  151. package/template/apps/web/src/components/layout/footer.tsx +0 -3
  152. package/template/apps/web/src/config/landing-data.ts +0 -152
  153. package/template/apps/web/src/config/site.ts +0 -5
  154. package/template/apps/web/src/hooks/api/__tests__/use-users.test.tsx +0 -181
  155. package/template/apps/web/src/hooks/api/use-admin-sessions.ts +0 -75
  156. package/template/apps/web/src/hooks/api/use-admin-stats.ts +0 -33
  157. package/template/apps/web/src/hooks/api/use-sessions.ts +0 -52
  158. package/template/apps/web/src/hooks/api/use-uploads.ts +0 -156
  159. package/template/apps/web/src/hooks/api/use-users.ts +0 -149
  160. package/template/apps/web/src/hooks/use-mobile.ts +0 -21
  161. package/template/apps/web/src/hooks/use-realtime-metrics.ts +0 -120
  162. package/template/apps/web/src/lib/__tests__/utils.test.ts +0 -29
  163. package/template/apps/web/src/lib/api.ts +0 -151
  164. package/template/apps/web/src/lib/auth.ts +0 -13
  165. package/template/apps/web/src/lib/env.ts +0 -52
  166. package/template/apps/web/src/lib/form-utils.ts +0 -11
  167. package/template/apps/web/src/lib/utils.ts +0 -1
  168. package/template/apps/web/src/providers.tsx +0 -34
  169. package/template/apps/web/src/store/atoms.ts +0 -15
  170. package/template/apps/web/src/test/helpers/test-utils.tsx +0 -44
  171. package/template/apps/web/src/test/setup.ts +0 -8
  172. package/template/apps/web/tailwind.config.ts +0 -5
  173. package/template/apps/web/tsconfig.json +0 -26
  174. package/template/apps/web/vitest.config.ts +0 -32
  175. package/template/assets/logo-512.png +0 -0
  176. package/template/assets/logo.svg +0 -4
  177. package/template/docker-compose.prod.yml +0 -66
  178. package/template/docker-compose.yml +0 -36
  179. package/template/eslint.config.ts +0 -119
  180. package/template/package.json +0 -77
  181. package/template/packages/tailwind-config/package.json +0 -9
  182. package/template/packages/tailwind-config/theme.css +0 -179
  183. package/template/packages/types/package.json +0 -29
  184. package/template/packages/types/src/__tests__/schemas.test.ts +0 -255
  185. package/template/packages/types/src/api-response.ts +0 -53
  186. package/template/packages/types/src/health-check.ts +0 -11
  187. package/template/packages/types/src/pagination.ts +0 -41
  188. package/template/packages/types/src/role.ts +0 -5
  189. package/template/packages/types/src/session.ts +0 -48
  190. package/template/packages/types/src/stats.ts +0 -113
  191. package/template/packages/types/src/upload.ts +0 -51
  192. package/template/packages/types/src/user.ts +0 -36
  193. package/template/packages/types/tsconfig.json +0 -5
  194. package/template/packages/types/vitest.config.ts +0 -21
  195. package/template/packages/ui/components.json +0 -21
  196. package/template/packages/ui/package.json +0 -108
  197. package/template/packages/ui/src/__tests__/button.test.tsx +0 -70
  198. package/template/packages/ui/src/alert-dialog.tsx +0 -141
  199. package/template/packages/ui/src/alert.tsx +0 -66
  200. package/template/packages/ui/src/animated-theme-toggler.tsx +0 -167
  201. package/template/packages/ui/src/avatar.tsx +0 -53
  202. package/template/packages/ui/src/badge.tsx +0 -36
  203. package/template/packages/ui/src/button.tsx +0 -84
  204. package/template/packages/ui/src/card.tsx +0 -92
  205. package/template/packages/ui/src/checkbox.tsx +0 -32
  206. package/template/packages/ui/src/data-table/data-table-column-header.tsx +0 -68
  207. package/template/packages/ui/src/data-table/data-table-pagination.tsx +0 -99
  208. package/template/packages/ui/src/data-table/data-table-toolbar.tsx +0 -55
  209. package/template/packages/ui/src/data-table/data-table-view-options.tsx +0 -63
  210. package/template/packages/ui/src/data-table/data-table.tsx +0 -167
  211. package/template/packages/ui/src/dialog.tsx +0 -143
  212. package/template/packages/ui/src/dropdown-menu.tsx +0 -257
  213. package/template/packages/ui/src/empty-state.tsx +0 -52
  214. package/template/packages/ui/src/file-upload-input.tsx +0 -202
  215. package/template/packages/ui/src/form.tsx +0 -168
  216. package/template/packages/ui/src/hooks/use-mobile.ts +0 -19
  217. package/template/packages/ui/src/icons/brand-icons.tsx +0 -16
  218. package/template/packages/ui/src/input.tsx +0 -21
  219. package/template/packages/ui/src/label.tsx +0 -24
  220. package/template/packages/ui/src/lib/utils.ts +0 -6
  221. package/template/packages/ui/src/password-input.tsx +0 -102
  222. package/template/packages/ui/src/popover.tsx +0 -48
  223. package/template/packages/ui/src/radio-group.tsx +0 -45
  224. package/template/packages/ui/src/scroll-area.tsx +0 -58
  225. package/template/packages/ui/src/select.tsx +0 -187
  226. package/template/packages/ui/src/separator.tsx +0 -28
  227. package/template/packages/ui/src/sheet.tsx +0 -139
  228. package/template/packages/ui/src/sidebar.tsx +0 -726
  229. package/template/packages/ui/src/skeleton-variants.tsx +0 -87
  230. package/template/packages/ui/src/skeleton.tsx +0 -13
  231. package/template/packages/ui/src/slider.tsx +0 -63
  232. package/template/packages/ui/src/sonner.tsx +0 -25
  233. package/template/packages/ui/src/spinner.tsx +0 -16
  234. package/template/packages/ui/src/switch.tsx +0 -31
  235. package/template/packages/ui/src/table.tsx +0 -116
  236. package/template/packages/ui/src/tabs.tsx +0 -66
  237. package/template/packages/ui/src/textarea.tsx +0 -18
  238. package/template/packages/ui/src/tooltip.tsx +0 -61
  239. package/template/packages/ui/src/user-avatar.tsx +0 -97
  240. package/template/packages/ui/test-config.js +0 -3
  241. package/template/packages/ui/tsconfig.json +0 -12
  242. package/template/packages/ui/turbo.json +0 -18
  243. package/template/packages/ui/vitest.config.ts +0 -17
  244. package/template/packages/ui/vitest.setup.ts +0 -1
  245. package/template/packages/utils/package.json +0 -23
  246. package/template/packages/utils/src/__tests__/utils.test.ts +0 -223
  247. package/template/packages/utils/src/array.ts +0 -18
  248. package/template/packages/utils/src/async.ts +0 -3
  249. package/template/packages/utils/src/date.ts +0 -77
  250. package/template/packages/utils/src/errors.ts +0 -73
  251. package/template/packages/utils/src/number.ts +0 -11
  252. package/template/packages/utils/src/string.ts +0 -13
  253. package/template/packages/utils/tsconfig.json +0 -5
  254. package/template/packages/utils/vitest.config.ts +0 -21
  255. package/template/pnpm-workspace.yaml +0 -4
  256. package/template/tsconfig.base.json +0 -32
  257. package/template/turbo.json +0 -133
  258. package/template/vitest.shared.ts +0 -26
  259. package/template/vitest.workspace.ts +0 -9
@@ -1,372 +0,0 @@
1
- 'use client';
2
-
3
- import type { AdminSession, QuerySessions } from '@repo/packages-types/session';
4
- import { Badge } from '@repo/packages-ui/badge';
5
- import { Button } from '@repo/packages-ui/button';
6
- import { DataTable } from '@repo/packages-ui/data-table/data-table';
7
- import { DataTableColumnHeader } from '@repo/packages-ui/data-table/data-table-column-header';
8
- import {
9
- DropdownMenu,
10
- DropdownMenuContent,
11
- DropdownMenuItem,
12
- DropdownMenuLabel,
13
- DropdownMenuRadioGroup,
14
- DropdownMenuRadioItem,
15
- DropdownMenuSeparator,
16
- DropdownMenuTrigger,
17
- } from '@repo/packages-ui/dropdown-menu';
18
- import { UserAvatar } from '@repo/packages-ui/user-avatar';
19
- import type {
20
- ColumnDef,
21
- ColumnFiltersState,
22
- PaginationState,
23
- SortingState,
24
- } from '@tanstack/react-table';
25
- import { formatDistanceToNow } from 'date-fns';
26
- import {
27
- Filter,
28
- Monitor,
29
- MoreHorizontal,
30
- Smartphone,
31
- Tablet,
32
- } from 'lucide-react';
33
- import * as React from 'react';
34
- import { toast } from 'sonner';
35
-
36
- import { SessionRevokeAllDialog } from '@/components/admin/session-revoke-all-dialog';
37
- import { SessionRevokeDialog } from '@/components/admin/session-revoke-dialog';
38
- import { useFetchAdminSessions } from '@/hooks/api/use-admin-sessions';
39
-
40
- function copyToClipboard(text: string, label: string) {
41
- navigator.clipboard.writeText(text);
42
- toast.success(`${label} copied to clipboard`);
43
- }
44
-
45
- function parseUserAgent(ua: string | null): string {
46
- if (!ua) return 'Unknown';
47
-
48
- if (ua.includes('Chrome') && !ua.includes('Edg')) return 'Chrome';
49
- if (ua.includes('Firefox')) return 'Firefox';
50
- if (ua.includes('Safari') && !ua.includes('Chrome')) return 'Safari';
51
- if (ua.includes('Edg')) return 'Edge';
52
- if (ua.includes('Opera') || ua.includes('OPR')) return 'Opera';
53
-
54
- return 'Other';
55
- }
56
-
57
- type DeviceType = 'Mobile' | 'Tablet' | 'Desktop' | 'Unknown';
58
-
59
- function getDeviceType(ua: string | null): DeviceType {
60
- if (!ua) return 'Unknown';
61
-
62
- if (ua.includes('Mobile') || ua.includes('Android')) return 'Mobile';
63
- if (ua.includes('Tablet') || ua.includes('iPad')) return 'Tablet';
64
- return 'Desktop';
65
- }
66
-
67
- const deviceIcons: Record<DeviceType, typeof Monitor> = {
68
- Desktop: Monitor,
69
- Mobile: Smartphone,
70
- Tablet: Tablet,
71
- Unknown: Monitor,
72
- };
73
-
74
- function isSessionActive(expiresAt: Date): boolean {
75
- return new Date(expiresAt) > new Date();
76
- }
77
-
78
- const columns: ColumnDef<AdminSession>[] = [
79
- {
80
- accessorKey: 'user',
81
- header: ({ column }) => (
82
- <DataTableColumnHeader column={column} title="User" />
83
- ),
84
- meta: { label: 'User' },
85
- cell: ({ row }) => {
86
- const user = row.original.user;
87
-
88
- return (
89
- <div className="flex items-center gap-3">
90
- <UserAvatar
91
- src={user.image}
92
- name={user.name}
93
- email={user.email}
94
- size="sm"
95
- />
96
- <div>
97
- <div className="font-medium">{user.name || 'No name'}</div>
98
- <div className="text-muted-foreground font-mono text-xs">
99
- {user.email}
100
- </div>
101
- </div>
102
- </div>
103
- );
104
- },
105
- enableSorting: false,
106
- },
107
- {
108
- accessorKey: 'ipAddress',
109
- header: ({ column }) => (
110
- <DataTableColumnHeader column={column} title="IP Address" />
111
- ),
112
- meta: { label: 'IP Address' },
113
- cell: ({ row }) => (
114
- <code className="bg-muted rounded px-1.5 py-0.5 text-xs">
115
- {row.getValue('ipAddress') || 'Unknown'}
116
- </code>
117
- ),
118
- enableSorting: false,
119
- },
120
- {
121
- accessorKey: 'userAgent',
122
- header: ({ column }) => (
123
- <DataTableColumnHeader column={column} title="Device" />
124
- ),
125
- meta: { label: 'Device' },
126
- cell: ({ row }) => {
127
- const ua = row.getValue('userAgent') as string | null;
128
- const browser = parseUserAgent(ua);
129
- const device = getDeviceType(ua);
130
- const DeviceIcon = deviceIcons[device];
131
-
132
- return (
133
- <div className="flex items-center gap-2">
134
- <div className="bg-muted flex h-8 w-8 items-center justify-center rounded-md">
135
- <DeviceIcon className="text-muted-foreground h-4 w-4" />
136
- </div>
137
- <div className="text-sm">
138
- <div className="font-medium">{browser}</div>
139
- <div className="text-muted-foreground text-xs">{device}</div>
140
- </div>
141
- </div>
142
- );
143
- },
144
- enableSorting: false,
145
- },
146
- {
147
- accessorKey: 'createdAt',
148
- header: ({ column }) => (
149
- <DataTableColumnHeader column={column} title="Created" />
150
- ),
151
- meta: { label: 'Created' },
152
- cell: ({ row }) => {
153
- const date = row.getValue('createdAt') as string | Date;
154
- return (
155
- <div className="text-muted-foreground text-sm">
156
- {formatDistanceToNow(new Date(date), { addSuffix: true })}
157
- </div>
158
- );
159
- },
160
- },
161
- {
162
- id: 'status',
163
- accessorKey: 'expiresAt',
164
- header: ({ column }) => (
165
- <DataTableColumnHeader column={column} title="Status" />
166
- ),
167
- meta: { label: 'Status' },
168
- cell: ({ row }) => {
169
- const expiresAt = row.getValue('status') as string | Date;
170
- const active = isSessionActive(new Date(expiresAt));
171
-
172
- return (
173
- <Badge variant={active ? 'default' : 'secondary'}>
174
- {active ? 'Active' : 'Expired'}
175
- </Badge>
176
- );
177
- },
178
- },
179
- {
180
- id: 'actions',
181
- cell: ({ row }) => {
182
- const session = row.original;
183
- const active = isSessionActive(new Date(session.expiresAt));
184
-
185
- if (!active) return null;
186
-
187
- return (
188
- <div className="flex items-center justify-end gap-2">
189
- <SessionRevokeDialog session={session} />
190
- <DropdownMenu>
191
- <DropdownMenuTrigger asChild>
192
- <Button variant="ghost" className="h-8 w-8 p-0">
193
- <span className="sr-only">More options</span>
194
- <MoreHorizontal className="h-4 w-4" />
195
- </Button>
196
- </DropdownMenuTrigger>
197
- <DropdownMenuContent align="end">
198
- <DropdownMenuLabel>More Actions</DropdownMenuLabel>
199
- <DropdownMenuItem
200
- onClick={() => copyToClipboard(session.id, 'Session ID')}
201
- >
202
- Copy Session ID
203
- </DropdownMenuItem>
204
- <DropdownMenuItem
205
- onClick={() => copyToClipboard(session.user.id, 'User ID')}
206
- >
207
- Copy User ID
208
- </DropdownMenuItem>
209
- {session.ipAddress && (
210
- <DropdownMenuItem
211
- onClick={() =>
212
- copyToClipboard(session.ipAddress!, 'IP Address')
213
- }
214
- >
215
- Copy IP Address
216
- </DropdownMenuItem>
217
- )}
218
- </DropdownMenuContent>
219
- </DropdownMenu>
220
- </div>
221
- );
222
- },
223
- },
224
- ];
225
-
226
- type StatusFilter = 'active' | 'expired' | 'all';
227
-
228
- export function SessionsManagementTable() {
229
- const [pagination, setPagination] = React.useState<PaginationState>({
230
- pageIndex: 0,
231
- pageSize: 10,
232
- });
233
-
234
- const [sorting, setSorting] = React.useState<SortingState>([
235
- { id: 'createdAt', desc: true },
236
- ]);
237
-
238
- const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
239
- []
240
- );
241
- const [statusFilter, setStatusFilter] =
242
- React.useState<StatusFilter>('active');
243
-
244
- const searchFilter = columnFilters.find((filter) => filter.id === 'user');
245
- const searchValue = searchFilter?.value as string | undefined;
246
-
247
- const queryParams: QuerySessions = {
248
- page: pagination.pageIndex + 1,
249
- limit: pagination.pageSize,
250
- sortBy: (sorting[0]?.id as 'createdAt' | 'expiresAt') || 'createdAt',
251
- sortOrder: sorting[0]?.desc ? 'desc' : 'asc',
252
- status: statusFilter,
253
- ...(searchValue && { search: searchValue }),
254
- };
255
-
256
- const {
257
- data: response,
258
- isLoading,
259
- isError,
260
- error,
261
- } = useFetchAdminSessions(queryParams);
262
-
263
- const sessions = React.useMemo(() => response?.data || [], [response?.data]);
264
- const total = response?.pagination?.total || 0;
265
-
266
- const userGroups = React.useMemo(() => {
267
- const groups = new Map<
268
- string,
269
- { user: AdminSession['user']; count: number }
270
- >();
271
- sessions.forEach((s) => {
272
- const existing = groups.get(s.user.id);
273
- if (existing) {
274
- existing.count++;
275
- } else {
276
- groups.set(s.user.id, { user: s.user, count: 1 });
277
- }
278
- });
279
- return Array.from(groups.values()).filter((g) => g.count > 1);
280
- }, [sessions]);
281
-
282
- if (isError) {
283
- return (
284
- <div className="border-destructive/50 bg-destructive/10 rounded-lg border p-4">
285
- <p className="text-destructive text-sm">
286
- Error loading sessions: {error?.message || 'Unknown error'}
287
- </p>
288
- </div>
289
- );
290
- }
291
-
292
- return (
293
- <div className="space-y-4">
294
- <div className="flex items-center justify-between gap-4">
295
- <div className="flex items-center gap-2">
296
- <DropdownMenu>
297
- <DropdownMenuTrigger asChild>
298
- <Button variant="outline" size="sm">
299
- <Filter className="mr-2 h-4 w-4" />
300
- Status:{' '}
301
- {statusFilter.charAt(0).toUpperCase() + statusFilter.slice(1)}
302
- </Button>
303
- </DropdownMenuTrigger>
304
- <DropdownMenuContent align="start" className="w-48">
305
- <DropdownMenuLabel>Filter by Status</DropdownMenuLabel>
306
- <DropdownMenuSeparator />
307
- <DropdownMenuRadioGroup
308
- value={statusFilter}
309
- onValueChange={(value) =>
310
- setStatusFilter(value as StatusFilter)
311
- }
312
- >
313
- <DropdownMenuRadioItem value="active">
314
- Active
315
- </DropdownMenuRadioItem>
316
- <DropdownMenuRadioItem value="expired">
317
- Expired
318
- </DropdownMenuRadioItem>
319
- <DropdownMenuRadioItem value="all">All</DropdownMenuRadioItem>
320
- </DropdownMenuRadioGroup>
321
- </DropdownMenuContent>
322
- </DropdownMenu>
323
-
324
- {userGroups.length > 0 && (
325
- <DropdownMenu>
326
- <DropdownMenuTrigger asChild>
327
- <Button variant="outline" size="sm">
328
- Bulk Actions
329
- </Button>
330
- </DropdownMenuTrigger>
331
- <DropdownMenuContent align="start" className="w-64">
332
- <DropdownMenuLabel>Revoke All Sessions For</DropdownMenuLabel>
333
- <DropdownMenuSeparator />
334
- {userGroups.slice(0, 5).map(({ user, count }) => (
335
- <div key={user.id} className="px-2 py-1.5">
336
- <SessionRevokeAllDialog
337
- userId={user.id}
338
- userName={user.name || ''}
339
- userEmail={user.email}
340
- />
341
- <span className="text-muted-foreground ml-2 text-xs">
342
- ({count} sessions)
343
- </span>
344
- </div>
345
- ))}
346
- </DropdownMenuContent>
347
- </DropdownMenu>
348
- )}
349
- </div>
350
-
351
- <div className="text-muted-foreground text-sm">
352
- {total} session{total !== 1 ? 's' : ''}
353
- </div>
354
- </div>
355
-
356
- <DataTable
357
- columns={columns}
358
- data={sessions}
359
- pagination={pagination}
360
- onPaginationChange={setPagination}
361
- sorting={sorting}
362
- onSortingChange={setSorting}
363
- columnFilters={columnFilters}
364
- onColumnFiltersChange={setColumnFilters}
365
- rowCount={total}
366
- isLoading={isLoading}
367
- searchPlaceholder="Search by user email or name..."
368
- searchColumnId="user"
369
- />
370
- </div>
371
- );
372
- }
@@ -1,137 +0,0 @@
1
- 'use client';
2
-
3
- import { cn } from '@repo/packages-ui/lib/utils';
4
- import { Skeleton } from '@repo/packages-ui/skeleton';
5
- import { animate, motion, useMotionValue, useTransform } from 'framer-motion';
6
- import type { LucideIcon } from 'lucide-react';
7
- import { useEffect } from 'react';
8
-
9
- interface StatCardProps {
10
- title: string;
11
- value: number;
12
- icon: LucideIcon;
13
- formatter?: (value: number) => string;
14
- trend?: {
15
- value: number;
16
- label: string;
17
- };
18
- className?: string;
19
- accentColor?: 'emerald' | 'blue' | 'amber' | 'rose' | 'violet' | 'cyan';
20
- }
21
-
22
- const accentColors = {
23
- emerald: {
24
- bg: 'bg-emerald-500/10',
25
- icon: 'text-emerald-500',
26
- trend: 'text-emerald-600',
27
- },
28
- blue: {
29
- bg: 'bg-blue-500/10',
30
- icon: 'text-blue-500',
31
- trend: 'text-blue-600',
32
- },
33
- amber: {
34
- bg: 'bg-amber-500/10',
35
- icon: 'text-amber-500',
36
- trend: 'text-amber-600',
37
- },
38
- rose: {
39
- bg: 'bg-rose-500/10',
40
- icon: 'text-rose-500',
41
- trend: 'text-rose-600',
42
- },
43
- violet: {
44
- bg: 'bg-violet-500/10',
45
- icon: 'text-violet-500',
46
- trend: 'text-violet-600',
47
- },
48
- cyan: {
49
- bg: 'bg-cyan-500/10',
50
- icon: 'text-cyan-500',
51
- trend: 'text-cyan-600',
52
- },
53
- };
54
-
55
- function AnimatedNumber({
56
- value,
57
- formatter = (v) => v.toLocaleString(),
58
- }: {
59
- value: number;
60
- formatter?: (value: number) => string;
61
- }) {
62
- const motionValue = useMotionValue(0);
63
- const rounded = useTransform(motionValue, (latest) =>
64
- formatter(Math.round(latest))
65
- );
66
-
67
- useEffect(() => {
68
- const controls = animate(motionValue, value, {
69
- duration: 0.8,
70
- ease: 'easeOut',
71
- });
72
- return controls.stop;
73
- }, [value, motionValue]);
74
-
75
- return <motion.span>{rounded}</motion.span>;
76
- }
77
-
78
- export function StatCard({
79
- title,
80
- value,
81
- icon: Icon,
82
- formatter,
83
- trend,
84
- className,
85
- accentColor = 'blue',
86
- }: StatCardProps) {
87
- const colors = accentColors[accentColor];
88
-
89
- return (
90
- <motion.div
91
- initial={{ opacity: 0, y: 20 }}
92
- animate={{ opacity: 1, y: 0 }}
93
- className={cn(
94
- 'bg-card relative overflow-hidden rounded-xl border p-5',
95
- className
96
- )}
97
- >
98
- <div className="flex items-start justify-between">
99
- <div className="flex-1 space-y-1">
100
- <p className="text-muted-foreground text-sm font-medium">{title}</p>
101
- <p className="text-2xl font-semibold tracking-tight">
102
- <AnimatedNumber value={value} formatter={formatter} />
103
- </p>
104
- {trend && (
105
- <p className={cn('text-xs font-medium', colors.trend)}>
106
- +{trend.value} {trend.label}
107
- </p>
108
- )}
109
- </div>
110
- <div className={cn('rounded-lg p-2.5', colors.bg)}>
111
- <Icon className={cn('size-5', colors.icon)} />
112
- </div>
113
- </div>
114
- <div
115
- className={cn(
116
- 'absolute -bottom-8 -right-8 size-24 rounded-full opacity-5',
117
- colors.bg
118
- )}
119
- />
120
- </motion.div>
121
- );
122
- }
123
-
124
- export function StatCardSkeleton() {
125
- return (
126
- <div className="bg-card rounded-xl border p-5">
127
- <div className="flex items-start justify-between">
128
- <div className="flex-1 space-y-2">
129
- <Skeleton className="h-4 w-24" />
130
- <Skeleton className="h-8 w-16" />
131
- <Skeleton className="h-3 w-20" />
132
- </div>
133
- <Skeleton className="size-10 rounded-lg" />
134
- </div>
135
- </div>
136
- );
137
- }
@@ -1,152 +0,0 @@
1
- 'use client';
2
-
3
- import type { Role } from '@repo/packages-types/role';
4
- import { RoleSchema } from '@repo/packages-types/role';
5
- import type { CreateUser } from '@repo/packages-types/user';
6
- import { Button } from '@repo/packages-ui/button';
7
- import {
8
- Dialog,
9
- DialogContent,
10
- DialogDescription,
11
- DialogFooter,
12
- DialogHeader,
13
- DialogTitle,
14
- DialogTrigger,
15
- } from '@repo/packages-ui/dialog';
16
- import { Input } from '@repo/packages-ui/input';
17
- import { Label } from '@repo/packages-ui/label';
18
- import {
19
- Select,
20
- SelectContent,
21
- SelectItem,
22
- SelectTrigger,
23
- SelectValue,
24
- } from '@repo/packages-ui/select';
25
- import { UserPlus } from 'lucide-react';
26
- import * as React from 'react';
27
- import { toast } from 'sonner';
28
-
29
- import { useCreateUser } from '@/hooks/api/use-users';
30
-
31
- const INITIAL_FORM_DATA: CreateUser = {
32
- name: '',
33
- email: '',
34
- role: 'user',
35
- };
36
-
37
- export function UserCreateDialog() {
38
- const [open, setOpen] = React.useState(false);
39
- const [formData, setFormData] = React.useState<CreateUser>(INITIAL_FORM_DATA);
40
-
41
- const { mutate: createUser, isPending } = useCreateUser();
42
-
43
- const handleSubmit = (e: React.FormEvent) => {
44
- e.preventDefault();
45
-
46
- createUser(formData, {
47
- onSuccess: () => {
48
- toast.success('User created successfully');
49
- setFormData(INITIAL_FORM_DATA);
50
- setOpen(false);
51
- },
52
- onError: (error) => {
53
- toast.error(error.message || 'Failed to create user');
54
- },
55
- });
56
- };
57
-
58
- React.useEffect(() => {
59
- if (!open) {
60
- setFormData(INITIAL_FORM_DATA);
61
- }
62
- }, [open]);
63
-
64
- return (
65
- <Dialog open={open} onOpenChange={setOpen}>
66
- <DialogTrigger asChild>
67
- <Button>
68
- <UserPlus className="mr-2 h-4 w-4" />
69
- Add User
70
- </Button>
71
- </DialogTrigger>
72
- <DialogContent className="sm:max-w-[500px]">
73
- <form onSubmit={handleSubmit}>
74
- <DialogHeader>
75
- <DialogTitle>Create New User</DialogTitle>
76
- <DialogDescription>
77
- Add a new user to the system with role assignment
78
- </DialogDescription>
79
- </DialogHeader>
80
-
81
- <div className="grid gap-4 py-4">
82
- <div className="grid gap-2">
83
- <Label htmlFor="create-name">Name</Label>
84
- <Input
85
- id="create-name"
86
- value={formData.name}
87
- onChange={(e) =>
88
- setFormData((prev) => ({ ...prev, name: e.target.value }))
89
- }
90
- placeholder="Enter user name"
91
- required
92
- autoComplete="off"
93
- />
94
- </div>
95
-
96
- <div className="grid gap-2">
97
- <Label htmlFor="create-email">Email</Label>
98
- <Input
99
- id="create-email"
100
- type="email"
101
- value={formData.email}
102
- onChange={(e) =>
103
- setFormData((prev) => ({ ...prev, email: e.target.value }))
104
- }
105
- placeholder="user@example.com"
106
- required
107
- autoComplete="off"
108
- />
109
- </div>
110
-
111
- <div className="grid gap-2">
112
- <Label htmlFor="create-role">Role</Label>
113
- <Select
114
- value={formData.role}
115
- onValueChange={(value) =>
116
- setFormData((prev) => ({ ...prev, role: value as Role }))
117
- }
118
- >
119
- <SelectTrigger id="create-role">
120
- <SelectValue placeholder="Select role" />
121
- </SelectTrigger>
122
- <SelectContent>
123
- {RoleSchema.options.map((role) => (
124
- <SelectItem key={role} value={role}>
125
- {role === 'super_admin'
126
- ? 'Super Admin'
127
- : role.charAt(0).toUpperCase() + role.slice(1)}
128
- </SelectItem>
129
- ))}
130
- </SelectContent>
131
- </Select>
132
- </div>
133
- </div>
134
-
135
- <DialogFooter>
136
- <Button
137
- type="button"
138
- variant="outline"
139
- onClick={() => setOpen(false)}
140
- disabled={isPending}
141
- >
142
- Cancel
143
- </Button>
144
- <Button type="submit" disabled={isPending}>
145
- {isPending ? 'Creating...' : 'Create User'}
146
- </Button>
147
- </DialogFooter>
148
- </form>
149
- </DialogContent>
150
- </Dialog>
151
- );
152
- }