create-tigra 3.0.5 → 3.1.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 (85) hide show
  1. package/README.md +4 -2
  2. package/bin/create-tigra.js +5 -19
  3. package/modules/email-verification/client/hooks/useVerification.ts +3 -3
  4. package/package.json +1 -1
  5. package/template/.agents/skills/security-audit/AI-AND-LLM.md +83 -0
  6. package/template/.agents/skills/security-audit/ATTACK-CLASSES.md +130 -0
  7. package/template/.agents/skills/security-audit/CLIENT-SIDE.md +83 -0
  8. package/template/.agents/skills/security-audit/CLOUD-AND-DEPLOYMENT.md +86 -0
  9. package/template/.agents/skills/security-audit/DATA-ISOLATION-AND-LIFECYCLE.md +84 -0
  10. package/template/.agents/skills/security-audit/DESKTOP-MOBILE-AND-LOCAL-IPC.md +89 -0
  11. package/template/.agents/skills/security-audit/HUNTING.md +251 -0
  12. package/template/.agents/skills/security-audit/LICENSE +21 -0
  13. package/template/.agents/skills/security-audit/MEMORY-SAFETY-AND-BINARY.md +101 -0
  14. package/template/.agents/skills/security-audit/PROTOCOLS-RPC-AND-MESSAGING.md +81 -0
  15. package/template/.agents/skills/security-audit/RECONNAISSANCE.md +156 -0
  16. package/template/.agents/skills/security-audit/RESOURCE-EXHAUSTION-AND-AVAILABILITY.md +78 -0
  17. package/template/.agents/skills/security-audit/SKILL.md +192 -0
  18. package/template/.agents/skills/security-audit/SOURCE.md +5 -0
  19. package/template/.agents/skills/security-audit/SUPPLY-CHAIN-AND-RELEASE.md +73 -0
  20. package/template/.agents/skills/security-audit/VALIDATION-AND-REPORTING.md +186 -0
  21. package/template/.agents/skills/security-audit/WEB-PROTOCOL-AND-AUTH.md +105 -0
  22. package/template/.agents/skills/security-audit/report-schema.json +461 -0
  23. package/template/.agents/skills/security-audit/validate-coverage-ledger.cjs +872 -0
  24. package/template/.agents/skills/security-audit/validate-coverage-ledger.test.cjs +740 -0
  25. package/template/.agents/skills/security-audit/validate-findings.cjs +773 -0
  26. package/template/.agents/skills/security-audit/validate-findings.test.cjs +652 -0
  27. package/template/AGENTS.md +46 -0
  28. package/template/client/AGENTS.md +23 -0
  29. package/template/client/package-lock.json +410 -324
  30. package/template/client/package.json +3 -3
  31. package/template/client/src/app/(auth)/layout.tsx +9 -0
  32. package/template/client/src/app/(auth)/loading.tsx +7 -0
  33. package/template/client/src/app/(main)/layout.tsx +11 -0
  34. package/template/client/src/app/(main)/loading.tsx +7 -0
  35. package/template/client/src/app/globals.css +4 -0
  36. package/template/client/src/app/layout.tsx +9 -2
  37. package/template/client/src/app/loading.tsx +2 -6
  38. package/template/client/src/app/not-found.tsx +2 -3
  39. package/template/client/src/app/providers.tsx +6 -3
  40. package/template/client/src/components/common/AppLink.tsx +84 -0
  41. package/template/client/src/components/common/EmptyState.tsx +2 -2
  42. package/template/client/src/components/common/Pagination.tsx +3 -2
  43. package/template/client/src/components/common/RouteLoadingShell.tsx +21 -0
  44. package/template/client/src/components/common/SmoothNavigationProvider.tsx +151 -0
  45. package/template/client/src/components/layout/Header.tsx +12 -12
  46. package/template/client/src/features/admin/hooks/useAdminSessions.ts +2 -2
  47. package/template/client/src/features/admin/hooks/useAdminUsers.ts +3 -3
  48. package/template/client/src/features/auth/components/AuthInitializer.tsx +3 -2
  49. package/template/client/src/features/auth/components/LoginForm.tsx +3 -3
  50. package/template/client/src/features/auth/components/RegisterForm.tsx +3 -3
  51. package/template/client/src/features/auth/hooks/useAuth.ts +2 -2
  52. package/template/client/src/features/auth/hooks/usePasswordReset.ts +2 -2
  53. package/template/client/src/hooks/useAppRouter.ts +40 -0
  54. package/template/client/src/styles/fonts/inter-jetbrains.css +4 -2
  55. package/template/client/src/styles/themes/default.css +1 -1
  56. package/template/gitignore +0 -6
  57. package/template/server/AGENTS.md +28 -0
  58. package/template/server/package-lock.json +671 -522
  59. package/template/server/package.json +8 -8
  60. package/template/_claude/QUICK_REFERENCE.md +0 -193
  61. package/template/_claude/README.md +0 -53
  62. package/template/_claude/commands/create-client.md +0 -878
  63. package/template/_claude/commands/create-server.md +0 -388
  64. package/template/_claude/hooks/restrict-paths.sh +0 -51
  65. package/template/_claude/rules/client/01-project-structure.md +0 -147
  66. package/template/_claude/rules/client/02-components-and-types.md +0 -146
  67. package/template/_claude/rules/client/03-data-and-state.md +0 -195
  68. package/template/_claude/rules/client/04-design-system.md +0 -408
  69. package/template/_claude/rules/client/05-security.md +0 -55
  70. package/template/_claude/rules/client/06-ux-checklist.md +0 -111
  71. package/template/_claude/rules/client/07-deployment.md +0 -99
  72. package/template/_claude/rules/client/08-lockfile-cross-platform.md +0 -79
  73. package/template/_claude/rules/client/core.md +0 -46
  74. package/template/_claude/rules/global/completion-reports.md +0 -178
  75. package/template/_claude/rules/global/core.md +0 -104
  76. package/template/_claude/rules/global/investigation-before-conclusions.md +0 -57
  77. package/template/_claude/rules/server/core.md +0 -52
  78. package/template/_claude/rules/server/database.md +0 -124
  79. package/template/_claude/rules/server/deployment.md +0 -78
  80. package/template/_claude/rules/server/project-conventions.md +0 -254
  81. package/template/_claude/rules/server/response-handling.md +0 -144
  82. package/template/_claude/settings.json +0 -15
  83. package/template/_claude/skills/clean-ui/SKILL.md +0 -63
  84. package/template/_claude/skills/role/SKILL.md +0 -39
  85. package/template/_claude/skills/theme/SKILL.md +0 -109
@@ -1,878 +0,0 @@
1
- # Create Client Project
2
-
3
- You are scaffolding a new Next.js App Router + TypeScript client project. The project name is: **$ARGUMENTS**
4
-
5
- If no project name is provided, ask the user for one before proceeding.
6
-
7
- ---
8
-
9
- ## Instructions
10
-
11
- Create a complete, production-ready Next.js client project following the architecture and rules defined in `.claude/rules/client/`. Read those rules before generating any code.
12
-
13
- ### Step 1: Create Next.js Project
14
-
15
- Run:
16
- ```bash
17
- npx create-next-app@latest $ARGUMENTS --typescript --tailwind --eslint --app --src-dir --import-alias "@/*" --no-turbopack
18
- ```
19
-
20
- Then install additional dependencies:
21
- ```bash
22
- # State & Data
23
- npm install @reduxjs/toolkit react-redux @tanstack/react-query axios
24
-
25
- # Dev tools
26
- npm install -D @tanstack/react-query-devtools
27
-
28
- # Forms & Validation
29
- npm install react-hook-form @hookform/resolvers zod
30
-
31
- # UI
32
- npm install sonner next-themes lucide-react class-variance-authority clsx tailwind-merge tailwindcss-animate
33
-
34
- # Init shadcn/ui
35
- npx shadcn@latest init --defaults
36
- ```
37
-
38
- Then install core shadcn components:
39
- ```bash
40
- npx shadcn@latest add button card input label skeleton alert-dialog select table badge
41
- ```
42
-
43
- ---
44
-
45
- ### Step 2: Project Structure
46
-
47
- Create the following directory structure under `src/`:
48
-
49
- ```
50
- src/
51
- ├── app/
52
- │ ├── layout.tsx # Root layout
53
- │ ├── page.tsx # Home page
54
- │ ├── providers.tsx # Client providers (Redux, React Query, Theme)
55
- │ ├── globals.css # Global styles with CSS variables
56
- │ ├── loading.tsx # Global loading
57
- │ ├── error.tsx # Global error boundary
58
- │ ├── not-found.tsx # 404 page
59
- │ ├── (auth)/
60
- │ │ ├── layout.tsx # Auth layout (centered, minimal)
61
- │ │ ├── login/
62
- │ │ │ └── page.tsx # Login page
63
- │ │ └── register/
64
- │ │ └── page.tsx # Register page
65
- │ └── (main)/
66
- │ ├── layout.tsx # Main layout with Header/Footer
67
- │ └── dashboard/
68
- │ └── page.tsx # Dashboard page (protected)
69
- ├── components/
70
- │ ├── ui/ # shadcn/ui components (auto-generated)
71
- │ ├── layout/
72
- │ │ ├── Header.tsx
73
- │ │ ├── Footer.tsx
74
- │ │ └── MainLayout.tsx
75
- │ └── common/
76
- │ ├── LoadingSpinner.tsx
77
- │ ├── EmptyState.tsx
78
- │ ├── Pagination.tsx
79
- │ └── ConfirmDialog.tsx
80
- ├── features/
81
- │ └── auth/
82
- │ ├── components/
83
- │ │ ├── LoginForm.tsx
84
- │ │ └── RegisterForm.tsx
85
- │ ├── hooks/
86
- │ │ └── useAuth.ts
87
- │ ├── services/
88
- │ │ └── auth.service.ts
89
- │ ├── store/
90
- │ │ └── authSlice.ts
91
- │ └── types/
92
- │ └── auth.types.ts
93
- ├── hooks/
94
- │ ├── useDebounce.ts
95
- │ ├── useLocalStorage.ts
96
- │ └── useMediaQuery.ts
97
- ├── lib/
98
- │ ├── api/
99
- │ │ ├── axios.config.ts
100
- │ │ └── api.types.ts
101
- │ ├── constants/
102
- │ │ ├── routes.ts
103
- │ │ ├── api-endpoints.ts
104
- │ │ └── app.constants.ts
105
- │ ├── utils/
106
- │ │ ├── format.ts
107
- │ │ ├── error.ts
108
- │ │ └── security.ts
109
- │ └── utils.ts # cn() helper
110
- ├── store/
111
- │ ├── index.ts
112
- │ └── hooks.ts
113
- ├── types/
114
- │ └── index.ts
115
- └── middleware.ts
116
- ```
117
-
118
- ---
119
-
120
- ### Step 3: Configuration Files
121
-
122
- #### `tailwind.config.ts`
123
- Set up the full Tailwind config as specified in `04-design-system.md`:
124
- - darkMode: "class"
125
- - Content paths include src/features/**
126
- - Extend colors with ALL semantic tokens: primary, secondary, destructive, muted, accent, popover, card, border, input, ring, brand (primary, secondary, accent), success, warning, info
127
- - Container config: centered, 2rem padding, 1400px max
128
- - Include tailwindcss-animate plugin
129
-
130
- #### `src/app/globals.css`
131
- Implement the FULL CSS variable system from `04-design-system.md`:
132
- - `:root` with ALL light mode variables (background, foreground, primary, secondary, muted, accent, destructive, popover, card, border, input, ring, radius, brand colors, semantic colors: success, warning, info)
133
- - `.dark` with ALL dark mode variables
134
- - Base layer: `border-border` on all elements, `bg-background text-foreground` on body
135
-
136
- #### `next.config.ts`
137
- - Add security headers from `05-security.md`: X-Frame-Options, X-Content-Type-Options, Referrer-Policy, X-XSS-Protection
138
- - Configure images.remotePatterns if needed
139
-
140
- #### `.env.example` and `.env.local`
141
- ```env
142
- NEXT_PUBLIC_API_BASE_URL=http://localhost:3000/api/v1
143
- NEXT_PUBLIC_APP_NAME=$ARGUMENTS
144
- ```
145
-
146
- ---
147
-
148
- ### Step 4: Core Library Files
149
-
150
- #### `src/lib/utils.ts`
151
- ```typescript
152
- import { type ClassValue, clsx } from "clsx";
153
- import { twMerge } from "tailwind-merge";
154
-
155
- export function cn(...inputs: ClassValue[]): string {
156
- return twMerge(clsx(inputs));
157
- }
158
- ```
159
-
160
- #### `src/lib/api/api.types.ts`
161
- Define ALL API response types from `02-components-and-types.md`:
162
- - `ApiResponse<T>` — `{ success: true, message: string, data: T }`
163
- - `PaginatedApiResponse<T>` — with items array and pagination object (page, limit, totalItems, totalPages, hasNextPage, hasPreviousPage)
164
- - `ApiError` — `{ success: false, error: { code: string, message: string } }`
165
- - `PaginationParams` — `{ page?: number, limit?: number }`
166
-
167
- #### `src/lib/api/axios.config.ts`
168
- Implement the FULL Axios config from `03-data-and-state.md`:
169
- - Base URL from env
170
- - Request interceptor: attach Bearer token from Redux store (client-side only with `typeof window` check)
171
- - Response interceptor: handle 401 with token refresh flow, dispatch logout on failure, redirect to `/login`
172
- - 30s timeout
173
-
174
- #### `src/lib/constants/api-endpoints.ts`
175
- ```typescript
176
- export const API_ENDPOINTS = {
177
- AUTH: {
178
- REGISTER: '/auth/register',
179
- LOGIN: '/auth/login',
180
- LOGOUT: '/auth/logout',
181
- REFRESH: '/auth/refresh',
182
- ME: '/auth/me',
183
- REQUEST_PASSWORD_RESET: '/auth/request-password-reset',
184
- RESET_PASSWORD: '/auth/reset-password',
185
- },
186
- USERS: {
187
- ME: '/users/me',
188
- UPDATE_ME: '/users/me',
189
- DELETE_ME: '/users/me',
190
- },
191
- } as const;
192
- ```
193
-
194
- #### `src/lib/constants/routes.ts`
195
- ```typescript
196
- export const ROUTES = {
197
- HOME: '/',
198
- LOGIN: '/login',
199
- REGISTER: '/register',
200
- RESET_PASSWORD: '/reset-password',
201
- DASHBOARD: '/dashboard',
202
- PROFILE: '/profile',
203
- } as const;
204
- ```
205
-
206
- #### `src/lib/constants/app.constants.ts`
207
- ```typescript
208
- export const APP_NAME = process.env.NEXT_PUBLIC_APP_NAME || '$ARGUMENTS';
209
-
210
- export const PAGINATION = {
211
- DEFAULT_PAGE: 1,
212
- DEFAULT_LIMIT: 10,
213
- MAX_LIMIT: 100,
214
- } as const;
215
-
216
- export const USER_ROLES = {
217
- USER: 'USER',
218
- COMPANY: 'COMPANY',
219
- ADMIN: 'ADMIN',
220
- GUIDE: 'GUIDE',
221
- DRIVER: 'DRIVER',
222
- } as const;
223
- ```
224
-
225
- #### `src/lib/utils/error.ts`
226
- Implement the error utility from `03-data-and-state.md`:
227
- ```typescript
228
- import axios from 'axios';
229
- import type { ApiError } from '@/lib/api/api.types';
230
-
231
- export const getErrorMessage = (error: unknown): string => {
232
- if (axios.isAxiosError(error)) {
233
- const apiError = error.response?.data as ApiError;
234
- if (apiError?.error?.message) {
235
- return apiError.error.message;
236
- }
237
- if (error.code === 'ERR_NETWORK') {
238
- return 'Network error. Check your connection.';
239
- }
240
- return error.message;
241
- }
242
- if (error instanceof Error) {
243
- return error.message;
244
- }
245
- return 'An unexpected error occurred';
246
- };
247
-
248
- export const getErrorCode = (error: unknown): string | undefined => {
249
- if (axios.isAxiosError(error)) {
250
- return error.response?.data?.error?.code;
251
- }
252
- return undefined;
253
- };
254
-
255
- export const isErrorCode = (error: unknown, code: string): boolean => {
256
- return getErrorCode(error) === code;
257
- };
258
- ```
259
-
260
- #### `src/lib/utils/format.ts`
261
- ```typescript
262
- export const formatCurrency = (amount: number, currency = 'GEL'): string => {
263
- return new Intl.NumberFormat('en-US', {
264
- style: 'currency',
265
- currency,
266
- minimumFractionDigits: 0,
267
- maximumFractionDigits: 2,
268
- }).format(amount);
269
- };
270
-
271
- export const formatDate = (date: string | Date): string => {
272
- return new Intl.DateTimeFormat('en-US', {
273
- year: 'numeric',
274
- month: 'long',
275
- day: 'numeric',
276
- }).format(new Date(date));
277
- };
278
-
279
- export const formatRelativeTime = (date: string | Date): string => {
280
- const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
281
- const now = new Date();
282
- const then = new Date(date);
283
- const diffInSeconds = (then.getTime() - now.getTime()) / 1000;
284
-
285
- const units: { unit: Intl.RelativeTimeFormatUnit; seconds: number }[] = [
286
- { unit: 'year', seconds: 31536000 },
287
- { unit: 'month', seconds: 2592000 },
288
- { unit: 'day', seconds: 86400 },
289
- { unit: 'hour', seconds: 3600 },
290
- { unit: 'minute', seconds: 60 },
291
- ];
292
-
293
- for (const { unit, seconds } of units) {
294
- if (Math.abs(diffInSeconds) >= seconds) {
295
- return rtf.format(Math.round(diffInSeconds / seconds), unit);
296
- }
297
- }
298
- return rtf.format(Math.round(diffInSeconds), 'second');
299
- };
300
-
301
- export const truncate = (str: string, length: number): string => {
302
- return str.length > length ? `${str.substring(0, length)}...` : str;
303
- };
304
- ```
305
-
306
- #### `src/lib/utils/security.ts`
307
- Implement from `05-security.md`:
308
- ```typescript
309
- export const isSafeUrl = (url: string): boolean => {
310
- try {
311
- const parsed = new URL(url);
312
- return ['http:', 'https:', 'mailto:'].includes(parsed.protocol);
313
- } catch {
314
- return false;
315
- }
316
- };
317
-
318
- export const sanitizeString = (input: string): string => {
319
- return input.trim().replace(/[<>]/g, '').slice(0, 1000);
320
- };
321
-
322
- export const sanitizeEmail = (email: string): string => {
323
- return email.toLowerCase().trim().slice(0, 255);
324
- };
325
-
326
- export const maskEmail = (email: string): string => {
327
- const [local, domain] = email.split('@');
328
- if (!local || !domain) return email;
329
- return `${local[0]}***${local[local.length - 1]}@${domain}`;
330
- };
331
- ```
332
-
333
- ---
334
-
335
- ### Step 5: State Management
336
-
337
- #### `src/store/index.ts`
338
- Redux store setup from `03-data-and-state.md`:
339
- - Configure store with auth reducer
340
- - Load auth state from localStorage (with SSR guard: `typeof window !== 'undefined'`)
341
- - Subscribe to save auth state to localStorage (with SSR guard)
342
- - Export `RootState` and `AppDispatch` types
343
-
344
- #### `src/store/hooks.ts`
345
- Typed Redux hooks:
346
- ```typescript
347
- import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
348
- import type { RootState, AppDispatch } from './index';
349
-
350
- export const useAppDispatch: () => AppDispatch = useDispatch;
351
- export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
352
- ```
353
-
354
- #### `src/features/auth/store/authSlice.ts`
355
- Auth slice from `03-data-and-state.md`:
356
- - State: `{ user: IUser | null, tokens: IAuthTokens | null, isAuthenticated: boolean }`
357
- - Reducers: `setCredentials`, `updateTokens`, `logout`
358
-
359
- #### `src/features/auth/types/auth.types.ts`
360
- Auth types from `02-components-and-types.md`:
361
- - `IUser` interface
362
- - `UserRole` type (`'USER' | 'COMPANY' | 'ADMIN' | 'GUIDE' | 'DRIVER'`)
363
- - `IAuthTokens` interface
364
- - `ILoginRequest`, `IRegisterRequest` interfaces
365
- - `IAuthState` interface
366
-
367
- #### `src/features/auth/services/auth.service.ts`
368
- Auth service class from `03-data-and-state.md`:
369
- - `register`, `login`, `logout`, `refreshToken`, `getMe`, `requestPasswordReset`, `resetPassword`
370
- - Uses `apiClient` and `API_ENDPOINTS`
371
- - Singleton export: `export const authService = new AuthService();`
372
-
373
- #### `src/features/auth/hooks/useAuth.ts`
374
- ```typescript
375
- 'use client';
376
-
377
- import { useAppDispatch, useAppSelector } from '@/store/hooks';
378
- import { useMutation } from '@tanstack/react-query';
379
- import { authService } from '../services/auth.service';
380
- import { setCredentials, logout as logoutAction } from '../store/authSlice';
381
- import { useRouter } from 'next/navigation';
382
-
383
- export const useAuth = () => {
384
- const dispatch = useAppDispatch();
385
- const router = useRouter();
386
- const { user, isAuthenticated, tokens } = useAppSelector((state) => state.auth);
387
-
388
- const loginMutation = useMutation({
389
- mutationFn: authService.login,
390
- onSuccess: (data) => {
391
- dispatch(setCredentials(data));
392
- router.push('/dashboard');
393
- },
394
- });
395
-
396
- const logout = async (): Promise<void> => {
397
- try {
398
- await authService.logout();
399
- } finally {
400
- dispatch(logoutAction());
401
- if (typeof window !== 'undefined') {
402
- localStorage.removeItem('auth');
403
- sessionStorage.clear();
404
- }
405
- router.push('/login');
406
- }
407
- };
408
-
409
- return {
410
- user,
411
- isAuthenticated,
412
- login: loginMutation.mutate,
413
- logout,
414
- isLoggingIn: loginMutation.isPending,
415
- };
416
- };
417
- ```
418
-
419
- ---
420
-
421
- ### Step 6: Providers & Layout
422
-
423
- #### `src/app/providers.tsx`
424
- ```typescript
425
- 'use client';
426
-
427
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
428
- import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
429
- import { Provider as ReduxProvider } from 'react-redux';
430
- import { ThemeProvider } from 'next-themes';
431
- import { Toaster } from 'sonner';
432
- import { useState } from 'react';
433
- import { store } from '@/store';
434
-
435
- export function Providers({ children }: { children: React.ReactNode }): React.ReactElement {
436
- const [queryClient] = useState(
437
- () =>
438
- new QueryClient({
439
- defaultOptions: {
440
- queries: {
441
- staleTime: 5 * 60 * 1000,
442
- gcTime: 10 * 60 * 1000,
443
- refetchOnWindowFocus: false,
444
- retry: 1,
445
- },
446
- },
447
- })
448
- );
449
-
450
- return (
451
- <ReduxProvider store={store}>
452
- <QueryClientProvider client={queryClient}>
453
- <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
454
- {children}
455
- <Toaster position="top-right" richColors />
456
- </ThemeProvider>
457
- <ReactQueryDevtools initialIsOpen={false} />
458
- </QueryClientProvider>
459
- </ReduxProvider>
460
- );
461
- }
462
- ```
463
-
464
- #### `src/app/layout.tsx`
465
- Root layout:
466
- - Import and use `next/font/google` (fonts defined by the active font preset — default: Inter + JetBrains Mono)
467
- - `<html lang="en" suppressHydrationWarning>`
468
- - Wrap children in `<Providers>`
469
- - Default metadata with app name
470
-
471
- #### `src/app/(main)/layout.tsx`
472
- Main layout with Header and Footer wrapping children in a flex column min-h-screen structure.
473
-
474
- #### `src/app/(auth)/layout.tsx`
475
- Auth layout: centered, minimal — flex items-center justify-center min-h-screen.
476
-
477
- ---
478
-
479
- ### Step 7: Common Components
480
-
481
- #### `src/components/layout/Header.tsx`
482
- Client component with:
483
- - App name / logo link to home
484
- - Navigation links
485
- - Auth-aware: show Login/Register or user menu based on auth state
486
- - Theme toggle button (Sun/Moon icons with `next-themes` `useTheme`)
487
- - Responsive (mobile menu)
488
-
489
- #### `src/components/layout/Footer.tsx`
490
- Server component, simple footer with copyright.
491
-
492
- #### `src/components/common/LoadingSpinner.tsx`
493
- Using Lucide `Loader2` icon with spin animation. Accept `size` prop.
494
-
495
- #### `src/components/common/EmptyState.tsx`
496
- ```typescript
497
- import { FileQuestion } from 'lucide-react';
498
- import { Button } from '@/components/ui/button';
499
- import Link from 'next/link';
500
-
501
- interface EmptyStateProps {
502
- title: string;
503
- description: string;
504
- actionLabel?: string;
505
- actionHref?: string;
506
- }
507
-
508
- export const EmptyState = ({
509
- title,
510
- description,
511
- actionLabel,
512
- actionHref,
513
- }: EmptyStateProps): React.ReactElement => (
514
- <div className="flex min-h-[400px] flex-col items-center justify-center p-8 text-center">
515
- <FileQuestion className="mb-4 h-16 w-16 text-muted-foreground" />
516
- <h3 className="mb-2 text-xl font-semibold">{title}</h3>
517
- <p className="mb-4 text-muted-foreground">{description}</p>
518
- {actionLabel && actionHref && (
519
- <Button asChild>
520
- <Link href={actionHref}>{actionLabel}</Link>
521
- </Button>
522
- )}
523
- </div>
524
- );
525
- ```
526
-
527
- #### `src/components/common/Pagination.tsx`
528
- ```typescript
529
- 'use client';
530
-
531
- import { useRouter, useSearchParams, usePathname } from 'next/navigation';
532
- import { Button } from '@/components/ui/button';
533
- import { ChevronLeft, ChevronRight } from 'lucide-react';
534
- import { useCallback } from 'react';
535
-
536
- interface PaginationProps {
537
- page: number;
538
- totalPages: number;
539
- }
540
-
541
- export const Pagination = ({ page, totalPages }: PaginationProps): React.ReactElement => {
542
- const router = useRouter();
543
- const pathname = usePathname();
544
- const searchParams = useSearchParams();
545
-
546
- const createPageUrl = useCallback(
547
- (pageNumber: number): string => {
548
- const params = new URLSearchParams(searchParams.toString());
549
- params.set('page', pageNumber.toString());
550
- return `${pathname}?${params.toString()}`;
551
- },
552
- [pathname, searchParams]
553
- );
554
-
555
- return (
556
- <div className="flex items-center justify-center gap-2">
557
- <Button
558
- variant="outline"
559
- size="sm"
560
- onClick={() => router.push(createPageUrl(page - 1))}
561
- disabled={page <= 1}
562
- aria-label="Previous page"
563
- >
564
- <ChevronLeft className="h-4 w-4" />
565
- </Button>
566
- <span className="text-sm text-muted-foreground">
567
- Page {page} of {totalPages}
568
- </span>
569
- <Button
570
- variant="outline"
571
- size="sm"
572
- onClick={() => router.push(createPageUrl(page + 1))}
573
- disabled={page >= totalPages}
574
- aria-label="Next page"
575
- >
576
- <ChevronRight className="h-4 w-4" />
577
- </Button>
578
- </div>
579
- );
580
- };
581
- ```
582
-
583
- #### `src/components/common/ConfirmDialog.tsx`
584
- ```typescript
585
- 'use client';
586
-
587
- import {
588
- AlertDialog,
589
- AlertDialogAction,
590
- AlertDialogCancel,
591
- AlertDialogContent,
592
- AlertDialogDescription,
593
- AlertDialogFooter,
594
- AlertDialogHeader,
595
- AlertDialogTitle,
596
- } from '@/components/ui/alert-dialog';
597
-
598
- interface ConfirmDialogProps {
599
- open: boolean;
600
- onOpenChange: (open: boolean) => void;
601
- onConfirm: () => void;
602
- title: string;
603
- description: string;
604
- confirmLabel?: string;
605
- isDestructive?: boolean;
606
- }
607
-
608
- export const ConfirmDialog = ({
609
- open,
610
- onOpenChange,
611
- onConfirm,
612
- title,
613
- description,
614
- confirmLabel = 'Confirm',
615
- isDestructive = false,
616
- }: ConfirmDialogProps): React.ReactElement => (
617
- <AlertDialog open={open} onOpenChange={onOpenChange}>
618
- <AlertDialogContent>
619
- <AlertDialogHeader>
620
- <AlertDialogTitle>{title}</AlertDialogTitle>
621
- <AlertDialogDescription>{description}</AlertDialogDescription>
622
- </AlertDialogHeader>
623
- <AlertDialogFooter>
624
- <AlertDialogCancel>Cancel</AlertDialogCancel>
625
- <AlertDialogAction
626
- onClick={onConfirm}
627
- className={isDestructive ? 'bg-destructive text-destructive-foreground hover:bg-destructive/90' : ''}
628
- >
629
- {confirmLabel}
630
- </AlertDialogAction>
631
- </AlertDialogFooter>
632
- </AlertDialogContent>
633
- </AlertDialog>
634
- );
635
- ```
636
-
637
- ---
638
-
639
- ### Step 8: Global Custom Hooks
640
-
641
- #### `src/hooks/useDebounce.ts`
642
- ```typescript
643
- 'use client';
644
-
645
- import { useState, useEffect } from 'react';
646
-
647
- export const useDebounce = <T,>(value: T, delay = 500): T => {
648
- const [debouncedValue, setDebouncedValue] = useState(value);
649
-
650
- useEffect(() => {
651
- const timer = setTimeout(() => setDebouncedValue(value), delay);
652
- return () => clearTimeout(timer);
653
- }, [value, delay]);
654
-
655
- return debouncedValue;
656
- };
657
- ```
658
-
659
- #### `src/hooks/useLocalStorage.ts`
660
- ```typescript
661
- 'use client';
662
-
663
- import { useState, useEffect } from 'react';
664
-
665
- export const useLocalStorage = <T,>(key: string, initialValue: T) => {
666
- const [storedValue, setStoredValue] = useState<T>(initialValue);
667
- const [isHydrated, setIsHydrated] = useState(false);
668
-
669
- useEffect(() => {
670
- try {
671
- const item = window.localStorage.getItem(key);
672
- if (item) {
673
- setStoredValue(JSON.parse(item));
674
- }
675
- } catch {
676
- // Ignore errors
677
- }
678
- setIsHydrated(true);
679
- }, [key]);
680
-
681
- const setValue = (value: T | ((val: T) => T)): void => {
682
- try {
683
- const valueToStore = value instanceof Function ? value(storedValue) : value;
684
- setStoredValue(valueToStore);
685
- window.localStorage.setItem(key, JSON.stringify(valueToStore));
686
- } catch {
687
- // Ignore write errors
688
- }
689
- };
690
-
691
- return [storedValue, setValue, isHydrated] as const;
692
- };
693
- ```
694
-
695
- #### `src/hooks/useMediaQuery.ts`
696
- ```typescript
697
- 'use client';
698
-
699
- import { useState, useEffect } from 'react';
700
-
701
- export const useMediaQuery = (query: string): boolean => {
702
- const [matches, setMatches] = useState(false);
703
-
704
- useEffect(() => {
705
- const media = window.matchMedia(query);
706
- setMatches(media.matches);
707
-
708
- const listener = (e: MediaQueryListEvent): void => setMatches(e.matches);
709
- media.addEventListener('change', listener);
710
- return () => media.removeEventListener('change', listener);
711
- }, [query]);
712
-
713
- return matches;
714
- };
715
-
716
- export const useIsMobile = (): boolean => useMediaQuery('(max-width: 768px)');
717
- export const useIsTablet = (): boolean => useMediaQuery('(max-width: 1024px)');
718
- ```
719
-
720
- ---
721
-
722
- ### Step 9: Auth Pages
723
-
724
- #### `src/app/(auth)/login/page.tsx`
725
- Server component page that renders `LoginForm`.
726
-
727
- #### `src/app/(auth)/register/page.tsx`
728
- Server component page that renders `RegisterForm`.
729
-
730
- #### `src/features/auth/components/LoginForm.tsx`
731
- Client component:
732
- - React Hook Form + Zod validation (email + password)
733
- - Submit button with loading state ("Signing in..." / "Sign in")
734
- - Link to register page
735
- - Uses `useAuth` hook for login
736
- - Field-level error messages below each field (red text + red border)
737
-
738
- #### `src/features/auth/components/RegisterForm.tsx`
739
- Client component:
740
- - React Hook Form + Zod validation
741
- - First name, last name, email, password, confirm password
742
- - Password schema: min 8 chars, must contain uppercase, lowercase, and number
743
- - Submit button with loading state
744
- - Link to login page
745
-
746
- ---
747
-
748
- ### Step 10: Dashboard & Home Pages
749
-
750
- #### `src/app/page.tsx`
751
- Simple home page (Server Component). Welcome message with links to login/register or dashboard.
752
-
753
- #### `src/app/(main)/dashboard/page.tsx`
754
- Simple dashboard page placeholder (Server Component). Shows "Welcome to your dashboard."
755
-
756
- #### `src/app/loading.tsx`
757
- Global loading component with LoadingSpinner.
758
-
759
- #### `src/app/error.tsx`
760
- Client component global error boundary:
761
- ```typescript
762
- 'use client';
763
-
764
- import { useEffect } from 'react';
765
- import { Button } from '@/components/ui/button';
766
- import { AlertCircle } from 'lucide-react';
767
-
768
- export default function GlobalError({
769
- error,
770
- reset,
771
- }: {
772
- error: Error & { digest?: string };
773
- reset: () => void;
774
- }): React.ReactElement {
775
- useEffect(() => {
776
- // Log to error reporting service in production
777
- }, [error]);
778
-
779
- return (
780
- <div className="flex min-h-[400px] flex-col items-center justify-center p-8 text-center">
781
- <AlertCircle className="mb-4 h-12 w-12 text-destructive" />
782
- <h2 className="mb-2 text-2xl font-bold">Something went wrong</h2>
783
- <p className="mb-4 text-muted-foreground">{error.message}</p>
784
- <Button onClick={reset}>Try again</Button>
785
- </div>
786
- );
787
- }
788
- ```
789
-
790
- #### `src/app/not-found.tsx`
791
- Simple 404 page with link to home.
792
-
793
- ---
794
-
795
- ### Step 11: Middleware
796
-
797
- #### `src/middleware.ts`
798
-
799
- > **Note**: The current auth system stores tokens in Redux + localStorage. Next.js middleware runs on the Edge and cannot access localStorage. This middleware checks for an `accessToken` cookie. For full protection, the login flow should also set an httpOnly cookie via a Server Action or API route. As a fallback, this middleware provides basic route protection — full auth verification happens client-side via the axios interceptor.
800
-
801
- ```typescript
802
- import { NextResponse } from 'next/server';
803
- import type { NextRequest } from 'next/server';
804
-
805
- const protectedPaths = ['/dashboard', '/profile', '/my-tours', '/admin'];
806
- const authPaths = ['/login', '/register'];
807
-
808
- export function middleware(request: NextRequest): NextResponse {
809
- const { pathname } = request.nextUrl;
810
- const token = request.cookies.get('accessToken')?.value;
811
-
812
- const isProtectedPath = protectedPaths.some((path) =>
813
- pathname.startsWith(path)
814
- );
815
-
816
- if (isProtectedPath && !token) {
817
- const loginUrl = new URL('/login', request.url);
818
- loginUrl.searchParams.set('from', pathname);
819
- return NextResponse.redirect(loginUrl);
820
- }
821
-
822
- const isAuthPath = authPaths.some((path) => pathname.startsWith(path));
823
-
824
- if (isAuthPath && token) {
825
- return NextResponse.redirect(new URL('/dashboard', request.url));
826
- }
827
-
828
- return NextResponse.next();
829
- }
830
-
831
- export const config = {
832
- matcher: [
833
- '/dashboard/:path*',
834
- '/profile/:path*',
835
- '/my-tours/:path*',
836
- '/admin/:path*',
837
- '/login',
838
- '/register',
839
- ],
840
- };
841
- ```
842
-
843
- ---
844
-
845
- ### Step 12: Global Types
846
-
847
- #### `src/types/index.ts`
848
- Shared utility types:
849
- ```typescript
850
- export type Nullable<T> = T | null;
851
- export type Optional<T> = T | undefined;
852
- export type AsyncResult<T> = Promise<T>;
853
- ```
854
-
855
- ---
856
-
857
- ### Step 13: Final Verification
858
-
859
- After creating all files:
860
- 1. Run `npm run dev` to verify the project starts without errors
861
- 2. Check that the home page renders at `http://localhost:3001` (or the configured port)
862
- 3. Verify no TypeScript errors
863
-
864
- ---
865
-
866
- ## IMPORTANT RULES
867
-
868
- - Follow ALL rules from `.claude/rules/client/` and `.claude/rules/global/`
869
- - Server Components by default — only add `'use client'` when hooks/state/events are needed
870
- - Max 250 lines per component, max 5 props, max 3 JSX nesting levels
871
- - No hardcoded colors — use Tailwind semantic tokens only (`bg-primary`, `text-foreground`)
872
- - No inline styles — Tailwind only with `cn()` for conditional classes
873
- - Import order: React/Next → third-party → UI → local → hooks → services → types → utils
874
- - Forms: validate with Zod client-side AND server-side
875
- - Never prefix secrets with `NEXT_PUBLIC_`
876
- - Use `next/image` for images, `next/link` for navigation
877
- - Pair background with foreground colors (`bg-primary text-primary-foreground`)
878
- - All interactive elements need hover/active/focus-visible states