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.
- package/LICENSE +21 -21
- package/README.md +80 -87
- package/bin/create-tigra.js +259 -308
- package/package.json +49 -41
- package/template/_claude/QUICK_REFERENCE.md +193 -0
- package/template/_claude/README.md +53 -0
- package/template/_claude/commands/create-client.md +881 -0
- package/template/_claude/commands/create-server.md +383 -0
- package/template/_claude/rules/client/01-project-structure.md +133 -0
- package/template/_claude/rules/client/02-components-and-types.md +146 -0
- package/template/_claude/rules/client/03-data-and-state.md +156 -0
- package/template/_claude/rules/client/04-design-system.md +185 -0
- package/template/_claude/rules/client/05-security.md +55 -0
- package/template/_claude/rules/client/06-ux-checklist.md +81 -0
- package/template/_claude/rules/client/core.md +42 -0
- package/template/_claude/rules/global/core.md +77 -0
- package/template/_claude/rules/server/core.md +50 -0
- package/template/_claude/rules/server/database.md +124 -0
- package/template/_claude/rules/server/project-conventions.md +150 -0
- package/template/_claude/rules/server/response-handling.md +144 -0
- package/template/client/.env.example +5 -0
- package/template/client/README.md +36 -0
- package/template/client/components.json +23 -0
- package/template/client/eslint.config.mjs +18 -0
- package/template/client/next.config.ts +34 -0
- package/template/client/package.json +44 -0
- package/template/client/postcss.config.mjs +7 -0
- package/template/client/src/app/(auth)/layout.tsx +18 -0
- package/template/client/src/app/(auth)/login/page.tsx +13 -0
- package/template/client/src/app/(auth)/register/page.tsx +13 -0
- package/template/client/src/app/(main)/dashboard/page.tsx +22 -0
- package/template/client/src/app/(main)/layout.tsx +11 -0
- package/template/client/src/app/error.tsx +27 -0
- package/template/client/src/app/favicon.ico +0 -0
- package/template/client/src/app/globals.css +145 -0
- package/template/client/src/app/layout.tsx +36 -0
- package/template/client/src/app/loading.tsx +11 -0
- package/template/client/src/app/not-found.tsx +23 -0
- package/template/client/src/app/page.tsx +45 -0
- package/template/client/src/app/providers.tsx +43 -0
- package/template/client/src/components/common/ConfirmDialog.tsx +56 -0
- package/template/client/src/components/common/EmptyState.tsx +31 -0
- package/template/client/src/components/common/LoadingSpinner.tsx +30 -0
- package/template/client/src/components/common/Pagination.tsx +55 -0
- package/template/client/src/components/layout/Footer.tsx +17 -0
- package/template/client/src/components/layout/Header.tsx +173 -0
- package/template/client/src/components/layout/MainLayout.tsx +18 -0
- package/template/client/src/components/ui/alert-dialog.tsx +196 -0
- package/template/client/src/components/ui/badge.tsx +48 -0
- package/template/client/src/components/ui/button.tsx +64 -0
- package/template/client/src/components/ui/card.tsx +92 -0
- package/template/client/src/components/ui/input.tsx +21 -0
- package/template/client/src/components/ui/label.tsx +24 -0
- package/template/client/src/components/ui/select.tsx +190 -0
- package/template/client/src/components/ui/skeleton.tsx +13 -0
- package/template/client/src/components/ui/table.tsx +116 -0
- package/template/client/src/features/auth/components/AuthInitializer.tsx +55 -0
- package/template/client/src/features/auth/components/LoginForm.tsx +107 -0
- package/template/client/src/features/auth/components/RegisterForm.tsx +178 -0
- package/template/client/src/features/auth/hooks/useAuth.ts +84 -0
- package/template/client/src/features/auth/services/auth.service.ts +52 -0
- package/template/client/src/features/auth/store/authSlice.ts +38 -0
- package/template/client/src/features/auth/types/auth.types.ts +32 -0
- package/template/client/src/hooks/useDebounce.ts +14 -0
- package/template/client/src/hooks/useLocalStorage.ts +55 -0
- package/template/client/src/hooks/useMediaQuery.ts +27 -0
- package/template/client/src/lib/api/api.types.ts +34 -0
- package/template/client/src/lib/api/axios.config.ts +98 -0
- package/template/client/src/lib/constants/api-endpoints.ts +18 -0
- package/template/client/src/lib/constants/app.constants.ts +12 -0
- package/template/client/src/lib/constants/routes.ts +9 -0
- package/template/client/src/lib/utils/error.ts +32 -0
- package/template/client/src/lib/utils/format.ts +37 -0
- package/template/client/src/lib/utils/security.ts +34 -0
- package/template/client/src/lib/utils.ts +6 -0
- package/template/client/src/middleware.ts +57 -0
- package/template/client/src/store/hooks.ts +7 -0
- package/template/client/src/store/index.ts +12 -0
- package/template/client/src/types/index.ts +3 -0
- package/template/client/tsconfig.json +34 -0
- package/template/gitignore +34 -0
- package/template/server/.dockerignore +66 -0
- package/template/server/.env.example +96 -69
- package/template/server/.env.production.example +90 -0
- package/template/server/Dockerfile +94 -0
- package/template/server/docker-compose.yml +82 -111
- package/template/server/docs/logging.md +62 -0
- package/template/server/eslint.config.mjs +17 -0
- package/template/server/package.json +68 -81
- package/template/server/phpmyadmin-config.php +26 -0
- package/template/server/postman_collection.json +666 -0
- package/template/server/prisma/schema.prisma +77 -93
- package/template/server/prisma/seed.ts +46 -142
- package/template/server/scripts/flush-redis.ts +41 -0
- package/template/server/src/app.ts +243 -71
- package/template/server/src/config/env.ts +67 -94
- package/template/server/src/libs/auth.ts +88 -0
- package/template/server/src/libs/cleanup.ts +35 -0
- package/template/server/src/libs/cookies.ts +46 -0
- package/template/server/src/libs/logger.ts +33 -60
- package/template/server/src/libs/monitoring.ts +205 -0
- package/template/server/src/libs/password.ts +38 -0
- package/template/server/src/libs/prisma.ts +68 -0
- package/template/server/src/libs/redis.ts +60 -79
- package/template/server/src/libs/requestLogger.ts +66 -0
- package/template/server/src/libs/storage/file-storage.service.ts +211 -0
- package/template/server/src/libs/storage/file-validator.ts +97 -0
- package/template/server/src/libs/storage/filename-sanitizer.ts +71 -0
- package/template/server/src/libs/storage/image-optimizer.service.ts +144 -0
- package/template/server/src/modules/auth/__tests__/auth.service.test.ts +365 -0
- package/template/server/src/modules/auth/auth.controller.ts +90 -141
- package/template/server/src/modules/auth/auth.repo.ts +120 -218
- package/template/server/src/modules/auth/auth.routes.ts +96 -83
- package/template/server/src/modules/auth/auth.schemas.ts +35 -137
- package/template/server/src/modules/auth/auth.service.ts +286 -329
- package/template/server/src/modules/auth/session.repo.ts +110 -0
- package/template/server/src/modules/users/users.controller.ts +120 -0
- package/template/server/src/modules/users/users.repo.ts +77 -0
- package/template/server/src/modules/users/users.routes.ts +89 -0
- package/template/server/src/modules/users/users.schemas.ts +21 -0
- package/template/server/src/modules/users/users.service.ts +169 -0
- package/template/server/src/server.ts +58 -139
- package/template/server/src/shared/errors/AppError.ts +21 -0
- package/template/server/src/shared/errors/errors.ts +43 -0
- package/template/server/src/shared/responses/paginatedResponse.ts +38 -0
- package/template/server/src/shared/responses/successResponse.ts +17 -0
- package/template/server/src/shared/schemas/pagination.schema.ts +12 -0
- package/template/server/src/shared/types/index.ts +26 -0
- package/template/server/src/test/setup.ts +74 -38
- package/template/server/tsconfig.json +27 -89
- package/template/server/uploads/avatars/.gitkeep +1 -0
- package/template/server/vitest.config.ts +43 -98
- package/template/.agent/rules/client/01-project-structure.md +0 -326
- package/template/.agent/rules/client/02-component-patterns.md +0 -249
- package/template/.agent/rules/client/03-typescript-rules.md +0 -226
- package/template/.agent/rules/client/04-state-management.md +0 -474
- package/template/.agent/rules/client/05-api-integration.md +0 -129
- package/template/.agent/rules/client/06-forms-validation.md +0 -129
- package/template/.agent/rules/client/07-common-patterns.md +0 -150
- package/template/.agent/rules/client/08-color-system.md +0 -93
- package/template/.agent/rules/client/09-security-rules.md +0 -97
- package/template/.agent/rules/client/10-testing-strategy.md +0 -370
- package/template/.agent/rules/global/ai-edit-safety.md +0 -38
- package/template/.agent/rules/server/01-db-and-migrations.md +0 -242
- package/template/.agent/rules/server/02-general-rules.md +0 -111
- package/template/.agent/rules/server/03-migrations.md +0 -20
- package/template/.agent/rules/server/04-pagination.md +0 -130
- package/template/.agent/rules/server/05-project-conventions.md +0 -71
- package/template/.agent/rules/server/06-response-handling.md +0 -173
- package/template/.agent/rules/server/07-testing-strategy.md +0 -506
- package/template/.agent/rules/server/08-observability.md +0 -180
- package/template/.agent/rules/server/10-background-jobs-v2.md +0 -185
- package/template/.agent/rules/server/11-rate-limiting-v2.md +0 -210
- package/template/.agent/rules/server/12-performance-optimization.md +0 -567
- package/template/.claude/rules/client-01-project-structure.md +0 -327
- package/template/.claude/rules/client-02-component-patterns.md +0 -250
- package/template/.claude/rules/client-03-typescript-rules.md +0 -227
- package/template/.claude/rules/client-04-state-management.md +0 -475
- package/template/.claude/rules/client-05-api-integration.md +0 -130
- package/template/.claude/rules/client-06-forms-validation.md +0 -130
- package/template/.claude/rules/client-07-common-patterns.md +0 -151
- package/template/.claude/rules/client-08-color-system.md +0 -94
- package/template/.claude/rules/client-09-security-rules.md +0 -98
- package/template/.claude/rules/client-10-testing-strategy.md +0 -371
- package/template/.claude/rules/global-ai-edit-safety.md +0 -39
- package/template/.claude/rules/server-01-db-and-migrations.md +0 -243
- package/template/.claude/rules/server-02-general-rules.md +0 -112
- package/template/.claude/rules/server-03-migrations.md +0 -21
- package/template/.claude/rules/server-04-pagination.md +0 -131
- package/template/.claude/rules/server-05-project-conventions.md +0 -72
- package/template/.claude/rules/server-06-response-handling.md +0 -174
- package/template/.claude/rules/server-07-testing-strategy.md +0 -507
- package/template/.claude/rules/server-08-observability.md +0 -181
- package/template/.claude/rules/server-10-background-jobs-v2.md +0 -186
- package/template/.claude/rules/server-11-rate-limiting-v2.md +0 -211
- package/template/.claude/rules/server-12-performance-optimization.md +0 -568
- package/template/.cursor/rules/client-01-project-structure.mdc +0 -327
- package/template/.cursor/rules/client-02-component-patterns.mdc +0 -250
- package/template/.cursor/rules/client-03-typescript-rules.mdc +0 -227
- package/template/.cursor/rules/client-04-state-management.mdc +0 -475
- package/template/.cursor/rules/client-05-api-integration.mdc +0 -130
- package/template/.cursor/rules/client-06-forms-validation.mdc +0 -130
- package/template/.cursor/rules/client-07-common-patterns.mdc +0 -151
- package/template/.cursor/rules/client-08-color-system.mdc +0 -94
- package/template/.cursor/rules/client-09-security-rules.mdc +0 -98
- package/template/.cursor/rules/client-10-testing-strategy.mdc +0 -371
- package/template/.cursor/rules/global-ai-edit-safety.mdc +0 -39
- package/template/.cursor/rules/server-01-db-and-migrations.mdc +0 -243
- package/template/.cursor/rules/server-02-general-rules.mdc +0 -112
- package/template/.cursor/rules/server-03-migrations.mdc +0 -21
- package/template/.cursor/rules/server-04-pagination.mdc +0 -131
- package/template/.cursor/rules/server-05-project-conventions.mdc +0 -72
- package/template/.cursor/rules/server-06-response-handling.mdc +0 -174
- package/template/.cursor/rules/server-07-testing-strategy.mdc +0 -507
- package/template/.cursor/rules/server-08-observability.mdc +0 -181
- package/template/.cursor/rules/server-09-api-documentation-v2.mdc +0 -169
- package/template/.cursor/rules/server-10-background-jobs-v2.mdc +0 -186
- package/template/.cursor/rules/server-11-rate-limiting-v2.mdc +0 -211
- package/template/.cursor/rules/server-12-performance-optimization.mdc +0 -568
- package/template/CLAUDE.md +0 -207
- package/template/server/.tsc-aliasrc.json +0 -13
- package/template/server/IMPORT_FIX_CHECKLIST.md +0 -98
- package/template/server/IMPORT_FIX_COMPLETE.md +0 -89
- package/template/server/README.md +0 -183
- package/template/server/REMAINING_IMPORT_FIXES.md +0 -150
- package/template/server/SECURITY.md +0 -190
- package/template/server/Tigra-API.postman_collection.json +0 -733
- package/template/server/biome.json +0 -42
- package/template/server/scripts/fix-all-imports.ps1 +0 -52
- package/template/server/scripts/fix-imports-reference.ps1 +0 -16
- package/template/server/scripts/fix-imports.mjs +0 -55
- package/template/server/scripts/setup-env.js +0 -50
- package/template/server/scripts/wait-for-db.js +0 -60
- package/template/server/src/hooks/request-timing.hook.ts +0 -26
- package/template/server/src/libs/auth/authenticate.middleware.ts +0 -22
- package/template/server/src/libs/auth/rbac.middleware.test.ts +0 -134
- package/template/server/src/libs/auth/rbac.middleware.ts +0 -147
- package/template/server/src/libs/db.ts +0 -76
- package/template/server/src/libs/error-handler.ts +0 -89
- package/template/server/src/libs/queue.ts +0 -79
- package/template/server/src/modules/admin/admin.controller.ts +0 -122
- package/template/server/src/modules/admin/admin.routes.ts +0 -62
- package/template/server/src/modules/admin/admin.schemas.ts +0 -35
- package/template/server/src/modules/admin/admin.service.ts +0 -167
- package/template/server/src/modules/auth/auth.integration.test.ts +0 -150
- package/template/server/src/modules/auth/auth.service.test.ts +0 -119
- package/template/server/src/modules/auth/auth.types.ts +0 -97
- package/template/server/src/modules/resources/resources.controller.ts +0 -218
- package/template/server/src/modules/resources/resources.repo.ts +0 -253
- package/template/server/src/modules/resources/resources.routes.ts +0 -116
- package/template/server/src/modules/resources/resources.schemas.ts +0 -146
- package/template/server/src/modules/resources/resources.service.ts +0 -218
- package/template/server/src/modules/resources/resources.types.ts +0 -73
- package/template/server/src/plugins/rate-limit.plugin.ts +0 -21
- package/template/server/src/plugins/security.plugin.ts +0 -21
- package/template/server/src/routes/health.routes.ts +0 -31
- package/template/server/src/types/fastify.d.ts +0 -36
- package/template/server/src/utils/errors.ts +0 -108
- package/template/server/src/utils/pagination.ts +0 -120
- package/template/server/src/utils/response.ts +0 -110
- package/template/server/src/workers/file.worker.ts +0 -106
- package/template/server/tsconfig.build.json +0 -30
- package/template/server/tsconfig.test.json +0 -22
|
@@ -0,0 +1,881 @@
|
|
|
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
|
+
VERIFY_EMAIL: '/auth/verify-email',
|
|
184
|
+
RESEND_VERIFICATION: '/auth/resend-verification',
|
|
185
|
+
REQUEST_PASSWORD_RESET: '/auth/request-password-reset',
|
|
186
|
+
RESET_PASSWORD: '/auth/reset-password',
|
|
187
|
+
},
|
|
188
|
+
USERS: {
|
|
189
|
+
ME: '/users/me',
|
|
190
|
+
UPDATE_ME: '/users/me',
|
|
191
|
+
DELETE_ME: '/users/me',
|
|
192
|
+
},
|
|
193
|
+
} as const;
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
#### `src/lib/constants/routes.ts`
|
|
197
|
+
```typescript
|
|
198
|
+
export const ROUTES = {
|
|
199
|
+
HOME: '/',
|
|
200
|
+
LOGIN: '/login',
|
|
201
|
+
REGISTER: '/register',
|
|
202
|
+
VERIFY_EMAIL: '/verify-email',
|
|
203
|
+
RESET_PASSWORD: '/reset-password',
|
|
204
|
+
DASHBOARD: '/dashboard',
|
|
205
|
+
PROFILE: '/profile',
|
|
206
|
+
} as const;
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
#### `src/lib/constants/app.constants.ts`
|
|
210
|
+
```typescript
|
|
211
|
+
export const APP_NAME = process.env.NEXT_PUBLIC_APP_NAME || '$ARGUMENTS';
|
|
212
|
+
|
|
213
|
+
export const PAGINATION = {
|
|
214
|
+
DEFAULT_PAGE: 1,
|
|
215
|
+
DEFAULT_LIMIT: 10,
|
|
216
|
+
MAX_LIMIT: 100,
|
|
217
|
+
} as const;
|
|
218
|
+
|
|
219
|
+
export const USER_ROLES = {
|
|
220
|
+
USER: 'USER',
|
|
221
|
+
COMPANY: 'COMPANY',
|
|
222
|
+
ADMIN: 'ADMIN',
|
|
223
|
+
GUIDE: 'GUIDE',
|
|
224
|
+
DRIVER: 'DRIVER',
|
|
225
|
+
} as const;
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
#### `src/lib/utils/error.ts`
|
|
229
|
+
Implement the error utility from `03-data-and-state.md`:
|
|
230
|
+
```typescript
|
|
231
|
+
import axios from 'axios';
|
|
232
|
+
import type { ApiError } from '@/lib/api/api.types';
|
|
233
|
+
|
|
234
|
+
export const getErrorMessage = (error: unknown): string => {
|
|
235
|
+
if (axios.isAxiosError(error)) {
|
|
236
|
+
const apiError = error.response?.data as ApiError;
|
|
237
|
+
if (apiError?.error?.message) {
|
|
238
|
+
return apiError.error.message;
|
|
239
|
+
}
|
|
240
|
+
if (error.code === 'ERR_NETWORK') {
|
|
241
|
+
return 'Network error. Check your connection.';
|
|
242
|
+
}
|
|
243
|
+
return error.message;
|
|
244
|
+
}
|
|
245
|
+
if (error instanceof Error) {
|
|
246
|
+
return error.message;
|
|
247
|
+
}
|
|
248
|
+
return 'An unexpected error occurred';
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
export const getErrorCode = (error: unknown): string | undefined => {
|
|
252
|
+
if (axios.isAxiosError(error)) {
|
|
253
|
+
return error.response?.data?.error?.code;
|
|
254
|
+
}
|
|
255
|
+
return undefined;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
export const isErrorCode = (error: unknown, code: string): boolean => {
|
|
259
|
+
return getErrorCode(error) === code;
|
|
260
|
+
};
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
#### `src/lib/utils/format.ts`
|
|
264
|
+
```typescript
|
|
265
|
+
export const formatCurrency = (amount: number, currency = 'GEL'): string => {
|
|
266
|
+
return new Intl.NumberFormat('en-US', {
|
|
267
|
+
style: 'currency',
|
|
268
|
+
currency,
|
|
269
|
+
minimumFractionDigits: 0,
|
|
270
|
+
maximumFractionDigits: 2,
|
|
271
|
+
}).format(amount);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
export const formatDate = (date: string | Date): string => {
|
|
275
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
276
|
+
year: 'numeric',
|
|
277
|
+
month: 'long',
|
|
278
|
+
day: 'numeric',
|
|
279
|
+
}).format(new Date(date));
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
export const formatRelativeTime = (date: string | Date): string => {
|
|
283
|
+
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
|
|
284
|
+
const now = new Date();
|
|
285
|
+
const then = new Date(date);
|
|
286
|
+
const diffInSeconds = (then.getTime() - now.getTime()) / 1000;
|
|
287
|
+
|
|
288
|
+
const units: { unit: Intl.RelativeTimeFormatUnit; seconds: number }[] = [
|
|
289
|
+
{ unit: 'year', seconds: 31536000 },
|
|
290
|
+
{ unit: 'month', seconds: 2592000 },
|
|
291
|
+
{ unit: 'day', seconds: 86400 },
|
|
292
|
+
{ unit: 'hour', seconds: 3600 },
|
|
293
|
+
{ unit: 'minute', seconds: 60 },
|
|
294
|
+
];
|
|
295
|
+
|
|
296
|
+
for (const { unit, seconds } of units) {
|
|
297
|
+
if (Math.abs(diffInSeconds) >= seconds) {
|
|
298
|
+
return rtf.format(Math.round(diffInSeconds / seconds), unit);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return rtf.format(Math.round(diffInSeconds), 'second');
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
export const truncate = (str: string, length: number): string => {
|
|
305
|
+
return str.length > length ? `${str.substring(0, length)}...` : str;
|
|
306
|
+
};
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
#### `src/lib/utils/security.ts`
|
|
310
|
+
Implement from `05-security.md`:
|
|
311
|
+
```typescript
|
|
312
|
+
export const isSafeUrl = (url: string): boolean => {
|
|
313
|
+
try {
|
|
314
|
+
const parsed = new URL(url);
|
|
315
|
+
return ['http:', 'https:', 'mailto:'].includes(parsed.protocol);
|
|
316
|
+
} catch {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
export const sanitizeString = (input: string): string => {
|
|
322
|
+
return input.trim().replace(/[<>]/g, '').slice(0, 1000);
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
export const sanitizeEmail = (email: string): string => {
|
|
326
|
+
return email.toLowerCase().trim().slice(0, 255);
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
export const maskEmail = (email: string): string => {
|
|
330
|
+
const [local, domain] = email.split('@');
|
|
331
|
+
if (!local || !domain) return email;
|
|
332
|
+
return `${local[0]}***${local[local.length - 1]}@${domain}`;
|
|
333
|
+
};
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
### Step 5: State Management
|
|
339
|
+
|
|
340
|
+
#### `src/store/index.ts`
|
|
341
|
+
Redux store setup from `03-data-and-state.md`:
|
|
342
|
+
- Configure store with auth reducer
|
|
343
|
+
- Load auth state from localStorage (with SSR guard: `typeof window !== 'undefined'`)
|
|
344
|
+
- Subscribe to save auth state to localStorage (with SSR guard)
|
|
345
|
+
- Export `RootState` and `AppDispatch` types
|
|
346
|
+
|
|
347
|
+
#### `src/store/hooks.ts`
|
|
348
|
+
Typed Redux hooks:
|
|
349
|
+
```typescript
|
|
350
|
+
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
|
|
351
|
+
import type { RootState, AppDispatch } from './index';
|
|
352
|
+
|
|
353
|
+
export const useAppDispatch: () => AppDispatch = useDispatch;
|
|
354
|
+
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
#### `src/features/auth/store/authSlice.ts`
|
|
358
|
+
Auth slice from `03-data-and-state.md`:
|
|
359
|
+
- State: `{ user: IUser | null, tokens: IAuthTokens | null, isAuthenticated: boolean }`
|
|
360
|
+
- Reducers: `setCredentials`, `updateTokens`, `logout`
|
|
361
|
+
|
|
362
|
+
#### `src/features/auth/types/auth.types.ts`
|
|
363
|
+
Auth types from `02-components-and-types.md`:
|
|
364
|
+
- `IUser` interface
|
|
365
|
+
- `UserRole` type (`'USER' | 'COMPANY' | 'ADMIN' | 'GUIDE' | 'DRIVER'`)
|
|
366
|
+
- `IAuthTokens` interface
|
|
367
|
+
- `ILoginRequest`, `IRegisterRequest` interfaces
|
|
368
|
+
- `IAuthState` interface
|
|
369
|
+
|
|
370
|
+
#### `src/features/auth/services/auth.service.ts`
|
|
371
|
+
Auth service class from `03-data-and-state.md`:
|
|
372
|
+
- `register`, `login`, `logout`, `refreshToken`, `getMe`, `verifyEmail`, `requestPasswordReset`, `resetPassword`
|
|
373
|
+
- Uses `apiClient` and `API_ENDPOINTS`
|
|
374
|
+
- Singleton export: `export const authService = new AuthService();`
|
|
375
|
+
|
|
376
|
+
#### `src/features/auth/hooks/useAuth.ts`
|
|
377
|
+
```typescript
|
|
378
|
+
'use client';
|
|
379
|
+
|
|
380
|
+
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
|
381
|
+
import { useMutation } from '@tanstack/react-query';
|
|
382
|
+
import { authService } from '../services/auth.service';
|
|
383
|
+
import { setCredentials, logout as logoutAction } from '../store/authSlice';
|
|
384
|
+
import { useRouter } from 'next/navigation';
|
|
385
|
+
|
|
386
|
+
export const useAuth = () => {
|
|
387
|
+
const dispatch = useAppDispatch();
|
|
388
|
+
const router = useRouter();
|
|
389
|
+
const { user, isAuthenticated, tokens } = useAppSelector((state) => state.auth);
|
|
390
|
+
|
|
391
|
+
const loginMutation = useMutation({
|
|
392
|
+
mutationFn: authService.login,
|
|
393
|
+
onSuccess: (data) => {
|
|
394
|
+
dispatch(setCredentials(data));
|
|
395
|
+
router.push('/dashboard');
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const logout = async (): Promise<void> => {
|
|
400
|
+
try {
|
|
401
|
+
await authService.logout();
|
|
402
|
+
} finally {
|
|
403
|
+
dispatch(logoutAction());
|
|
404
|
+
if (typeof window !== 'undefined') {
|
|
405
|
+
localStorage.removeItem('auth');
|
|
406
|
+
sessionStorage.clear();
|
|
407
|
+
}
|
|
408
|
+
router.push('/login');
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
user,
|
|
414
|
+
isAuthenticated,
|
|
415
|
+
login: loginMutation.mutate,
|
|
416
|
+
logout,
|
|
417
|
+
isLoggingIn: loginMutation.isPending,
|
|
418
|
+
};
|
|
419
|
+
};
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
### Step 6: Providers & Layout
|
|
425
|
+
|
|
426
|
+
#### `src/app/providers.tsx`
|
|
427
|
+
```typescript
|
|
428
|
+
'use client';
|
|
429
|
+
|
|
430
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
431
|
+
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
|
432
|
+
import { Provider as ReduxProvider } from 'react-redux';
|
|
433
|
+
import { ThemeProvider } from 'next-themes';
|
|
434
|
+
import { Toaster } from 'sonner';
|
|
435
|
+
import { useState } from 'react';
|
|
436
|
+
import { store } from '@/store';
|
|
437
|
+
|
|
438
|
+
export function Providers({ children }: { children: React.ReactNode }): React.ReactElement {
|
|
439
|
+
const [queryClient] = useState(
|
|
440
|
+
() =>
|
|
441
|
+
new QueryClient({
|
|
442
|
+
defaultOptions: {
|
|
443
|
+
queries: {
|
|
444
|
+
staleTime: 5 * 60 * 1000,
|
|
445
|
+
gcTime: 10 * 60 * 1000,
|
|
446
|
+
refetchOnWindowFocus: false,
|
|
447
|
+
retry: 1,
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
})
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
return (
|
|
454
|
+
<ReduxProvider store={store}>
|
|
455
|
+
<QueryClientProvider client={queryClient}>
|
|
456
|
+
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
|
|
457
|
+
{children}
|
|
458
|
+
<Toaster position="top-right" richColors />
|
|
459
|
+
</ThemeProvider>
|
|
460
|
+
<ReactQueryDevtools initialIsOpen={false} />
|
|
461
|
+
</QueryClientProvider>
|
|
462
|
+
</ReduxProvider>
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
#### `src/app/layout.tsx`
|
|
468
|
+
Root layout:
|
|
469
|
+
- Import and use `next/font` (Inter or Geist Sans)
|
|
470
|
+
- `<html lang="en" suppressHydrationWarning>`
|
|
471
|
+
- Wrap children in `<Providers>`
|
|
472
|
+
- Default metadata with app name
|
|
473
|
+
|
|
474
|
+
#### `src/app/(main)/layout.tsx`
|
|
475
|
+
Main layout with Header and Footer wrapping children in a flex column min-h-screen structure.
|
|
476
|
+
|
|
477
|
+
#### `src/app/(auth)/layout.tsx`
|
|
478
|
+
Auth layout: centered, minimal — flex items-center justify-center min-h-screen.
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
### Step 7: Common Components
|
|
483
|
+
|
|
484
|
+
#### `src/components/layout/Header.tsx`
|
|
485
|
+
Client component with:
|
|
486
|
+
- App name / logo link to home
|
|
487
|
+
- Navigation links
|
|
488
|
+
- Auth-aware: show Login/Register or user menu based on auth state
|
|
489
|
+
- Theme toggle button (Sun/Moon icons with `next-themes` `useTheme`)
|
|
490
|
+
- Responsive (mobile menu)
|
|
491
|
+
|
|
492
|
+
#### `src/components/layout/Footer.tsx`
|
|
493
|
+
Server component, simple footer with copyright.
|
|
494
|
+
|
|
495
|
+
#### `src/components/common/LoadingSpinner.tsx`
|
|
496
|
+
Using Lucide `Loader2` icon with spin animation. Accept `size` prop.
|
|
497
|
+
|
|
498
|
+
#### `src/components/common/EmptyState.tsx`
|
|
499
|
+
```typescript
|
|
500
|
+
import { FileQuestion } from 'lucide-react';
|
|
501
|
+
import { Button } from '@/components/ui/button';
|
|
502
|
+
import Link from 'next/link';
|
|
503
|
+
|
|
504
|
+
interface EmptyStateProps {
|
|
505
|
+
title: string;
|
|
506
|
+
description: string;
|
|
507
|
+
actionLabel?: string;
|
|
508
|
+
actionHref?: string;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export const EmptyState = ({
|
|
512
|
+
title,
|
|
513
|
+
description,
|
|
514
|
+
actionLabel,
|
|
515
|
+
actionHref,
|
|
516
|
+
}: EmptyStateProps): React.ReactElement => (
|
|
517
|
+
<div className="flex min-h-[400px] flex-col items-center justify-center p-8 text-center">
|
|
518
|
+
<FileQuestion className="mb-4 h-16 w-16 text-muted-foreground" />
|
|
519
|
+
<h3 className="mb-2 text-xl font-semibold">{title}</h3>
|
|
520
|
+
<p className="mb-4 text-muted-foreground">{description}</p>
|
|
521
|
+
{actionLabel && actionHref && (
|
|
522
|
+
<Button asChild>
|
|
523
|
+
<Link href={actionHref}>{actionLabel}</Link>
|
|
524
|
+
</Button>
|
|
525
|
+
)}
|
|
526
|
+
</div>
|
|
527
|
+
);
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
#### `src/components/common/Pagination.tsx`
|
|
531
|
+
```typescript
|
|
532
|
+
'use client';
|
|
533
|
+
|
|
534
|
+
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
|
|
535
|
+
import { Button } from '@/components/ui/button';
|
|
536
|
+
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
537
|
+
import { useCallback } from 'react';
|
|
538
|
+
|
|
539
|
+
interface PaginationProps {
|
|
540
|
+
page: number;
|
|
541
|
+
totalPages: number;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export const Pagination = ({ page, totalPages }: PaginationProps): React.ReactElement => {
|
|
545
|
+
const router = useRouter();
|
|
546
|
+
const pathname = usePathname();
|
|
547
|
+
const searchParams = useSearchParams();
|
|
548
|
+
|
|
549
|
+
const createPageUrl = useCallback(
|
|
550
|
+
(pageNumber: number): string => {
|
|
551
|
+
const params = new URLSearchParams(searchParams.toString());
|
|
552
|
+
params.set('page', pageNumber.toString());
|
|
553
|
+
return `${pathname}?${params.toString()}`;
|
|
554
|
+
},
|
|
555
|
+
[pathname, searchParams]
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
return (
|
|
559
|
+
<div className="flex items-center justify-center gap-2">
|
|
560
|
+
<Button
|
|
561
|
+
variant="outline"
|
|
562
|
+
size="sm"
|
|
563
|
+
onClick={() => router.push(createPageUrl(page - 1))}
|
|
564
|
+
disabled={page <= 1}
|
|
565
|
+
aria-label="Previous page"
|
|
566
|
+
>
|
|
567
|
+
<ChevronLeft className="h-4 w-4" />
|
|
568
|
+
</Button>
|
|
569
|
+
<span className="text-sm text-muted-foreground">
|
|
570
|
+
Page {page} of {totalPages}
|
|
571
|
+
</span>
|
|
572
|
+
<Button
|
|
573
|
+
variant="outline"
|
|
574
|
+
size="sm"
|
|
575
|
+
onClick={() => router.push(createPageUrl(page + 1))}
|
|
576
|
+
disabled={page >= totalPages}
|
|
577
|
+
aria-label="Next page"
|
|
578
|
+
>
|
|
579
|
+
<ChevronRight className="h-4 w-4" />
|
|
580
|
+
</Button>
|
|
581
|
+
</div>
|
|
582
|
+
);
|
|
583
|
+
};
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
#### `src/components/common/ConfirmDialog.tsx`
|
|
587
|
+
```typescript
|
|
588
|
+
'use client';
|
|
589
|
+
|
|
590
|
+
import {
|
|
591
|
+
AlertDialog,
|
|
592
|
+
AlertDialogAction,
|
|
593
|
+
AlertDialogCancel,
|
|
594
|
+
AlertDialogContent,
|
|
595
|
+
AlertDialogDescription,
|
|
596
|
+
AlertDialogFooter,
|
|
597
|
+
AlertDialogHeader,
|
|
598
|
+
AlertDialogTitle,
|
|
599
|
+
} from '@/components/ui/alert-dialog';
|
|
600
|
+
|
|
601
|
+
interface ConfirmDialogProps {
|
|
602
|
+
open: boolean;
|
|
603
|
+
onOpenChange: (open: boolean) => void;
|
|
604
|
+
onConfirm: () => void;
|
|
605
|
+
title: string;
|
|
606
|
+
description: string;
|
|
607
|
+
confirmLabel?: string;
|
|
608
|
+
isDestructive?: boolean;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export const ConfirmDialog = ({
|
|
612
|
+
open,
|
|
613
|
+
onOpenChange,
|
|
614
|
+
onConfirm,
|
|
615
|
+
title,
|
|
616
|
+
description,
|
|
617
|
+
confirmLabel = 'Confirm',
|
|
618
|
+
isDestructive = false,
|
|
619
|
+
}: ConfirmDialogProps): React.ReactElement => (
|
|
620
|
+
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
|
621
|
+
<AlertDialogContent>
|
|
622
|
+
<AlertDialogHeader>
|
|
623
|
+
<AlertDialogTitle>{title}</AlertDialogTitle>
|
|
624
|
+
<AlertDialogDescription>{description}</AlertDialogDescription>
|
|
625
|
+
</AlertDialogHeader>
|
|
626
|
+
<AlertDialogFooter>
|
|
627
|
+
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
628
|
+
<AlertDialogAction
|
|
629
|
+
onClick={onConfirm}
|
|
630
|
+
className={isDestructive ? 'bg-destructive text-destructive-foreground hover:bg-destructive/90' : ''}
|
|
631
|
+
>
|
|
632
|
+
{confirmLabel}
|
|
633
|
+
</AlertDialogAction>
|
|
634
|
+
</AlertDialogFooter>
|
|
635
|
+
</AlertDialogContent>
|
|
636
|
+
</AlertDialog>
|
|
637
|
+
);
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
---
|
|
641
|
+
|
|
642
|
+
### Step 8: Global Custom Hooks
|
|
643
|
+
|
|
644
|
+
#### `src/hooks/useDebounce.ts`
|
|
645
|
+
```typescript
|
|
646
|
+
'use client';
|
|
647
|
+
|
|
648
|
+
import { useState, useEffect } from 'react';
|
|
649
|
+
|
|
650
|
+
export const useDebounce = <T,>(value: T, delay = 500): T => {
|
|
651
|
+
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
652
|
+
|
|
653
|
+
useEffect(() => {
|
|
654
|
+
const timer = setTimeout(() => setDebouncedValue(value), delay);
|
|
655
|
+
return () => clearTimeout(timer);
|
|
656
|
+
}, [value, delay]);
|
|
657
|
+
|
|
658
|
+
return debouncedValue;
|
|
659
|
+
};
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
#### `src/hooks/useLocalStorage.ts`
|
|
663
|
+
```typescript
|
|
664
|
+
'use client';
|
|
665
|
+
|
|
666
|
+
import { useState, useEffect } from 'react';
|
|
667
|
+
|
|
668
|
+
export const useLocalStorage = <T,>(key: string, initialValue: T) => {
|
|
669
|
+
const [storedValue, setStoredValue] = useState<T>(initialValue);
|
|
670
|
+
const [isHydrated, setIsHydrated] = useState(false);
|
|
671
|
+
|
|
672
|
+
useEffect(() => {
|
|
673
|
+
try {
|
|
674
|
+
const item = window.localStorage.getItem(key);
|
|
675
|
+
if (item) {
|
|
676
|
+
setStoredValue(JSON.parse(item));
|
|
677
|
+
}
|
|
678
|
+
} catch {
|
|
679
|
+
// Ignore errors
|
|
680
|
+
}
|
|
681
|
+
setIsHydrated(true);
|
|
682
|
+
}, [key]);
|
|
683
|
+
|
|
684
|
+
const setValue = (value: T | ((val: T) => T)): void => {
|
|
685
|
+
try {
|
|
686
|
+
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
|
687
|
+
setStoredValue(valueToStore);
|
|
688
|
+
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
|
689
|
+
} catch {
|
|
690
|
+
// Ignore write errors
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
return [storedValue, setValue, isHydrated] as const;
|
|
695
|
+
};
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
#### `src/hooks/useMediaQuery.ts`
|
|
699
|
+
```typescript
|
|
700
|
+
'use client';
|
|
701
|
+
|
|
702
|
+
import { useState, useEffect } from 'react';
|
|
703
|
+
|
|
704
|
+
export const useMediaQuery = (query: string): boolean => {
|
|
705
|
+
const [matches, setMatches] = useState(false);
|
|
706
|
+
|
|
707
|
+
useEffect(() => {
|
|
708
|
+
const media = window.matchMedia(query);
|
|
709
|
+
setMatches(media.matches);
|
|
710
|
+
|
|
711
|
+
const listener = (e: MediaQueryListEvent): void => setMatches(e.matches);
|
|
712
|
+
media.addEventListener('change', listener);
|
|
713
|
+
return () => media.removeEventListener('change', listener);
|
|
714
|
+
}, [query]);
|
|
715
|
+
|
|
716
|
+
return matches;
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
export const useIsMobile = (): boolean => useMediaQuery('(max-width: 768px)');
|
|
720
|
+
export const useIsTablet = (): boolean => useMediaQuery('(max-width: 1024px)');
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
---
|
|
724
|
+
|
|
725
|
+
### Step 9: Auth Pages
|
|
726
|
+
|
|
727
|
+
#### `src/app/(auth)/login/page.tsx`
|
|
728
|
+
Server component page that renders `LoginForm`.
|
|
729
|
+
|
|
730
|
+
#### `src/app/(auth)/register/page.tsx`
|
|
731
|
+
Server component page that renders `RegisterForm`.
|
|
732
|
+
|
|
733
|
+
#### `src/features/auth/components/LoginForm.tsx`
|
|
734
|
+
Client component:
|
|
735
|
+
- React Hook Form + Zod validation (email + password)
|
|
736
|
+
- Submit button with loading state ("Signing in..." / "Sign in")
|
|
737
|
+
- Link to register page
|
|
738
|
+
- Uses `useAuth` hook for login
|
|
739
|
+
- Field-level error messages below each field (red text + red border)
|
|
740
|
+
|
|
741
|
+
#### `src/features/auth/components/RegisterForm.tsx`
|
|
742
|
+
Client component:
|
|
743
|
+
- React Hook Form + Zod validation
|
|
744
|
+
- First name, last name, email, password, confirm password
|
|
745
|
+
- Password schema: min 8 chars, must contain uppercase, lowercase, and number
|
|
746
|
+
- Submit button with loading state
|
|
747
|
+
- Link to login page
|
|
748
|
+
|
|
749
|
+
---
|
|
750
|
+
|
|
751
|
+
### Step 10: Dashboard & Home Pages
|
|
752
|
+
|
|
753
|
+
#### `src/app/page.tsx`
|
|
754
|
+
Simple home page (Server Component). Welcome message with links to login/register or dashboard.
|
|
755
|
+
|
|
756
|
+
#### `src/app/(main)/dashboard/page.tsx`
|
|
757
|
+
Simple dashboard page placeholder (Server Component). Shows "Welcome to your dashboard."
|
|
758
|
+
|
|
759
|
+
#### `src/app/loading.tsx`
|
|
760
|
+
Global loading component with LoadingSpinner.
|
|
761
|
+
|
|
762
|
+
#### `src/app/error.tsx`
|
|
763
|
+
Client component global error boundary:
|
|
764
|
+
```typescript
|
|
765
|
+
'use client';
|
|
766
|
+
|
|
767
|
+
import { useEffect } from 'react';
|
|
768
|
+
import { Button } from '@/components/ui/button';
|
|
769
|
+
import { AlertCircle } from 'lucide-react';
|
|
770
|
+
|
|
771
|
+
export default function GlobalError({
|
|
772
|
+
error,
|
|
773
|
+
reset,
|
|
774
|
+
}: {
|
|
775
|
+
error: Error & { digest?: string };
|
|
776
|
+
reset: () => void;
|
|
777
|
+
}): React.ReactElement {
|
|
778
|
+
useEffect(() => {
|
|
779
|
+
// Log to error reporting service in production
|
|
780
|
+
}, [error]);
|
|
781
|
+
|
|
782
|
+
return (
|
|
783
|
+
<div className="flex min-h-[400px] flex-col items-center justify-center p-8 text-center">
|
|
784
|
+
<AlertCircle className="mb-4 h-12 w-12 text-destructive" />
|
|
785
|
+
<h2 className="mb-2 text-2xl font-bold">Something went wrong</h2>
|
|
786
|
+
<p className="mb-4 text-muted-foreground">{error.message}</p>
|
|
787
|
+
<Button onClick={reset}>Try again</Button>
|
|
788
|
+
</div>
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
#### `src/app/not-found.tsx`
|
|
794
|
+
Simple 404 page with link to home.
|
|
795
|
+
|
|
796
|
+
---
|
|
797
|
+
|
|
798
|
+
### Step 11: Middleware
|
|
799
|
+
|
|
800
|
+
#### `src/middleware.ts`
|
|
801
|
+
|
|
802
|
+
> **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.
|
|
803
|
+
|
|
804
|
+
```typescript
|
|
805
|
+
import { NextResponse } from 'next/server';
|
|
806
|
+
import type { NextRequest } from 'next/server';
|
|
807
|
+
|
|
808
|
+
const protectedPaths = ['/dashboard', '/profile', '/my-tours', '/admin'];
|
|
809
|
+
const authPaths = ['/login', '/register'];
|
|
810
|
+
|
|
811
|
+
export function middleware(request: NextRequest): NextResponse {
|
|
812
|
+
const { pathname } = request.nextUrl;
|
|
813
|
+
const token = request.cookies.get('accessToken')?.value;
|
|
814
|
+
|
|
815
|
+
const isProtectedPath = protectedPaths.some((path) =>
|
|
816
|
+
pathname.startsWith(path)
|
|
817
|
+
);
|
|
818
|
+
|
|
819
|
+
if (isProtectedPath && !token) {
|
|
820
|
+
const loginUrl = new URL('/login', request.url);
|
|
821
|
+
loginUrl.searchParams.set('from', pathname);
|
|
822
|
+
return NextResponse.redirect(loginUrl);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const isAuthPath = authPaths.some((path) => pathname.startsWith(path));
|
|
826
|
+
|
|
827
|
+
if (isAuthPath && token) {
|
|
828
|
+
return NextResponse.redirect(new URL('/dashboard', request.url));
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
return NextResponse.next();
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
export const config = {
|
|
835
|
+
matcher: [
|
|
836
|
+
'/dashboard/:path*',
|
|
837
|
+
'/profile/:path*',
|
|
838
|
+
'/my-tours/:path*',
|
|
839
|
+
'/admin/:path*',
|
|
840
|
+
'/login',
|
|
841
|
+
'/register',
|
|
842
|
+
],
|
|
843
|
+
};
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
---
|
|
847
|
+
|
|
848
|
+
### Step 12: Global Types
|
|
849
|
+
|
|
850
|
+
#### `src/types/index.ts`
|
|
851
|
+
Shared utility types:
|
|
852
|
+
```typescript
|
|
853
|
+
export type Nullable<T> = T | null;
|
|
854
|
+
export type Optional<T> = T | undefined;
|
|
855
|
+
export type AsyncResult<T> = Promise<T>;
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
---
|
|
859
|
+
|
|
860
|
+
### Step 13: Final Verification
|
|
861
|
+
|
|
862
|
+
After creating all files:
|
|
863
|
+
1. Run `npm run dev` to verify the project starts without errors
|
|
864
|
+
2. Check that the home page renders at `http://localhost:3001` (or the configured port)
|
|
865
|
+
3. Verify no TypeScript errors
|
|
866
|
+
|
|
867
|
+
---
|
|
868
|
+
|
|
869
|
+
## IMPORTANT RULES
|
|
870
|
+
|
|
871
|
+
- Follow ALL rules from `.claude/rules/client/` and `.claude/rules/global/`
|
|
872
|
+
- Server Components by default — only add `'use client'` when hooks/state/events are needed
|
|
873
|
+
- Max 250 lines per component, max 5 props, max 3 JSX nesting levels
|
|
874
|
+
- No hardcoded colors — use Tailwind semantic tokens only (`bg-primary`, `text-foreground`)
|
|
875
|
+
- No inline styles — Tailwind only with `cn()` for conditional classes
|
|
876
|
+
- Import order: React/Next → third-party → UI → local → hooks → services → types → utils
|
|
877
|
+
- Forms: validate with Zod client-side AND server-side
|
|
878
|
+
- Never prefix secrets with `NEXT_PUBLIC_`
|
|
879
|
+
- Use `next/image` for images, `next/link` for navigation
|
|
880
|
+
- Pair background with foreground colors (`bg-primary text-primary-foreground`)
|
|
881
|
+
- All interactive elements need hover/active/focus-visible states
|