create-tigra 1.1.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +80 -87
  3. package/bin/create-tigra.js +259 -308
  4. package/package.json +49 -41
  5. package/template/_claude/QUICK_REFERENCE.md +193 -0
  6. package/template/_claude/README.md +53 -0
  7. package/template/_claude/commands/create-client.md +881 -0
  8. package/template/_claude/commands/create-server.md +383 -0
  9. package/template/_claude/rules/client/01-project-structure.md +133 -0
  10. package/template/_claude/rules/client/02-components-and-types.md +146 -0
  11. package/template/_claude/rules/client/03-data-and-state.md +156 -0
  12. package/template/_claude/rules/client/04-design-system.md +185 -0
  13. package/template/_claude/rules/client/05-security.md +55 -0
  14. package/template/_claude/rules/client/06-ux-checklist.md +81 -0
  15. package/template/_claude/rules/client/core.md +42 -0
  16. package/template/_claude/rules/global/core.md +77 -0
  17. package/template/_claude/rules/server/core.md +50 -0
  18. package/template/_claude/rules/server/database.md +124 -0
  19. package/template/_claude/rules/server/project-conventions.md +150 -0
  20. package/template/_claude/rules/server/response-handling.md +144 -0
  21. package/template/client/.env.example +5 -0
  22. package/template/client/README.md +36 -0
  23. package/template/client/components.json +23 -0
  24. package/template/client/eslint.config.mjs +18 -0
  25. package/template/client/next.config.ts +34 -0
  26. package/template/client/package.json +44 -0
  27. package/template/client/postcss.config.mjs +7 -0
  28. package/template/client/src/app/(auth)/layout.tsx +18 -0
  29. package/template/client/src/app/(auth)/login/page.tsx +13 -0
  30. package/template/client/src/app/(auth)/register/page.tsx +13 -0
  31. package/template/client/src/app/(main)/dashboard/page.tsx +22 -0
  32. package/template/client/src/app/(main)/layout.tsx +11 -0
  33. package/template/client/src/app/error.tsx +27 -0
  34. package/template/client/src/app/favicon.ico +0 -0
  35. package/template/client/src/app/globals.css +145 -0
  36. package/template/client/src/app/layout.tsx +36 -0
  37. package/template/client/src/app/loading.tsx +11 -0
  38. package/template/client/src/app/not-found.tsx +23 -0
  39. package/template/client/src/app/page.tsx +45 -0
  40. package/template/client/src/app/providers.tsx +43 -0
  41. package/template/client/src/components/common/ConfirmDialog.tsx +56 -0
  42. package/template/client/src/components/common/EmptyState.tsx +31 -0
  43. package/template/client/src/components/common/LoadingSpinner.tsx +30 -0
  44. package/template/client/src/components/common/Pagination.tsx +55 -0
  45. package/template/client/src/components/layout/Footer.tsx +17 -0
  46. package/template/client/src/components/layout/Header.tsx +173 -0
  47. package/template/client/src/components/layout/MainLayout.tsx +18 -0
  48. package/template/client/src/components/ui/alert-dialog.tsx +196 -0
  49. package/template/client/src/components/ui/badge.tsx +48 -0
  50. package/template/client/src/components/ui/button.tsx +64 -0
  51. package/template/client/src/components/ui/card.tsx +92 -0
  52. package/template/client/src/components/ui/input.tsx +21 -0
  53. package/template/client/src/components/ui/label.tsx +24 -0
  54. package/template/client/src/components/ui/select.tsx +190 -0
  55. package/template/client/src/components/ui/skeleton.tsx +13 -0
  56. package/template/client/src/components/ui/table.tsx +116 -0
  57. package/template/client/src/features/auth/components/AuthInitializer.tsx +55 -0
  58. package/template/client/src/features/auth/components/LoginForm.tsx +107 -0
  59. package/template/client/src/features/auth/components/RegisterForm.tsx +178 -0
  60. package/template/client/src/features/auth/hooks/useAuth.ts +84 -0
  61. package/template/client/src/features/auth/services/auth.service.ts +52 -0
  62. package/template/client/src/features/auth/store/authSlice.ts +38 -0
  63. package/template/client/src/features/auth/types/auth.types.ts +32 -0
  64. package/template/client/src/hooks/useDebounce.ts +14 -0
  65. package/template/client/src/hooks/useLocalStorage.ts +55 -0
  66. package/template/client/src/hooks/useMediaQuery.ts +27 -0
  67. package/template/client/src/lib/api/api.types.ts +34 -0
  68. package/template/client/src/lib/api/axios.config.ts +98 -0
  69. package/template/client/src/lib/constants/api-endpoints.ts +18 -0
  70. package/template/client/src/lib/constants/app.constants.ts +12 -0
  71. package/template/client/src/lib/constants/routes.ts +9 -0
  72. package/template/client/src/lib/utils/error.ts +32 -0
  73. package/template/client/src/lib/utils/format.ts +37 -0
  74. package/template/client/src/lib/utils/security.ts +34 -0
  75. package/template/client/src/lib/utils.ts +6 -0
  76. package/template/client/src/middleware.ts +57 -0
  77. package/template/client/src/store/hooks.ts +7 -0
  78. package/template/client/src/store/index.ts +12 -0
  79. package/template/client/src/types/index.ts +3 -0
  80. package/template/client/tsconfig.json +34 -0
  81. package/template/gitignore +34 -0
  82. package/template/server/.dockerignore +66 -0
  83. package/template/server/.env.example +96 -69
  84. package/template/server/.env.production.example +90 -0
  85. package/template/server/Dockerfile +94 -0
  86. package/template/server/docker-compose.yml +82 -111
  87. package/template/server/docs/logging.md +62 -0
  88. package/template/server/eslint.config.mjs +17 -0
  89. package/template/server/package.json +68 -81
  90. package/template/server/phpmyadmin-config.php +26 -0
  91. package/template/server/postman_collection.json +666 -0
  92. package/template/server/prisma/schema.prisma +77 -93
  93. package/template/server/prisma/seed.ts +46 -142
  94. package/template/server/scripts/flush-redis.ts +41 -0
  95. package/template/server/src/app.ts +243 -71
  96. package/template/server/src/config/env.ts +67 -94
  97. package/template/server/src/libs/auth.ts +88 -0
  98. package/template/server/src/libs/cleanup.ts +35 -0
  99. package/template/server/src/libs/cookies.ts +46 -0
  100. package/template/server/src/libs/logger.ts +33 -60
  101. package/template/server/src/libs/monitoring.ts +205 -0
  102. package/template/server/src/libs/password.ts +38 -0
  103. package/template/server/src/libs/prisma.ts +68 -0
  104. package/template/server/src/libs/redis.ts +60 -79
  105. package/template/server/src/libs/requestLogger.ts +66 -0
  106. package/template/server/src/libs/storage/file-storage.service.ts +211 -0
  107. package/template/server/src/libs/storage/file-validator.ts +97 -0
  108. package/template/server/src/libs/storage/filename-sanitizer.ts +71 -0
  109. package/template/server/src/libs/storage/image-optimizer.service.ts +144 -0
  110. package/template/server/src/modules/auth/__tests__/auth.service.test.ts +365 -0
  111. package/template/server/src/modules/auth/auth.controller.ts +90 -141
  112. package/template/server/src/modules/auth/auth.repo.ts +120 -218
  113. package/template/server/src/modules/auth/auth.routes.ts +96 -83
  114. package/template/server/src/modules/auth/auth.schemas.ts +35 -137
  115. package/template/server/src/modules/auth/auth.service.ts +286 -329
  116. package/template/server/src/modules/auth/session.repo.ts +110 -0
  117. package/template/server/src/modules/users/users.controller.ts +120 -0
  118. package/template/server/src/modules/users/users.repo.ts +77 -0
  119. package/template/server/src/modules/users/users.routes.ts +89 -0
  120. package/template/server/src/modules/users/users.schemas.ts +21 -0
  121. package/template/server/src/modules/users/users.service.ts +169 -0
  122. package/template/server/src/server.ts +58 -139
  123. package/template/server/src/shared/errors/AppError.ts +21 -0
  124. package/template/server/src/shared/errors/errors.ts +43 -0
  125. package/template/server/src/shared/responses/paginatedResponse.ts +38 -0
  126. package/template/server/src/shared/responses/successResponse.ts +17 -0
  127. package/template/server/src/shared/schemas/pagination.schema.ts +12 -0
  128. package/template/server/src/shared/types/index.ts +26 -0
  129. package/template/server/src/test/setup.ts +74 -38
  130. package/template/server/tsconfig.json +27 -89
  131. package/template/server/uploads/avatars/.gitkeep +1 -0
  132. package/template/server/vitest.config.ts +43 -98
  133. package/template/.agent/rules/client/01-project-structure.md +0 -326
  134. package/template/.agent/rules/client/02-component-patterns.md +0 -249
  135. package/template/.agent/rules/client/03-typescript-rules.md +0 -226
  136. package/template/.agent/rules/client/04-state-management.md +0 -474
  137. package/template/.agent/rules/client/05-api-integration.md +0 -129
  138. package/template/.agent/rules/client/06-forms-validation.md +0 -129
  139. package/template/.agent/rules/client/07-common-patterns.md +0 -150
  140. package/template/.agent/rules/client/08-color-system.md +0 -93
  141. package/template/.agent/rules/client/09-security-rules.md +0 -97
  142. package/template/.agent/rules/client/10-testing-strategy.md +0 -370
  143. package/template/.agent/rules/global/ai-edit-safety.md +0 -38
  144. package/template/.agent/rules/server/01-db-and-migrations.md +0 -242
  145. package/template/.agent/rules/server/02-general-rules.md +0 -111
  146. package/template/.agent/rules/server/03-migrations.md +0 -20
  147. package/template/.agent/rules/server/04-pagination.md +0 -130
  148. package/template/.agent/rules/server/05-project-conventions.md +0 -71
  149. package/template/.agent/rules/server/06-response-handling.md +0 -173
  150. package/template/.agent/rules/server/07-testing-strategy.md +0 -506
  151. package/template/.agent/rules/server/08-observability.md +0 -180
  152. package/template/.agent/rules/server/10-background-jobs-v2.md +0 -185
  153. package/template/.agent/rules/server/11-rate-limiting-v2.md +0 -210
  154. package/template/.agent/rules/server/12-performance-optimization.md +0 -567
  155. package/template/.claude/rules/client-01-project-structure.md +0 -327
  156. package/template/.claude/rules/client-02-component-patterns.md +0 -250
  157. package/template/.claude/rules/client-03-typescript-rules.md +0 -227
  158. package/template/.claude/rules/client-04-state-management.md +0 -475
  159. package/template/.claude/rules/client-05-api-integration.md +0 -130
  160. package/template/.claude/rules/client-06-forms-validation.md +0 -130
  161. package/template/.claude/rules/client-07-common-patterns.md +0 -151
  162. package/template/.claude/rules/client-08-color-system.md +0 -94
  163. package/template/.claude/rules/client-09-security-rules.md +0 -98
  164. package/template/.claude/rules/client-10-testing-strategy.md +0 -371
  165. package/template/.claude/rules/global-ai-edit-safety.md +0 -39
  166. package/template/.claude/rules/server-01-db-and-migrations.md +0 -243
  167. package/template/.claude/rules/server-02-general-rules.md +0 -112
  168. package/template/.claude/rules/server-03-migrations.md +0 -21
  169. package/template/.claude/rules/server-04-pagination.md +0 -131
  170. package/template/.claude/rules/server-05-project-conventions.md +0 -72
  171. package/template/.claude/rules/server-06-response-handling.md +0 -174
  172. package/template/.claude/rules/server-07-testing-strategy.md +0 -507
  173. package/template/.claude/rules/server-08-observability.md +0 -181
  174. package/template/.claude/rules/server-10-background-jobs-v2.md +0 -186
  175. package/template/.claude/rules/server-11-rate-limiting-v2.md +0 -211
  176. package/template/.claude/rules/server-12-performance-optimization.md +0 -568
  177. package/template/.cursor/rules/client-01-project-structure.mdc +0 -327
  178. package/template/.cursor/rules/client-02-component-patterns.mdc +0 -250
  179. package/template/.cursor/rules/client-03-typescript-rules.mdc +0 -227
  180. package/template/.cursor/rules/client-04-state-management.mdc +0 -475
  181. package/template/.cursor/rules/client-05-api-integration.mdc +0 -130
  182. package/template/.cursor/rules/client-06-forms-validation.mdc +0 -130
  183. package/template/.cursor/rules/client-07-common-patterns.mdc +0 -151
  184. package/template/.cursor/rules/client-08-color-system.mdc +0 -94
  185. package/template/.cursor/rules/client-09-security-rules.mdc +0 -98
  186. package/template/.cursor/rules/client-10-testing-strategy.mdc +0 -371
  187. package/template/.cursor/rules/global-ai-edit-safety.mdc +0 -39
  188. package/template/.cursor/rules/server-01-db-and-migrations.mdc +0 -243
  189. package/template/.cursor/rules/server-02-general-rules.mdc +0 -112
  190. package/template/.cursor/rules/server-03-migrations.mdc +0 -21
  191. package/template/.cursor/rules/server-04-pagination.mdc +0 -131
  192. package/template/.cursor/rules/server-05-project-conventions.mdc +0 -72
  193. package/template/.cursor/rules/server-06-response-handling.mdc +0 -174
  194. package/template/.cursor/rules/server-07-testing-strategy.mdc +0 -507
  195. package/template/.cursor/rules/server-08-observability.mdc +0 -181
  196. package/template/.cursor/rules/server-09-api-documentation-v2.mdc +0 -169
  197. package/template/.cursor/rules/server-10-background-jobs-v2.mdc +0 -186
  198. package/template/.cursor/rules/server-11-rate-limiting-v2.mdc +0 -211
  199. package/template/.cursor/rules/server-12-performance-optimization.mdc +0 -568
  200. package/template/CLAUDE.md +0 -207
  201. package/template/server/.tsc-aliasrc.json +0 -13
  202. package/template/server/IMPORT_FIX_CHECKLIST.md +0 -98
  203. package/template/server/IMPORT_FIX_COMPLETE.md +0 -89
  204. package/template/server/README.md +0 -183
  205. package/template/server/REMAINING_IMPORT_FIXES.md +0 -150
  206. package/template/server/SECURITY.md +0 -190
  207. package/template/server/Tigra-API.postman_collection.json +0 -733
  208. package/template/server/biome.json +0 -42
  209. package/template/server/scripts/fix-all-imports.ps1 +0 -52
  210. package/template/server/scripts/fix-imports-reference.ps1 +0 -16
  211. package/template/server/scripts/fix-imports.mjs +0 -55
  212. package/template/server/scripts/setup-env.js +0 -50
  213. package/template/server/scripts/wait-for-db.js +0 -60
  214. package/template/server/src/hooks/request-timing.hook.ts +0 -26
  215. package/template/server/src/libs/auth/authenticate.middleware.ts +0 -22
  216. package/template/server/src/libs/auth/rbac.middleware.test.ts +0 -134
  217. package/template/server/src/libs/auth/rbac.middleware.ts +0 -147
  218. package/template/server/src/libs/db.ts +0 -76
  219. package/template/server/src/libs/error-handler.ts +0 -89
  220. package/template/server/src/libs/queue.ts +0 -79
  221. package/template/server/src/modules/admin/admin.controller.ts +0 -122
  222. package/template/server/src/modules/admin/admin.routes.ts +0 -62
  223. package/template/server/src/modules/admin/admin.schemas.ts +0 -35
  224. package/template/server/src/modules/admin/admin.service.ts +0 -167
  225. package/template/server/src/modules/auth/auth.integration.test.ts +0 -150
  226. package/template/server/src/modules/auth/auth.service.test.ts +0 -119
  227. package/template/server/src/modules/auth/auth.types.ts +0 -97
  228. package/template/server/src/modules/resources/resources.controller.ts +0 -218
  229. package/template/server/src/modules/resources/resources.repo.ts +0 -253
  230. package/template/server/src/modules/resources/resources.routes.ts +0 -116
  231. package/template/server/src/modules/resources/resources.schemas.ts +0 -146
  232. package/template/server/src/modules/resources/resources.service.ts +0 -218
  233. package/template/server/src/modules/resources/resources.types.ts +0 -73
  234. package/template/server/src/plugins/rate-limit.plugin.ts +0 -21
  235. package/template/server/src/plugins/security.plugin.ts +0 -21
  236. package/template/server/src/routes/health.routes.ts +0 -31
  237. package/template/server/src/types/fastify.d.ts +0 -36
  238. package/template/server/src/utils/errors.ts +0 -108
  239. package/template/server/src/utils/pagination.ts +0 -120
  240. package/template/server/src/utils/response.ts +0 -110
  241. package/template/server/src/workers/file.worker.ts +0 -106
  242. package/template/server/tsconfig.build.json +0 -30
  243. package/template/server/tsconfig.test.json +0 -22
@@ -0,0 +1,84 @@
1
+ 'use client';
2
+
3
+ import { useCallback } from 'react';
4
+
5
+ import { useMutation } from '@tanstack/react-query';
6
+ import { useRouter, useSearchParams } from 'next/navigation';
7
+ import { toast } from 'sonner';
8
+
9
+ import { useAppDispatch, useAppSelector } from '@/store/hooks';
10
+ import { getErrorMessage } from '@/lib/utils/error';
11
+ import { ROUTES } from '@/lib/constants/routes';
12
+ import { authService } from '../services/auth.service';
13
+ import { setUser, setLoggingOut, logout as logoutAction } from '../store/authSlice';
14
+
15
+ import type { ILoginRequest, IRegisterRequest, IUser } from '../types/auth.types';
16
+
17
+ interface UseAuthReturn {
18
+ user: IUser | null;
19
+ isAuthenticated: boolean;
20
+ isInitializing: boolean;
21
+ login: (data: ILoginRequest) => void;
22
+ register: (data: IRegisterRequest) => void;
23
+ logout: () => Promise<void>;
24
+ isLoggingIn: boolean;
25
+ isRegistering: boolean;
26
+ isLoggingOut: boolean;
27
+ }
28
+
29
+ export const useAuth = (): UseAuthReturn => {
30
+ const dispatch = useAppDispatch();
31
+ const router = useRouter();
32
+ const searchParams = useSearchParams();
33
+ const { user, isAuthenticated, isInitializing, isLoggingOut } = useAppSelector((state) => state.auth);
34
+
35
+ const loginMutation = useMutation({
36
+ mutationFn: (data: ILoginRequest) => authService.login(data),
37
+ onSuccess: (data) => {
38
+ dispatch(setUser(data.user));
39
+ toast.success('Signed in successfully');
40
+ const from = searchParams.get('from');
41
+ const redirectTo = from && from.startsWith('/') && !from.startsWith('//') ? from : '/';
42
+ router.push(redirectTo);
43
+ },
44
+ onError: (error) => {
45
+ toast.error(getErrorMessage(error));
46
+ },
47
+ });
48
+
49
+ const registerMutation = useMutation({
50
+ mutationFn: (data: IRegisterRequest) => authService.register(data),
51
+ onSuccess: (data) => {
52
+ dispatch(setUser(data.user));
53
+ toast.success('Account created successfully');
54
+ router.push(ROUTES.HOME);
55
+ },
56
+ onError: (error) => {
57
+ toast.error(getErrorMessage(error));
58
+ },
59
+ });
60
+
61
+ const logout = useCallback(async (): Promise<void> => {
62
+ dispatch(setLoggingOut(true));
63
+ try {
64
+ await authService.logout();
65
+ } catch {
66
+ // Proceed with local logout even if server call fails
67
+ } finally {
68
+ dispatch(logoutAction());
69
+ router.push(ROUTES.LOGIN);
70
+ }
71
+ }, [dispatch, router]);
72
+
73
+ return {
74
+ user,
75
+ isAuthenticated,
76
+ isInitializing,
77
+ login: loginMutation.mutate,
78
+ register: registerMutation.mutate,
79
+ logout,
80
+ isLoggingIn: loginMutation.isPending,
81
+ isRegistering: registerMutation.isPending,
82
+ isLoggingOut,
83
+ };
84
+ };
@@ -0,0 +1,52 @@
1
+ import { apiClient } from '@/lib/api/axios.config';
2
+ import { API_ENDPOINTS } from '@/lib/constants/api-endpoints';
3
+
4
+ import type { ApiResponse } from '@/lib/api/api.types';
5
+ import type { IUser, ILoginRequest, IRegisterRequest } from '../types/auth.types';
6
+
7
+ interface AuthResponse {
8
+ user: IUser;
9
+ }
10
+
11
+ class AuthService {
12
+ async register(data: IRegisterRequest): Promise<AuthResponse> {
13
+ const response = await apiClient.post<ApiResponse<AuthResponse>>(
14
+ API_ENDPOINTS.AUTH.REGISTER,
15
+ data
16
+ );
17
+ return response.data.data;
18
+ }
19
+
20
+ async login(data: ILoginRequest): Promise<AuthResponse> {
21
+ const response = await apiClient.post<ApiResponse<AuthResponse>>(
22
+ API_ENDPOINTS.AUTH.LOGIN,
23
+ data
24
+ );
25
+ return response.data.data;
26
+ }
27
+
28
+ async logout(): Promise<void> {
29
+ await apiClient.post(API_ENDPOINTS.AUTH.LOGOUT);
30
+ }
31
+
32
+ async getMe(): Promise<IUser> {
33
+ const response = await apiClient.get<ApiResponse<IUser>>(
34
+ API_ENDPOINTS.AUTH.ME
35
+ );
36
+ return response.data.data;
37
+ }
38
+
39
+ async verifyEmail(token: string): Promise<void> {
40
+ await apiClient.post(API_ENDPOINTS.AUTH.VERIFY_EMAIL, { token });
41
+ }
42
+
43
+ async requestPasswordReset(email: string): Promise<void> {
44
+ await apiClient.post(API_ENDPOINTS.AUTH.REQUEST_PASSWORD_RESET, { email });
45
+ }
46
+
47
+ async resetPassword(token: string, newPassword: string): Promise<void> {
48
+ await apiClient.post(API_ENDPOINTS.AUTH.RESET_PASSWORD, { token, newPassword });
49
+ }
50
+ }
51
+
52
+ export const authService = new AuthService();
@@ -0,0 +1,38 @@
1
+ import { createSlice } from '@reduxjs/toolkit';
2
+
3
+ import type { PayloadAction } from '@reduxjs/toolkit';
4
+ import type { IAuthState, IUser } from '../types/auth.types';
5
+
6
+ const initialState: IAuthState = {
7
+ user: null,
8
+ isAuthenticated: false,
9
+ isInitializing: true,
10
+ isLoggingOut: false,
11
+ };
12
+
13
+ const authSlice = createSlice({
14
+ name: 'auth',
15
+ initialState,
16
+ reducers: {
17
+ setUser: (state, action: PayloadAction<IUser>) => {
18
+ state.user = action.payload;
19
+ state.isAuthenticated = true;
20
+ state.isInitializing = false;
21
+ state.isLoggingOut = false;
22
+ },
23
+ setInitialized: (state) => {
24
+ state.isInitializing = false;
25
+ },
26
+ setLoggingOut: (state, action: PayloadAction<boolean>) => {
27
+ state.isLoggingOut = action.payload;
28
+ },
29
+ logout: (state) => {
30
+ state.user = null;
31
+ state.isAuthenticated = false;
32
+ state.isLoggingOut = false;
33
+ },
34
+ },
35
+ });
36
+
37
+ export const { setUser, setInitialized, setLoggingOut, logout } = authSlice.actions;
38
+ export default authSlice.reducer;
@@ -0,0 +1,32 @@
1
+ export interface IUser {
2
+ id: string;
3
+ email: string;
4
+ firstName: string;
5
+ lastName: string;
6
+ role: UserRole;
7
+ isActive: boolean;
8
+ avatarUrl: string | null;
9
+ createdAt: string;
10
+ updatedAt: string;
11
+ }
12
+
13
+ export type UserRole = 'USER' | 'ADMIN';
14
+
15
+ export interface ILoginRequest {
16
+ email: string;
17
+ password: string;
18
+ }
19
+
20
+ export interface IRegisterRequest {
21
+ email: string;
22
+ password: string;
23
+ firstName: string;
24
+ lastName: string;
25
+ }
26
+
27
+ export interface IAuthState {
28
+ user: IUser | null;
29
+ isAuthenticated: boolean;
30
+ isInitializing: boolean;
31
+ isLoggingOut: boolean;
32
+ }
@@ -0,0 +1,14 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+
5
+ export const useDebounce = <T,>(value: T, delay = 500): T => {
6
+ const [debouncedValue, setDebouncedValue] = useState(value);
7
+
8
+ useEffect(() => {
9
+ const timer = setTimeout(() => setDebouncedValue(value), delay);
10
+ return () => clearTimeout(timer);
11
+ }, [value, delay]);
12
+
13
+ return debouncedValue;
14
+ };
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useSyncExternalStore } from 'react';
4
+
5
+ export const useLocalStorage = <T,>(key: string, initialValue: T): readonly [T, (value: T | ((val: T) => T)) => void] => {
6
+ const subscribe = useCallback(
7
+ (callback: () => void): (() => void) => {
8
+ const handler = (e: StorageEvent): void => {
9
+ if (e.key === key) callback();
10
+ };
11
+ window.addEventListener('storage', handler);
12
+ return () => window.removeEventListener('storage', handler);
13
+ },
14
+ [key],
15
+ );
16
+
17
+ const getSnapshot = useCallback((): string | null => {
18
+ return window.localStorage.getItem(key);
19
+ }, [key]);
20
+
21
+ const getServerSnapshot = useCallback((): string | null => {
22
+ return null;
23
+ }, []);
24
+
25
+ const rawValue = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
26
+
27
+ const storedValue: T = rawValue !== null
28
+ ? (() => {
29
+ try { return JSON.parse(rawValue) as T; }
30
+ catch { return initialValue; }
31
+ })()
32
+ : initialValue;
33
+
34
+ const setValue = useCallback(
35
+ (value: T | ((val: T) => T)): void => {
36
+ try {
37
+ const currentRaw = window.localStorage.getItem(key);
38
+ const current: T = currentRaw !== null
39
+ ? (JSON.parse(currentRaw) as T)
40
+ : initialValue;
41
+ const valueToStore = value instanceof Function ? value(current) : value;
42
+ window.localStorage.setItem(key, JSON.stringify(valueToStore));
43
+ // Dispatch a storage event so useSyncExternalStore picks up the change
44
+ window.dispatchEvent(new StorageEvent('storage', { key }));
45
+ } catch (error) {
46
+ if (process.env.NODE_ENV === 'development') {
47
+ console.warn(`[useLocalStorage] Failed to write key "${key}":`, error);
48
+ }
49
+ }
50
+ },
51
+ [key, initialValue],
52
+ );
53
+
54
+ return [storedValue, setValue] as const;
55
+ };
@@ -0,0 +1,27 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useSyncExternalStore } from 'react';
4
+
5
+ export const useMediaQuery = (query: string): boolean => {
6
+ const subscribe = useCallback(
7
+ (callback: () => void): (() => void) => {
8
+ const media = window.matchMedia(query);
9
+ media.addEventListener('change', callback);
10
+ return () => media.removeEventListener('change', callback);
11
+ },
12
+ [query],
13
+ );
14
+
15
+ const getSnapshot = (): boolean => {
16
+ return window.matchMedia(query).matches;
17
+ };
18
+
19
+ const getServerSnapshot = (): boolean => {
20
+ return false;
21
+ };
22
+
23
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
24
+ };
25
+
26
+ export const useIsMobile = (): boolean => useMediaQuery('(max-width: 768px)');
27
+ export const useIsTablet = (): boolean => useMediaQuery('(max-width: 1024px)');
@@ -0,0 +1,34 @@
1
+ export interface ApiResponse<T> {
2
+ success: boolean;
3
+ message: string;
4
+ data: T;
5
+ }
6
+
7
+ export interface PaginatedApiResponse<T> {
8
+ success: boolean;
9
+ message: string;
10
+ data: {
11
+ items: T[];
12
+ pagination: {
13
+ page: number;
14
+ limit: number;
15
+ totalItems: number;
16
+ totalPages: number;
17
+ hasNextPage: boolean;
18
+ hasPreviousPage: boolean;
19
+ };
20
+ };
21
+ }
22
+
23
+ export interface ApiError {
24
+ success: false;
25
+ error: {
26
+ code: string;
27
+ message: string;
28
+ };
29
+ }
30
+
31
+ export interface PaginationParams {
32
+ page?: number;
33
+ limit?: number;
34
+ }
@@ -0,0 +1,98 @@
1
+ import axios from 'axios';
2
+
3
+ import type { InternalAxiosRequestConfig } from 'axios';
4
+
5
+ import { API_ENDPOINTS } from '@/lib/constants/api-endpoints';
6
+ import { ROUTES } from '@/lib/constants/routes';
7
+
8
+ const apiClient = axios.create({
9
+ baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000/api/v1',
10
+ timeout: 30000,
11
+ withCredentials: true, // Send cookies with every request
12
+ headers: {
13
+ 'Content-Type': 'application/json',
14
+ },
15
+ });
16
+
17
+ // No request interceptor needed — cookies are sent automatically
18
+
19
+ let isRefreshing = false;
20
+ let refreshTimestamp = 0;
21
+ const REFRESH_TIMEOUT_MS = 15000;
22
+ let failedQueue: { resolve: () => void; reject: (error: unknown) => void }[] = [];
23
+
24
+ const processQueue = (error: unknown): void => {
25
+ failedQueue.forEach((promise) => {
26
+ if (error) {
27
+ promise.reject(error);
28
+ } else {
29
+ promise.resolve();
30
+ }
31
+ });
32
+ failedQueue = [];
33
+ };
34
+
35
+ const resetRefreshState = (): void => {
36
+ isRefreshing = false;
37
+ refreshTimestamp = 0;
38
+ };
39
+
40
+ apiClient.interceptors.response.use(
41
+ (response) => response,
42
+ async (error) => {
43
+ const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
44
+
45
+ if (error.response?.status !== 401 || originalRequest._retry) {
46
+ return Promise.reject(error);
47
+ }
48
+
49
+ // Don't retry refresh endpoint itself
50
+ if (originalRequest.url === API_ENDPOINTS.AUTH.REFRESH) {
51
+ return Promise.reject(error);
52
+ }
53
+
54
+ // If a refresh is in progress but has exceeded the timeout, reset the stale flag
55
+ if (isRefreshing && Date.now() - refreshTimestamp > REFRESH_TIMEOUT_MS) {
56
+ processQueue(new Error('Token refresh timed out'));
57
+ resetRefreshState();
58
+ }
59
+
60
+ if (isRefreshing) {
61
+ return new Promise<void>((resolve, reject) => {
62
+ failedQueue.push({ resolve, reject });
63
+ }).then(() => {
64
+ return apiClient(originalRequest);
65
+ });
66
+ }
67
+
68
+ originalRequest._retry = true;
69
+ isRefreshing = true;
70
+ refreshTimestamp = Date.now();
71
+
72
+ try {
73
+ // Refresh — cookie is sent automatically
74
+ await apiClient.post(API_ENDPOINTS.AUTH.REFRESH);
75
+
76
+ processQueue(null);
77
+
78
+ // Retry original request — new cookie is set automatically
79
+ return apiClient(originalRequest);
80
+ } catch (refreshError) {
81
+ processQueue(refreshError);
82
+
83
+ const { logout } = await import('@/features/auth/store/authSlice');
84
+ const { store } = await import('@/store');
85
+ store.dispatch(logout());
86
+
87
+ if (typeof window !== 'undefined') {
88
+ window.location.href = ROUTES.LOGIN;
89
+ }
90
+
91
+ return Promise.reject(refreshError);
92
+ } finally {
93
+ resetRefreshState();
94
+ }
95
+ }
96
+ );
97
+
98
+ export { apiClient };
@@ -0,0 +1,18 @@
1
+ export const API_ENDPOINTS = {
2
+ AUTH: {
3
+ REGISTER: '/auth/register',
4
+ LOGIN: '/auth/login',
5
+ LOGOUT: '/auth/logout',
6
+ REFRESH: '/auth/refresh',
7
+ ME: '/auth/me',
8
+ VERIFY_EMAIL: '/auth/verify-email',
9
+ RESEND_VERIFICATION: '/auth/resend-verification',
10
+ REQUEST_PASSWORD_RESET: '/auth/request-password-reset',
11
+ RESET_PASSWORD: '/auth/reset-password',
12
+ },
13
+ USERS: {
14
+ ME: '/users/me',
15
+ UPDATE_ME: '/users/me',
16
+ DELETE_ME: '/users/me',
17
+ },
18
+ } as const;
@@ -0,0 +1,12 @@
1
+ export const APP_NAME = process.env.NEXT_PUBLIC_APP_NAME || 'My App';
2
+
3
+ export const PAGINATION = {
4
+ DEFAULT_PAGE: 1,
5
+ DEFAULT_LIMIT: 10,
6
+ MAX_LIMIT: 100,
7
+ } as const;
8
+
9
+ export const USER_ROLES = {
10
+ USER: 'USER',
11
+ ADMIN: 'ADMIN',
12
+ } as const;
@@ -0,0 +1,9 @@
1
+ export const ROUTES = {
2
+ HOME: '/',
3
+ LOGIN: '/login',
4
+ REGISTER: '/register',
5
+ VERIFY_EMAIL: '/verify-email',
6
+ RESET_PASSWORD: '/reset-password',
7
+ DASHBOARD: '/dashboard',
8
+ PROFILE: '/profile',
9
+ } as const;
@@ -0,0 +1,32 @@
1
+ import axios from 'axios';
2
+
3
+ import type { ApiError } from '@/lib/api/api.types';
4
+
5
+ export const getErrorMessage = (error: unknown): string => {
6
+ if (axios.isAxiosError(error)) {
7
+ const apiError = error.response?.data as ApiError;
8
+ if (apiError?.error?.message) {
9
+ return apiError.error.message;
10
+ }
11
+ if (error.code === 'ERR_NETWORK') {
12
+ return 'Network error. Check your connection.';
13
+ }
14
+ return error.message;
15
+ }
16
+ if (error instanceof Error) {
17
+ return error.message;
18
+ }
19
+ return 'An unexpected error occurred';
20
+ };
21
+
22
+ export const getErrorCode = (error: unknown): string | undefined => {
23
+ if (axios.isAxiosError(error)) {
24
+ const data = error.response?.data as ApiError | undefined;
25
+ return data?.error?.code;
26
+ }
27
+ return undefined;
28
+ };
29
+
30
+ export const isErrorCode = (error: unknown, code: string): boolean => {
31
+ return getErrorCode(error) === code;
32
+ };
@@ -0,0 +1,37 @@
1
+ export const formatDate = (date: string | Date): string => {
2
+ const parsed = new Date(date);
3
+ if (isNaN(parsed.getTime())) return 'Invalid date';
4
+ return new Intl.DateTimeFormat('en-US', {
5
+ year: 'numeric',
6
+ month: 'long',
7
+ day: 'numeric',
8
+ }).format(parsed);
9
+ };
10
+
11
+ export const formatRelativeTime = (date: string | Date): string => {
12
+ const then = new Date(date);
13
+ if (isNaN(then.getTime())) return 'Invalid date';
14
+
15
+ const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
16
+ const now = new Date();
17
+ const diffInSeconds = (then.getTime() - now.getTime()) / 1000;
18
+
19
+ const units: { unit: Intl.RelativeTimeFormatUnit; seconds: number }[] = [
20
+ { unit: 'year', seconds: 31536000 },
21
+ { unit: 'month', seconds: 2592000 },
22
+ { unit: 'day', seconds: 86400 },
23
+ { unit: 'hour', seconds: 3600 },
24
+ { unit: 'minute', seconds: 60 },
25
+ ];
26
+
27
+ for (const { unit, seconds } of units) {
28
+ if (Math.abs(diffInSeconds) >= seconds) {
29
+ return rtf.format(Math.round(diffInSeconds / seconds), unit);
30
+ }
31
+ }
32
+ return rtf.format(Math.round(diffInSeconds), 'second');
33
+ };
34
+
35
+ export const truncate = (str: string, length: number): string => {
36
+ return str.length > length ? `${str.substring(0, length)}...` : str;
37
+ };
@@ -0,0 +1,34 @@
1
+ export const isSafeUrl = (url: string): boolean => {
2
+ try {
3
+ const parsed = new URL(url);
4
+ return ['http:', 'https:', 'mailto:'].includes(parsed.protocol);
5
+ } catch {
6
+ return false;
7
+ }
8
+ };
9
+
10
+ export const sanitizeString = (input: string): string => {
11
+ return input
12
+ .trim()
13
+ .replace(/[<>&"']/g, (char) => {
14
+ const entities: Record<string, string> = {
15
+ '<': '&lt;',
16
+ '>': '&gt;',
17
+ '&': '&amp;',
18
+ '"': '&quot;',
19
+ "'": '&#39;',
20
+ };
21
+ return entities[char] ?? char;
22
+ })
23
+ .slice(0, 1000);
24
+ };
25
+
26
+ export const sanitizeEmail = (email: string): string => {
27
+ return email.toLowerCase().trim().slice(0, 255);
28
+ };
29
+
30
+ export const maskEmail = (email: string): string => {
31
+ const [local, domain] = email.split('@');
32
+ if (!local || !domain) return email;
33
+ return `${local[0]}***${local[local.length - 1]}@${domain}`;
34
+ };
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]): string {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,57 @@
1
+ import { NextResponse } from 'next/server';
2
+
3
+ import type { NextRequest } from 'next/server';
4
+
5
+ const protectedPaths = ['/', '/dashboard', '/profile', '/admin'];
6
+ const authPaths = ['/login', '/register'];
7
+
8
+ function isTokenExpired(token: string): boolean {
9
+ try {
10
+ const [, payload] = token.split('.');
11
+ const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
12
+ return !decoded.exp || decoded.exp * 1000 < Date.now();
13
+ } catch {
14
+ return true;
15
+ }
16
+ }
17
+
18
+ export function middleware(request: NextRequest): NextResponse {
19
+ const { pathname } = request.nextUrl;
20
+ const token = request.cookies.get('access_token')?.value;
21
+
22
+ const isProtectedPath = protectedPaths.some((path) =>
23
+ path === '/' ? pathname === '/' : pathname.startsWith(path)
24
+ );
25
+
26
+ if (isProtectedPath && (!token || isTokenExpired(token))) {
27
+ const loginUrl = new URL('/login', request.url);
28
+ loginUrl.searchParams.set('from', pathname);
29
+ return NextResponse.redirect(loginUrl);
30
+ }
31
+
32
+ const isAuthPath = authPaths.some((path) => pathname.startsWith(path));
33
+
34
+ if (isAuthPath && token) {
35
+ if (isTokenExpired(token)) {
36
+ // Only delete the expired access token — keep the refresh token
37
+ // so the client-side interceptor can still recover the session
38
+ const response = NextResponse.next();
39
+ response.cookies.delete('access_token');
40
+ return response;
41
+ }
42
+ return NextResponse.redirect(new URL('/', request.url));
43
+ }
44
+
45
+ return NextResponse.next();
46
+ }
47
+
48
+ export const config = {
49
+ matcher: [
50
+ '/',
51
+ '/dashboard/:path*',
52
+ '/profile/:path*',
53
+ '/admin/:path*',
54
+ '/login',
55
+ '/register',
56
+ ],
57
+ };
@@ -0,0 +1,7 @@
1
+ import { useDispatch, useSelector } from 'react-redux';
2
+
3
+ import type { TypedUseSelectorHook } from 'react-redux';
4
+ import type { RootState, AppDispatch } from './index';
5
+
6
+ export const useAppDispatch: () => AppDispatch = useDispatch;
7
+ export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
@@ -0,0 +1,12 @@
1
+ import { configureStore } from '@reduxjs/toolkit';
2
+
3
+ import authReducer from '@/features/auth/store/authSlice';
4
+
5
+ export const store = configureStore({
6
+ reducer: {
7
+ auth: authReducer,
8
+ },
9
+ });
10
+
11
+ export type RootState = ReturnType<typeof store.getState>;
12
+ export type AppDispatch = typeof store.dispatch;
@@ -0,0 +1,3 @@
1
+ export type Nullable<T> = T | null;
2
+ export type Optional<T> = T | undefined;
3
+ export type AsyncResult<T> = Promise<T>;