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.
Files changed (42) hide show
  1. package/README.md +279 -0
  2. package/bin/cli.js +198 -0
  3. package/package.json +45 -0
  4. package/template/_env +1 -0
  5. package/template/_env.example +1 -0
  6. package/template/_gitignore +35 -0
  7. package/template/create-src-files.sh +785 -0
  8. package/template/setup-template.sh +277 -0
  9. package/template/src/app/App.tsx +25 -0
  10. package/template/src/domain/entities/User.ts +8 -0
  11. package/template/src/domain/repositories/UserRepository.ts +11 -0
  12. package/template/src/domain/types/.gitkeep +0 -0
  13. package/template/src/domain/usecases/users/CreateUserUseCase.ts +15 -0
  14. package/template/src/domain/usecases/users/GetAllUsersUseCase.ts +10 -0
  15. package/template/src/infrastructure/api/apiClient.ts +73 -0
  16. package/template/src/infrastructure/config/.gitkeep +0 -0
  17. package/template/src/infrastructure/data/.gitkeep +0 -0
  18. package/template/src/infrastructure/repositories/UserRepositoryImpl.ts +32 -0
  19. package/template/src/infrastructure/services/.gitkeep +0 -0
  20. package/template/src/lib/utils.ts +6 -0
  21. package/template/src/main.tsx +10 -0
  22. package/template/src/presentation/assets/logos/.gitkeep +0 -0
  23. package/template/src/presentation/components/layouts/.gitkeep +0 -0
  24. package/template/src/presentation/components/shared/.gitkeep +0 -0
  25. package/template/src/presentation/components/tables/.gitkeep +0 -0
  26. package/template/src/presentation/context/auth/AuthContext.tsx +84 -0
  27. package/template/src/presentation/context/auth/useAuthContext.tsx +12 -0
  28. package/template/src/presentation/pages/Index.tsx +76 -0
  29. package/template/src/presentation/pages/auth/LoginPage.tsx +39 -0
  30. package/template/src/presentation/routes/config/.gitkeep +0 -0
  31. package/template/src/presentation/routes/guards/.gitkeep +0 -0
  32. package/template/src/presentation/utils/.gitkeep +0 -0
  33. package/template/src/presentation/viewmodels/hooks/.gitkeep +0 -0
  34. package/template/src/shared/config/env.ts +9 -0
  35. package/template/src/shared/constants/index.ts +29 -0
  36. package/template/src/shared/hooks/.gitkeep +0 -0
  37. package/template/src/shared/lib/.gitkeep +0 -0
  38. package/template/src/shared/types/index.ts +42 -0
  39. package/template/src/shared/utils/format.ts +30 -0
  40. package/template/src/shared/utils/validation.ts +22 -0
  41. package/template/src/styles/index.css +59 -0
  42. 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
+