@adlas/create-app 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +476 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +39 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/figma.d.ts +16 -0
- package/dist/commands/figma.d.ts.map +1 -0
- package/dist/commands/figma.js +172 -0
- package/dist/commands/figma.js.map +1 -0
- package/dist/commands/index.d.ts +5 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +5 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +1471 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/swagger.d.ts +16 -0
- package/dist/commands/swagger.d.ts.map +1 -0
- package/dist/commands/swagger.js +404 -0
- package/dist/commands/swagger.js.map +1 -0
- package/dist/commands/update.d.ts +15 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +93 -0
- package/dist/commands/update.js.map +1 -0
- package/package.json +63 -0
- package/templates/.vscode/extensions.json +9 -0
- package/templates/.vscode/launch.json +26 -0
- package/templates/.vscode/settings.json +67 -0
- package/templates/.vscode/tasks.json +21 -0
- package/templates/boilerplate/config/fonts.ts +10 -0
- package/templates/boilerplate/config/navigationUrls.ts +47 -0
- package/templates/boilerplate/config/site.ts +96 -0
- package/templates/boilerplate/libs/I18n.ts +15 -0
- package/templates/boilerplate/libs/I18nNavigation.ts +5 -0
- package/templates/boilerplate/libs/I18nRouting.ts +9 -0
- package/templates/boilerplate/libs/env.ts +21 -0
- package/templates/boilerplate/libs/react-query/ReactQueryProvider.tsx +21 -0
- package/templates/boilerplate/libs/react-query/index.ts +2 -0
- package/templates/boilerplate/libs/react-query/queryClient.ts +62 -0
- package/templates/boilerplate/libs/react-query/queryKeys.ts +5 -0
- package/templates/boilerplate/public/images/index.ts +1 -0
- package/templates/boilerplate/reset.d.ts +2 -0
- package/templates/boilerplate/styles/globals.css +308 -0
- package/templates/boilerplate/types/i18n.ts +10 -0
- package/templates/boilerplate/types/locale.ts +8 -0
- package/templates/boilerplate/utils/file/fileConfig.ts +123 -0
- package/templates/boilerplate/utils/file/fileValidation.ts +78 -0
- package/templates/boilerplate/utils/file/imageCompression.ts +182 -0
- package/templates/boilerplate/utils/file/index.ts +3 -0
- package/templates/boilerplate/utils/helpers.ts +55 -0
- package/templates/boilerplate/validations/auth.validation.ts +92 -0
- package/templates/boilerplate/validations/commonValidations.ts +258 -0
- package/templates/boilerplate/validations/zodErrorMap.ts +101 -0
- package/templates/configs/.env.example +8 -0
- package/templates/configs/.prettierignore +23 -0
- package/templates/configs/.prettierrc.cjs +26 -0
- package/templates/configs/.prettierrc.icons.cjs +11 -0
- package/templates/configs/Dockerfile +6 -0
- package/templates/configs/commitlint.config.ts +8 -0
- package/templates/configs/eslint.config.mjs +119 -0
- package/templates/configs/knip.config.ts +32 -0
- package/templates/configs/lefthook.yml +42 -0
- package/templates/configs/lint-staged.config.js +8 -0
- package/templates/configs/next.config.template.ts +77 -0
- package/templates/configs/next.config.ts +43 -0
- package/templates/configs/package.json +75 -0
- package/templates/configs/postcss.config.mjs +15 -0
- package/templates/configs/svgr.config.mjs +129 -0
- package/templates/configs/tsconfig.json +75 -0
- package/templates/docs/AI_QUICK_REFERENCE.md +379 -0
- package/templates/docs/ARCHITECTURE_PATTERNS.md +927 -0
- package/templates/docs/DOCUMENTATION_INDEX.md +411 -0
- package/templates/docs/FIGMA_TO_CODE_GUIDE.md +768 -0
- package/templates/docs/IMPLEMENTATION_GUIDE.md +892 -0
- package/templates/docs/PROJECT_OVERVIEW.md +302 -0
- package/templates/docs/REFACTOR_PROGRESS.md +1113 -0
- package/templates/docs/SHADCN_TO_HEROUI_MIGRATION.md +1375 -0
- package/templates/docs/UI_COMPONENTS_GUIDE.md +893 -0
|
@@ -0,0 +1,927 @@
|
|
|
1
|
+
# Architecture Patterns
|
|
2
|
+
|
|
3
|
+
Comprehensive guide to architectural patterns used in the Berndorf e-commerce project. Follow these patterns for consistency and maintainability.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Service Module Pattern](#service-module-pattern)
|
|
8
|
+
2. [State Management with Zustand](#state-management-with-zustand)
|
|
9
|
+
3. [Data Fetching with React Query](#data-fetching-with-react-query)
|
|
10
|
+
4. [Form Patterns](#form-patterns)
|
|
11
|
+
5. [Validation Patterns](#validation-patterns)
|
|
12
|
+
6. [Internationalization (i18n)](#internationalization-i18n)
|
|
13
|
+
7. [Authentication Patterns](#authentication-patterns)
|
|
14
|
+
8. [API Communication](#api-communication)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Service Module Pattern
|
|
19
|
+
|
|
20
|
+
All features follow a consistent service module structure for organization and maintainability.
|
|
21
|
+
|
|
22
|
+
### Directory Structure
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
src/services/[feature]/
|
|
26
|
+
├── index.ts # Public exports
|
|
27
|
+
├── api/ # API client functions
|
|
28
|
+
│ └── [feature].api.ts
|
|
29
|
+
├── queries/ # React Query hooks
|
|
30
|
+
│ └── [feature].queries.ts
|
|
31
|
+
├── hooks/ # Custom hooks
|
|
32
|
+
│ └── use[Feature].ts
|
|
33
|
+
├── types/ # TypeScript types
|
|
34
|
+
│ └── [feature].types.ts
|
|
35
|
+
├── actions/ # Server actions
|
|
36
|
+
│ └── [feature].actions.ts
|
|
37
|
+
├── store/ # Zustand stores
|
|
38
|
+
│ └── [feature].store.ts
|
|
39
|
+
├── constants/ # Constants
|
|
40
|
+
│ └── [feature].constants.ts
|
|
41
|
+
└── helpers/ # Helper functions
|
|
42
|
+
└── [feature].helpers.ts
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Complete Example: Product Service Module
|
|
46
|
+
|
|
47
|
+
#### 1. Index File (Public API)
|
|
48
|
+
|
|
49
|
+
**File:** `src/services/product/index.ts`
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// Export all public APIs
|
|
53
|
+
export * from './api/product.api';
|
|
54
|
+
export * from './queries/product.queries';
|
|
55
|
+
export * from './hooks/useProducts';
|
|
56
|
+
export * from './types/product.types';
|
|
57
|
+
export * from './actions/product.actions';
|
|
58
|
+
export * from './store/product.store';
|
|
59
|
+
export * from './constants/product.constants';
|
|
60
|
+
export * from './helpers/product.helpers';
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
#### 2. Types Definition
|
|
64
|
+
|
|
65
|
+
**File:** `src/services/product/types/product.types.ts`
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// API Response types
|
|
69
|
+
export interface ProductApiResponse {
|
|
70
|
+
id: string;
|
|
71
|
+
name: string;
|
|
72
|
+
description: string;
|
|
73
|
+
price: number;
|
|
74
|
+
images: string[];
|
|
75
|
+
category: string;
|
|
76
|
+
stock: number;
|
|
77
|
+
createdAt: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Frontend model types
|
|
81
|
+
export interface Product {
|
|
82
|
+
id: string;
|
|
83
|
+
name: string;
|
|
84
|
+
description: string;
|
|
85
|
+
price: number;
|
|
86
|
+
formattedPrice: string;
|
|
87
|
+
images: string[];
|
|
88
|
+
primaryImage: string;
|
|
89
|
+
category: string;
|
|
90
|
+
inStock: boolean;
|
|
91
|
+
createdAt: Date;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Request types
|
|
95
|
+
export interface CreateProductRequest {
|
|
96
|
+
name: string;
|
|
97
|
+
description: string;
|
|
98
|
+
price: number;
|
|
99
|
+
categoryId: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Filter types
|
|
103
|
+
export interface ProductFilters {
|
|
104
|
+
category?: string;
|
|
105
|
+
minPrice?: number;
|
|
106
|
+
maxPrice?: number;
|
|
107
|
+
inStock?: boolean;
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### 3. API Client
|
|
112
|
+
|
|
113
|
+
**File:** `src/services/product/api/product.api.ts`
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { apiRequest } from '@/lib/apiRequest';
|
|
117
|
+
import type { Product, ProductApiResponse, CreateProductRequest, ProductFilters } from '../types/product.types';
|
|
118
|
+
import { transformProductResponse } from '../helpers/product.helpers';
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Fetch all products with optional filters
|
|
122
|
+
*/
|
|
123
|
+
export async function getProducts(filters?: ProductFilters): Promise<Product[]> {
|
|
124
|
+
const queryParams = new URLSearchParams();
|
|
125
|
+
|
|
126
|
+
if (filters?.category) queryParams.append('category', filters.category);
|
|
127
|
+
if (filters?.minPrice) queryParams.append('minPrice', String(filters.minPrice));
|
|
128
|
+
if (filters?.maxPrice) queryParams.append('maxPrice', String(filters.maxPrice));
|
|
129
|
+
if (filters?.inStock !== undefined) queryParams.append('inStock', String(filters.inStock));
|
|
130
|
+
|
|
131
|
+
const url = `/api/products${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
|
|
132
|
+
|
|
133
|
+
const response = await apiRequest<ProductApiResponse[]>({
|
|
134
|
+
url,
|
|
135
|
+
method: 'GET',
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return response.map(transformProductResponse);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Fetch a single product by ID
|
|
143
|
+
*/
|
|
144
|
+
export async function getProduct(id: string): Promise<Product> {
|
|
145
|
+
const response = await apiRequest<ProductApiResponse>({
|
|
146
|
+
url: `/api/products/${id}`,
|
|
147
|
+
method: 'GET',
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return transformProductResponse(response);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Create a new product
|
|
155
|
+
*/
|
|
156
|
+
export async function createProduct(data: CreateProductRequest): Promise<Product> {
|
|
157
|
+
const response = await apiRequest<ProductApiResponse>({
|
|
158
|
+
url: '/api/products',
|
|
159
|
+
method: 'POST',
|
|
160
|
+
data,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return transformProductResponse(response);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Update an existing product
|
|
168
|
+
*/
|
|
169
|
+
export async function updateProduct(id: string, data: Partial<CreateProductRequest>): Promise<Product> {
|
|
170
|
+
const response = await apiRequest<ProductApiResponse>({
|
|
171
|
+
url: `/api/products/${id}`,
|
|
172
|
+
method: 'PATCH',
|
|
173
|
+
data,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return transformProductResponse(response);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Delete a product
|
|
181
|
+
*/
|
|
182
|
+
export async function deleteProduct(id: string): Promise<void> {
|
|
183
|
+
await apiRequest<void>({
|
|
184
|
+
url: `/api/products/${id}`,
|
|
185
|
+
method: 'DELETE',
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
#### 4. Helper Functions
|
|
191
|
+
|
|
192
|
+
**File:** `src/services/product/helpers/product.helpers.ts`
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
import type { Product, ProductApiResponse } from '../types/product.types';
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Transform API response to frontend model
|
|
199
|
+
*/
|
|
200
|
+
export function transformProductResponse(data: ProductApiResponse): Product {
|
|
201
|
+
return {
|
|
202
|
+
id: data.id,
|
|
203
|
+
name: data.name,
|
|
204
|
+
description: data.description,
|
|
205
|
+
price: data.price,
|
|
206
|
+
formattedPrice: formatPrice(data.price),
|
|
207
|
+
images: data.images,
|
|
208
|
+
primaryImage: data.images[0] || '/placeholder.jpg',
|
|
209
|
+
category: data.category,
|
|
210
|
+
inStock: data.stock > 0,
|
|
211
|
+
createdAt: new Date(data.createdAt),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Format price with currency
|
|
217
|
+
*/
|
|
218
|
+
export function formatPrice(price: number): string {
|
|
219
|
+
return new Intl.NumberFormat('de-AT', {
|
|
220
|
+
style: 'currency',
|
|
221
|
+
currency: 'EUR',
|
|
222
|
+
}).format(price);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Check if product is new (created within last 30 days)
|
|
227
|
+
*/
|
|
228
|
+
export function isNewProduct(createdAt: Date): boolean {
|
|
229
|
+
const thirtyDaysAgo = new Date();
|
|
230
|
+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
231
|
+
return createdAt > thirtyDaysAgo;
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
#### 5. React Query Hooks
|
|
236
|
+
|
|
237
|
+
**File:** `src/services/product/queries/product.queries.ts`
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
241
|
+
import { toast } from 'sonner';
|
|
242
|
+
import type { ProductFilters, CreateProductRequest } from '../types/product.types';
|
|
243
|
+
import {
|
|
244
|
+
getProducts,
|
|
245
|
+
getProduct,
|
|
246
|
+
createProduct,
|
|
247
|
+
updateProduct,
|
|
248
|
+
deleteProduct,
|
|
249
|
+
} from '../api/product.api';
|
|
250
|
+
|
|
251
|
+
// Query Keys
|
|
252
|
+
export const productKeys = {
|
|
253
|
+
all: ['products'] as const,
|
|
254
|
+
lists: () => [...productKeys.all, 'list'] as const,
|
|
255
|
+
list: (filters?: ProductFilters) => [...productKeys.lists(), filters] as const,
|
|
256
|
+
details: () => [...productKeys.all, 'detail'] as const,
|
|
257
|
+
detail: (id: string) => [...productKeys.details(), id] as const,
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Fetch all products with optional filters
|
|
262
|
+
*/
|
|
263
|
+
export function useProductsQuery(filters?: ProductFilters) {
|
|
264
|
+
return useQuery({
|
|
265
|
+
queryKey: productKeys.list(filters),
|
|
266
|
+
queryFn: () => getProducts(filters),
|
|
267
|
+
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Fetch a single product by ID
|
|
273
|
+
*/
|
|
274
|
+
export function useProductQuery(id: string) {
|
|
275
|
+
return useQuery({
|
|
276
|
+
queryKey: productKeys.detail(id),
|
|
277
|
+
queryFn: () => getProduct(id),
|
|
278
|
+
enabled: !!id,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Create a new product
|
|
284
|
+
*/
|
|
285
|
+
export function useCreateProductMutation() {
|
|
286
|
+
const queryClient = useQueryClient();
|
|
287
|
+
|
|
288
|
+
return useMutation({
|
|
289
|
+
mutationFn: createProduct,
|
|
290
|
+
onSuccess: () => {
|
|
291
|
+
queryClient.invalidateQueries({ queryKey: productKeys.lists() });
|
|
292
|
+
toast.success('Product created successfully');
|
|
293
|
+
},
|
|
294
|
+
onError: (error: Error) => {
|
|
295
|
+
toast.error(error.message || 'Failed to create product');
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Update an existing product
|
|
302
|
+
*/
|
|
303
|
+
export function useUpdateProductMutation() {
|
|
304
|
+
const queryClient = useQueryClient();
|
|
305
|
+
|
|
306
|
+
return useMutation({
|
|
307
|
+
mutationFn: ({ id, data }: { id: string; data: Partial<CreateProductRequest> }) =>
|
|
308
|
+
updateProduct(id, data),
|
|
309
|
+
onSuccess: (_, variables) => {
|
|
310
|
+
queryClient.invalidateQueries({ queryKey: productKeys.lists() });
|
|
311
|
+
queryClient.invalidateQueries({ queryKey: productKeys.detail(variables.id) });
|
|
312
|
+
toast.success('Product updated successfully');
|
|
313
|
+
},
|
|
314
|
+
onError: (error: Error) => {
|
|
315
|
+
toast.error(error.message || 'Failed to update product');
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Delete a product
|
|
322
|
+
*/
|
|
323
|
+
export function useDeleteProductMutation() {
|
|
324
|
+
const queryClient = useQueryClient();
|
|
325
|
+
|
|
326
|
+
return useMutation({
|
|
327
|
+
mutationFn: deleteProduct,
|
|
328
|
+
onSuccess: () => {
|
|
329
|
+
queryClient.invalidateQueries({ queryKey: productKeys.lists() });
|
|
330
|
+
toast.success('Product deleted successfully');
|
|
331
|
+
},
|
|
332
|
+
onError: (error: Error) => {
|
|
333
|
+
toast.error(error.message || 'Failed to delete product');
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
#### 6. Custom Hooks
|
|
340
|
+
|
|
341
|
+
**File:** `src/services/product/hooks/useProducts.ts`
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
import { useMemo } from 'react';
|
|
345
|
+
import { useProductsQuery } from '../queries/product.queries';
|
|
346
|
+
import type { ProductFilters } from '../types/product.types';
|
|
347
|
+
import { isNewProduct } from '../helpers/product.helpers';
|
|
348
|
+
|
|
349
|
+
export function useProducts(filters?: ProductFilters) {
|
|
350
|
+
const { data, isLoading, error } = useProductsQuery(filters);
|
|
351
|
+
|
|
352
|
+
const products = useMemo(() => {
|
|
353
|
+
if (!data) return [];
|
|
354
|
+
|
|
355
|
+
return data.map(product => ({
|
|
356
|
+
...product,
|
|
357
|
+
isNew: isNewProduct(product.createdAt),
|
|
358
|
+
}));
|
|
359
|
+
}, [data]);
|
|
360
|
+
|
|
361
|
+
const inStockProducts = useMemo(
|
|
362
|
+
() => products.filter(p => p.inStock),
|
|
363
|
+
[products],
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
const newProducts = useMemo(
|
|
367
|
+
() => products.filter(p => p.isNew),
|
|
368
|
+
[products],
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
products,
|
|
373
|
+
inStockProducts,
|
|
374
|
+
newProducts,
|
|
375
|
+
isLoading,
|
|
376
|
+
error,
|
|
377
|
+
isEmpty: products.length === 0,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
#### 7. Server Actions
|
|
383
|
+
|
|
384
|
+
**File:** `src/services/product/actions/product.actions.ts`
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
'use server';
|
|
388
|
+
|
|
389
|
+
import { revalidatePath } from 'next/cache';
|
|
390
|
+
import type { CreateProductRequest } from '../types/product.types';
|
|
391
|
+
|
|
392
|
+
export async function createProductAction(data: CreateProductRequest) {
|
|
393
|
+
try {
|
|
394
|
+
const response = await fetch(`${process.env.API_URL}/products`, {
|
|
395
|
+
method: 'POST',
|
|
396
|
+
headers: {
|
|
397
|
+
'Content-Type': 'application/json',
|
|
398
|
+
},
|
|
399
|
+
body: JSON.stringify(data),
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
if (!response.ok) {
|
|
403
|
+
const error = await response.json();
|
|
404
|
+
return { success: false, error: error.message };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const product = await response.json();
|
|
408
|
+
|
|
409
|
+
// Revalidate relevant paths
|
|
410
|
+
revalidatePath('/products');
|
|
411
|
+
revalidatePath(`/products/${product.id}`);
|
|
412
|
+
|
|
413
|
+
return { success: true, data: product };
|
|
414
|
+
} catch (error) {
|
|
415
|
+
console.error('Create product action error:', error);
|
|
416
|
+
return { success: false, error: 'Failed to create product' };
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export async function deleteProductAction(id: string) {
|
|
421
|
+
try {
|
|
422
|
+
const response = await fetch(`${process.env.API_URL}/products/${id}`, {
|
|
423
|
+
method: 'DELETE',
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
if (!response.ok) {
|
|
427
|
+
const error = await response.json();
|
|
428
|
+
return { success: false, error: error.message };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Revalidate relevant paths
|
|
432
|
+
revalidatePath('/products');
|
|
433
|
+
|
|
434
|
+
return { success: true };
|
|
435
|
+
} catch (error) {
|
|
436
|
+
console.error('Delete product action error:', error);
|
|
437
|
+
return { success: false, error: 'Failed to delete product' };
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
#### 8. Zustand Store
|
|
443
|
+
|
|
444
|
+
**File:** `src/services/product/store/product.store.ts`
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
import { create } from 'zustand';
|
|
448
|
+
import type { ProductFilters } from '../types/product.types';
|
|
449
|
+
|
|
450
|
+
interface ProductStore {
|
|
451
|
+
filters: ProductFilters;
|
|
452
|
+
selectedProductId: string | null;
|
|
453
|
+
setFilters: (filters: ProductFilters) => void;
|
|
454
|
+
setSelectedProductId: (id: string | null) => void;
|
|
455
|
+
resetFilters: () => void;
|
|
456
|
+
reset: () => void;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const initialState = {
|
|
460
|
+
filters: {},
|
|
461
|
+
selectedProductId: null,
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
export const useProductStore = create<ProductStore>((set) => ({
|
|
465
|
+
...initialState,
|
|
466
|
+
|
|
467
|
+
setFilters: (filters) => set({ filters }),
|
|
468
|
+
|
|
469
|
+
setSelectedProductId: (id) => set({ selectedProductId: id }),
|
|
470
|
+
|
|
471
|
+
resetFilters: () => set({ filters: {} }),
|
|
472
|
+
|
|
473
|
+
reset: () => set(initialState),
|
|
474
|
+
}));
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
#### 9. Constants
|
|
478
|
+
|
|
479
|
+
**File:** `src/services/product/constants/product.constants.ts`
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
export const PRODUCT_CONSTANTS = {
|
|
483
|
+
MAX_IMAGES: 5,
|
|
484
|
+
MIN_PRICE: 0,
|
|
485
|
+
MAX_PRICE: 10000,
|
|
486
|
+
PRICE_STEP: 0.01,
|
|
487
|
+
NEW_PRODUCT_DAYS: 30,
|
|
488
|
+
} as const;
|
|
489
|
+
|
|
490
|
+
export const PRODUCT_CATEGORIES = [
|
|
491
|
+
{ id: 'cutlery', label: 'Cutlery' },
|
|
492
|
+
{ id: 'cookware', label: 'Cookware' },
|
|
493
|
+
{ id: 'tableware', label: 'Tableware' },
|
|
494
|
+
] as const;
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## State Management with Zustand
|
|
500
|
+
|
|
501
|
+
Use Zustand for global state that needs to be shared across components.
|
|
502
|
+
|
|
503
|
+
### When to Use Zustand
|
|
504
|
+
|
|
505
|
+
- User authentication state
|
|
506
|
+
- Shopping cart
|
|
507
|
+
- UI preferences (theme, language)
|
|
508
|
+
- Filter/search state across pages
|
|
509
|
+
- Global modals/overlays
|
|
510
|
+
|
|
511
|
+
### Zustand Store Pattern
|
|
512
|
+
|
|
513
|
+
```typescript
|
|
514
|
+
import { create } from 'zustand';
|
|
515
|
+
|
|
516
|
+
interface Store {
|
|
517
|
+
// State
|
|
518
|
+
count: number;
|
|
519
|
+
items: string[];
|
|
520
|
+
|
|
521
|
+
// Actions
|
|
522
|
+
increment: () => void;
|
|
523
|
+
decrement: () => void;
|
|
524
|
+
addItem: (item: string) => void;
|
|
525
|
+
removeItem: (id: string) => void;
|
|
526
|
+
reset: () => void;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const initialState = {
|
|
530
|
+
count: 0,
|
|
531
|
+
items: [],
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
export const useStore = create<Store>((set) => ({
|
|
535
|
+
...initialState,
|
|
536
|
+
|
|
537
|
+
increment: () => set((state) => ({ count: state.count + 1 })),
|
|
538
|
+
|
|
539
|
+
decrement: () => set((state) => ({ count: state.count - 1 })),
|
|
540
|
+
|
|
541
|
+
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
|
|
542
|
+
|
|
543
|
+
removeItem: (id) => set((state) => ({
|
|
544
|
+
items: state.items.filter((item) => item !== id),
|
|
545
|
+
})),
|
|
546
|
+
|
|
547
|
+
reset: () => set(initialState),
|
|
548
|
+
}));
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### Using Store in Components
|
|
552
|
+
|
|
553
|
+
```tsx
|
|
554
|
+
'use client';
|
|
555
|
+
|
|
556
|
+
import { useStore } from '@/services/store/myStore';
|
|
557
|
+
|
|
558
|
+
export function Counter() {
|
|
559
|
+
const count = useStore((state) => state.count);
|
|
560
|
+
const increment = useStore((state) => state.increment);
|
|
561
|
+
const decrement = useStore((state) => state.decrement);
|
|
562
|
+
|
|
563
|
+
return (
|
|
564
|
+
<div>
|
|
565
|
+
<p>Count: {count}</p>
|
|
566
|
+
<button onClick={increment}>+</button>
|
|
567
|
+
<button onClick={decrement}>-</button>
|
|
568
|
+
</div>
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
---
|
|
574
|
+
|
|
575
|
+
## Data Fetching with React Query
|
|
576
|
+
|
|
577
|
+
Use React Query (@tanstack/react-query) for server state management.
|
|
578
|
+
|
|
579
|
+
### Query Pattern
|
|
580
|
+
|
|
581
|
+
```typescript
|
|
582
|
+
import { useQuery } from '@tanstack/react-query';
|
|
583
|
+
|
|
584
|
+
export function useDataQuery(id: string) {
|
|
585
|
+
return useQuery({
|
|
586
|
+
queryKey: ['data', id],
|
|
587
|
+
queryFn: () => fetchData(id),
|
|
588
|
+
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
589
|
+
gcTime: 10 * 60 * 1000, // 10 minutes (formerly cacheTime)
|
|
590
|
+
enabled: !!id, // Only run if id exists
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### Mutation Pattern
|
|
596
|
+
|
|
597
|
+
```typescript
|
|
598
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
599
|
+
import { toast } from 'sonner';
|
|
600
|
+
|
|
601
|
+
export function useCreateMutation() {
|
|
602
|
+
const queryClient = useQueryClient();
|
|
603
|
+
|
|
604
|
+
return useMutation({
|
|
605
|
+
mutationFn: createData,
|
|
606
|
+
onSuccess: () => {
|
|
607
|
+
queryClient.invalidateQueries({ queryKey: ['data'] });
|
|
608
|
+
toast.success('Created successfully');
|
|
609
|
+
},
|
|
610
|
+
onError: (error: Error) => {
|
|
611
|
+
toast.error(error.message);
|
|
612
|
+
},
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
### Optimistic Updates
|
|
618
|
+
|
|
619
|
+
```typescript
|
|
620
|
+
export function useUpdateMutation() {
|
|
621
|
+
const queryClient = useQueryClient();
|
|
622
|
+
|
|
623
|
+
return useMutation({
|
|
624
|
+
mutationFn: updateData,
|
|
625
|
+
onMutate: async (newData) => {
|
|
626
|
+
// Cancel outgoing queries
|
|
627
|
+
await queryClient.cancelQueries({ queryKey: ['data', newData.id] });
|
|
628
|
+
|
|
629
|
+
// Snapshot previous value
|
|
630
|
+
const previousData = queryClient.getQueryData(['data', newData.id]);
|
|
631
|
+
|
|
632
|
+
// Optimistically update
|
|
633
|
+
queryClient.setQueryData(['data', newData.id], newData);
|
|
634
|
+
|
|
635
|
+
return { previousData };
|
|
636
|
+
},
|
|
637
|
+
onError: (err, newData, context) => {
|
|
638
|
+
// Rollback on error
|
|
639
|
+
queryClient.setQueryData(['data', newData.id], context?.previousData);
|
|
640
|
+
},
|
|
641
|
+
onSettled: (data, error, variables) => {
|
|
642
|
+
queryClient.invalidateQueries({ queryKey: ['data', variables.id] });
|
|
643
|
+
},
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
---
|
|
649
|
+
|
|
650
|
+
## Form Patterns
|
|
651
|
+
|
|
652
|
+
Use react-hook-form with Zod validation.
|
|
653
|
+
|
|
654
|
+
### Basic Form Pattern
|
|
655
|
+
|
|
656
|
+
```tsx
|
|
657
|
+
'use client';
|
|
658
|
+
|
|
659
|
+
import { useForm } from 'react-hook-form';
|
|
660
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
661
|
+
import { z } from 'zod';
|
|
662
|
+
import { Form } from '@/components/ui/form/Form';
|
|
663
|
+
import { Input } from '@/components/ui/Input';
|
|
664
|
+
import { Button } from '@/components/ui/Button';
|
|
665
|
+
|
|
666
|
+
const schema = z.object({
|
|
667
|
+
email: z.string().email('Errors.validation.email'),
|
|
668
|
+
password: z.string().min(8, 'Errors.validation.minLength|{"min":8}'),
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
type FormData = z.infer<typeof schema>;
|
|
672
|
+
|
|
673
|
+
export function LoginForm() {
|
|
674
|
+
const form = useForm<FormData>({
|
|
675
|
+
resolver: zodResolver(schema),
|
|
676
|
+
defaultValues: {
|
|
677
|
+
email: '',
|
|
678
|
+
password: '',
|
|
679
|
+
},
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
const onSubmit = (data: FormData) => {
|
|
683
|
+
console.log(data);
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
return (
|
|
687
|
+
<Form form={form} validationSchema={schema}>
|
|
688
|
+
<Form.Item name="email">
|
|
689
|
+
{(props) => <Input {...props} label="Email" type="email" />}
|
|
690
|
+
</Form.Item>
|
|
691
|
+
|
|
692
|
+
<Form.Item name="password">
|
|
693
|
+
{(props) => <PasswordInput {...props} label="Password" />}
|
|
694
|
+
</Form.Item>
|
|
695
|
+
|
|
696
|
+
<Button onPress={form.handleSubmit(onSubmit)} isLoading={form.formState.isSubmitting}>
|
|
697
|
+
Login
|
|
698
|
+
</Button>
|
|
699
|
+
</Form>
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
### Form with Mutation
|
|
705
|
+
|
|
706
|
+
```tsx
|
|
707
|
+
import { useLoginMutation } from '@/services/auth';
|
|
708
|
+
|
|
709
|
+
export function LoginForm() {
|
|
710
|
+
const form = useForm<FormData>();
|
|
711
|
+
const loginMutation = useLoginMutation();
|
|
712
|
+
|
|
713
|
+
const onSubmit = (data: FormData) => {
|
|
714
|
+
loginMutation.mutate(data);
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
return (
|
|
718
|
+
<Form form={form}>
|
|
719
|
+
{/* Form fields */}
|
|
720
|
+
<Button onPress={form.handleSubmit(onSubmit)} isLoading={loginMutation.isPending}>
|
|
721
|
+
Login
|
|
722
|
+
</Button>
|
|
723
|
+
</Form>
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
---
|
|
729
|
+
|
|
730
|
+
## Validation Patterns
|
|
731
|
+
|
|
732
|
+
### Zod Schema Patterns
|
|
733
|
+
|
|
734
|
+
```typescript
|
|
735
|
+
import { z } from 'zod';
|
|
736
|
+
|
|
737
|
+
// Basic validations
|
|
738
|
+
const basicSchema = z.object({
|
|
739
|
+
email: z.string().email('Errors.validation.email'),
|
|
740
|
+
password: z.string().min(8, 'Errors.validation.minLength|{"min":8}'),
|
|
741
|
+
age: z.number().min(18, 'Errors.validation.minAge|{"age":18}'),
|
|
742
|
+
website: z.string().url('Errors.validation.url').optional(),
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
// Complex validations
|
|
746
|
+
const complexSchema = z.object({
|
|
747
|
+
password: z.string()
|
|
748
|
+
.min(8, 'Password must be at least 8 characters')
|
|
749
|
+
.regex(/[A-Z]/, 'Password must contain uppercase letter')
|
|
750
|
+
.regex(/[a-z]/, 'Password must contain lowercase letter')
|
|
751
|
+
.regex(/[0-9]/, 'Password must contain number'),
|
|
752
|
+
|
|
753
|
+
confirmPassword: z.string(),
|
|
754
|
+
}).refine((data) => data.password === data.confirmPassword, {
|
|
755
|
+
message: 'Passwords do not match',
|
|
756
|
+
path: ['confirmPassword'],
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
// Array validation
|
|
760
|
+
const arraySchema = z.object({
|
|
761
|
+
tags: z.array(z.string()).min(1, 'At least one tag required'),
|
|
762
|
+
items: z.array(z.object({
|
|
763
|
+
id: z.string(),
|
|
764
|
+
quantity: z.number().min(1),
|
|
765
|
+
})),
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
// Conditional validation
|
|
769
|
+
const conditionalSchema = z.object({
|
|
770
|
+
type: z.enum(['individual', 'company']),
|
|
771
|
+
companyName: z.string().optional(),
|
|
772
|
+
}).refine((data) => {
|
|
773
|
+
if (data.type === 'company') {
|
|
774
|
+
return !!data.companyName;
|
|
775
|
+
}
|
|
776
|
+
return true;
|
|
777
|
+
}, {
|
|
778
|
+
message: 'Company name is required',
|
|
779
|
+
path: ['companyName'],
|
|
780
|
+
});
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
---
|
|
784
|
+
|
|
785
|
+
## Internationalization (i18n)
|
|
786
|
+
|
|
787
|
+
Use next-intl for translations.
|
|
788
|
+
|
|
789
|
+
### Translation Files
|
|
790
|
+
|
|
791
|
+
**English:** `/src/locales/en.json`
|
|
792
|
+
**German:** `/src/locales/de.json`
|
|
793
|
+
|
|
794
|
+
```json
|
|
795
|
+
{
|
|
796
|
+
"Common": {
|
|
797
|
+
"submit": "Submit",
|
|
798
|
+
"cancel": "Cancel",
|
|
799
|
+
"save": "Save"
|
|
800
|
+
},
|
|
801
|
+
"Errors": {
|
|
802
|
+
"validation": {
|
|
803
|
+
"required": "This field is required",
|
|
804
|
+
"email": "Invalid email address",
|
|
805
|
+
"minLength": "Minimum {min} characters required"
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
### Using Translations
|
|
812
|
+
|
|
813
|
+
```tsx
|
|
814
|
+
'use client';
|
|
815
|
+
|
|
816
|
+
import { useTranslations } from 'next-intl';
|
|
817
|
+
|
|
818
|
+
export function MyComponent() {
|
|
819
|
+
const t = useTranslations('Common');
|
|
820
|
+
|
|
821
|
+
return (
|
|
822
|
+
<div>
|
|
823
|
+
<h1>{t('title')}</h1>
|
|
824
|
+
<Button>{t('submit')}</Button>
|
|
825
|
+
</div>
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
### Translation with Parameters
|
|
831
|
+
|
|
832
|
+
```tsx
|
|
833
|
+
const t = useTranslations('Errors.validation');
|
|
834
|
+
|
|
835
|
+
// Usage
|
|
836
|
+
t('minLength', { min: 8 }); // "Minimum 8 characters required"
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
---
|
|
840
|
+
|
|
841
|
+
## Authentication Patterns
|
|
842
|
+
|
|
843
|
+
### Auth Hook Pattern
|
|
844
|
+
|
|
845
|
+
```typescript
|
|
846
|
+
'use client';
|
|
847
|
+
|
|
848
|
+
import { useMutation } from '@tanstack/react-query';
|
|
849
|
+
import { useRouter } from 'next/navigation';
|
|
850
|
+
import { toast } from 'sonner';
|
|
851
|
+
|
|
852
|
+
export function useLoginMutation(redirectUrl?: string) {
|
|
853
|
+
const router = useRouter();
|
|
854
|
+
|
|
855
|
+
return useMutation({
|
|
856
|
+
mutationFn: loginAction,
|
|
857
|
+
onSuccess: async (result) => {
|
|
858
|
+
if (result.success) {
|
|
859
|
+
toast.success('Login successful');
|
|
860
|
+
|
|
861
|
+
// Wait for cookies to propagate
|
|
862
|
+
await new Promise(resolve => setTimeout(resolve, 800));
|
|
863
|
+
|
|
864
|
+
// Force reload for clean state
|
|
865
|
+
window.location.href = redirectUrl || '/dashboard';
|
|
866
|
+
} else {
|
|
867
|
+
toast.error(result.error);
|
|
868
|
+
}
|
|
869
|
+
},
|
|
870
|
+
onError: (error: Error) => {
|
|
871
|
+
toast.error(error.message);
|
|
872
|
+
},
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
---
|
|
878
|
+
|
|
879
|
+
## API Communication
|
|
880
|
+
|
|
881
|
+
### API Request Utility
|
|
882
|
+
|
|
883
|
+
```typescript
|
|
884
|
+
interface ApiRequestOptions {
|
|
885
|
+
url: string;
|
|
886
|
+
method: 'GET' | 'POST' | 'PATCH' | 'DELETE';
|
|
887
|
+
data?: unknown;
|
|
888
|
+
headers?: Record<string, string>;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
export async function apiRequest<T>({ url, method, data, headers }: ApiRequestOptions): Promise<T> {
|
|
892
|
+
const response = await fetch(url, {
|
|
893
|
+
method,
|
|
894
|
+
headers: {
|
|
895
|
+
'Content-Type': 'application/json',
|
|
896
|
+
...headers,
|
|
897
|
+
},
|
|
898
|
+
...(data && { body: JSON.stringify(data) }),
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
if (!response.ok) {
|
|
902
|
+
const error = await response.json();
|
|
903
|
+
throw new Error(error.message || 'API request failed');
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
return response.json();
|
|
907
|
+
}
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
---
|
|
911
|
+
|
|
912
|
+
## Summary
|
|
913
|
+
|
|
914
|
+
Follow these architectural patterns for:
|
|
915
|
+
|
|
916
|
+
1. **Consistency** - All features use the same structure
|
|
917
|
+
2. **Maintainability** - Easy to find and update code
|
|
918
|
+
3. **Type Safety** - Full TypeScript coverage
|
|
919
|
+
4. **Performance** - Optimized data fetching and caching
|
|
920
|
+
5. **Developer Experience** - Clear patterns and conventions
|
|
921
|
+
|
|
922
|
+
## Need More Information?
|
|
923
|
+
|
|
924
|
+
- **Quick Reference** → See `AI_QUICK_REFERENCE.md`
|
|
925
|
+
- **UI Components** → See `UI_COMPONENTS_GUIDE.md`
|
|
926
|
+
- **Implementation Guide** → See `IMPLEMENTATION_GUIDE.md`
|
|
927
|
+
- **Figma to Code** → See `FIGMA_TO_CODE_GUIDE.md`
|