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.
- package/dist/index.js +452 -0
- package/package.json +57 -0
- package/template/.dockerignore +59 -0
- package/template/.github/workflows/ci.yml +157 -0
- package/template/.husky/pre-commit +1 -0
- package/template/.husky/pre-push +1 -0
- package/template/.lintstagedrc.cjs +4 -0
- package/template/.nvmrc +1 -0
- package/template/.prettierrc +9 -0
- package/template/.vscode/settings.json +13 -0
- package/template/CLAUDE.md +175 -0
- package/template/CONTRIBUTING.md +32 -0
- package/template/Dockerfile +90 -0
- package/template/GETTING_STARTED.md +35 -0
- package/template/LICENSE +21 -0
- package/template/README.md +116 -0
- package/template/apps/api/.dockerignore +51 -0
- package/template/apps/api/.env.local.example +62 -0
- package/template/apps/api/emails/account-deleted-email.tsx +69 -0
- package/template/apps/api/emails/components/email-layout.tsx +154 -0
- package/template/apps/api/emails/config.ts +22 -0
- package/template/apps/api/emails/password-changed-email.tsx +88 -0
- package/template/apps/api/emails/password-reset-email.tsx +86 -0
- package/template/apps/api/emails/verification-email.tsx +85 -0
- package/template/apps/api/emails/welcome-email.tsx +70 -0
- package/template/apps/api/package.json +84 -0
- package/template/apps/api/prisma/migrations/20251012111439_init/migration.sql +13 -0
- package/template/apps/api/prisma/migrations/20251018162629_add_better_auth_fields/migration.sql +67 -0
- package/template/apps/api/prisma/migrations/20251019142208_add_user_role_enum/migration.sql +5 -0
- package/template/apps/api/prisma/migrations/20251019182151_user_auth/migration.sql +7 -0
- package/template/apps/api/prisma/migrations/20251019211416_faster_session_lookup/migration.sql +2 -0
- package/template/apps/api/prisma/migrations/20251119124337_add_upload_model/migration.sql +26 -0
- package/template/apps/api/prisma/migrations/20251120071241_add_scope_to_account/migration.sql +2 -0
- package/template/apps/api/prisma/migrations/20251120072608_add_oauth_token_expiration_fields/migration.sql +10 -0
- package/template/apps/api/prisma/migrations/20251120144705_add_audit_logs/migration.sql +29 -0
- package/template/apps/api/prisma/migrations/20251127123614_remove_impersonated_by/migration.sql +8 -0
- package/template/apps/api/prisma/migrations/20251127125630_remove_audit_logs/migration.sql +11 -0
- package/template/apps/api/prisma/migrations/migration_lock.toml +3 -0
- package/template/apps/api/prisma/schema.prisma +116 -0
- package/template/apps/api/prisma/seed.ts +159 -0
- package/template/apps/api/prisma.config.ts +14 -0
- package/template/apps/api/src/app.ts +377 -0
- package/template/apps/api/src/common/logger.service.ts +227 -0
- package/template/apps/api/src/config/env.ts +60 -0
- package/template/apps/api/src/config/rate-limit.ts +29 -0
- package/template/apps/api/src/hooks/auth.ts +122 -0
- package/template/apps/api/src/plugins/auth.ts +198 -0
- package/template/apps/api/src/plugins/database.ts +45 -0
- package/template/apps/api/src/plugins/logger.ts +33 -0
- package/template/apps/api/src/plugins/multipart.ts +16 -0
- package/template/apps/api/src/plugins/scalar.ts +20 -0
- package/template/apps/api/src/plugins/schedule.ts +52 -0
- package/template/apps/api/src/plugins/services.ts +66 -0
- package/template/apps/api/src/plugins/swagger.ts +56 -0
- package/template/apps/api/src/routes/accounts.ts +91 -0
- package/template/apps/api/src/routes/admin-sessions.ts +92 -0
- package/template/apps/api/src/routes/metrics.ts +71 -0
- package/template/apps/api/src/routes/password.ts +46 -0
- package/template/apps/api/src/routes/sessions.ts +53 -0
- package/template/apps/api/src/routes/stats.ts +38 -0
- package/template/apps/api/src/routes/uploads-serve.ts +27 -0
- package/template/apps/api/src/routes/uploads.ts +154 -0
- package/template/apps/api/src/routes/users.ts +114 -0
- package/template/apps/api/src/routes/verification.ts +90 -0
- package/template/apps/api/src/server.ts +34 -0
- package/template/apps/api/src/services/accounts.service.ts +125 -0
- package/template/apps/api/src/services/authorization.service.ts +162 -0
- package/template/apps/api/src/services/email.service.ts +170 -0
- package/template/apps/api/src/services/file-storage.service.ts +267 -0
- package/template/apps/api/src/services/metrics.service.ts +175 -0
- package/template/apps/api/src/services/password.service.ts +56 -0
- package/template/apps/api/src/services/sessions.service.spec.ts +134 -0
- package/template/apps/api/src/services/sessions.service.ts +276 -0
- package/template/apps/api/src/services/stats.service.ts +273 -0
- package/template/apps/api/src/services/uploads.service.ts +163 -0
- package/template/apps/api/src/services/users.service.spec.ts +249 -0
- package/template/apps/api/src/services/users.service.ts +198 -0
- package/template/apps/api/src/utils/file-validation.ts +108 -0
- package/template/apps/api/start.sh +33 -0
- package/template/apps/api/test/helpers/fastify-app.ts +24 -0
- package/template/apps/api/test/helpers/mock-authorization.ts +16 -0
- package/template/apps/api/test/helpers/mock-logger.ts +28 -0
- package/template/apps/api/test/helpers/mock-prisma.ts +30 -0
- package/template/apps/api/test/helpers/test-db.ts +125 -0
- package/template/apps/api/test/integration/auth-flow.integration.spec.ts +449 -0
- package/template/apps/api/test/integration/password.integration.spec.ts +427 -0
- package/template/apps/api/test/integration/rate-limit.integration.spec.ts +51 -0
- package/template/apps/api/test/integration/sessions.integration.spec.ts +445 -0
- package/template/apps/api/test/integration/users.integration.spec.ts +211 -0
- package/template/apps/api/test/setup.ts +31 -0
- package/template/apps/api/tsconfig.json +26 -0
- package/template/apps/api/vitest.config.ts +35 -0
- package/template/apps/web/.env.local.example +11 -0
- package/template/apps/web/components.json +24 -0
- package/template/apps/web/next.config.ts +22 -0
- package/template/apps/web/package.json +56 -0
- package/template/apps/web/postcss.config.js +5 -0
- package/template/apps/web/public/apple-icon.png +0 -0
- package/template/apps/web/public/icon.png +0 -0
- package/template/apps/web/public/robots.txt +3 -0
- package/template/apps/web/src/app/(admin)/admin/layout.tsx +222 -0
- package/template/apps/web/src/app/(admin)/admin/page.tsx +157 -0
- package/template/apps/web/src/app/(admin)/admin/sessions/page.tsx +18 -0
- package/template/apps/web/src/app/(admin)/admin/users/page.tsx +20 -0
- package/template/apps/web/src/app/(auth)/forgot-password/page.tsx +177 -0
- package/template/apps/web/src/app/(auth)/login/page.tsx +159 -0
- package/template/apps/web/src/app/(auth)/reset-password/page.tsx +245 -0
- package/template/apps/web/src/app/(auth)/signup/page.tsx +153 -0
- package/template/apps/web/src/app/dashboard/change-password/page.tsx +255 -0
- package/template/apps/web/src/app/dashboard/page.tsx +296 -0
- package/template/apps/web/src/app/error.tsx +32 -0
- package/template/apps/web/src/app/examples/file-upload/page.tsx +200 -0
- package/template/apps/web/src/app/favicon.ico +0 -0
- package/template/apps/web/src/app/global-error.tsx +96 -0
- package/template/apps/web/src/app/globals.css +22 -0
- package/template/apps/web/src/app/icon.png +0 -0
- package/template/apps/web/src/app/layout.tsx +34 -0
- package/template/apps/web/src/app/not-found.tsx +28 -0
- package/template/apps/web/src/app/page.tsx +192 -0
- package/template/apps/web/src/components/admin/activity-feed.tsx +101 -0
- package/template/apps/web/src/components/admin/charts/auth-breakdown-chart.tsx +114 -0
- package/template/apps/web/src/components/admin/charts/chart-tooltip.tsx +124 -0
- package/template/apps/web/src/components/admin/charts/realtime-metrics-chart.tsx +511 -0
- package/template/apps/web/src/components/admin/charts/role-distribution-chart.tsx +102 -0
- package/template/apps/web/src/components/admin/charts/session-activity-chart.tsx +90 -0
- package/template/apps/web/src/components/admin/charts/user-growth-chart.tsx +108 -0
- package/template/apps/web/src/components/admin/health-indicator.tsx +175 -0
- package/template/apps/web/src/components/admin/refresh-control.tsx +90 -0
- package/template/apps/web/src/components/admin/session-revoke-all-dialog.tsx +79 -0
- package/template/apps/web/src/components/admin/session-revoke-dialog.tsx +74 -0
- package/template/apps/web/src/components/admin/sessions-management-table.tsx +372 -0
- package/template/apps/web/src/components/admin/stat-card.tsx +137 -0
- package/template/apps/web/src/components/admin/user-create-dialog.tsx +152 -0
- package/template/apps/web/src/components/admin/user-delete-dialog.tsx +73 -0
- package/template/apps/web/src/components/admin/user-edit-dialog.tsx +170 -0
- package/template/apps/web/src/components/admin/users-management-table.tsx +285 -0
- package/template/apps/web/src/components/auth/email-verification-banner.tsx +85 -0
- package/template/apps/web/src/components/auth/github-button.tsx +40 -0
- package/template/apps/web/src/components/auth/google-button.tsx +54 -0
- package/template/apps/web/src/components/auth/protected-route.tsx +66 -0
- package/template/apps/web/src/components/auth/redirect-if-authenticated.tsx +31 -0
- package/template/apps/web/src/components/auth/with-auth.tsx +30 -0
- package/template/apps/web/src/components/error/error-card.tsx +47 -0
- package/template/apps/web/src/components/error/forbidden.tsx +25 -0
- package/template/apps/web/src/components/landing/command-block.tsx +64 -0
- package/template/apps/web/src/components/landing/feature-card.tsx +60 -0
- package/template/apps/web/src/components/landing/included-feature-card.tsx +63 -0
- package/template/apps/web/src/components/landing/logo.tsx +41 -0
- package/template/apps/web/src/components/landing/tech-badge.tsx +11 -0
- package/template/apps/web/src/components/layout/auth-nav.tsx +58 -0
- package/template/apps/web/src/components/layout/footer.tsx +3 -0
- package/template/apps/web/src/config/landing-data.ts +152 -0
- package/template/apps/web/src/config/site.ts +5 -0
- package/template/apps/web/src/hooks/api/__tests__/use-users.test.tsx +181 -0
- package/template/apps/web/src/hooks/api/use-admin-sessions.ts +75 -0
- package/template/apps/web/src/hooks/api/use-admin-stats.ts +33 -0
- package/template/apps/web/src/hooks/api/use-sessions.ts +52 -0
- package/template/apps/web/src/hooks/api/use-uploads.ts +156 -0
- package/template/apps/web/src/hooks/api/use-users.ts +149 -0
- package/template/apps/web/src/hooks/use-mobile.ts +21 -0
- package/template/apps/web/src/hooks/use-realtime-metrics.ts +120 -0
- package/template/apps/web/src/lib/__tests__/utils.test.ts +29 -0
- package/template/apps/web/src/lib/api.ts +151 -0
- package/template/apps/web/src/lib/auth.ts +13 -0
- package/template/apps/web/src/lib/env.ts +52 -0
- package/template/apps/web/src/lib/form-utils.ts +11 -0
- package/template/apps/web/src/lib/utils.ts +1 -0
- package/template/apps/web/src/providers.tsx +34 -0
- package/template/apps/web/src/store/atoms.ts +15 -0
- package/template/apps/web/src/test/helpers/test-utils.tsx +44 -0
- package/template/apps/web/src/test/setup.ts +8 -0
- package/template/apps/web/tailwind.config.ts +5 -0
- package/template/apps/web/tsconfig.json +26 -0
- package/template/apps/web/vitest.config.ts +32 -0
- package/template/assets/logo-512.png +0 -0
- package/template/assets/logo.svg +4 -0
- package/template/docker-compose.prod.yml +66 -0
- package/template/docker-compose.yml +36 -0
- package/template/eslint.config.ts +119 -0
- package/template/package.json +77 -0
- package/template/packages/tailwind-config/package.json +9 -0
- package/template/packages/tailwind-config/theme.css +179 -0
- package/template/packages/types/package.json +29 -0
- package/template/packages/types/src/__tests__/schemas.test.ts +255 -0
- package/template/packages/types/src/api-response.ts +53 -0
- package/template/packages/types/src/health-check.ts +11 -0
- package/template/packages/types/src/pagination.ts +41 -0
- package/template/packages/types/src/role.ts +5 -0
- package/template/packages/types/src/session.ts +48 -0
- package/template/packages/types/src/stats.ts +113 -0
- package/template/packages/types/src/upload.ts +51 -0
- package/template/packages/types/src/user.ts +36 -0
- package/template/packages/types/tsconfig.json +5 -0
- package/template/packages/types/vitest.config.ts +21 -0
- package/template/packages/ui/components.json +21 -0
- package/template/packages/ui/package.json +108 -0
- package/template/packages/ui/src/__tests__/button.test.tsx +70 -0
- package/template/packages/ui/src/alert-dialog.tsx +141 -0
- package/template/packages/ui/src/alert.tsx +66 -0
- package/template/packages/ui/src/animated-theme-toggler.tsx +167 -0
- package/template/packages/ui/src/avatar.tsx +53 -0
- package/template/packages/ui/src/badge.tsx +36 -0
- package/template/packages/ui/src/button.tsx +84 -0
- package/template/packages/ui/src/card.tsx +92 -0
- package/template/packages/ui/src/checkbox.tsx +32 -0
- package/template/packages/ui/src/data-table/data-table-column-header.tsx +68 -0
- package/template/packages/ui/src/data-table/data-table-pagination.tsx +99 -0
- package/template/packages/ui/src/data-table/data-table-toolbar.tsx +55 -0
- package/template/packages/ui/src/data-table/data-table-view-options.tsx +63 -0
- package/template/packages/ui/src/data-table/data-table.tsx +167 -0
- package/template/packages/ui/src/dialog.tsx +143 -0
- package/template/packages/ui/src/dropdown-menu.tsx +257 -0
- package/template/packages/ui/src/empty-state.tsx +52 -0
- package/template/packages/ui/src/file-upload-input.tsx +202 -0
- package/template/packages/ui/src/form.tsx +168 -0
- package/template/packages/ui/src/hooks/use-mobile.ts +19 -0
- package/template/packages/ui/src/icons/brand-icons.tsx +16 -0
- package/template/packages/ui/src/input.tsx +21 -0
- package/template/packages/ui/src/label.tsx +24 -0
- package/template/packages/ui/src/lib/utils.ts +6 -0
- package/template/packages/ui/src/password-input.tsx +102 -0
- package/template/packages/ui/src/popover.tsx +48 -0
- package/template/packages/ui/src/radio-group.tsx +45 -0
- package/template/packages/ui/src/scroll-area.tsx +58 -0
- package/template/packages/ui/src/select.tsx +187 -0
- package/template/packages/ui/src/separator.tsx +28 -0
- package/template/packages/ui/src/sheet.tsx +139 -0
- package/template/packages/ui/src/sidebar.tsx +726 -0
- package/template/packages/ui/src/skeleton-variants.tsx +87 -0
- package/template/packages/ui/src/skeleton.tsx +13 -0
- package/template/packages/ui/src/slider.tsx +63 -0
- package/template/packages/ui/src/sonner.tsx +25 -0
- package/template/packages/ui/src/spinner.tsx +16 -0
- package/template/packages/ui/src/switch.tsx +31 -0
- package/template/packages/ui/src/table.tsx +116 -0
- package/template/packages/ui/src/tabs.tsx +66 -0
- package/template/packages/ui/src/textarea.tsx +18 -0
- package/template/packages/ui/src/tooltip.tsx +61 -0
- package/template/packages/ui/src/user-avatar.tsx +97 -0
- package/template/packages/ui/test-config.js +3 -0
- package/template/packages/ui/tsconfig.json +12 -0
- package/template/packages/ui/turbo.json +18 -0
- package/template/packages/ui/vitest.config.ts +17 -0
- package/template/packages/ui/vitest.setup.ts +1 -0
- package/template/packages/utils/package.json +23 -0
- package/template/packages/utils/src/__tests__/utils.test.ts +223 -0
- package/template/packages/utils/src/array.ts +18 -0
- package/template/packages/utils/src/async.ts +3 -0
- package/template/packages/utils/src/date.ts +77 -0
- package/template/packages/utils/src/errors.ts +73 -0
- package/template/packages/utils/src/number.ts +11 -0
- package/template/packages/utils/src/string.ts +13 -0
- package/template/packages/utils/tsconfig.json +5 -0
- package/template/packages/utils/vitest.config.ts +21 -0
- package/template/pnpm-workspace.yaml +4 -0
- package/template/tsconfig.base.json +32 -0
- package/template/turbo.json +133 -0
- package/template/vitest.shared.ts +26 -0
- package/template/vitest.workspace.ts +9 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PaginatedResponse,
|
|
3
|
+
QueryUsers,
|
|
4
|
+
} from '@repo/packages-types/pagination';
|
|
5
|
+
import type { CreateUser, UpdateUser, User } from '@repo/packages-types/user';
|
|
6
|
+
import {
|
|
7
|
+
useMutation,
|
|
8
|
+
useQuery,
|
|
9
|
+
useQueryClient,
|
|
10
|
+
type UseQueryOptions,
|
|
11
|
+
} from '@tanstack/react-query';
|
|
12
|
+
|
|
13
|
+
import { api } from '@/lib/api';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Query Keys
|
|
17
|
+
* Centralized query keys for cache management and invalidation
|
|
18
|
+
*/
|
|
19
|
+
export const userKeys = {
|
|
20
|
+
all: ['users'] as const,
|
|
21
|
+
lists: () => [...userKeys.all, 'list'] as const,
|
|
22
|
+
list: (params: QueryUsers) => [...userKeys.lists(), params] as const,
|
|
23
|
+
details: () => [...userKeys.all, 'detail'] as const,
|
|
24
|
+
detail: (id: string) => [...userKeys.details(), id] as const,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Fetch paginated users with filtering and sorting
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```tsx
|
|
32
|
+
* const { data, isLoading, error } = useFetchUsers({
|
|
33
|
+
* page: 1,
|
|
34
|
+
* limit: 10,
|
|
35
|
+
* search: 'john',
|
|
36
|
+
* sortBy: 'createdAt',
|
|
37
|
+
* sortOrder: 'desc'
|
|
38
|
+
* });
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function useFetchUsers(
|
|
42
|
+
params: QueryUsers,
|
|
43
|
+
options?: Omit<
|
|
44
|
+
UseQueryOptions<PaginatedResponse<User>>,
|
|
45
|
+
'queryKey' | 'queryFn'
|
|
46
|
+
>
|
|
47
|
+
) {
|
|
48
|
+
return useQuery({
|
|
49
|
+
queryKey: userKeys.list(params),
|
|
50
|
+
queryFn: () =>
|
|
51
|
+
api.get<PaginatedResponse<User>>('/users', {
|
|
52
|
+
params: params as Record<string, string | number | boolean>,
|
|
53
|
+
}),
|
|
54
|
+
...options,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Fetch a single user by ID
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```tsx
|
|
63
|
+
* const { data: user, isLoading } = useFetchUser('123e4567-e89b-12d3-a456-426614174000');
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export function useFetchUser(
|
|
67
|
+
id: string,
|
|
68
|
+
options?: Omit<UseQueryOptions<User>, 'queryKey' | 'queryFn'>
|
|
69
|
+
) {
|
|
70
|
+
return useQuery({
|
|
71
|
+
queryKey: userKeys.detail(id),
|
|
72
|
+
queryFn: () => api.get<User>(`/users/${id}`),
|
|
73
|
+
enabled: !!id,
|
|
74
|
+
...options,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Create a new user
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```tsx
|
|
83
|
+
* const { mutate: createUser, isPending } = useCreateUser();
|
|
84
|
+
*
|
|
85
|
+
* const handleSubmit = (data: CreateUser) => {
|
|
86
|
+
* createUser(data);
|
|
87
|
+
* };
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export function useCreateUser() {
|
|
91
|
+
const queryClient = useQueryClient();
|
|
92
|
+
|
|
93
|
+
return useMutation({
|
|
94
|
+
mutationFn: (data: CreateUser) => api.post<User>('/users', data),
|
|
95
|
+
onSuccess: (user) => {
|
|
96
|
+
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
|
|
97
|
+
queryClient.setQueryData(userKeys.detail(user.id), user);
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Update an existing user
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```tsx
|
|
107
|
+
* const { mutate: updateUser, isPending } = useUpdateUser();
|
|
108
|
+
*
|
|
109
|
+
* const handleUpdate = (id: string, data: UpdateUser) => {
|
|
110
|
+
* updateUser({ id, data });
|
|
111
|
+
* };
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
export function useUpdateUser() {
|
|
115
|
+
const queryClient = useQueryClient();
|
|
116
|
+
|
|
117
|
+
return useMutation({
|
|
118
|
+
mutationFn: ({ id, data }: { id: string; data: UpdateUser }) =>
|
|
119
|
+
api.patch<User>(`/users/${id}`, data),
|
|
120
|
+
onSuccess: (user) => {
|
|
121
|
+
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
|
|
122
|
+
queryClient.setQueryData(userKeys.detail(user.id), user);
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Delete a user
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```tsx
|
|
132
|
+
* const { mutate: deleteUser, isPending } = useDeleteUser();
|
|
133
|
+
*
|
|
134
|
+
* const handleDelete = (id: string) => {
|
|
135
|
+
* deleteUser(id);
|
|
136
|
+
* };
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
export function useDeleteUser() {
|
|
140
|
+
const queryClient = useQueryClient();
|
|
141
|
+
|
|
142
|
+
return useMutation({
|
|
143
|
+
mutationFn: (id: string) => api.delete(`/users/${id}`),
|
|
144
|
+
onSuccess: (_, id) => {
|
|
145
|
+
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
|
|
146
|
+
queryClient.removeQueries({ queryKey: userKeys.detail(id) });
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
const MOBILE_BREAKPOINT = 768;
|
|
4
|
+
|
|
5
|
+
export function useIsMobile() {
|
|
6
|
+
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
|
7
|
+
undefined
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
React.useEffect(() => {
|
|
11
|
+
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
|
12
|
+
const onChange = () => {
|
|
13
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
14
|
+
};
|
|
15
|
+
mql.addEventListener('change', onChange);
|
|
16
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
17
|
+
return () => mql.removeEventListener('change', onChange);
|
|
18
|
+
}, []);
|
|
19
|
+
|
|
20
|
+
return !!isMobile;
|
|
21
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { RealtimeMetricsPoint } from '@repo/packages-types/stats';
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import { env } from '@/lib/env';
|
|
5
|
+
|
|
6
|
+
interface UseRealtimeMetricsOptions {
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
maxDataPoints?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface UseRealtimeMetricsResult {
|
|
12
|
+
data: RealtimeMetricsPoint[];
|
|
13
|
+
isConnected: boolean;
|
|
14
|
+
error: Error | null;
|
|
15
|
+
reconnect: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function useRealtimeMetrics(
|
|
19
|
+
options: UseRealtimeMetricsOptions = {}
|
|
20
|
+
): UseRealtimeMetricsResult {
|
|
21
|
+
const { enabled = true, maxDataPoints = 60 } = options;
|
|
22
|
+
|
|
23
|
+
const [data, setData] = useState<RealtimeMetricsPoint[]>([]);
|
|
24
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
25
|
+
const [error, setError] = useState<Error | null>(null);
|
|
26
|
+
|
|
27
|
+
const eventSourceRef = useRef<EventSource | null>(null);
|
|
28
|
+
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
29
|
+
const reconnectAttempts = useRef(0);
|
|
30
|
+
const connectRef = useRef<(() => void) | null>(null);
|
|
31
|
+
|
|
32
|
+
const connect = useCallback(() => {
|
|
33
|
+
if (eventSourceRef.current) {
|
|
34
|
+
eventSourceRef.current.close();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const url = `${env.apiUrl}/admin/metrics/stream`;
|
|
38
|
+
|
|
39
|
+
const es = new EventSource(url, { withCredentials: true });
|
|
40
|
+
eventSourceRef.current = es;
|
|
41
|
+
|
|
42
|
+
es.onopen = () => {
|
|
43
|
+
setIsConnected(true);
|
|
44
|
+
setError(null);
|
|
45
|
+
reconnectAttempts.current = 0;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
es.addEventListener('history', (event: MessageEvent) => {
|
|
49
|
+
try {
|
|
50
|
+
const history = JSON.parse(event.data) as RealtimeMetricsPoint[];
|
|
51
|
+
setData(history.slice(-maxDataPoints));
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.error('Failed to parse history:', e);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
es.addEventListener('metrics', (event: MessageEvent) => {
|
|
58
|
+
try {
|
|
59
|
+
const metrics = JSON.parse(event.data) as RealtimeMetricsPoint;
|
|
60
|
+
setData((prev) => {
|
|
61
|
+
const updated = [...prev, metrics];
|
|
62
|
+
return updated.slice(-maxDataPoints);
|
|
63
|
+
});
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.error('Failed to parse metrics:', e);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
es.onerror = () => {
|
|
70
|
+
setIsConnected(false);
|
|
71
|
+
es.close();
|
|
72
|
+
eventSourceRef.current = null;
|
|
73
|
+
|
|
74
|
+
const backoff = Math.min(
|
|
75
|
+
1000 * Math.pow(2, reconnectAttempts.current),
|
|
76
|
+
30000
|
|
77
|
+
);
|
|
78
|
+
reconnectAttempts.current++;
|
|
79
|
+
|
|
80
|
+
setError(new Error('Connection lost. Reconnecting...'));
|
|
81
|
+
|
|
82
|
+
reconnectTimeoutRef.current = setTimeout(() => {
|
|
83
|
+
if (enabled) {
|
|
84
|
+
connectRef.current?.();
|
|
85
|
+
}
|
|
86
|
+
}, backoff);
|
|
87
|
+
};
|
|
88
|
+
}, [enabled, maxDataPoints]);
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
connectRef.current = connect;
|
|
92
|
+
}, [connect]);
|
|
93
|
+
|
|
94
|
+
const reconnect = useCallback(() => {
|
|
95
|
+
reconnectAttempts.current = 0;
|
|
96
|
+
if (reconnectTimeoutRef.current) {
|
|
97
|
+
clearTimeout(reconnectTimeoutRef.current);
|
|
98
|
+
}
|
|
99
|
+
connect();
|
|
100
|
+
}, [connect]);
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (enabled) {
|
|
104
|
+
connect();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return () => {
|
|
108
|
+
if (eventSourceRef.current) {
|
|
109
|
+
eventSourceRef.current.close();
|
|
110
|
+
eventSourceRef.current = null;
|
|
111
|
+
}
|
|
112
|
+
if (reconnectTimeoutRef.current) {
|
|
113
|
+
clearTimeout(reconnectTimeoutRef.current);
|
|
114
|
+
reconnectTimeoutRef.current = null;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}, [enabled, connect]);
|
|
118
|
+
|
|
119
|
+
return { data, isConnected, error, reconnect };
|
|
120
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@/lib/utils';
|
|
4
|
+
|
|
5
|
+
describe('cn', () => {
|
|
6
|
+
it('should merge class names', () => {
|
|
7
|
+
expect(cn('px-2 py-1', 'bg-red-500')).toBe('px-2 py-1 bg-red-500');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should handle conflicting Tailwind classes', () => {
|
|
11
|
+
expect(cn('px-2', 'px-4')).toBe('px-4');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should handle conditional classes', () => {
|
|
15
|
+
const isActive = true;
|
|
16
|
+
const isDisabled = false;
|
|
17
|
+
expect(cn('base', isActive && 'active', isDisabled && 'disabled')).toBe(
|
|
18
|
+
'base active'
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should handle undefined and null', () => {
|
|
23
|
+
expect(cn('base', undefined, null)).toBe('base');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should handle arrays', () => {
|
|
27
|
+
expect(cn(['px-2', 'py-1'])).toBe('px-2 py-1');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { env } from './env';
|
|
2
|
+
|
|
3
|
+
export class ApiError extends Error {
|
|
4
|
+
constructor(
|
|
5
|
+
message: string,
|
|
6
|
+
public status: number,
|
|
7
|
+
public data?: unknown
|
|
8
|
+
) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'ApiError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface FetcherOptions extends RequestInit {
|
|
15
|
+
params?: Record<string, string | number | boolean>;
|
|
16
|
+
timeout?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function fetcher<T>(
|
|
20
|
+
endpoint: string,
|
|
21
|
+
options: FetcherOptions = {}
|
|
22
|
+
): Promise<T> {
|
|
23
|
+
const { params, timeout, signal, ...init } = options;
|
|
24
|
+
|
|
25
|
+
// Build URL with query params
|
|
26
|
+
const url = new URL(`${env.apiUrl}${endpoint}`);
|
|
27
|
+
if (params) {
|
|
28
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
29
|
+
url.searchParams.append(key, String(value));
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Handle timeout with AbortController
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const timeoutId = timeout
|
|
36
|
+
? setTimeout(() => controller.abort(), timeout)
|
|
37
|
+
: null;
|
|
38
|
+
|
|
39
|
+
// Combine with external signal if provided
|
|
40
|
+
if (signal) {
|
|
41
|
+
signal.addEventListener('abort', () => controller.abort());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const response = await fetch(url.toString(), {
|
|
46
|
+
...init,
|
|
47
|
+
signal: controller.signal,
|
|
48
|
+
headers: {
|
|
49
|
+
...(init.body ? { 'Content-Type': 'application/json' } : {}),
|
|
50
|
+
...init.headers,
|
|
51
|
+
},
|
|
52
|
+
credentials: 'include',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
56
|
+
|
|
57
|
+
// Parse response based on content type
|
|
58
|
+
let data: unknown;
|
|
59
|
+
const contentType = response.headers.get('content-type');
|
|
60
|
+
|
|
61
|
+
if (contentType?.includes('application/json')) {
|
|
62
|
+
data = await response.json();
|
|
63
|
+
} else {
|
|
64
|
+
data = await response.text();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
const errorData = data as {
|
|
69
|
+
error?: { message?: string; code?: string; details?: unknown };
|
|
70
|
+
message?: string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const errorMessage =
|
|
74
|
+
errorData.error?.message ||
|
|
75
|
+
errorData.message ||
|
|
76
|
+
`HTTP ${response.status}`;
|
|
77
|
+
|
|
78
|
+
throw new ApiError(errorMessage, response.status, data);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Unwrap { data: T } or { message: string } wrappers from backend
|
|
82
|
+
// BUT preserve pagination responses ({ data: T[], pagination: {...} })
|
|
83
|
+
const wrappedData = data as
|
|
84
|
+
| { data: T; pagination?: unknown }
|
|
85
|
+
| { message: string }
|
|
86
|
+
| T
|
|
87
|
+
| undefined;
|
|
88
|
+
|
|
89
|
+
if (wrappedData && typeof wrappedData === 'object') {
|
|
90
|
+
// Don't unwrap if it has pagination metadata (paginated responses)
|
|
91
|
+
if ('data' in wrappedData && 'pagination' in wrappedData) {
|
|
92
|
+
return wrappedData as T;
|
|
93
|
+
}
|
|
94
|
+
// Unwrap single data responses
|
|
95
|
+
if ('data' in wrappedData) {
|
|
96
|
+
return wrappedData.data as T;
|
|
97
|
+
}
|
|
98
|
+
if ('message' in wrappedData) {
|
|
99
|
+
return wrappedData as T;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return data as T;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
106
|
+
|
|
107
|
+
// Re-throw ApiError as-is
|
|
108
|
+
if (error instanceof ApiError) {
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Handle timeout
|
|
113
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
114
|
+
throw new Error('Request timeout');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const api = {
|
|
122
|
+
get: <T>(url: string, options?: FetcherOptions) =>
|
|
123
|
+
fetcher<T>(url, { ...options, method: 'GET' }),
|
|
124
|
+
|
|
125
|
+
post: <T>(url: string, body?: unknown, options?: FetcherOptions) =>
|
|
126
|
+
fetcher<T>(url, { ...options, method: 'POST', body: JSON.stringify(body) }),
|
|
127
|
+
|
|
128
|
+
put: <T>(url: string, body?: unknown, options?: FetcherOptions) =>
|
|
129
|
+
fetcher<T>(url, { ...options, method: 'PUT', body: JSON.stringify(body) }),
|
|
130
|
+
|
|
131
|
+
patch: <T>(url: string, body?: unknown, options?: FetcherOptions) =>
|
|
132
|
+
fetcher<T>(url, {
|
|
133
|
+
...options,
|
|
134
|
+
method: 'PATCH',
|
|
135
|
+
body: JSON.stringify(body),
|
|
136
|
+
}),
|
|
137
|
+
|
|
138
|
+
delete: <T>(url: string, options?: FetcherOptions) =>
|
|
139
|
+
fetcher<T>(url, { ...options, method: 'DELETE' }),
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Error helpers
|
|
143
|
+
export function isApiError(error: unknown): error is ApiError {
|
|
144
|
+
return error instanceof ApiError;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function getErrorMessage(error: unknown): string {
|
|
148
|
+
if (isApiError(error)) return error.message;
|
|
149
|
+
if (error instanceof Error) return error.message;
|
|
150
|
+
return 'An unexpected error occurred';
|
|
151
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { adminClient } from 'better-auth/client/plugins';
|
|
2
|
+
import { createAuthClient } from 'better-auth/react';
|
|
3
|
+
|
|
4
|
+
import { env } from '@/lib/env';
|
|
5
|
+
|
|
6
|
+
export const authClient = createAuthClient({
|
|
7
|
+
baseURL: `${env.apiUrl}/auth`,
|
|
8
|
+
plugins: [adminClient()],
|
|
9
|
+
// ! IMPORTANT: Enable credentials to send/receive cookies cross-domain
|
|
10
|
+
fetchOptions: {
|
|
11
|
+
credentials: 'include',
|
|
12
|
+
},
|
|
13
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Client-side environment variable validation
|
|
5
|
+
*
|
|
6
|
+
* IMPORTANT: Only NEXT_PUBLIC_* variables are exposed to the browser
|
|
7
|
+
* All other env vars are server-side only and should NOT be included here
|
|
8
|
+
*/
|
|
9
|
+
const envSchema = z.object({
|
|
10
|
+
NEXT_PUBLIC_API_URL: z
|
|
11
|
+
.string()
|
|
12
|
+
.url('NEXT_PUBLIC_API_URL must be a valid URL')
|
|
13
|
+
.min(1, 'NEXT_PUBLIC_API_URL is required'),
|
|
14
|
+
NODE_ENV: z
|
|
15
|
+
.enum(['development', 'production', 'test'])
|
|
16
|
+
.default('development'),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validate and parse environment variables
|
|
21
|
+
* Throws if validation fails, preventing the app from running with invalid config
|
|
22
|
+
*/
|
|
23
|
+
function validateEnv() {
|
|
24
|
+
try {
|
|
25
|
+
return envSchema.parse({
|
|
26
|
+
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
|
|
27
|
+
NODE_ENV: process.env.NODE_ENV,
|
|
28
|
+
});
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (error instanceof z.ZodError) {
|
|
31
|
+
console.error('❌ Invalid environment variables:', error.issues);
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Environment validation failed: ${error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Validate on module load (fails fast if config is wrong)
|
|
41
|
+
const validatedEnv = validateEnv();
|
|
42
|
+
|
|
43
|
+
// Environment helpers
|
|
44
|
+
export const isDevelopment = () => validatedEnv.NODE_ENV === 'development';
|
|
45
|
+
export const isProduction = () => validatedEnv.NODE_ENV === 'production';
|
|
46
|
+
export const isTest = () => validatedEnv.NODE_ENV === 'test';
|
|
47
|
+
|
|
48
|
+
// Client-side environment variables (type-safe and validated)
|
|
49
|
+
export const env = {
|
|
50
|
+
apiUrl: validatedEnv.NEXT_PUBLIC_API_URL,
|
|
51
|
+
nodeEnv: validatedEnv.NODE_ENV,
|
|
52
|
+
} as const;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Create a type-safe form resolver from a Zod schema
|
|
5
|
+
* Usage: const form = useForm({ resolver: createFormResolver(MySchema) })
|
|
6
|
+
*/
|
|
7
|
+
export function createFormResolver<T extends Parameters<typeof zodResolver>[0]>(
|
|
8
|
+
schema: T
|
|
9
|
+
) {
|
|
10
|
+
return zodResolver(schema);
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { cn } from '@repo/packages-ui/lib/utils';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
4
|
+
import { ThemeProvider } from 'next-themes';
|
|
5
|
+
import { useState } from 'react';
|
|
6
|
+
|
|
7
|
+
function QueryProvider({ children }: { children: React.ReactNode }) {
|
|
8
|
+
const [queryClient] = useState(
|
|
9
|
+
() =>
|
|
10
|
+
new QueryClient({
|
|
11
|
+
defaultOptions: {
|
|
12
|
+
queries: {
|
|
13
|
+
staleTime: 60 * 1000,
|
|
14
|
+
refetchOnWindowFocus: false,
|
|
15
|
+
retry: 1,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
27
|
+
return (
|
|
28
|
+
<QueryProvider>
|
|
29
|
+
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
|
30
|
+
{children}
|
|
31
|
+
</ThemeProvider>
|
|
32
|
+
</QueryProvider>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { atom } from 'jotai';
|
|
2
|
+
|
|
3
|
+
// Example atoms - you can extend these based on your needs
|
|
4
|
+
|
|
5
|
+
// User preference example
|
|
6
|
+
export const userPreferenceAtom = atom<{
|
|
7
|
+
sidebarCollapsed: boolean;
|
|
8
|
+
notificationsEnabled: boolean;
|
|
9
|
+
}>({
|
|
10
|
+
sidebarCollapsed: false,
|
|
11
|
+
notificationsEnabled: true,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Counter example (for demonstration)
|
|
15
|
+
export const counterAtom = atom(0);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
2
|
+
import {
|
|
3
|
+
render,
|
|
4
|
+
type RenderOptions,
|
|
5
|
+
type RenderResult,
|
|
6
|
+
} from '@testing-library/react';
|
|
7
|
+
import { type ReactElement, type ReactNode } from 'react';
|
|
8
|
+
|
|
9
|
+
interface AllTheProvidersProps {
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function createTestQueryClient() {
|
|
14
|
+
return new QueryClient({
|
|
15
|
+
defaultOptions: {
|
|
16
|
+
queries: {
|
|
17
|
+
retry: false,
|
|
18
|
+
},
|
|
19
|
+
mutations: {
|
|
20
|
+
retry: false,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function AllTheProviders({ children }: AllTheProvidersProps) {
|
|
27
|
+
const testQueryClient = createTestQueryClient();
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<QueryClientProvider client={testQueryClient}>
|
|
31
|
+
{children}
|
|
32
|
+
</QueryClientProvider>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function renderWithProviders(
|
|
37
|
+
ui: ReactElement,
|
|
38
|
+
options?: Omit<RenderOptions, 'wrapper'>
|
|
39
|
+
): RenderResult {
|
|
40
|
+
return render(ui, { wrapper: AllTheProviders, ...options });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export * from '@testing-library/react';
|
|
44
|
+
export { renderWithProviders as render };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"lib": ["dom", "dom.iterable", "ES2022"],
|
|
6
|
+
"jsx": "preserve",
|
|
7
|
+
"module": "esnext",
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"resolveJsonModule": true,
|
|
10
|
+
"isolatedModules": true,
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"baseUrl": "../../",
|
|
14
|
+
"paths": {
|
|
15
|
+
"@/*": ["apps/web/src/*"]
|
|
16
|
+
},
|
|
17
|
+
"plugins": [
|
|
18
|
+
{
|
|
19
|
+
"name": "next"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"incremental": true
|
|
23
|
+
},
|
|
24
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
25
|
+
"exclude": ["node_modules"]
|
|
26
|
+
}
|