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,223 @@
1
+ import { groupBy, unique } from '@repo/packages-utils/array';
2
+ import { sleep } from '@repo/packages-utils/async';
3
+ import {
4
+ formatDate,
5
+ formatDateTime,
6
+ getRelativeTime,
7
+ } from '@repo/packages-utils/date';
8
+ import { clamp, formatBytes } from '@repo/packages-utils/number';
9
+ import { slugify, truncate } from '@repo/packages-utils/string';
10
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
11
+
12
+ describe('slugify', () => {
13
+ it('should convert text to slug format', () => {
14
+ expect(slugify('Hello World')).toBe('hello-world');
15
+ expect(slugify('TypeScript is Great!')).toBe('typescript-is-great');
16
+ });
17
+
18
+ it('should handle special characters', () => {
19
+ expect(slugify('React & Vue.js')).toBe('react-vuejs');
20
+ expect(slugify('C++ Programming')).toBe('c-programming');
21
+ });
22
+
23
+ it('should handle multiple spaces and trim', () => {
24
+ expect(slugify(' too many spaces ')).toBe('too-many-spaces');
25
+ });
26
+
27
+ it('should handle underscores', () => {
28
+ expect(slugify('snake_case_text')).toBe('snake-case-text');
29
+ });
30
+ });
31
+
32
+ describe('truncate', () => {
33
+ it('should truncate text longer than maxLength', () => {
34
+ expect(truncate('This is a long text', 10)).toBe('This is...');
35
+ });
36
+
37
+ it('should not truncate text shorter than maxLength', () => {
38
+ expect(truncate('Short', 10)).toBe('Short');
39
+ });
40
+
41
+ it('should return text as-is when exactly maxLength', () => {
42
+ expect(truncate('Exactly10!', 10)).toBe('Exactly10!');
43
+ });
44
+ });
45
+
46
+ describe('formatDate', () => {
47
+ it('should format Date object to YYYY-MM-DD', () => {
48
+ const date = new Date('2024-01-15T10:30:00Z');
49
+ expect(formatDate(date)).toBe('2024-01-15');
50
+ });
51
+
52
+ it('should format ISO string to YYYY-MM-DD', () => {
53
+ expect(formatDate('2024-01-15T10:30:00Z')).toBe('2024-01-15');
54
+ });
55
+ });
56
+
57
+ describe('formatDateTime', () => {
58
+ it('should format Date object to localized string', () => {
59
+ const date = new Date('2024-01-15T10:30:00Z');
60
+ const formatted = formatDateTime(date);
61
+ expect(formatted).toContain('Jan');
62
+ expect(formatted).toContain('15');
63
+ expect(formatted).toContain('2024');
64
+ });
65
+
66
+ it('should format ISO string to localized string', () => {
67
+ const formatted = formatDateTime('2024-01-15T10:30:00Z');
68
+ expect(formatted).toContain('Jan');
69
+ expect(formatted).toContain('15');
70
+ expect(formatted).toContain('2024');
71
+ });
72
+ });
73
+
74
+ describe('getRelativeTime', () => {
75
+ beforeEach(() => {
76
+ vi.useFakeTimers();
77
+ });
78
+
79
+ it('should return "just now" for recent dates', () => {
80
+ const now = new Date();
81
+ vi.setSystemTime(now);
82
+ const recentDate = new Date(now.getTime() - 30000);
83
+ expect(getRelativeTime(recentDate)).toBe('just now');
84
+ });
85
+
86
+ it('should return minutes ago', () => {
87
+ const now = new Date();
88
+ vi.setSystemTime(now);
89
+ const pastDate = new Date(now.getTime() - 5 * 60 * 1000);
90
+ expect(getRelativeTime(pastDate)).toBe('5 minutes ago');
91
+ });
92
+
93
+ it('should return hours ago', () => {
94
+ const now = new Date();
95
+ vi.setSystemTime(now);
96
+ const pastDate = new Date(now.getTime() - 3 * 60 * 60 * 1000);
97
+ expect(getRelativeTime(pastDate)).toBe('3 hours ago');
98
+ });
99
+
100
+ it('should return days ago', () => {
101
+ const now = new Date();
102
+ vi.setSystemTime(now);
103
+ const pastDate = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000);
104
+ expect(getRelativeTime(pastDate)).toBe('2 days ago');
105
+ });
106
+
107
+ it('should return formatted date for old dates', () => {
108
+ const now = new Date();
109
+ vi.setSystemTime(now);
110
+ const oldDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
111
+ const result = getRelativeTime(oldDate);
112
+ expect(result).toMatch(/\d{4}-\d{2}-\d{2}/);
113
+ });
114
+ });
115
+
116
+ describe('sleep', () => {
117
+ it('should resolve after specified milliseconds', async () => {
118
+ vi.useFakeTimers();
119
+ const promise = sleep(1000);
120
+ vi.advanceTimersByTime(1000);
121
+ await expect(promise).resolves.toBeUndefined();
122
+ vi.useRealTimers();
123
+ });
124
+ });
125
+
126
+ describe('formatBytes', () => {
127
+ it('should format zero bytes', () => {
128
+ expect(formatBytes(0)).toBe('0 Bytes');
129
+ });
130
+
131
+ it('should format bytes', () => {
132
+ expect(formatBytes(500)).toBe('500 Bytes');
133
+ });
134
+
135
+ it('should format kilobytes', () => {
136
+ expect(formatBytes(1024)).toBe('1 KB');
137
+ expect(formatBytes(1536)).toBe('1.5 KB');
138
+ });
139
+
140
+ it('should format megabytes', () => {
141
+ expect(formatBytes(1048576)).toBe('1 MB');
142
+ expect(formatBytes(5242880)).toBe('5 MB');
143
+ });
144
+
145
+ it('should format gigabytes', () => {
146
+ expect(formatBytes(1073741824)).toBe('1 GB');
147
+ });
148
+
149
+ it('should respect decimals parameter', () => {
150
+ expect(formatBytes(1536, 0)).toBe('2 KB');
151
+ expect(formatBytes(1536, 3)).toBe('1.5 KB');
152
+ });
153
+ });
154
+
155
+ describe('clamp', () => {
156
+ it('should clamp value within range', () => {
157
+ expect(clamp(5, 0, 10)).toBe(5);
158
+ });
159
+
160
+ it('should clamp to minimum', () => {
161
+ expect(clamp(-5, 0, 10)).toBe(0);
162
+ });
163
+
164
+ it('should clamp to maximum', () => {
165
+ expect(clamp(15, 0, 10)).toBe(10);
166
+ });
167
+
168
+ it('should handle negative ranges', () => {
169
+ expect(clamp(-15, -10, -5)).toBe(-10);
170
+ expect(clamp(-3, -10, -5)).toBe(-5);
171
+ });
172
+ });
173
+
174
+ describe('unique', () => {
175
+ it('should remove duplicates from array', () => {
176
+ expect(unique([1, 2, 2, 3, 3, 3, 4])).toEqual([1, 2, 3, 4]);
177
+ });
178
+
179
+ it('should work with strings', () => {
180
+ expect(unique(['a', 'b', 'a', 'c', 'b'])).toEqual(['a', 'b', 'c']);
181
+ });
182
+
183
+ it('should return empty array for empty input', () => {
184
+ expect(unique([])).toEqual([]);
185
+ });
186
+
187
+ it('should preserve single item arrays', () => {
188
+ expect(unique([1])).toEqual([1]);
189
+ });
190
+ });
191
+
192
+ describe('groupBy', () => {
193
+ it('should group objects by key function', () => {
194
+ const items = [
195
+ { name: 'Alice', age: 25 },
196
+ { name: 'Bob', age: 30 },
197
+ { name: 'Charlie', age: 25 },
198
+ ];
199
+
200
+ const grouped = groupBy(items, (item) => item.age);
201
+ expect(grouped[25]).toHaveLength(2);
202
+ expect(grouped[30]).toHaveLength(1);
203
+ expect(grouped[25][0].name).toBe('Alice');
204
+ expect(grouped[25][1].name).toBe('Charlie');
205
+ });
206
+
207
+ it('should work with string keys', () => {
208
+ const items = [
209
+ { type: 'fruit', name: 'apple' },
210
+ { type: 'vegetable', name: 'carrot' },
211
+ { type: 'fruit', name: 'banana' },
212
+ ];
213
+
214
+ const grouped = groupBy(items, (item) => item.type);
215
+ expect(grouped.fruit).toHaveLength(2);
216
+ expect(grouped.vegetable).toHaveLength(1);
217
+ });
218
+
219
+ it('should handle empty array', () => {
220
+ const grouped = groupBy([], (item: { id: number }) => item.id);
221
+ expect(grouped).toEqual({});
222
+ });
223
+ });
@@ -0,0 +1,18 @@
1
+ export function unique<T>(array: T[]): T[] {
2
+ return [...new Set(array)];
3
+ }
4
+
5
+ export function groupBy<T, K extends string | number | symbol>(
6
+ array: T[],
7
+ keyFn: (item: T) => K
8
+ ): Record<K, T[]> {
9
+ return array.reduce(
10
+ (acc, item) => {
11
+ const key = keyFn(item);
12
+ if (!acc[key]) acc[key] = [];
13
+ acc[key].push(item);
14
+ return acc;
15
+ },
16
+ {} as Record<K, T[]>
17
+ );
18
+ }
@@ -0,0 +1,3 @@
1
+ export function sleep(ms: number): Promise<void> {
2
+ return new Promise((resolve) => setTimeout(resolve, ms));
3
+ }
@@ -0,0 +1,77 @@
1
+ export function formatDate(date: Date | string): string {
2
+ const d = typeof date === 'string' ? new Date(date) : date;
3
+ return d.toISOString().split('T')[0];
4
+ }
5
+
6
+ export function formatDateTime(date: Date | string): string {
7
+ const d = typeof date === 'string' ? new Date(date) : date;
8
+ return d.toLocaleString('en-US', {
9
+ year: 'numeric',
10
+ month: 'short',
11
+ day: 'numeric',
12
+ hour: '2-digit',
13
+ minute: '2-digit',
14
+ });
15
+ }
16
+
17
+ export function formatShortDate(
18
+ date: Date | string,
19
+ includeYear = false
20
+ ): string {
21
+ const d = typeof date === 'string' ? new Date(date) : date;
22
+ return d.toLocaleString('en-US', {
23
+ month: 'short',
24
+ day: 'numeric',
25
+ ...(includeYear && { year: 'numeric' }),
26
+ });
27
+ }
28
+
29
+ export function getRelativeTime(
30
+ date: Date | string,
31
+ options: { short?: boolean; alwaysShowTime?: boolean } = {}
32
+ ): string {
33
+ const { short = false, alwaysShowTime = false } = options;
34
+ const d = typeof date === 'string' ? new Date(date) : date;
35
+ const now = new Date();
36
+ const seconds = Math.floor((now.getTime() - d.getTime()) / 1000);
37
+
38
+ if (seconds < 60) {
39
+ return alwaysShowTime ? (short ? '1m ago' : '1 minute ago') : 'just now';
40
+ }
41
+
42
+ const minutes = Math.floor(seconds / 60);
43
+ if (minutes < 60) {
44
+ return short ? `${minutes}m ago` : `${minutes} minutes ago`;
45
+ }
46
+
47
+ const hours = Math.floor(seconds / 3600);
48
+ if (hours < 24) {
49
+ return short ? `${hours}h ago` : `${hours} hours ago`;
50
+ }
51
+
52
+ const days = Math.floor(seconds / 86400);
53
+ if (days < 7) {
54
+ return short ? `${days}d ago` : `${days} days ago`;
55
+ }
56
+
57
+ const weeks = Math.floor(days / 7);
58
+ if (weeks < 4) {
59
+ return short ? `${weeks}w ago` : `${weeks} weeks ago`;
60
+ }
61
+
62
+ return formatDate(d);
63
+ }
64
+
65
+ export function formatTime(date: Date | string | number): string {
66
+ const d =
67
+ typeof date === 'number'
68
+ ? new Date(date)
69
+ : typeof date === 'string'
70
+ ? new Date(date)
71
+ : date;
72
+ return d.toLocaleTimeString(undefined, {
73
+ hour: '2-digit',
74
+ minute: '2-digit',
75
+ second: '2-digit',
76
+ });
77
+ }
@@ -0,0 +1,73 @@
1
+ export class AppError extends Error {
2
+ readonly statusCode: number;
3
+ readonly code: string;
4
+ readonly details?: Record<string, unknown>;
5
+
6
+ constructor(
7
+ message: string,
8
+ statusCode: number,
9
+ code: string,
10
+ details?: Record<string, unknown>
11
+ ) {
12
+ super(message);
13
+ this.statusCode = statusCode;
14
+ this.code = code;
15
+ this.details = details;
16
+ this.name = this.constructor.name;
17
+ Error.captureStackTrace(this, this.constructor);
18
+ }
19
+ }
20
+
21
+ export class ValidationError extends AppError {
22
+ constructor(message: string, details?: Record<string, unknown>) {
23
+ super(message, 400, 'VALIDATION_ERROR', details);
24
+ }
25
+ }
26
+
27
+ export class UnauthorizedError extends AppError {
28
+ constructor(
29
+ message: string = 'Authentication required',
30
+ details?: Record<string, unknown>
31
+ ) {
32
+ super(message, 401, 'UNAUTHORIZED', details);
33
+ }
34
+ }
35
+
36
+ export class ForbiddenError extends AppError {
37
+ constructor(
38
+ message: string = 'Insufficient permissions',
39
+ details?: Record<string, unknown>
40
+ ) {
41
+ super(message, 403, 'FORBIDDEN', details);
42
+ }
43
+ }
44
+
45
+ export class NotFoundError extends AppError {
46
+ constructor(message: string, details?: Record<string, unknown>) {
47
+ super(message, 404, 'NOT_FOUND', details);
48
+ }
49
+ }
50
+
51
+ export class ConflictError extends AppError {
52
+ constructor(message: string, details?: Record<string, unknown>) {
53
+ super(message, 409, 'CONFLICT', details);
54
+ }
55
+ }
56
+
57
+ export class RateLimitError extends AppError {
58
+ constructor(
59
+ message: string = 'Rate limit exceeded',
60
+ details?: Record<string, unknown>
61
+ ) {
62
+ super(message, 429, 'RATE_LIMIT_EXCEEDED', details);
63
+ }
64
+ }
65
+
66
+ export class InternalServerError extends AppError {
67
+ constructor(
68
+ message: string = 'Internal server error',
69
+ details?: Record<string, unknown>
70
+ ) {
71
+ super(message, 500, 'INTERNAL_ERROR', details);
72
+ }
73
+ }
@@ -0,0 +1,11 @@
1
+ export function formatBytes(bytes: number, decimals = 2): string {
2
+ if (bytes === 0) return '0 Bytes';
3
+ const k = 1024;
4
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
5
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
6
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(decimals))} ${sizes[i]}`;
7
+ }
8
+
9
+ export function clamp(value: number, min: number, max: number): number {
10
+ return Math.min(Math.max(value, min), max);
11
+ }
@@ -0,0 +1,13 @@
1
+ export function slugify(text: string): string {
2
+ return text
3
+ .toLowerCase()
4
+ .trim()
5
+ .replace(/[^\w\s-]/g, '')
6
+ .replace(/[\s_-]+/g, '-')
7
+ .replace(/^-+|-+$/g, '');
8
+ }
9
+
10
+ export function truncate(text: string, maxLength: number): string {
11
+ if (text.length <= maxLength) return text;
12
+ return text.slice(0, maxLength - 3) + '...';
13
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "include": ["src/**/*"],
4
+ "exclude": ["node_modules"]
5
+ }
@@ -0,0 +1,21 @@
1
+ import { defineConfig, mergeConfig } from 'vitest/config';
2
+
3
+ import { sharedConfig } from '../../vitest.shared';
4
+
5
+ export default mergeConfig(
6
+ sharedConfig,
7
+ defineConfig({
8
+ test: {
9
+ name: '@repo/packages-utils',
10
+ include: ['src/**/*.{test,spec}.{ts,tsx}'],
11
+ coverage: {
12
+ thresholds: {
13
+ lines: 70,
14
+ functions: 70,
15
+ branches: 60,
16
+ statements: 70,
17
+ },
18
+ },
19
+ },
20
+ })
21
+ );
@@ -0,0 +1,4 @@
1
+ packages:
2
+ - 'apps/*'
3
+ - 'packages/*'
4
+ - 'create-blitzpack'
@@ -0,0 +1,32 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "lib": ["ES2022"],
6
+ "moduleResolution": "bundler",
7
+ "resolveJsonModule": true,
8
+ "allowJs": true,
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+ "baseUrl": ".",
17
+ "paths": {
18
+ "@repo/packages-types": ["packages/types/src/index.ts"],
19
+ "@repo/packages-types/*": ["packages/types/src/*"],
20
+ "@repo/packages-utils": ["packages/utils/src/index.ts"],
21
+ "@repo/packages-utils/*": ["packages/utils/src/*"]
22
+ }
23
+ },
24
+ "exclude": [
25
+ "node_modules",
26
+ "dist",
27
+ "build",
28
+ ".next",
29
+ "out",
30
+ "**/node_modules"
31
+ ]
32
+ }
@@ -0,0 +1,133 @@
1
+ {
2
+ "$schema": "https://turbo.build/schema.json",
3
+ "ui": "stream",
4
+ "globalDependencies": ["tsconfig.json", "turbo.json", "eslint.config.ts"],
5
+ "tasks": {
6
+ "build": {
7
+ "dependsOn": ["build:components"],
8
+ "outputs": ["dist/**", ".next/**", "build/**"],
9
+ "inputs": [
10
+ "src/**",
11
+ "prisma/**",
12
+ "package.json",
13
+ "tsconfig.json",
14
+ "next.config.ts",
15
+ "tailwind.config.ts"
16
+ ]
17
+ },
18
+ "build:components": {
19
+ "outputs": ["dist/**"],
20
+ "inputs": ["src/**/*.tsx", "src/**/*.ts", "tsconfig.json"]
21
+ },
22
+ "typecheck": {
23
+ "dependsOn": ["db:generate"],
24
+ "cache": true,
25
+ "inputs": ["src/**", "prisma/**", "tsconfig.json", "package.json"]
26
+ },
27
+ "dev": {
28
+ "dependsOn": ["db:generate"],
29
+ "cache": false,
30
+ "persistent": true,
31
+ "with": ["dev:components"]
32
+ },
33
+ "dev:components": {
34
+ "cache": false,
35
+ "persistent": true,
36
+ "outputs": ["dist/**"]
37
+ },
38
+ "db:generate": {
39
+ "cache": false
40
+ },
41
+ "lint": {
42
+ "dependsOn": [],
43
+ "inputs": ["src/**", "eslint.config.*", "package.json"],
44
+ "outputs": []
45
+ },
46
+ "lint:fix": {
47
+ "dependsOn": [],
48
+ "cache": false,
49
+ "inputs": ["src/**", "eslint.config.*", "package.json"]
50
+ },
51
+ "test": {
52
+ "dependsOn": [],
53
+ "inputs": [
54
+ "src/**",
55
+ "**/*.test.ts",
56
+ "**/*.test.tsx",
57
+ "vitest.config.ts",
58
+ "package.json"
59
+ ],
60
+ "outputs": ["coverage/**"]
61
+ },
62
+ "test:unit": {
63
+ "dependsOn": [],
64
+ "inputs": [
65
+ "src/**",
66
+ "**/*.test.ts",
67
+ "**/*.test.tsx",
68
+ "vitest.config.ts",
69
+ "package.json"
70
+ ],
71
+ "outputs": ["coverage/**"]
72
+ },
73
+ "test:integration": {
74
+ "dependsOn": [],
75
+ "inputs": [
76
+ "src/**",
77
+ "test/**",
78
+ "**/*.integration.spec.ts",
79
+ "vitest.config.ts",
80
+ "package.json"
81
+ ],
82
+ "outputs": ["coverage/**"]
83
+ },
84
+ "test:watch": {
85
+ "dependsOn": [],
86
+ "cache": false,
87
+ "persistent": true
88
+ },
89
+ "test:coverage": {
90
+ "dependsOn": [],
91
+ "outputs": ["coverage/**"],
92
+ "inputs": [
93
+ "src/**",
94
+ "**/*.test.ts",
95
+ "**/*.test.tsx",
96
+ "vitest.config.ts",
97
+ "package.json"
98
+ ]
99
+ },
100
+ "clean": {
101
+ "cache": false,
102
+ "outputs": []
103
+ },
104
+ "format": {
105
+ "cache": false,
106
+ "outputs": []
107
+ },
108
+ "format:check": {
109
+ "cache": true,
110
+ "outputs": []
111
+ },
112
+ "db:migrate": {
113
+ "cache": false,
114
+ "outputs": []
115
+ },
116
+ "db:push": {
117
+ "cache": false,
118
+ "outputs": []
119
+ },
120
+ "db:studio": {
121
+ "cache": false,
122
+ "persistent": true
123
+ },
124
+ "db:seed": {
125
+ "cache": false,
126
+ "outputs": []
127
+ },
128
+ "email:dev": {
129
+ "cache": false,
130
+ "persistent": true
131
+ }
132
+ }
133
+ }
@@ -0,0 +1,26 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export const sharedConfig = defineConfig({
4
+ test: {
5
+ globals: true,
6
+ passWithNoTests: true,
7
+ coverage: {
8
+ provider: 'v8',
9
+ reporter: ['text', 'json', 'html', 'lcov'],
10
+ exclude: [
11
+ 'node_modules/**',
12
+ 'dist/**',
13
+ 'build/**',
14
+ '.next/**',
15
+ '**/*.config.{js,ts,mjs,mts}',
16
+ '**/*.d.ts',
17
+ '**/index.ts',
18
+ '**/__tests__/**',
19
+ '**/test/**',
20
+ '**/*.spec.{ts,tsx}',
21
+ '**/*.test.{ts,tsx}',
22
+ ],
23
+ },
24
+ setupFiles: [],
25
+ },
26
+ });
@@ -0,0 +1,9 @@
1
+ import { defineWorkspace } from 'vitest/config';
2
+
3
+ export default defineWorkspace([
4
+ 'shared/types/vitest.config.ts',
5
+ 'shared/utils/vitest.config.ts',
6
+ 'shared/config/vitest.config.ts',
7
+ 'apps/api/vitest.config.ts',
8
+ 'apps/web/vitest.config.ts',
9
+ ]);