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,84 @@
1
+ import { Slot } from '@radix-ui/react-slot';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+ import { Loader2 } from 'lucide-react';
4
+ import * as React from 'react';
5
+
6
+ import { cn } from './lib/utils';
7
+
8
+ const buttonVariants = cva(
9
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all cursor-pointer disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
10
+ {
11
+ variants: {
12
+ variant: {
13
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
14
+ destructive:
15
+ 'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
16
+ outline:
17
+ 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
18
+ secondary:
19
+ 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
20
+ ghost:
21
+ 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
22
+ link: 'text-primary underline-offset-4 hover:underline',
23
+ },
24
+ size: {
25
+ default: 'h-9 px-4 py-2 has-[>svg]:px-3',
26
+ sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
27
+ lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
28
+ icon: 'size-9',
29
+ 'icon-sm': 'size-8',
30
+ 'icon-lg': 'size-10',
31
+ },
32
+ },
33
+ defaultVariants: {
34
+ variant: 'default',
35
+ size: 'default',
36
+ },
37
+ }
38
+ );
39
+
40
+ interface ButtonProps
41
+ extends React.ComponentProps<'button'>,
42
+ VariantProps<typeof buttonVariants> {
43
+ asChild?: boolean;
44
+ isLoading?: boolean;
45
+ }
46
+
47
+ function Button({
48
+ className,
49
+ variant,
50
+ size,
51
+ asChild = false,
52
+ isLoading = false,
53
+ children,
54
+ disabled,
55
+ ...props
56
+ }: ButtonProps) {
57
+ const Comp = asChild ? Slot : 'button';
58
+
59
+ if (asChild) {
60
+ return (
61
+ <Comp
62
+ data-slot="button"
63
+ className={cn(buttonVariants({ variant, size, className }))}
64
+ {...props}
65
+ >
66
+ {children}
67
+ </Comp>
68
+ );
69
+ }
70
+
71
+ return (
72
+ <Comp
73
+ data-slot="button"
74
+ className={cn(buttonVariants({ variant, size, className }))}
75
+ disabled={disabled || isLoading}
76
+ {...props}
77
+ >
78
+ {isLoading && <Loader2 className="animate-spin" />}
79
+ {children}
80
+ </Comp>
81
+ );
82
+ }
83
+
84
+ export { Button, buttonVariants };
@@ -0,0 +1,92 @@
1
+ import * as React from 'react';
2
+
3
+ import { cn } from './lib/utils';
4
+
5
+ function Card({ className, ...props }: React.ComponentProps<'div'>) {
6
+ return (
7
+ <div
8
+ data-slot="card"
9
+ className={cn(
10
+ 'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ );
16
+ }
17
+
18
+ function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
19
+ return (
20
+ <div
21
+ data-slot="card-header"
22
+ className={cn(
23
+ '@container/card-header has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6 grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6',
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ );
29
+ }
30
+
31
+ function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
32
+ return (
33
+ <div
34
+ data-slot="card-title"
35
+ className={cn('font-semibold leading-none', className)}
36
+ {...props}
37
+ />
38
+ );
39
+ }
40
+
41
+ function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
42
+ return (
43
+ <div
44
+ data-slot="card-description"
45
+ className={cn('text-muted-foreground text-sm', className)}
46
+ {...props}
47
+ />
48
+ );
49
+ }
50
+
51
+ function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
52
+ return (
53
+ <div
54
+ data-slot="card-action"
55
+ className={cn(
56
+ 'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
57
+ className
58
+ )}
59
+ {...props}
60
+ />
61
+ );
62
+ }
63
+
64
+ function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
65
+ return (
66
+ <div
67
+ data-slot="card-content"
68
+ className={cn('px-6', className)}
69
+ {...props}
70
+ />
71
+ );
72
+ }
73
+
74
+ function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
75
+ return (
76
+ <div
77
+ data-slot="card-footer"
78
+ className={cn('[.border-t]:pt-6 flex items-center px-6', className)}
79
+ {...props}
80
+ />
81
+ );
82
+ }
83
+
84
+ export {
85
+ Card,
86
+ CardAction,
87
+ CardContent,
88
+ CardDescription,
89
+ CardFooter,
90
+ CardHeader,
91
+ CardTitle,
92
+ };
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+
3
+ import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
4
+ import { CheckIcon } from 'lucide-react';
5
+ import * as React from 'react';
6
+
7
+ import { cn } from './lib/utils';
8
+
9
+ function Checkbox({
10
+ className,
11
+ ...props
12
+ }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
13
+ return (
14
+ <CheckboxPrimitive.Root
15
+ data-slot="checkbox"
16
+ className={cn(
17
+ 'border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive shadow-xs peer size-4 shrink-0 cursor-pointer rounded-[4px] border outline-none transition-shadow focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
18
+ className
19
+ )}
20
+ {...props}
21
+ >
22
+ <CheckboxPrimitive.Indicator
23
+ data-slot="checkbox-indicator"
24
+ className="flex items-center justify-center text-current transition-none"
25
+ >
26
+ <CheckIcon className="size-3.5" />
27
+ </CheckboxPrimitive.Indicator>
28
+ </CheckboxPrimitive.Root>
29
+ );
30
+ }
31
+
32
+ export { Checkbox };
@@ -0,0 +1,68 @@
1
+ 'use client';
2
+
3
+ import type { Column } from '@tanstack/react-table';
4
+ import { ArrowDown, ArrowUp, ChevronsUpDown, EyeOff } from 'lucide-react';
5
+
6
+ import { Button } from '../button';
7
+ import {
8
+ DropdownMenu,
9
+ DropdownMenuContent,
10
+ DropdownMenuItem,
11
+ DropdownMenuSeparator,
12
+ DropdownMenuTrigger,
13
+ } from '../dropdown-menu';
14
+ import { cn } from '../lib/utils';
15
+
16
+ interface DataTableColumnHeaderProps<TData, TValue>
17
+ extends React.HTMLAttributes<HTMLDivElement> {
18
+ column: Column<TData, TValue>;
19
+ title: string;
20
+ }
21
+
22
+ export function DataTableColumnHeader<TData, TValue>({
23
+ column,
24
+ title,
25
+ className,
26
+ }: DataTableColumnHeaderProps<TData, TValue>) {
27
+ if (!column.getCanSort()) {
28
+ return <div className={cn(className)}>{title}</div>;
29
+ }
30
+
31
+ return (
32
+ <div className={cn('flex items-center space-x-2', className)}>
33
+ <DropdownMenu>
34
+ <DropdownMenuTrigger asChild>
35
+ <Button
36
+ variant="ghost"
37
+ size="sm"
38
+ className="data-[state=open]:bg-accent -ml-3 h-8"
39
+ >
40
+ <span>{title}</span>
41
+ {column.getIsSorted() === 'desc' ? (
42
+ <ArrowDown className="ml-2 h-4 w-4" />
43
+ ) : column.getIsSorted() === 'asc' ? (
44
+ <ArrowUp className="ml-2 h-4 w-4" />
45
+ ) : (
46
+ <ChevronsUpDown className="ml-2 h-4 w-4" />
47
+ )}
48
+ </Button>
49
+ </DropdownMenuTrigger>
50
+ <DropdownMenuContent align="start">
51
+ <DropdownMenuItem onClick={() => column.toggleSorting(false)}>
52
+ <ArrowUp className="text-muted-foreground/70 mr-2 h-3.5 w-3.5" />
53
+ Asc
54
+ </DropdownMenuItem>
55
+ <DropdownMenuItem onClick={() => column.toggleSorting(true)}>
56
+ <ArrowDown className="text-muted-foreground/70 mr-2 h-3.5 w-3.5" />
57
+ Desc
58
+ </DropdownMenuItem>
59
+ <DropdownMenuSeparator />
60
+ <DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
61
+ <EyeOff className="text-muted-foreground/70 mr-2 h-3.5 w-3.5" />
62
+ Hide
63
+ </DropdownMenuItem>
64
+ </DropdownMenuContent>
65
+ </DropdownMenu>
66
+ </div>
67
+ );
68
+ }
@@ -0,0 +1,99 @@
1
+ 'use client';
2
+
3
+ import type { Table } from '@tanstack/react-table';
4
+ import {
5
+ ChevronLeft,
6
+ ChevronRight,
7
+ ChevronsLeft,
8
+ ChevronsRight,
9
+ } from 'lucide-react';
10
+
11
+ import { Button } from '../button';
12
+ import {
13
+ Select,
14
+ SelectContent,
15
+ SelectItem,
16
+ SelectTrigger,
17
+ SelectValue,
18
+ } from '../select';
19
+
20
+ interface DataTablePaginationProps<TData> {
21
+ table: Table<TData>;
22
+ }
23
+
24
+ export function DataTablePagination<TData>({
25
+ table,
26
+ }: DataTablePaginationProps<TData>) {
27
+ return (
28
+ <div className="flex items-center justify-between px-2">
29
+ <div className="text-muted-foreground flex-1 text-sm">
30
+ {table.getFilteredSelectedRowModel().rows.length} of{' '}
31
+ {table.getFilteredRowModel().rows.length} row(s) selected.
32
+ </div>
33
+ <div className="flex items-center space-x-6 lg:space-x-8">
34
+ <div className="flex items-center space-x-2">
35
+ <p className="text-sm font-medium">Rows per page</p>
36
+ <Select
37
+ value={`${table.getState().pagination.pageSize}`}
38
+ onValueChange={(value) => {
39
+ table.setPageSize(Number(value));
40
+ }}
41
+ >
42
+ <SelectTrigger className="h-8 w-[70px]">
43
+ <SelectValue placeholder={table.getState().pagination.pageSize} />
44
+ </SelectTrigger>
45
+ <SelectContent side="top">
46
+ {[10, 20, 30, 40, 50].map((pageSize) => (
47
+ <SelectItem key={pageSize} value={`${pageSize}`}>
48
+ {pageSize}
49
+ </SelectItem>
50
+ ))}
51
+ </SelectContent>
52
+ </Select>
53
+ </div>
54
+ <div className="flex w-[100px] items-center justify-center text-sm font-medium">
55
+ Page {table.getState().pagination.pageIndex + 1} of{' '}
56
+ {table.getPageCount()}
57
+ </div>
58
+ <div className="flex items-center space-x-2">
59
+ <Button
60
+ variant="outline"
61
+ className="hidden h-8 w-8 p-0 lg:flex"
62
+ onClick={() => table.setPageIndex(0)}
63
+ disabled={!table.getCanPreviousPage()}
64
+ >
65
+ <span className="sr-only">Go to first page</span>
66
+ <ChevronsLeft className="h-4 w-4" />
67
+ </Button>
68
+ <Button
69
+ variant="outline"
70
+ className="h-8 w-8 p-0"
71
+ onClick={() => table.previousPage()}
72
+ disabled={!table.getCanPreviousPage()}
73
+ >
74
+ <span className="sr-only">Go to previous page</span>
75
+ <ChevronLeft className="h-4 w-4" />
76
+ </Button>
77
+ <Button
78
+ variant="outline"
79
+ className="h-8 w-8 p-0"
80
+ onClick={() => table.nextPage()}
81
+ disabled={!table.getCanNextPage()}
82
+ >
83
+ <span className="sr-only">Go to next page</span>
84
+ <ChevronRight className="h-4 w-4" />
85
+ </Button>
86
+ <Button
87
+ variant="outline"
88
+ className="hidden h-8 w-8 p-0 lg:flex"
89
+ onClick={() => table.setPageIndex(table.getPageCount() - 1)}
90
+ disabled={!table.getCanNextPage()}
91
+ >
92
+ <span className="sr-only">Go to last page</span>
93
+ <ChevronsRight className="h-4 w-4" />
94
+ </Button>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ );
99
+ }
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+
3
+ import type { Table } from '@tanstack/react-table';
4
+ import { X } from 'lucide-react';
5
+
6
+ import { Button } from '../button';
7
+ import { Input } from '../input';
8
+ import { DataTableViewOptions } from './data-table-view-options';
9
+
10
+ interface DataTableToolbarProps<TData> {
11
+ table: Table<TData>;
12
+ searchPlaceholder?: string;
13
+ searchColumnId?: string;
14
+ }
15
+
16
+ export function DataTableToolbar<TData>({
17
+ table,
18
+ searchPlaceholder = 'Search...',
19
+ searchColumnId,
20
+ }: DataTableToolbarProps<TData>) {
21
+ const isFiltered = table.getState().columnFilters.length > 0;
22
+
23
+ return (
24
+ <div className="flex items-center justify-between">
25
+ <div className="flex flex-1 items-center space-x-2">
26
+ {searchColumnId && (
27
+ <Input
28
+ placeholder={searchPlaceholder}
29
+ value={
30
+ (table.getColumn(searchColumnId)?.getFilterValue() as string) ??
31
+ ''
32
+ }
33
+ onChange={(event) =>
34
+ table
35
+ .getColumn(searchColumnId)
36
+ ?.setFilterValue(event.target.value)
37
+ }
38
+ className="h-8 w-[150px] lg:w-[250px]"
39
+ />
40
+ )}
41
+ {isFiltered && (
42
+ <Button
43
+ variant="ghost"
44
+ onClick={() => table.resetColumnFilters()}
45
+ className="h-8 px-2 lg:px-3"
46
+ >
47
+ Reset
48
+ <X className="ml-2 h-4 w-4" />
49
+ </Button>
50
+ )}
51
+ </div>
52
+ <DataTableViewOptions table={table} />
53
+ </div>
54
+ );
55
+ }
@@ -0,0 +1,63 @@
1
+ 'use client';
2
+
3
+ import type { Column, Table } from '@tanstack/react-table';
4
+ import { Settings2 } from 'lucide-react';
5
+
6
+ import { Button } from '../button';
7
+ import {
8
+ DropdownMenu,
9
+ DropdownMenuCheckboxItem,
10
+ DropdownMenuContent,
11
+ DropdownMenuLabel,
12
+ DropdownMenuSeparator,
13
+ DropdownMenuTrigger,
14
+ } from '../dropdown-menu';
15
+
16
+ interface DataTableViewOptionsProps<TData> {
17
+ table: Table<TData>;
18
+ }
19
+
20
+ function getColumnLabel<TData>(column: Column<TData, unknown>): string {
21
+ const meta = column.columnDef.meta as { label?: string } | undefined;
22
+ return meta?.label ?? column.id;
23
+ }
24
+
25
+ export function DataTableViewOptions<TData>({
26
+ table,
27
+ }: DataTableViewOptionsProps<TData>) {
28
+ return (
29
+ <DropdownMenu>
30
+ <DropdownMenuTrigger asChild>
31
+ <Button
32
+ variant="outline"
33
+ size="sm"
34
+ className="ml-auto hidden h-8 lg:flex"
35
+ >
36
+ <Settings2 className="mr-2 h-4 w-4" />
37
+ View
38
+ </Button>
39
+ </DropdownMenuTrigger>
40
+ <DropdownMenuContent align="end" className="w-[150px]">
41
+ <DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
42
+ <DropdownMenuSeparator />
43
+ {table
44
+ .getAllColumns()
45
+ .filter(
46
+ (column) =>
47
+ typeof column.accessorFn !== 'undefined' && column.getCanHide()
48
+ )
49
+ .map((column) => {
50
+ return (
51
+ <DropdownMenuCheckboxItem
52
+ key={column.id}
53
+ checked={column.getIsVisible()}
54
+ onCheckedChange={(value) => column.toggleVisibility(!!value)}
55
+ >
56
+ {getColumnLabel(column)}
57
+ </DropdownMenuCheckboxItem>
58
+ );
59
+ })}
60
+ </DropdownMenuContent>
61
+ </DropdownMenu>
62
+ );
63
+ }
@@ -0,0 +1,167 @@
1
+ 'use client';
2
+
3
+ import type {
4
+ ColumnDef,
5
+ ColumnFiltersState,
6
+ PaginationState,
7
+ SortingState,
8
+ VisibilityState,
9
+ } from '@tanstack/react-table';
10
+ import {
11
+ flexRender,
12
+ getCoreRowModel,
13
+ getFacetedRowModel,
14
+ getFacetedUniqueValues,
15
+ getFilteredRowModel,
16
+ getPaginationRowModel,
17
+ getSortedRowModel,
18
+ useReactTable,
19
+ } from '@tanstack/react-table';
20
+ import * as React from 'react';
21
+
22
+ import { Skeleton } from '../skeleton';
23
+ import {
24
+ Table,
25
+ TableBody,
26
+ TableCell,
27
+ TableHead,
28
+ TableHeader,
29
+ TableRow,
30
+ } from '../table';
31
+ import { DataTablePagination } from './data-table-pagination';
32
+ import { DataTableToolbar } from './data-table-toolbar';
33
+
34
+ interface DataTableProps<TData, TValue> {
35
+ columns: ColumnDef<TData, TValue>[];
36
+ data: TData[];
37
+ pagination?: PaginationState;
38
+ onPaginationChange?: React.Dispatch<React.SetStateAction<PaginationState>>;
39
+ sorting?: SortingState;
40
+ onSortingChange?: React.Dispatch<React.SetStateAction<SortingState>>;
41
+ columnFilters?: ColumnFiltersState;
42
+ onColumnFiltersChange?: React.Dispatch<
43
+ React.SetStateAction<ColumnFiltersState>
44
+ >;
45
+ rowCount?: number;
46
+ isLoading?: boolean;
47
+ searchPlaceholder?: string;
48
+ searchColumnId?: string;
49
+ }
50
+
51
+ export function DataTable<TData, TValue>({
52
+ columns,
53
+ data,
54
+ pagination,
55
+ onPaginationChange,
56
+ sorting,
57
+ onSortingChange,
58
+ columnFilters,
59
+ onColumnFiltersChange,
60
+ rowCount,
61
+ isLoading = false,
62
+ searchPlaceholder = 'Search...',
63
+ searchColumnId,
64
+ }: DataTableProps<TData, TValue>) {
65
+ const [rowSelection, setRowSelection] = React.useState({});
66
+ const [columnVisibility, setColumnVisibility] =
67
+ React.useState<VisibilityState>({});
68
+
69
+ const table = useReactTable({
70
+ data,
71
+ columns,
72
+ state: {
73
+ sorting,
74
+ columnVisibility,
75
+ rowSelection,
76
+ columnFilters,
77
+ pagination,
78
+ },
79
+ enableRowSelection: true,
80
+ onRowSelectionChange: setRowSelection,
81
+ onSortingChange,
82
+ onColumnFiltersChange,
83
+ onColumnVisibilityChange: setColumnVisibility,
84
+ onPaginationChange,
85
+ getCoreRowModel: getCoreRowModel(),
86
+ getFilteredRowModel: getFilteredRowModel(),
87
+ getPaginationRowModel: getPaginationRowModel(),
88
+ getSortedRowModel: getSortedRowModel(),
89
+ getFacetedRowModel: getFacetedRowModel(),
90
+ getFacetedUniqueValues: getFacetedUniqueValues(),
91
+ manualPagination: !!onPaginationChange,
92
+ manualSorting: !!onSortingChange,
93
+ manualFiltering: !!onColumnFiltersChange,
94
+ rowCount,
95
+ });
96
+
97
+ return (
98
+ <div className="space-y-4">
99
+ <DataTableToolbar
100
+ table={table}
101
+ searchPlaceholder={searchPlaceholder}
102
+ searchColumnId={searchColumnId}
103
+ />
104
+ <div className="rounded-md border">
105
+ <Table>
106
+ <TableHeader>
107
+ {table.getHeaderGroups().map((headerGroup) => (
108
+ <TableRow key={headerGroup.id}>
109
+ {headerGroup.headers.map((header) => {
110
+ return (
111
+ <TableHead key={header.id} colSpan={header.colSpan}>
112
+ {header.isPlaceholder
113
+ ? null
114
+ : flexRender(
115
+ header.column.columnDef.header,
116
+ header.getContext()
117
+ )}
118
+ </TableHead>
119
+ );
120
+ })}
121
+ </TableRow>
122
+ ))}
123
+ </TableHeader>
124
+ <TableBody>
125
+ {isLoading ? (
126
+ Array.from({ length: pagination?.pageSize || 10 }).map((_, i) => (
127
+ <TableRow key={i}>
128
+ {columns.map((_, colIndex) => (
129
+ <TableCell key={colIndex}>
130
+ <Skeleton className="h-4 w-full" />
131
+ </TableCell>
132
+ ))}
133
+ </TableRow>
134
+ ))
135
+ ) : table.getRowModel().rows?.length ? (
136
+ table.getRowModel().rows.map((row) => (
137
+ <TableRow
138
+ key={row.id}
139
+ data-state={row.getIsSelected() && 'selected'}
140
+ >
141
+ {row.getVisibleCells().map((cell) => (
142
+ <TableCell key={cell.id}>
143
+ {flexRender(
144
+ cell.column.columnDef.cell,
145
+ cell.getContext()
146
+ )}
147
+ </TableCell>
148
+ ))}
149
+ </TableRow>
150
+ ))
151
+ ) : (
152
+ <TableRow>
153
+ <TableCell
154
+ colSpan={columns.length}
155
+ className="h-24 text-center"
156
+ >
157
+ No results.
158
+ </TableCell>
159
+ </TableRow>
160
+ )}
161
+ </TableBody>
162
+ </Table>
163
+ </div>
164
+ <DataTablePagination table={table} />
165
+ </div>
166
+ );
167
+ }