create-react-zr-architecture 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 +279 -0
- package/bin/cli.js +198 -0
- package/package.json +45 -0
- package/template/_env +1 -0
- package/template/_env.example +1 -0
- package/template/_gitignore +35 -0
- package/template/create-src-files.sh +785 -0
- package/template/setup-template.sh +277 -0
- package/template/src/app/App.tsx +25 -0
- package/template/src/domain/entities/User.ts +8 -0
- package/template/src/domain/repositories/UserRepository.ts +11 -0
- package/template/src/domain/types/.gitkeep +0 -0
- package/template/src/domain/usecases/users/CreateUserUseCase.ts +15 -0
- package/template/src/domain/usecases/users/GetAllUsersUseCase.ts +10 -0
- package/template/src/infrastructure/api/apiClient.ts +73 -0
- package/template/src/infrastructure/config/.gitkeep +0 -0
- package/template/src/infrastructure/data/.gitkeep +0 -0
- package/template/src/infrastructure/repositories/UserRepositoryImpl.ts +32 -0
- package/template/src/infrastructure/services/.gitkeep +0 -0
- package/template/src/lib/utils.ts +6 -0
- package/template/src/main.tsx +10 -0
- package/template/src/presentation/assets/logos/.gitkeep +0 -0
- package/template/src/presentation/components/layouts/.gitkeep +0 -0
- package/template/src/presentation/components/shared/.gitkeep +0 -0
- package/template/src/presentation/components/tables/.gitkeep +0 -0
- package/template/src/presentation/context/auth/AuthContext.tsx +84 -0
- package/template/src/presentation/context/auth/useAuthContext.tsx +12 -0
- package/template/src/presentation/pages/Index.tsx +76 -0
- package/template/src/presentation/pages/auth/LoginPage.tsx +39 -0
- package/template/src/presentation/routes/config/.gitkeep +0 -0
- package/template/src/presentation/routes/guards/.gitkeep +0 -0
- package/template/src/presentation/utils/.gitkeep +0 -0
- package/template/src/presentation/viewmodels/hooks/.gitkeep +0 -0
- package/template/src/shared/config/env.ts +9 -0
- package/template/src/shared/constants/index.ts +29 -0
- package/template/src/shared/hooks/.gitkeep +0 -0
- package/template/src/shared/lib/.gitkeep +0 -0
- package/template/src/shared/types/index.ts +42 -0
- package/template/src/shared/utils/format.ts +30 -0
- package/template/src/shared/utils/validation.ts +22 -0
- package/template/src/styles/index.css +59 -0
- package/template/src/vite-env.d.ts +10 -0
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# ════════════════════════════════════════════════════════════
|
|
4
|
+
# Mega Script: Crear TODOS los archivos src/ del template
|
|
5
|
+
# Uso: ./create-src-files.sh (ejecutar desde template/)
|
|
6
|
+
# ════════════════════════════════════════════════════════════
|
|
7
|
+
|
|
8
|
+
echo "🚀 Creando estructura completa de archivos src/..."
|
|
9
|
+
|
|
10
|
+
# ════════════════════════════════════════════════════════════
|
|
11
|
+
# Crear estructura de carpetas
|
|
12
|
+
# ════════════════════════════════════════════════════════════
|
|
13
|
+
mkdir -p src/app
|
|
14
|
+
mkdir -p src/domain/entities
|
|
15
|
+
mkdir -p src/domain/repositories
|
|
16
|
+
mkdir -p src/domain/usecases/users
|
|
17
|
+
mkdir -p src/domain/types
|
|
18
|
+
mkdir -p src/infrastructure/api
|
|
19
|
+
mkdir -p src/infrastructure/config
|
|
20
|
+
mkdir -p src/infrastructure/data
|
|
21
|
+
mkdir -p src/infrastructure/repositories
|
|
22
|
+
mkdir -p src/infrastructure/services
|
|
23
|
+
mkdir -p src/presentation/components/layouts
|
|
24
|
+
mkdir -p src/presentation/components/shared
|
|
25
|
+
mkdir -p src/presentation/components/tables
|
|
26
|
+
mkdir -p src/presentation/components/ui
|
|
27
|
+
mkdir -p src/presentation/context/auth
|
|
28
|
+
mkdir -p src/presentation/pages/auth
|
|
29
|
+
mkdir -p src/presentation/pages/management
|
|
30
|
+
mkdir -p src/presentation/routes/config
|
|
31
|
+
mkdir -p src/presentation/routes/guards
|
|
32
|
+
mkdir -p src/presentation/utils
|
|
33
|
+
mkdir -p src/presentation/viewmodels/hooks
|
|
34
|
+
mkdir -p src/presentation/assets/logos
|
|
35
|
+
mkdir -p src/shared/config
|
|
36
|
+
mkdir -p src/shared/constants
|
|
37
|
+
mkdir -p src/shared/hooks
|
|
38
|
+
mkdir -p src/shared/lib
|
|
39
|
+
mkdir -p src/shared/types
|
|
40
|
+
mkdir -p src/shared/utils
|
|
41
|
+
mkdir -p src/lib
|
|
42
|
+
mkdir -p src/styles
|
|
43
|
+
|
|
44
|
+
# ════════════════════════════════════════════════════════════
|
|
45
|
+
# STYLES
|
|
46
|
+
# ════════════════════════════════════════════════════════════
|
|
47
|
+
cat > src/styles/index.css << 'EOF'
|
|
48
|
+
@tailwind base;
|
|
49
|
+
@tailwind components;
|
|
50
|
+
@tailwind utilities;
|
|
51
|
+
|
|
52
|
+
@layer base {
|
|
53
|
+
:root {
|
|
54
|
+
--background: 0 0% 100%;
|
|
55
|
+
--foreground: 222.2 84% 4.9%;
|
|
56
|
+
--card: 0 0% 100%;
|
|
57
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
58
|
+
--popover: 0 0% 100%;
|
|
59
|
+
--popover-foreground: 222.2 84% 4.9%;
|
|
60
|
+
--primary: 222.2 47.4% 11.2%;
|
|
61
|
+
--primary-foreground: 210 40% 98%;
|
|
62
|
+
--secondary: 210 40% 96.1%;
|
|
63
|
+
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
64
|
+
--muted: 210 40% 96.1%;
|
|
65
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
66
|
+
--accent: 210 40% 96.1%;
|
|
67
|
+
--accent-foreground: 222.2 47.4% 11.2%;
|
|
68
|
+
--destructive: 0 84.2% 60.2%;
|
|
69
|
+
--destructive-foreground: 210 40% 98%;
|
|
70
|
+
--border: 214.3 31.8% 91.4%;
|
|
71
|
+
--input: 214.3 31.8% 91.4%;
|
|
72
|
+
--ring: 222.2 84% 4.9%;
|
|
73
|
+
--radius: 0.5rem;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.dark {
|
|
77
|
+
--background: 222.2 84% 4.9%;
|
|
78
|
+
--foreground: 210 40% 98%;
|
|
79
|
+
--card: 222.2 84% 4.9%;
|
|
80
|
+
--card-foreground: 210 40% 98%;
|
|
81
|
+
--popover: 222.2 84% 4.9%;
|
|
82
|
+
--popover-foreground: 210 40% 98%;
|
|
83
|
+
--primary: 210 40% 98%;
|
|
84
|
+
--primary-foreground: 222.2 47.4% 11.2%;
|
|
85
|
+
--secondary: 217.2 32.6% 17.5%;
|
|
86
|
+
--secondary-foreground: 210 40% 98%;
|
|
87
|
+
--muted: 217.2 32.6% 17.5%;
|
|
88
|
+
--muted-foreground: 215 20.2% 65.1%;
|
|
89
|
+
--accent: 217.2 32.6% 17.5%;
|
|
90
|
+
--accent-foreground: 210 40% 98%;
|
|
91
|
+
--destructive: 0 62.8% 30.6%;
|
|
92
|
+
--destructive-foreground: 210 40% 98%;
|
|
93
|
+
--border: 217.2 32.6% 17.5%;
|
|
94
|
+
--input: 217.2 32.6% 17.5%;
|
|
95
|
+
--ring: 212.7 26.8% 83.9%;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@layer base {
|
|
100
|
+
* {
|
|
101
|
+
@apply border-border;
|
|
102
|
+
}
|
|
103
|
+
body {
|
|
104
|
+
@apply bg-background text-foreground;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
EOF
|
|
108
|
+
|
|
109
|
+
# ════════════════════════════════════════════════════════════
|
|
110
|
+
# LIB
|
|
111
|
+
# ════════════════════════════════════════════════════════════
|
|
112
|
+
cat > src/lib/utils.ts << 'EOF'
|
|
113
|
+
import { type ClassValue, clsx } from "clsx"
|
|
114
|
+
import { twMerge } from "tailwind-merge"
|
|
115
|
+
|
|
116
|
+
export function cn(...inputs: ClassValue[]) {
|
|
117
|
+
return twMerge(clsx(inputs))
|
|
118
|
+
}
|
|
119
|
+
EOF
|
|
120
|
+
|
|
121
|
+
# ════════════════════════════════════════════════════════════
|
|
122
|
+
# SHARED - Types
|
|
123
|
+
# ════════════════════════════════════════════════════════════
|
|
124
|
+
cat > src/shared/types/index.ts << 'EOF'
|
|
125
|
+
// ════════════════════════════════════════════════════════════
|
|
126
|
+
// Common Types
|
|
127
|
+
// ════════════════════════════════════════════════════════════
|
|
128
|
+
|
|
129
|
+
export interface BaseEntity {
|
|
130
|
+
id: string;
|
|
131
|
+
createdAt: Date;
|
|
132
|
+
updatedAt: Date;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface PaginatedResponse<T> {
|
|
136
|
+
data: T[];
|
|
137
|
+
total: number;
|
|
138
|
+
page: number;
|
|
139
|
+
pageSize: number;
|
|
140
|
+
totalPages: number;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface ApiResponse<T> {
|
|
144
|
+
success: boolean;
|
|
145
|
+
data?: T;
|
|
146
|
+
error?: string;
|
|
147
|
+
message?: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export type AsyncState<T> = {
|
|
151
|
+
data: T | null;
|
|
152
|
+
loading: boolean;
|
|
153
|
+
error: string | null;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export interface TableColumn<T> {
|
|
157
|
+
key: keyof T | string;
|
|
158
|
+
label: string;
|
|
159
|
+
sortable?: boolean;
|
|
160
|
+
render?: (value: any, row: T) => React.ReactNode;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface SelectOption {
|
|
164
|
+
value: string;
|
|
165
|
+
label: string;
|
|
166
|
+
}
|
|
167
|
+
EOF
|
|
168
|
+
|
|
169
|
+
# ════════════════════════════════════════════════════════════
|
|
170
|
+
# SHARED - Config
|
|
171
|
+
# ════════════════════════════════════════════════════════════
|
|
172
|
+
cat > src/shared/config/env.ts << 'EOF'
|
|
173
|
+
// ════════════════════════════════════════════════════════════
|
|
174
|
+
// Environment Variables
|
|
175
|
+
// ════════════════════════════════════════════════════════════
|
|
176
|
+
|
|
177
|
+
export const env = {
|
|
178
|
+
apiUrl: import.meta.env.VITE_API_URL || 'http://localhost:3000/api',
|
|
179
|
+
isDevelopment: import.meta.env.DEV,
|
|
180
|
+
isProduction: import.meta.env.PROD,
|
|
181
|
+
} as const;
|
|
182
|
+
EOF
|
|
183
|
+
|
|
184
|
+
# ════════════════════════════════════════════════════════════
|
|
185
|
+
# SHARED - Utils
|
|
186
|
+
# ════════════════════════════════════════════════════════════
|
|
187
|
+
cat > src/shared/utils/format.ts << 'EOF'
|
|
188
|
+
// ════════════════════════════════════════════════════════════
|
|
189
|
+
// Format Utilities
|
|
190
|
+
// ════════════════════════════════════════════════════════════
|
|
191
|
+
|
|
192
|
+
export const formatCurrency = (amount: number, currency = 'USD'): string => {
|
|
193
|
+
return new Intl.NumberFormat('en-US', {
|
|
194
|
+
style: 'currency',
|
|
195
|
+
currency,
|
|
196
|
+
}).format(amount);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
export const formatDate = (date: Date | string, format = 'short'): string => {
|
|
200
|
+
const d = typeof date === 'string' ? new Date(date) : date;
|
|
201
|
+
|
|
202
|
+
const options: Intl.DateTimeFormatOptions =
|
|
203
|
+
format === 'short'
|
|
204
|
+
? { year: 'numeric', month: 'short', day: 'numeric' }
|
|
205
|
+
: { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' };
|
|
206
|
+
|
|
207
|
+
return new Intl.DateTimeFormat('en-US', options).format(d);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
export const formatNumber = (num: number): string => {
|
|
211
|
+
return new Intl.NumberFormat('en-US').format(num);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
export const truncate = (str: string, length: number): string => {
|
|
215
|
+
if (str.length <= length) return str;
|
|
216
|
+
return str.substring(0, length) + '...';
|
|
217
|
+
};
|
|
218
|
+
EOF
|
|
219
|
+
|
|
220
|
+
cat > src/shared/utils/validation.ts << 'EOF'
|
|
221
|
+
// ════════════════════════════════════════════════════════════
|
|
222
|
+
// Validation Utilities
|
|
223
|
+
// ════════════════════════════════════════════════════════════
|
|
224
|
+
|
|
225
|
+
export const isValidEmail = (email: string): boolean => {
|
|
226
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
227
|
+
return emailRegex.test(email);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
export const isValidPhone = (phone: string): boolean => {
|
|
231
|
+
const phoneRegex = /^\+?[\d\s-()]+$/;
|
|
232
|
+
return phoneRegex.test(phone);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
export const isValidUrl = (url: string): boolean => {
|
|
236
|
+
try {
|
|
237
|
+
new URL(url);
|
|
238
|
+
return true;
|
|
239
|
+
} catch {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
EOF
|
|
244
|
+
|
|
245
|
+
# ════════════════════════════════════════════════════════════
|
|
246
|
+
# SHARED - Constants
|
|
247
|
+
# ════════════════════════════════════════════════════════════
|
|
248
|
+
cat > src/shared/constants/index.ts << 'EOF'
|
|
249
|
+
// ════════════════════════════════════════════════════════════
|
|
250
|
+
// Application Constants
|
|
251
|
+
// ════════════════════════════════════════════════════════════
|
|
252
|
+
|
|
253
|
+
export const APP_NAME = 'Clean Architecture App';
|
|
254
|
+
export const APP_VERSION = '1.0.0';
|
|
255
|
+
|
|
256
|
+
export const STORAGE_KEYS = {
|
|
257
|
+
TOKEN: 'auth_token',
|
|
258
|
+
USER: 'user_data',
|
|
259
|
+
THEME: 'app_theme',
|
|
260
|
+
} as const;
|
|
261
|
+
|
|
262
|
+
export const ROUTES = {
|
|
263
|
+
HOME: '/',
|
|
264
|
+
LOGIN: '/login',
|
|
265
|
+
DASHBOARD: '/dashboard',
|
|
266
|
+
SETTINGS: '/settings',
|
|
267
|
+
} as const;
|
|
268
|
+
|
|
269
|
+
export const HTTP_STATUS = {
|
|
270
|
+
OK: 200,
|
|
271
|
+
CREATED: 201,
|
|
272
|
+
BAD_REQUEST: 400,
|
|
273
|
+
UNAUTHORIZED: 401,
|
|
274
|
+
FORBIDDEN: 403,
|
|
275
|
+
NOT_FOUND: 404,
|
|
276
|
+
INTERNAL_SERVER_ERROR: 500,
|
|
277
|
+
} as const;
|
|
278
|
+
EOF
|
|
279
|
+
|
|
280
|
+
# ════════════════════════════════════════════════════════════
|
|
281
|
+
# INFRASTRUCTURE - API Client
|
|
282
|
+
# ════════════════════════════════════════════════════════════
|
|
283
|
+
cat > src/infrastructure/api/apiClient.ts << 'EOF'
|
|
284
|
+
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
|
285
|
+
import { env } from '@/shared/config/env';
|
|
286
|
+
|
|
287
|
+
class ApiClient {
|
|
288
|
+
private client: AxiosInstance;
|
|
289
|
+
|
|
290
|
+
constructor(baseURL: string) {
|
|
291
|
+
this.client = axios.create({
|
|
292
|
+
baseURL,
|
|
293
|
+
timeout: 30000,
|
|
294
|
+
headers: {
|
|
295
|
+
'Content-Type': 'application/json',
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
this.setupInterceptors();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private setupInterceptors() {
|
|
303
|
+
// Request interceptor
|
|
304
|
+
this.client.interceptors.request.use(
|
|
305
|
+
(config) => {
|
|
306
|
+
const token = localStorage.getItem('token');
|
|
307
|
+
if (token) {
|
|
308
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
309
|
+
}
|
|
310
|
+
return config;
|
|
311
|
+
},
|
|
312
|
+
(error) => {
|
|
313
|
+
return Promise.reject(error);
|
|
314
|
+
}
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
// Response interceptor
|
|
318
|
+
this.client.interceptors.response.use(
|
|
319
|
+
(response) => response,
|
|
320
|
+
(error) => {
|
|
321
|
+
if (error.response?.status === 401) {
|
|
322
|
+
localStorage.removeItem('token');
|
|
323
|
+
window.location.href = '/login';
|
|
324
|
+
}
|
|
325
|
+
return Promise.reject(error);
|
|
326
|
+
}
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
|
331
|
+
const response: AxiosResponse<T> = await this.client.get(url, config);
|
|
332
|
+
return response.data;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async post<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
|
336
|
+
const response: AxiosResponse<T> = await this.client.post(url, data, config);
|
|
337
|
+
return response.data;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async put<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
|
341
|
+
const response: AxiosResponse<T> = await this.client.put(url, data, config);
|
|
342
|
+
return response.data;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async patch<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
|
346
|
+
const response: AxiosResponse<T> = await this.client.patch(url, data, config);
|
|
347
|
+
return response.data;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
|
351
|
+
const response: AxiosResponse<T> = await this.client.delete(url, config);
|
|
352
|
+
return response.data;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export const apiClient = new ApiClient(env.apiUrl);
|
|
357
|
+
EOF
|
|
358
|
+
|
|
359
|
+
# ════════════════════════════════════════════════════════════
|
|
360
|
+
# DOMAIN - Entities Example
|
|
361
|
+
# ════════════════════════════════════════════════════════════
|
|
362
|
+
cat > src/domain/entities/User.ts << 'EOF'
|
|
363
|
+
import { BaseEntity } from '@/shared/types';
|
|
364
|
+
|
|
365
|
+
export interface User extends BaseEntity {
|
|
366
|
+
email: string;
|
|
367
|
+
nombre: string;
|
|
368
|
+
rol: string;
|
|
369
|
+
activo: boolean;
|
|
370
|
+
}
|
|
371
|
+
EOF
|
|
372
|
+
|
|
373
|
+
# ════════════════════════════════════════════════════════════
|
|
374
|
+
# DOMAIN - Repository Interface Example
|
|
375
|
+
# ════════════════════════════════════════════════════════════
|
|
376
|
+
cat > src/domain/repositories/UserRepository.ts << 'EOF'
|
|
377
|
+
import { User } from '@/domain/entities/User';
|
|
378
|
+
import { PaginatedResponse } from '@/shared/types';
|
|
379
|
+
|
|
380
|
+
export interface UserRepository {
|
|
381
|
+
getAll(): Promise<User[]>;
|
|
382
|
+
getById(id: string): Promise<User>;
|
|
383
|
+
create(user: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<User>;
|
|
384
|
+
update(id: string, user: Partial<User>): Promise<User>;
|
|
385
|
+
delete(id: string): Promise<void>;
|
|
386
|
+
getPaginated(page: number, pageSize: number): Promise<PaginatedResponse<User>>;
|
|
387
|
+
}
|
|
388
|
+
EOF
|
|
389
|
+
|
|
390
|
+
# ════════════════════════════════════════════════════════════
|
|
391
|
+
# INFRASTRUCTURE - Repository Implementation Example
|
|
392
|
+
# ════════════════════════════════════════════════════════════
|
|
393
|
+
cat > src/infrastructure/repositories/UserRepositoryImpl.ts << 'EOF'
|
|
394
|
+
import { UserRepository } from '@/domain/repositories/UserRepository';
|
|
395
|
+
import { User } from '@/domain/entities/User';
|
|
396
|
+
import { PaginatedResponse } from '@/shared/types';
|
|
397
|
+
import { apiClient } from '@/infrastructure/api/apiClient';
|
|
398
|
+
|
|
399
|
+
export class UserRepositoryImpl implements UserRepository {
|
|
400
|
+
async getAll(): Promise<User[]> {
|
|
401
|
+
return apiClient.get<User[]>('/users');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async getById(id: string): Promise<User> {
|
|
405
|
+
return apiClient.get<User>(`/users/${id}`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async create(user: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<User> {
|
|
409
|
+
return apiClient.post<User>('/users', user);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async update(id: string, user: Partial<User>): Promise<User> {
|
|
413
|
+
return apiClient.put<User>(`/users/${id}`, user);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async delete(id: string): Promise<void> {
|
|
417
|
+
return apiClient.delete<void>(`/users/${id}`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async getPaginated(page: number, pageSize: number): Promise<PaginatedResponse<User>> {
|
|
421
|
+
return apiClient.get<PaginatedResponse<User>>(`/users?page=${page}&pageSize=${pageSize}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export const userRepository = new UserRepositoryImpl();
|
|
426
|
+
EOF
|
|
427
|
+
|
|
428
|
+
# ════════════════════════════════════════════════════════════
|
|
429
|
+
# DOMAIN - Use Case Example
|
|
430
|
+
# ════════════════════════════════════════════════════════════
|
|
431
|
+
cat > src/domain/usecases/users/GetAllUsersUseCase.ts << 'EOF'
|
|
432
|
+
import { UserRepository } from '@/domain/repositories/UserRepository';
|
|
433
|
+
import { User } from '@/domain/entities/User';
|
|
434
|
+
|
|
435
|
+
export class GetAllUsersUseCase {
|
|
436
|
+
constructor(private userRepository: UserRepository) {}
|
|
437
|
+
|
|
438
|
+
async execute(): Promise<User[]> {
|
|
439
|
+
return this.userRepository.getAll();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
EOF
|
|
443
|
+
|
|
444
|
+
cat > src/domain/usecases/users/CreateUserUseCase.ts << 'EOF'
|
|
445
|
+
import { UserRepository } from '@/domain/repositories/UserRepository';
|
|
446
|
+
import { User } from '@/domain/entities/User';
|
|
447
|
+
|
|
448
|
+
export class CreateUserUseCase {
|
|
449
|
+
constructor(private userRepository: UserRepository) {}
|
|
450
|
+
|
|
451
|
+
async execute(userData: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<User> {
|
|
452
|
+
// Aquí puedes agregar validaciones de negocio
|
|
453
|
+
if (!userData.email || !userData.nombre) {
|
|
454
|
+
throw new Error('Email y nombre son requeridos');
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return this.userRepository.create(userData);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
EOF
|
|
461
|
+
|
|
462
|
+
# ════════════════════════════════════════════════════════════
|
|
463
|
+
# PRESENTATION - Auth Context
|
|
464
|
+
# ════════════════════════════════════════════════════════════
|
|
465
|
+
cat > src/presentation/context/auth/AuthContext.tsx << 'EOF'
|
|
466
|
+
import { createContext, useReducer, ReactNode, useEffect } from 'react';
|
|
467
|
+
|
|
468
|
+
interface User {
|
|
469
|
+
id: string;
|
|
470
|
+
email: string;
|
|
471
|
+
nombre: string;
|
|
472
|
+
rol: string;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
interface AuthState {
|
|
476
|
+
user: User | null;
|
|
477
|
+
token: string | null;
|
|
478
|
+
isAuthenticated: boolean;
|
|
479
|
+
isLoading: boolean;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
type AuthAction =
|
|
483
|
+
| { type: 'LOGIN'; payload: { user: User; token: string } }
|
|
484
|
+
| { type: 'LOGOUT' }
|
|
485
|
+
| { type: 'SET_LOADING'; payload: boolean };
|
|
486
|
+
|
|
487
|
+
const initialState: AuthState = {
|
|
488
|
+
user: null,
|
|
489
|
+
token: localStorage.getItem('token'),
|
|
490
|
+
isAuthenticated: false,
|
|
491
|
+
isLoading: true,
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const authReducer = (state: AuthState, action: AuthAction): AuthState => {
|
|
495
|
+
switch (action.type) {
|
|
496
|
+
case 'LOGIN':
|
|
497
|
+
localStorage.setItem('token', action.payload.token);
|
|
498
|
+
return {
|
|
499
|
+
...state,
|
|
500
|
+
user: action.payload.user,
|
|
501
|
+
token: action.payload.token,
|
|
502
|
+
isAuthenticated: true,
|
|
503
|
+
isLoading: false,
|
|
504
|
+
};
|
|
505
|
+
case 'LOGOUT':
|
|
506
|
+
localStorage.removeItem('token');
|
|
507
|
+
return {
|
|
508
|
+
...state,
|
|
509
|
+
user: null,
|
|
510
|
+
token: null,
|
|
511
|
+
isAuthenticated: false,
|
|
512
|
+
isLoading: false,
|
|
513
|
+
};
|
|
514
|
+
case 'SET_LOADING':
|
|
515
|
+
return {
|
|
516
|
+
...state,
|
|
517
|
+
isLoading: action.payload,
|
|
518
|
+
};
|
|
519
|
+
default:
|
|
520
|
+
return state;
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
export const AuthContext = createContext<{
|
|
525
|
+
state: AuthState;
|
|
526
|
+
dispatch: React.Dispatch<AuthAction>;
|
|
527
|
+
} | null>(null);
|
|
528
|
+
|
|
529
|
+
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
|
530
|
+
const [state, dispatch] = useReducer(authReducer, initialState);
|
|
531
|
+
|
|
532
|
+
useEffect(() => {
|
|
533
|
+
// Aquí podrías verificar el token al cargar la app
|
|
534
|
+
const token = localStorage.getItem('token');
|
|
535
|
+
if (token) {
|
|
536
|
+
// Verificar token con el backend
|
|
537
|
+
// Por ahora solo marcamos como no loading
|
|
538
|
+
dispatch({ type: 'SET_LOADING', payload: false });
|
|
539
|
+
} else {
|
|
540
|
+
dispatch({ type: 'SET_LOADING', payload: false });
|
|
541
|
+
}
|
|
542
|
+
}, []);
|
|
543
|
+
|
|
544
|
+
return (
|
|
545
|
+
<AuthContext.Provider value={{ state, dispatch }}>
|
|
546
|
+
{children}
|
|
547
|
+
</AuthContext.Provider>
|
|
548
|
+
);
|
|
549
|
+
};
|
|
550
|
+
EOF
|
|
551
|
+
|
|
552
|
+
cat > src/presentation/context/auth/useAuthContext.tsx << 'EOF'
|
|
553
|
+
import { useContext } from 'react';
|
|
554
|
+
import { AuthContext } from './AuthContext';
|
|
555
|
+
|
|
556
|
+
export const useAuthContext = () => {
|
|
557
|
+
const context = useContext(AuthContext);
|
|
558
|
+
|
|
559
|
+
if (!context) {
|
|
560
|
+
throw new Error('useAuthContext debe ser usado dentro de AuthProvider');
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return context;
|
|
564
|
+
};
|
|
565
|
+
EOF
|
|
566
|
+
|
|
567
|
+
# ════════════════════════════════════════════════════════════
|
|
568
|
+
# PRESENTATION - Pages
|
|
569
|
+
# ════════════════════════════════════════════════════════════
|
|
570
|
+
cat > src/presentation/pages/Index.tsx << 'EOF'
|
|
571
|
+
export const IndexPage = () => {
|
|
572
|
+
return (
|
|
573
|
+
<div className="min-h-screen flex items-center justify-center bg-background">
|
|
574
|
+
<div className="text-center space-y-6 p-8 max-w-4xl">
|
|
575
|
+
<h1 className="text-5xl font-bold text-foreground bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">
|
|
576
|
+
React Clean Architecture
|
|
577
|
+
</h1>
|
|
578
|
+
<p className="text-xl text-muted-foreground">
|
|
579
|
+
Template profesional con TypeScript, Vite, Tailwind CSS y shadcn/ui
|
|
580
|
+
</p>
|
|
581
|
+
|
|
582
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-12">
|
|
583
|
+
{/* Configurado */}
|
|
584
|
+
<div className="p-6 border rounded-lg bg-card hover:shadow-lg transition-shadow">
|
|
585
|
+
<div className="flex items-center gap-2 mb-4">
|
|
586
|
+
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
|
587
|
+
<h3 className="font-semibold text-lg">✅ Configurado</h3>
|
|
588
|
+
</div>
|
|
589
|
+
<ul className="text-left space-y-2 text-sm text-muted-foreground">
|
|
590
|
+
<li>• React Hook Form + Zod</li>
|
|
591
|
+
<li>• TanStack Query</li>
|
|
592
|
+
<li>• Zustand (state)</li>
|
|
593
|
+
<li>• Path Aliases (@/*)</li>
|
|
594
|
+
<li>• Tailwind + shadcn/ui</li>
|
|
595
|
+
<li>• Axios + Socket.io</li>
|
|
596
|
+
</ul>
|
|
597
|
+
</div>
|
|
598
|
+
|
|
599
|
+
{/* Estructura */}
|
|
600
|
+
<div className="p-6 border rounded-lg bg-card hover:shadow-lg transition-shadow">
|
|
601
|
+
<div className="flex items-center gap-2 mb-4">
|
|
602
|
+
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
|
603
|
+
<h3 className="font-semibold text-lg">📁 Clean Architecture</h3>
|
|
604
|
+
</div>
|
|
605
|
+
<ul className="text-left space-y-2 text-sm text-muted-foreground">
|
|
606
|
+
<li>• <span className="font-mono">domain/</span> (entities, usecases)</li>
|
|
607
|
+
<li>• <span className="font-mono">infrastructure/</span> (api, repos)</li>
|
|
608
|
+
<li>• <span className="font-mono">presentation/</span> (UI, pages)</li>
|
|
609
|
+
<li>• <span className="font-mono">shared/</span> (utils, types)</li>
|
|
610
|
+
</ul>
|
|
611
|
+
</div>
|
|
612
|
+
</div>
|
|
613
|
+
|
|
614
|
+
{/* Próximos pasos */}
|
|
615
|
+
<div className="mt-12 p-6 border rounded-lg bg-muted/50">
|
|
616
|
+
<h3 className="font-semibold mb-4">🚀 Próximos pasos</h3>
|
|
617
|
+
<div className="space-y-3 text-left">
|
|
618
|
+
<div>
|
|
619
|
+
<p className="text-sm text-muted-foreground mb-1">1. Agregar componentes shadcn:</p>
|
|
620
|
+
<code className="block p-3 bg-background rounded text-sm font-mono">
|
|
621
|
+
npx shadcn-ui@latest add button card form table
|
|
622
|
+
</code>
|
|
623
|
+
</div>
|
|
624
|
+
<div>
|
|
625
|
+
<p className="text-sm text-muted-foreground mb-1">2. Crear tus entidades en:</p>
|
|
626
|
+
<code className="block p-3 bg-background rounded text-sm font-mono">
|
|
627
|
+
src/domain/entities/
|
|
628
|
+
</code>
|
|
629
|
+
</div>
|
|
630
|
+
<div>
|
|
631
|
+
<p className="text-sm text-muted-foreground mb-1">3. Implementar tus use cases:</p>
|
|
632
|
+
<code className="block p-3 bg-background rounded text-sm font-mono">
|
|
633
|
+
src/domain/usecases/
|
|
634
|
+
</code>
|
|
635
|
+
</div>
|
|
636
|
+
</div>
|
|
637
|
+
</div>
|
|
638
|
+
|
|
639
|
+
<div className="mt-8 text-sm text-muted-foreground">
|
|
640
|
+
<p>📚 Documentación completa en <span className="font-mono">README.md</span></p>
|
|
641
|
+
<p className="mt-2">Creado con ❤️ usando Clean Architecture</p>
|
|
642
|
+
</div>
|
|
643
|
+
</div>
|
|
644
|
+
</div>
|
|
645
|
+
);
|
|
646
|
+
};
|
|
647
|
+
EOF
|
|
648
|
+
|
|
649
|
+
cat > src/presentation/pages/auth/LoginPage.tsx << 'EOF'
|
|
650
|
+
import { useAuthContext } from '@/presentation/context/auth/useAuthContext';
|
|
651
|
+
|
|
652
|
+
export const LoginPage = () => {
|
|
653
|
+
const { dispatch } = useAuthContext();
|
|
654
|
+
|
|
655
|
+
const handleLogin = () => {
|
|
656
|
+
// Ejemplo de login
|
|
657
|
+
dispatch({
|
|
658
|
+
type: 'LOGIN',
|
|
659
|
+
payload: {
|
|
660
|
+
user: {
|
|
661
|
+
id: '1',
|
|
662
|
+
email: 'demo@example.com',
|
|
663
|
+
nombre: 'Usuario Demo',
|
|
664
|
+
rol: 'admin',
|
|
665
|
+
},
|
|
666
|
+
token: 'demo-token-123',
|
|
667
|
+
},
|
|
668
|
+
});
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
return (
|
|
672
|
+
<div className="min-h-screen flex items-center justify-center bg-background">
|
|
673
|
+
<div className="w-full max-w-md p-8 space-y-6 border rounded-lg bg-card">
|
|
674
|
+
<div className="text-center">
|
|
675
|
+
<h1 className="text-3xl font-bold">Iniciar Sesión</h1>
|
|
676
|
+
<p className="text-muted-foreground mt-2">Template de autenticación</p>
|
|
677
|
+
</div>
|
|
678
|
+
|
|
679
|
+
<button
|
|
680
|
+
onClick={handleLogin}
|
|
681
|
+
className="w-full p-3 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
|
|
682
|
+
>
|
|
683
|
+
Login Demo
|
|
684
|
+
</button>
|
|
685
|
+
</div>
|
|
686
|
+
</div>
|
|
687
|
+
);
|
|
688
|
+
};
|
|
689
|
+
EOF
|
|
690
|
+
|
|
691
|
+
# ════════════════════════════════════════════════════════════
|
|
692
|
+
# APP
|
|
693
|
+
# ════════════════════════════════════════════════════════════
|
|
694
|
+
cat > src/app/App.tsx << 'EOF'
|
|
695
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
696
|
+
import { AuthProvider } from '@/presentation/context/auth/AuthContext';
|
|
697
|
+
import { IndexPage } from '@/presentation/pages/Index';
|
|
698
|
+
|
|
699
|
+
const queryClient = new QueryClient({
|
|
700
|
+
defaultOptions: {
|
|
701
|
+
queries: {
|
|
702
|
+
refetchOnWindowFocus: false,
|
|
703
|
+
retry: 1,
|
|
704
|
+
staleTime: 5 * 60 * 1000, // 5 minutos
|
|
705
|
+
},
|
|
706
|
+
},
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
function App() {
|
|
710
|
+
return (
|
|
711
|
+
<QueryClientProvider client={queryClient}>
|
|
712
|
+
<AuthProvider>
|
|
713
|
+
<IndexPage />
|
|
714
|
+
</AuthProvider>
|
|
715
|
+
</QueryClientProvider>
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
export default App;
|
|
720
|
+
EOF
|
|
721
|
+
|
|
722
|
+
# ════════════════════════════════════════════════════════════
|
|
723
|
+
# MAIN
|
|
724
|
+
# ════════════════════════════════════════════════════════════
|
|
725
|
+
cat > src/main.tsx << 'EOF'
|
|
726
|
+
import { StrictMode } from 'react'
|
|
727
|
+
import { createRoot } from 'react-dom/client'
|
|
728
|
+
import '@/styles/index.css'
|
|
729
|
+
import App from '@/app/App.tsx'
|
|
730
|
+
|
|
731
|
+
createRoot(document.getElementById('root')!).render(
|
|
732
|
+
<StrictMode>
|
|
733
|
+
<App />
|
|
734
|
+
</StrictMode>,
|
|
735
|
+
)
|
|
736
|
+
EOF
|
|
737
|
+
|
|
738
|
+
# ════════════════════════════════════════════════════════════
|
|
739
|
+
# VITE ENV
|
|
740
|
+
# ════════════════════════════════════════════════════════════
|
|
741
|
+
cat > src/vite-env.d.ts << 'EOF'
|
|
742
|
+
/// <reference types="vite/client" />
|
|
743
|
+
|
|
744
|
+
interface ImportMetaEnv {
|
|
745
|
+
readonly VITE_API_URL: string
|
|
746
|
+
// Agrega más variables aquí
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
interface ImportMeta {
|
|
750
|
+
readonly env: ImportMetaEnv
|
|
751
|
+
}
|
|
752
|
+
EOF
|
|
753
|
+
|
|
754
|
+
# ════════════════════════════════════════════════════════════
|
|
755
|
+
# .gitkeep files
|
|
756
|
+
# ════════════════════════════════════════════════════════════
|
|
757
|
+
touch src/domain/types/.gitkeep
|
|
758
|
+
touch src/infrastructure/config/.gitkeep
|
|
759
|
+
touch src/infrastructure/data/.gitkeep
|
|
760
|
+
touch src/infrastructure/services/.gitkeep
|
|
761
|
+
touch src/presentation/components/layouts/.gitkeep
|
|
762
|
+
touch src/presentation/components/shared/.gitkeep
|
|
763
|
+
touch src/presentation/components/tables/.gitkeep
|
|
764
|
+
touch src/presentation/routes/config/.gitkeep
|
|
765
|
+
touch src/presentation/routes/guards/.gitkeep
|
|
766
|
+
touch src/presentation/viewmodels/hooks/.gitkeep
|
|
767
|
+
touch src/presentation/assets/logos/.gitkeep
|
|
768
|
+
touch src/presentation/utils/.gitkeep
|
|
769
|
+
touch src/shared/hooks/.gitkeep
|
|
770
|
+
touch src/shared/lib/.gitkeep
|
|
771
|
+
|
|
772
|
+
echo ""
|
|
773
|
+
echo "✅ ¡Todos los archivos src/ creados exitosamente!"
|
|
774
|
+
echo ""
|
|
775
|
+
echo "📁 Estructura creada:"
|
|
776
|
+
echo " ✅ src/app/ (App.tsx)"
|
|
777
|
+
echo " ✅ src/domain/ (entities, repositories, usecases)"
|
|
778
|
+
echo " ✅ src/infrastructure/ (api, repositories)"
|
|
779
|
+
echo " ✅ src/presentation/ (context, pages)"
|
|
780
|
+
echo " ✅ src/shared/ (types, utils, config, constants)"
|
|
781
|
+
echo " ✅ src/lib/ (utils.ts)"
|
|
782
|
+
echo " ✅ src/styles/ (index.css)"
|
|
783
|
+
echo " ✅ src/main.tsx"
|
|
784
|
+
echo ""
|
|
785
|
+
|