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,296 @@
1
+ 'use client';
2
+
3
+ import { Button } from '@repo/packages-ui/button';
4
+ import { Skeleton } from '@repo/packages-ui/skeleton';
5
+ import { UserAvatar } from '@repo/packages-ui/user-avatar';
6
+ import {
7
+ formatDateTime,
8
+ formatShortDate,
9
+ getRelativeTime,
10
+ } from '@repo/packages-utils/date';
11
+ import {
12
+ Calendar,
13
+ Clock,
14
+ KeyRound,
15
+ LogOut,
16
+ Mail,
17
+ ShieldCheck,
18
+ ShieldUser,
19
+ User,
20
+ } from 'lucide-react';
21
+ import Link from 'next/link';
22
+ import { useRouter } from 'next/navigation';
23
+ import React from 'react';
24
+
25
+ import { EmailVerificationBanner } from '@/components/auth/email-verification-banner';
26
+ import { ProtectedRoute } from '@/components/auth/protected-route';
27
+ import { authClient } from '@/lib/auth';
28
+
29
+ function DashboardContent() {
30
+ const router = useRouter();
31
+ const { data: session } = authClient.useSession();
32
+
33
+ const handleSignOut = async () => {
34
+ await authClient.signOut();
35
+ router.push('/');
36
+ };
37
+
38
+ if (!session) return null;
39
+
40
+ const accountCreatedDate = session.user.createdAt
41
+ ? formatShortDate(session.user.createdAt)
42
+ : 'N/A';
43
+
44
+ const lastLoginDate = session.session.createdAt
45
+ ? getRelativeTime(session.session.createdAt, { alwaysShowTime: true })
46
+ : 'N/A';
47
+
48
+ const sessionExpiresDate = session.session.expiresAt
49
+ ? formatDateTime(session.session.expiresAt)
50
+ : 'N/A';
51
+
52
+ const isEmailVerified = session.user.emailVerified;
53
+ const isAdmin =
54
+ session.user.role === 'admin' || session.user.role === 'super_admin';
55
+
56
+ return (
57
+ <div className="container mx-auto max-w-6xl space-y-6 p-8">
58
+ <div className="mb-4">
59
+ <EmailVerificationBanner />
60
+ </div>
61
+
62
+ <div className="flex items-center gap-4">
63
+ <UserAvatar
64
+ src={session.user.image}
65
+ name={session.user.name}
66
+ email={session.user.email}
67
+ />
68
+ <div>
69
+ <h1 className="text-3xl font-bold">
70
+ Welcome back, {session.user.name || 'User'}!
71
+ </h1>
72
+ <p className="text-muted-foreground">
73
+ Here's what's happening with your account
74
+ </p>
75
+ </div>
76
+ </div>
77
+
78
+ <div className="grid gap-4 md:grid-cols-3">
79
+ <div className="border-border bg-card rounded-lg border p-6">
80
+ <div className="flex items-center gap-3">
81
+ <div className="bg-primary/10 text-primary rounded-full p-3">
82
+ <Calendar className="h-5 w-5" />
83
+ </div>
84
+ <div>
85
+ <p className="text-muted-foreground text-xs font-medium uppercase tracking-wide">
86
+ Account Created
87
+ </p>
88
+ <p className="text-lg font-semibold">{accountCreatedDate}</p>
89
+ </div>
90
+ </div>
91
+ </div>
92
+
93
+ <div className="border-border bg-card rounded-lg border p-6">
94
+ <div className="flex items-center gap-3">
95
+ <div className="bg-primary/10 text-primary rounded-full p-3">
96
+ <Clock className="h-5 w-5" />
97
+ </div>
98
+ <div>
99
+ <p className="text-muted-foreground text-xs font-medium uppercase tracking-wide">
100
+ Last Login
101
+ </p>
102
+ <p className="text-lg font-semibold">{lastLoginDate}</p>
103
+ </div>
104
+ </div>
105
+ </div>
106
+
107
+ <div className="border-border bg-card rounded-lg border p-6">
108
+ <div className="flex items-center gap-3">
109
+ <div
110
+ className={`rounded-full p-3 ${
111
+ isEmailVerified
112
+ ? 'bg-green-500/10 text-green-600 dark:text-green-500'
113
+ : 'bg-yellow-500/10 text-yellow-600 dark:text-yellow-500'
114
+ }`}
115
+ >
116
+ <ShieldCheck className="h-5 w-5" />
117
+ </div>
118
+ <div>
119
+ <p className="text-muted-foreground text-xs font-medium uppercase tracking-wide">
120
+ Email Status
121
+ </p>
122
+ <p className="text-lg font-semibold">
123
+ {isEmailVerified ? 'Verified' : 'Not Verified'}
124
+ </p>
125
+ </div>
126
+ </div>
127
+ </div>
128
+ </div>
129
+
130
+ <div className="grid gap-6 md:grid-cols-2">
131
+ <div className="border-border bg-card rounded-lg border p-8">
132
+ <div className="mb-4 flex items-center gap-2">
133
+ <User className="text-primary h-5 w-5" />
134
+ <h2 className="text-lg font-semibold">User Information</h2>
135
+ </div>
136
+ <div className="space-y-4">
137
+ <div className="flex items-start gap-3">
138
+ <Mail className="text-muted-foreground mt-0.5 h-4 w-4" />
139
+ <div className="flex-1">
140
+ <p className="text-muted-foreground text-xs font-medium">
141
+ Email Address
142
+ </p>
143
+ <p className="text-sm font-medium">{session.user.email}</p>
144
+ </div>
145
+ </div>
146
+ <div className="flex items-start gap-3">
147
+ <User className="text-muted-foreground mt-0.5 h-4 w-4" />
148
+ <div className="flex-1">
149
+ <p className="text-muted-foreground text-xs font-medium">
150
+ Display Name
151
+ </p>
152
+ <p className="text-sm font-medium">
153
+ {session.user.name || (
154
+ <span className="text-muted-foreground italic">
155
+ Not set
156
+ </span>
157
+ )}
158
+ </p>
159
+ </div>
160
+ </div>
161
+ <div className="flex items-start gap-3">
162
+ <ShieldUser className="text-muted-foreground mt-0.5 h-4 w-4" />
163
+ <div className="flex-1">
164
+ <p className="text-muted-foreground text-xs font-medium">
165
+ Role
166
+ </p>
167
+ <p className="text-sm font-medium capitalize">
168
+ {session.user.role || 'User'}
169
+ </p>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ </div>
174
+
175
+ <div className="border-border bg-card rounded-lg border p-8">
176
+ <div className="mb-4 flex items-center gap-2">
177
+ <Clock className="text-primary h-5 w-5" />
178
+ <h2 className="text-lg font-semibold">Session Details</h2>
179
+ </div>
180
+ <div className="space-y-4">
181
+ <div className="flex items-start gap-3">
182
+ <Calendar className="text-muted-foreground mt-0.5 h-4 w-4" />
183
+ <div className="flex-1">
184
+ <p className="text-muted-foreground text-xs font-medium">
185
+ Session Created
186
+ </p>
187
+ <p className="text-sm font-medium">
188
+ {formatDateTime(session.session.createdAt)}
189
+ </p>
190
+ </div>
191
+ </div>
192
+ <div className="flex items-start gap-3">
193
+ <Clock className="text-muted-foreground mt-0.5 h-4 w-4" />
194
+ <div className="flex-1">
195
+ <p className="text-muted-foreground text-xs font-medium">
196
+ Session Expires
197
+ </p>
198
+ <p className="text-sm font-medium">{sessionExpiresDate}</p>
199
+ </div>
200
+ </div>
201
+ </div>
202
+ </div>
203
+ </div>
204
+
205
+ <div className="border-border bg-card rounded-lg border p-8">
206
+ <h2 className="mb-4 text-lg font-semibold">Quick Actions</h2>
207
+ <div className="flex flex-wrap gap-3">
208
+ {isAdmin && (
209
+ <Link href="/admin">
210
+ <Button variant="default" className="gap-2">
211
+ <ShieldCheck className="h-4 w-4" />
212
+ Admin Dashboard
213
+ </Button>
214
+ </Link>
215
+ )}
216
+ <Link href="/dashboard/change-password">
217
+ <Button variant="default" className="gap-2">
218
+ <KeyRound className="h-4 w-4" />
219
+ Change Password
220
+ </Button>
221
+ </Link>
222
+ <Button onClick={handleSignOut} variant="outline" className="gap-2">
223
+ <LogOut className="h-4 w-4" />
224
+ Sign Out
225
+ </Button>
226
+ </div>
227
+ </div>
228
+ </div>
229
+ );
230
+ }
231
+
232
+ function DashboardSkeleton() {
233
+ return (
234
+ <div className="container mx-auto max-w-6xl space-y-6 p-8">
235
+ <div className="flex items-center gap-4">
236
+ <Skeleton className="h-16 w-16 rounded-full" />
237
+ <div className="space-y-2">
238
+ <Skeleton className="h-8 w-64" />
239
+ <Skeleton className="h-4 w-80" />
240
+ </div>
241
+ </div>
242
+
243
+ <div className="grid gap-4 md:grid-cols-3">
244
+ {[...Array(3)].map((_, i) => (
245
+ <div key={i} className="border-border bg-card rounded-lg border p-6">
246
+ <div className="flex items-center gap-3">
247
+ <Skeleton className="h-11 w-11 rounded-full" />
248
+ <div className="space-y-2">
249
+ <Skeleton className="h-3 w-24" />
250
+ <Skeleton className="h-5 w-32" />
251
+ </div>
252
+ </div>
253
+ </div>
254
+ ))}
255
+ </div>
256
+
257
+ <div className="grid gap-6 md:grid-cols-2">
258
+ {[...Array(2)].map((_, i) => (
259
+ <div key={i} className="border-border bg-card rounded-lg border p-8">
260
+ <div className="mb-4 flex items-center gap-2">
261
+ <Skeleton className="h-5 w-5 rounded" />
262
+ <Skeleton className="h-6 w-40" />
263
+ </div>
264
+ <div className="space-y-4">
265
+ {[...Array(3)].map((_, j) => (
266
+ <div key={j} className="flex items-start gap-3">
267
+ <Skeleton className="mt-0.5 h-4 w-4 rounded" />
268
+ <div className="flex-1 space-y-2">
269
+ <Skeleton className="h-3 w-24" />
270
+ <Skeleton className="h-4 w-full" />
271
+ </div>
272
+ </div>
273
+ ))}
274
+ </div>
275
+ </div>
276
+ ))}
277
+ </div>
278
+
279
+ <div className="border-border bg-card rounded-lg border p-8">
280
+ <Skeleton className="mb-4 h-6 w-32" />
281
+ <div className="flex flex-wrap gap-3">
282
+ <Skeleton className="h-10 w-40 rounded-md" />
283
+ <Skeleton className="h-10 w-28 rounded-md" />
284
+ </div>
285
+ </div>
286
+ </div>
287
+ );
288
+ }
289
+
290
+ export default function DashboardPage() {
291
+ return (
292
+ <ProtectedRoute redirectTo="/login" fallback={<DashboardSkeleton />}>
293
+ <DashboardContent />
294
+ </ProtectedRoute>
295
+ );
296
+ }
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+
3
+ import { Button } from '@repo/packages-ui/button';
4
+ import { ServerCrash } from 'lucide-react';
5
+ import Link from 'next/link';
6
+
7
+ import { ErrorCard } from '@/components/error/error-card';
8
+
9
+ interface ErrorProps {
10
+ error: Error & { digest?: string };
11
+ reset: () => void;
12
+ }
13
+
14
+ export default function Error({ reset }: ErrorProps) {
15
+ return (
16
+ <ErrorCard
17
+ icon={ServerCrash}
18
+ title="Something went wrong"
19
+ description="An unexpected error occurred. Please try again or return to the home page."
20
+ actions={
21
+ <>
22
+ <Button variant="outline" onClick={reset}>
23
+ Try again
24
+ </Button>
25
+ <Button asChild>
26
+ <Link href="/">Go home</Link>
27
+ </Button>
28
+ </>
29
+ }
30
+ />
31
+ );
32
+ }
@@ -0,0 +1,200 @@
1
+ 'use client';
2
+
3
+ import { Alert } from '@repo/packages-ui/alert';
4
+ import { Button } from '@repo/packages-ui/button';
5
+ import { FileUploadInput } from '@repo/packages-ui/file-upload-input';
6
+ import { FileText, Image, Loader2, Trash2 } from 'lucide-react';
7
+ import { type Upload } from 'packages/types/src/upload';
8
+ import React, { useState } from 'react';
9
+
10
+ import {
11
+ useDeleteUpload,
12
+ useFetchUploads,
13
+ useFetchUploadStats,
14
+ useUploadFile,
15
+ } from '@/hooks/api/use-uploads';
16
+
17
+ export default function FileUploadExamplePage() {
18
+ const [selectedFile, setSelectedFile] = useState<globalThis.File | null>(
19
+ null
20
+ );
21
+
22
+ const { data: uploads, isLoading: uploadsLoading } = useFetchUploads();
23
+ const { data: stats } = useFetchUploadStats();
24
+ const uploadMutation = useUploadFile();
25
+ const deleteMutation = useDeleteUpload();
26
+
27
+ const handleFileSelect = (file: globalThis.File) => {
28
+ setSelectedFile(file);
29
+ };
30
+
31
+ const handleUpload = () => {
32
+ if (!selectedFile) return;
33
+
34
+ uploadMutation.mutate(selectedFile, {
35
+ onSuccess: () => {
36
+ setSelectedFile(null);
37
+ },
38
+ });
39
+ };
40
+
41
+ const handleDelete = (uploadId: string) => {
42
+ if (globalThis.confirm('Are you sure you want to delete this file?')) {
43
+ deleteMutation.mutate({ id: uploadId });
44
+ }
45
+ };
46
+
47
+ const getFileIcon = (mimeType: string) => {
48
+ if (mimeType.startsWith('image/')) {
49
+ return <Image className="text-primary h-8 w-8" />;
50
+ }
51
+ return <FileText className="text-primary h-8 w-8" />;
52
+ };
53
+
54
+ const formatFileSize = (bytes: number) => {
55
+ if (bytes < 1024) return `${bytes} B`;
56
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
57
+ return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
58
+ };
59
+
60
+ const formatDate = (date: Date | string) => {
61
+ return new Date(date).toLocaleDateString('en-US', {
62
+ year: 'numeric',
63
+ month: 'short',
64
+ day: 'numeric',
65
+ hour: '2-digit',
66
+ minute: '2-digit',
67
+ });
68
+ };
69
+
70
+ return (
71
+ <div className="container mx-auto max-w-5xl px-4 py-12">
72
+ <div className="space-y-8">
73
+ {/* Header */}
74
+ <div>
75
+ <h1 className="text-3xl font-bold tracking-tight">File Upload</h1>
76
+ <p className="text-muted-foreground mt-2">
77
+ Upload images, PDFs, and documents up to 10MB
78
+ </p>
79
+ </div>
80
+
81
+ {/* Upload Stats */}
82
+ {stats && (
83
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
84
+ <div className="bg-card rounded-lg border p-6">
85
+ <div className="text-muted-foreground text-sm font-medium">
86
+ Total Files
87
+ </div>
88
+ <div className="mt-2 text-3xl font-bold">{stats.totalFiles}</div>
89
+ </div>
90
+ <div className="bg-card rounded-lg border p-6">
91
+ <div className="text-muted-foreground text-sm font-medium">
92
+ Total Size
93
+ </div>
94
+ <div className="mt-2 text-3xl font-bold">
95
+ {formatFileSize(stats.totalSize)}
96
+ </div>
97
+ </div>
98
+ </div>
99
+ )}
100
+
101
+ {/* Upload Section */}
102
+ <div className="bg-card space-y-4 rounded-lg border p-6">
103
+ <h2 className="text-lg font-semibold">Upload File</h2>
104
+
105
+ <FileUploadInput
106
+ onFileSelect={handleFileSelect}
107
+ disabled={uploadMutation.isPending}
108
+ />
109
+
110
+ {selectedFile && (
111
+ <div className="flex justify-end gap-2">
112
+ <Button
113
+ variant="outline"
114
+ onClick={() => setSelectedFile(null)}
115
+ disabled={uploadMutation.isPending}
116
+ >
117
+ Cancel
118
+ </Button>
119
+ <Button
120
+ onClick={handleUpload}
121
+ disabled={uploadMutation.isPending}
122
+ >
123
+ {uploadMutation.isPending && (
124
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
125
+ )}
126
+ Upload
127
+ </Button>
128
+ </div>
129
+ )}
130
+
131
+ {uploadMutation.isError && (
132
+ <Alert variant="destructive">
133
+ {uploadMutation.error?.message || 'Upload failed'}
134
+ </Alert>
135
+ )}
136
+
137
+ {uploadMutation.isSuccess && (
138
+ <Alert>File uploaded successfully!</Alert>
139
+ )}
140
+ </div>
141
+
142
+ {/* Uploads List */}
143
+ <div className="bg-card space-y-4 rounded-lg border p-6">
144
+ <h2 className="text-lg font-semibold">Your Uploads</h2>
145
+
146
+ {uploadsLoading ? (
147
+ <div className="flex justify-center py-8">
148
+ <Loader2 className="text-muted-foreground h-8 w-8 animate-spin" />
149
+ </div>
150
+ ) : uploads && uploads.length > 0 ? (
151
+ <div className="space-y-3">
152
+ {uploads.map((upload: Upload) => (
153
+ <div
154
+ key={upload.id}
155
+ className="hover:bg-accent/50 flex items-center justify-between rounded-lg border p-4 transition-colors"
156
+ >
157
+ <div className="flex min-w-0 flex-1 items-center gap-4">
158
+ {getFileIcon(upload.mimeType)}
159
+ <div className="min-w-0 flex-1">
160
+ <p className="truncate font-medium">
161
+ {upload.originalName}
162
+ </p>
163
+ <div className="text-muted-foreground mt-1 flex items-center gap-3 text-sm">
164
+ <span>{formatFileSize(upload.size)}</span>
165
+ <span>•</span>
166
+ <span>{formatDate(upload.createdAt)}</span>
167
+ </div>
168
+ </div>
169
+ </div>
170
+ <div className="flex items-center gap-2">
171
+ <Button
172
+ variant="outline"
173
+ size="sm"
174
+ onClick={() => window.open(upload.url, '_blank')}
175
+ >
176
+ View
177
+ </Button>
178
+ <Button
179
+ variant="ghost"
180
+ size="icon"
181
+ onClick={() => handleDelete(upload.id)}
182
+ disabled={deleteMutation.isPending}
183
+ >
184
+ <Trash2 className="text-destructive h-4 w-4" />
185
+ </Button>
186
+ </div>
187
+ </div>
188
+ ))}
189
+ </div>
190
+ ) : (
191
+ <div className="text-muted-foreground py-12 text-center">
192
+ <FileText className="mx-auto mb-4 h-12 w-12 opacity-50" />
193
+ <p>No files uploaded yet</p>
194
+ </div>
195
+ )}
196
+ </div>
197
+ </div>
198
+ </div>
199
+ );
200
+ }
@@ -0,0 +1,96 @@
1
+ 'use client';
2
+
3
+ import { AlertTriangle } from 'lucide-react';
4
+
5
+ interface GlobalErrorProps {
6
+ error: Error & { digest?: string };
7
+ reset: () => void;
8
+ }
9
+
10
+ export default function GlobalError({ reset }: GlobalErrorProps) {
11
+ return (
12
+ <html lang="en">
13
+ <body>
14
+ <div
15
+ style={{
16
+ display: 'flex',
17
+ minHeight: '100vh',
18
+ alignItems: 'center',
19
+ justifyContent: 'center',
20
+ padding: '2rem',
21
+ fontFamily: 'system-ui, sans-serif',
22
+ }}
23
+ >
24
+ <div
25
+ style={{
26
+ maxWidth: '28rem',
27
+ width: '100%',
28
+ padding: '2rem',
29
+ border: '1px solid #e5e5e5',
30
+ borderRadius: '0.5rem',
31
+ backgroundColor: '#ffffff',
32
+ }}
33
+ >
34
+ <div
35
+ style={{
36
+ display: 'flex',
37
+ flexDirection: 'column',
38
+ alignItems: 'center',
39
+ textAlign: 'center',
40
+ }}
41
+ >
42
+ <div
43
+ style={{
44
+ marginBottom: '1rem',
45
+ padding: '1rem',
46
+ borderRadius: '9999px',
47
+ backgroundColor: '#f5f5f5',
48
+ }}
49
+ >
50
+ <AlertTriangle
51
+ style={{ width: '2rem', height: '2rem', color: '#737373' }}
52
+ />
53
+ </div>
54
+ <h1
55
+ style={{
56
+ marginBottom: '0.5rem',
57
+ fontSize: '1.5rem',
58
+ fontWeight: '600',
59
+ color: '#171717',
60
+ }}
61
+ >
62
+ Critical error
63
+ </h1>
64
+ <p
65
+ style={{
66
+ marginBottom: '1.5rem',
67
+ fontSize: '0.875rem',
68
+ lineHeight: '1.5',
69
+ color: '#737373',
70
+ }}
71
+ >
72
+ A critical error occurred. Please refresh the page or contact
73
+ support if the problem persists.
74
+ </p>
75
+ <button
76
+ onClick={reset}
77
+ style={{
78
+ padding: '0.5rem 1rem',
79
+ fontSize: '0.875rem',
80
+ fontWeight: '500',
81
+ color: '#ffffff',
82
+ backgroundColor: '#171717',
83
+ border: 'none',
84
+ borderRadius: '0.375rem',
85
+ cursor: 'pointer',
86
+ }}
87
+ >
88
+ Try again
89
+ </button>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ </body>
94
+ </html>
95
+ );
96
+ }
@@ -0,0 +1,22 @@
1
+ @import 'tailwindcss';
2
+ @import 'tw-animate-css';
3
+ @import '@repo/tailwind-config/theme.css';
4
+
5
+ @config '../../tailwind.config.ts';
6
+
7
+ @source '../../../../packages/ui/src/**/*.{ts,tsx}';
8
+
9
+ @plugin "tailwindcss-debug-screens" {
10
+ classname: 'debug-screens';
11
+ position: 'bottom, right';
12
+ prefix: 'screen: ';
13
+ }
14
+
15
+ ::view-transition-old(root) {
16
+ animation: none;
17
+ }
18
+
19
+ ::view-transition-new(root) {
20
+ animation: none;
21
+ mix-blend-mode: normal;
22
+ }
@@ -0,0 +1,34 @@
1
+ import './globals.css';
2
+
3
+ import { Toaster } from '@repo/packages-ui/sonner';
4
+ import type { Metadata } from 'next';
5
+ import React from 'react';
6
+
7
+ import { Footer } from '@/components/layout/footer';
8
+ import { isDevelopment } from '@/lib/env';
9
+ import { Providers } from '@/providers';
10
+
11
+ export const metadata: Metadata = {
12
+ title: 'Blitzpack',
13
+ description: 'Production-ready TypeScript monorepo',
14
+ };
15
+
16
+ export default function RootLayout({
17
+ children,
18
+ }: Readonly<{
19
+ children: React.ReactNode;
20
+ }>) {
21
+ return (
22
+ <html lang="en" suppressHydrationWarning>
23
+ <body
24
+ className={`flex min-h-screen flex-col ${isDevelopment() ? 'debug-screens' : ''}`}
25
+ >
26
+ <Providers>
27
+ {children}
28
+ <Footer />
29
+ <Toaster />
30
+ </Providers>
31
+ </body>
32
+ </html>
33
+ );
34
+ }