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,277 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# ════════════════════════════════════════════════════════════
|
|
4
|
+
# Script para crear archivos del template
|
|
5
|
+
# ════════════════════════════════════════════════════════════
|
|
6
|
+
|
|
7
|
+
# _gitignore
|
|
8
|
+
cat > _gitignore << 'EOF'
|
|
9
|
+
# Logs
|
|
10
|
+
logs
|
|
11
|
+
*.log
|
|
12
|
+
npm-debug.log*
|
|
13
|
+
yarn-debug.log*
|
|
14
|
+
yarn-error.log*
|
|
15
|
+
pnpm-debug.log*
|
|
16
|
+
lerna-debug.log*
|
|
17
|
+
|
|
18
|
+
node_modules
|
|
19
|
+
dist
|
|
20
|
+
dist-ssr
|
|
21
|
+
*.local
|
|
22
|
+
|
|
23
|
+
# Editor directories and files
|
|
24
|
+
.vscode/*
|
|
25
|
+
!.vscode/extensions.json
|
|
26
|
+
.idea
|
|
27
|
+
.DS_Store
|
|
28
|
+
*.suo
|
|
29
|
+
*.ntvs*
|
|
30
|
+
*.njsproj
|
|
31
|
+
*.sln
|
|
32
|
+
*.sw?
|
|
33
|
+
|
|
34
|
+
# Environment
|
|
35
|
+
.env.local
|
|
36
|
+
.env.development.local
|
|
37
|
+
.env.test.local
|
|
38
|
+
.env.production.local
|
|
39
|
+
|
|
40
|
+
# Database
|
|
41
|
+
*.db
|
|
42
|
+
*.sqlite
|
|
43
|
+
*.sqlite3
|
|
44
|
+
EOF
|
|
45
|
+
|
|
46
|
+
# _env
|
|
47
|
+
cat > _env << 'EOF'
|
|
48
|
+
VITE_API_URL=http://localhost:3000/api
|
|
49
|
+
EOF
|
|
50
|
+
|
|
51
|
+
# _env.example
|
|
52
|
+
cat > _env.example << 'EOF'
|
|
53
|
+
VITE_API_URL=http://localhost:3000/api
|
|
54
|
+
EOF
|
|
55
|
+
|
|
56
|
+
# components.json
|
|
57
|
+
cat te",
|
|
58
|
+
"cssVariables": true
|
|
59
|
+
},
|
|
60
|
+
"aliases": {
|
|
61
|
+
"components": "@/presentation/components",
|
|
62
|
+
"utils": "@/lib/utils",
|
|
63
|
+
"ui": "@/presentation/components/ui",
|
|
64
|
+
"lib": "@/lib",
|
|
65
|
+
"hooks": "@/shared/hooks"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
EOF
|
|
69
|
+
|
|
70
|
+
# tailwind.config.js
|
|
71
|
+
cat > tailwind.config.js << 'EOF'
|
|
72
|
+
/** @type {import('tailwindcss').Config} */
|
|
73
|
+
export default {
|
|
74
|
+
darkMode: ["class"],
|
|
75
|
+
content: [
|
|
76
|
+
'./pages/**/*.{ts,tsx}',
|
|
77
|
+
'./components/**/*.{ts,tsx}',
|
|
78
|
+
'./app/**/*.{ts,tsx}',
|
|
79
|
+
'./src/**/*.{ts,tsx}',
|
|
80
|
+
],
|
|
81
|
+
prefix: "",
|
|
82
|
+
theme: {
|
|
83
|
+
container: {
|
|
84
|
+
center: true,
|
|
85
|
+
padding: "2rem",
|
|
86
|
+
screens: {
|
|
87
|
+
"2xl": "1400px",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
extend: {
|
|
91
|
+
colors: {
|
|
92
|
+
border: "hsl(var(--border))",
|
|
93
|
+
input: "hsl(var(--input))",
|
|
94
|
+
ring: "hsl(var(--ring))",
|
|
95
|
+
background: "hsl(var(--background))",
|
|
96
|
+
foreground: "hsl(var(--foreground))",
|
|
97
|
+
primary: {
|
|
98
|
+
DEFAULT: "hsl(var(--primary))",
|
|
99
|
+
foreground: "hsl(var(--primary-foreground))",
|
|
100
|
+
},
|
|
101
|
+
secondary: {
|
|
102
|
+
DEFAULT: "hsl(var(--secondary))",
|
|
103
|
+
foreground: "hsl(var(--secondary-foreground))",
|
|
104
|
+
},
|
|
105
|
+
destructive: {
|
|
106
|
+
DEFAULT: "hsl(var(--destructive))",
|
|
107
|
+
foreground: "hsl(var(--destructive-foreground))",
|
|
108
|
+
},
|
|
109
|
+
muted: {
|
|
110
|
+
DEFAULT: "hsl(var(--muted))",
|
|
111
|
+
foreground: "hsl(var(--muted-foreground))",
|
|
112
|
+
},
|
|
113
|
+
accent: {
|
|
114
|
+
DEFAULT: "hsl(var(--accent))",
|
|
115
|
+
foreground: "hsl(var(--accent-foreground))",
|
|
116
|
+
},
|
|
117
|
+
popover: {
|
|
118
|
+
DEFAULT: "hsl(var(--popover))",
|
|
119
|
+
foreground: "hsl(var(--popover-foreground))",
|
|
120
|
+
},
|
|
121
|
+
card: {
|
|
122
|
+
DEFAULT: "hsl(var(--card))",
|
|
123
|
+
foreground: "hsl(var(--card-foreground))",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
borderRadius: {
|
|
127
|
+
lg: "var(--radius)",
|
|
128
|
+
md: "calc(var(--radius) - 2px)",
|
|
129
|
+
sm: "calc(var(--radius) - 4px)",
|
|
130
|
+
},
|
|
131
|
+
keyframes: {
|
|
132
|
+
"accordion-down": {
|
|
133
|
+
from: { height: "0" },
|
|
134
|
+
to: { height: "var(--radix-accordion-content-height)" },
|
|
135
|
+
},
|
|
136
|
+
"accordion-up": {
|
|
137
|
+
from: { height: "var(--radix-accordion-content-height)" },
|
|
138
|
+
to: { height: "0" },
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
animation: {
|
|
142
|
+
"accordion-down": "accordion-down 0.2s ease-out",
|
|
143
|
+
"accordion-up": "accordion-up 0.2s ease-out",
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
plugins: [require("tailwindcss-animate")],
|
|
148
|
+
}
|
|
149
|
+
EOF
|
|
150
|
+
|
|
151
|
+
# tsconfig.json
|
|
152
|
+
cat > tsconfig.json << 'EOF'
|
|
153
|
+
{
|
|
154
|
+
"files": [],
|
|
155
|
+
"references": [
|
|
156
|
+
{ "path": "./tsconfig.app.json" },
|
|
157
|
+
{ "path": "./tsconfig.node.json" }
|
|
158
|
+
],
|
|
159
|
+
"compilerOptions": {
|
|
160
|
+
"baseUrl": ".",
|
|
161
|
+
"paths": {
|
|
162
|
+
"@/*": ["./src/*"]
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
EOF
|
|
167
|
+
|
|
168
|
+
# tsconfig.app.json
|
|
169
|
+
cat > tsconfig.app.json << 'EOF'
|
|
170
|
+
{
|
|
171
|
+
"compilerOptions": {
|
|
172
|
+
"target": "ES2020",
|
|
173
|
+
"useDefineForClassFields": true,
|
|
174
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
175
|
+
"module": "ESNext",
|
|
176
|
+
"skipLibCheck": true,
|
|
177
|
+
|
|
178
|
+
/* Bundler mode */
|
|
179
|
+
"moduleResolution": "bundler",
|
|
180
|
+
"allowImportingTsExtensions": true,
|
|
181
|
+
"isolatedModules": true,
|
|
182
|
+
"moduleDetection": "force",
|
|
183
|
+
"noEmit": true,
|
|
184
|
+
"jsx": "react-jsx",
|
|
185
|
+
|
|
186
|
+
/* Linting */
|
|
187
|
+
"strict": true,
|
|
188
|
+
"noUnusedLocals": true,
|
|
189
|
+
"noUnusedParameters": true,
|
|
190
|
+
"noFallthroughCasesInSwitch": true,
|
|
191
|
+
|
|
192
|
+
/* Path Aliases */
|
|
193
|
+
"baseUrl": ".",
|
|
194
|
+
"paths": {
|
|
195
|
+
"@/*": ["./src/*"]
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
"include": ["src"]
|
|
199
|
+
}
|
|
200
|
+
EOF
|
|
201
|
+
|
|
202
|
+
# tsconfig.node.json
|
|
203
|
+
cat > tsconfig.node.json << 'EOF'
|
|
204
|
+
{
|
|
205
|
+
"compilerOptions": {
|
|
206
|
+
"target": "ES2022",
|
|
207
|
+
"lib": ["ES2023"],
|
|
208
|
+
"module": "ESNext",
|
|
209
|
+
"skipLibCheck": true,
|
|
210
|
+
|
|
211
|
+
/* Bundler mode */
|
|
212
|
+
"moduleResolution": "bundler",
|
|
213
|
+
"allowImportingTsExtensions": true,
|
|
214
|
+
"isolatedModules": true,
|
|
215
|
+
"moduleDetection": "force",
|
|
216
|
+
"noEmit": true,
|
|
217
|
+
|
|
218
|
+
/* Linting */
|
|
219
|
+
"strict": true,
|
|
220
|
+
"noUnusedLocals": true,
|
|
221
|
+
"noUnusedParameters": true,
|
|
222
|
+
"noFallthroughCasesInSwitch": true
|
|
223
|
+
},
|
|
224
|
+
"include": ["vite.config.ts"]
|
|
225
|
+
}
|
|
226
|
+
EOF
|
|
227
|
+
|
|
228
|
+
# vite.config.ts
|
|
229
|
+
cat > vite.config.ts << 'EOF'
|
|
230
|
+
import { defineConfig } from "vite";
|
|
231
|
+
import react from "@vitejs/plugin-react-swc";
|
|
232
|
+
import path from "path";
|
|
233
|
+
|
|
234
|
+
export default defineConfig({
|
|
235
|
+
server: {
|
|
236
|
+
host: "::",
|
|
237
|
+
port: 8080,
|
|
238
|
+
strictPort: false,
|
|
239
|
+
open: false,
|
|
240
|
+
},
|
|
241
|
+
plugins: [react()],
|
|
242
|
+
resolve: {
|
|
243
|
+
alias: {
|
|
244
|
+
"@": path.resolve(__dirname, "./src"),
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
EOF
|
|
249
|
+
|
|
250
|
+
# postcss.config.js
|
|
251
|
+
cat > postcss.config.js << 'EOF'
|
|
252
|
+
export default {
|
|
253
|
+
plugins: {
|
|
254
|
+
tailwindcss: {},
|
|
255
|
+
autoprefixer: {},
|
|
256
|
+
},
|
|
257
|
+
}
|
|
258
|
+
EOF
|
|
259
|
+
|
|
260
|
+
# index.html
|
|
261
|
+
cat > index.html << 'EOF'
|
|
262
|
+
<!doctype html>
|
|
263
|
+
<html lang="en">
|
|
264
|
+
<head>
|
|
265
|
+
<meta charset="UTF-8" />
|
|
266
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
267
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
268
|
+
<title>React Clean Architecture</title>
|
|
269
|
+
</head>
|
|
270
|
+
<body>
|
|
271
|
+
<div id="root"></div>
|
|
272
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
273
|
+
</body>
|
|
274
|
+
</html>
|
|
275
|
+
EOF
|
|
276
|
+
|
|
277
|
+
echo "✅ Archivos de configuración creados"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
2
|
+
import { AuthProvider } from '@/presentation/context/auth/AuthContext';
|
|
3
|
+
import { IndexPage } from '@/presentation/pages/Index';
|
|
4
|
+
|
|
5
|
+
const queryClient = new QueryClient({
|
|
6
|
+
defaultOptions: {
|
|
7
|
+
queries: {
|
|
8
|
+
refetchOnWindowFocus: false,
|
|
9
|
+
retry: 1,
|
|
10
|
+
staleTime: 5 * 60 * 1000, // 5 minutos
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function App() {
|
|
16
|
+
return (
|
|
17
|
+
<QueryClientProvider client={queryClient}>
|
|
18
|
+
<AuthProvider>
|
|
19
|
+
<IndexPage />
|
|
20
|
+
</AuthProvider>
|
|
21
|
+
</QueryClientProvider>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default App;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { User } from '@/domain/entities/User';
|
|
2
|
+
import { PaginatedResponse } from '@/shared/types';
|
|
3
|
+
|
|
4
|
+
export interface UserRepository {
|
|
5
|
+
getAll(): Promise<User[]>;
|
|
6
|
+
getById(id: string): Promise<User>;
|
|
7
|
+
create(user: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<User>;
|
|
8
|
+
update(id: string, user: Partial<User>): Promise<User>;
|
|
9
|
+
delete(id: string): Promise<void>;
|
|
10
|
+
getPaginated(page: number, pageSize: number): Promise<PaginatedResponse<User>>;
|
|
11
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { UserRepository } from '@/domain/repositories/UserRepository';
|
|
2
|
+
import { User } from '@/domain/entities/User';
|
|
3
|
+
|
|
4
|
+
export class CreateUserUseCase {
|
|
5
|
+
constructor(private userRepository: UserRepository) {}
|
|
6
|
+
|
|
7
|
+
async execute(userData: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<User> {
|
|
8
|
+
// Aquí puedes agregar validaciones de negocio
|
|
9
|
+
if (!userData.email || !userData.nombre) {
|
|
10
|
+
throw new Error('Email y nombre son requeridos');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return this.userRepository.create(userData);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { UserRepository } from '@/domain/repositories/UserRepository';
|
|
2
|
+
import { User } from '@/domain/entities/User';
|
|
3
|
+
|
|
4
|
+
export class GetAllUsersUseCase {
|
|
5
|
+
constructor(private userRepository: UserRepository) {}
|
|
6
|
+
|
|
7
|
+
async execute(): Promise<User[]> {
|
|
8
|
+
return this.userRepository.getAll();
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
|
2
|
+
import { env } from '@/shared/config/env';
|
|
3
|
+
|
|
4
|
+
class ApiClient {
|
|
5
|
+
private client: AxiosInstance;
|
|
6
|
+
|
|
7
|
+
constructor(baseURL: string) {
|
|
8
|
+
this.client = axios.create({
|
|
9
|
+
baseURL,
|
|
10
|
+
timeout: 30000,
|
|
11
|
+
headers: {
|
|
12
|
+
'Content-Type': 'application/json',
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
this.setupInterceptors();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private setupInterceptors() {
|
|
20
|
+
// Request interceptor
|
|
21
|
+
this.client.interceptors.request.use(
|
|
22
|
+
(config) => {
|
|
23
|
+
const token = localStorage.getItem('token');
|
|
24
|
+
if (token) {
|
|
25
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
26
|
+
}
|
|
27
|
+
return config;
|
|
28
|
+
},
|
|
29
|
+
(error) => {
|
|
30
|
+
return Promise.reject(error);
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Response interceptor
|
|
35
|
+
this.client.interceptors.response.use(
|
|
36
|
+
(response) => response,
|
|
37
|
+
(error) => {
|
|
38
|
+
if (error.response?.status === 401) {
|
|
39
|
+
localStorage.removeItem('token');
|
|
40
|
+
window.location.href = '/login';
|
|
41
|
+
}
|
|
42
|
+
return Promise.reject(error);
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
|
48
|
+
const response: AxiosResponse<T> = await this.client.get(url, config);
|
|
49
|
+
return response.data;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async post<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
|
53
|
+
const response: AxiosResponse<T> = await this.client.post(url, data, config);
|
|
54
|
+
return response.data;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async put<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
|
58
|
+
const response: AxiosResponse<T> = await this.client.put(url, data, config);
|
|
59
|
+
return response.data;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async patch<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
|
|
63
|
+
const response: AxiosResponse<T> = await this.client.patch(url, data, config);
|
|
64
|
+
return response.data;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
|
68
|
+
const response: AxiosResponse<T> = await this.client.delete(url, config);
|
|
69
|
+
return response.data;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const apiClient = new ApiClient(env.apiUrl);
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { UserRepository } from '@/domain/repositories/UserRepository';
|
|
2
|
+
import { User } from '@/domain/entities/User';
|
|
3
|
+
import { PaginatedResponse } from '@/shared/types';
|
|
4
|
+
import { apiClient } from '@/infrastructure/api/apiClient';
|
|
5
|
+
|
|
6
|
+
export class UserRepositoryImpl implements UserRepository {
|
|
7
|
+
async getAll(): Promise<User[]> {
|
|
8
|
+
return apiClient.get<User[]>('/users');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async getById(id: string): Promise<User> {
|
|
12
|
+
return apiClient.get<User>(`/users/${id}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async create(user: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<User> {
|
|
16
|
+
return apiClient.post<User>('/users', user);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async update(id: string, user: Partial<User>): Promise<User> {
|
|
20
|
+
return apiClient.put<User>(`/users/${id}`, user);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async delete(id: string): Promise<void> {
|
|
24
|
+
return apiClient.delete<void>(`/users/${id}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async getPaginated(page: number, pageSize: number): Promise<PaginatedResponse<User>> {
|
|
28
|
+
return apiClient.get<PaginatedResponse<User>>(`/users?page=${page}&pageSize=${pageSize}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const userRepository = new UserRepositoryImpl();
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createContext, useReducer, ReactNode, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
interface User {
|
|
4
|
+
id: string;
|
|
5
|
+
email: string;
|
|
6
|
+
nombre: string;
|
|
7
|
+
rol: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface AuthState {
|
|
11
|
+
user: User | null;
|
|
12
|
+
token: string | null;
|
|
13
|
+
isAuthenticated: boolean;
|
|
14
|
+
isLoading: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type AuthAction =
|
|
18
|
+
| { type: 'LOGIN'; payload: { user: User; token: string } }
|
|
19
|
+
| { type: 'LOGOUT' }
|
|
20
|
+
| { type: 'SET_LOADING'; payload: boolean };
|
|
21
|
+
|
|
22
|
+
const initialState: AuthState = {
|
|
23
|
+
user: null,
|
|
24
|
+
token: localStorage.getItem('token'),
|
|
25
|
+
isAuthenticated: false,
|
|
26
|
+
isLoading: true,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const authReducer = (state: AuthState, action: AuthAction): AuthState => {
|
|
30
|
+
switch (action.type) {
|
|
31
|
+
case 'LOGIN':
|
|
32
|
+
localStorage.setItem('token', action.payload.token);
|
|
33
|
+
return {
|
|
34
|
+
...state,
|
|
35
|
+
user: action.payload.user,
|
|
36
|
+
token: action.payload.token,
|
|
37
|
+
isAuthenticated: true,
|
|
38
|
+
isLoading: false,
|
|
39
|
+
};
|
|
40
|
+
case 'LOGOUT':
|
|
41
|
+
localStorage.removeItem('token');
|
|
42
|
+
return {
|
|
43
|
+
...state,
|
|
44
|
+
user: null,
|
|
45
|
+
token: null,
|
|
46
|
+
isAuthenticated: false,
|
|
47
|
+
isLoading: false,
|
|
48
|
+
};
|
|
49
|
+
case 'SET_LOADING':
|
|
50
|
+
return {
|
|
51
|
+
...state,
|
|
52
|
+
isLoading: action.payload,
|
|
53
|
+
};
|
|
54
|
+
default:
|
|
55
|
+
return state;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const AuthContext = createContext<{
|
|
60
|
+
state: AuthState;
|
|
61
|
+
dispatch: React.Dispatch<AuthAction>;
|
|
62
|
+
} | null>(null);
|
|
63
|
+
|
|
64
|
+
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
|
65
|
+
const [state, dispatch] = useReducer(authReducer, initialState);
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
// Aquí podrías verificar el token al cargar la app
|
|
69
|
+
const token = localStorage.getItem('token');
|
|
70
|
+
if (token) {
|
|
71
|
+
// Verificar token con el backend
|
|
72
|
+
// Por ahora solo marcamos como no loading
|
|
73
|
+
dispatch({ type: 'SET_LOADING', payload: false });
|
|
74
|
+
} else {
|
|
75
|
+
dispatch({ type: 'SET_LOADING', payload: false });
|
|
76
|
+
}
|
|
77
|
+
}, []);
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<AuthContext.Provider value={{ state, dispatch }}>
|
|
81
|
+
{children}
|
|
82
|
+
</AuthContext.Provider>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { AuthContext } from './AuthContext';
|
|
3
|
+
|
|
4
|
+
export const useAuthContext = () => {
|
|
5
|
+
const context = useContext(AuthContext);
|
|
6
|
+
|
|
7
|
+
if (!context) {
|
|
8
|
+
throw new Error('useAuthContext debe ser usado dentro de AuthProvider');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return context;
|
|
12
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export const IndexPage = () => {
|
|
2
|
+
return (
|
|
3
|
+
<div className="min-h-screen flex items-center justify-center bg-background">
|
|
4
|
+
<div className="text-center space-y-6 p-8 max-w-4xl">
|
|
5
|
+
<h1 className="text-5xl font-bold text-foreground bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">
|
|
6
|
+
React Clean Architecture
|
|
7
|
+
</h1>
|
|
8
|
+
<p className="text-xl text-muted-foreground">
|
|
9
|
+
Template profesional con TypeScript, Vite, Tailwind CSS y shadcn/ui
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-12">
|
|
13
|
+
{/* Configurado */}
|
|
14
|
+
<div className="p-6 border rounded-lg bg-card hover:shadow-lg transition-shadow">
|
|
15
|
+
<div className="flex items-center gap-2 mb-4">
|
|
16
|
+
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
|
17
|
+
<h3 className="font-semibold text-lg">✅ Configurado</h3>
|
|
18
|
+
</div>
|
|
19
|
+
<ul className="text-left space-y-2 text-sm text-muted-foreground">
|
|
20
|
+
<li>• React Hook Form + Zod</li>
|
|
21
|
+
<li>• TanStack Query</li>
|
|
22
|
+
<li>• Zustand (state)</li>
|
|
23
|
+
<li>• Path Aliases (@/*)</li>
|
|
24
|
+
<li>• Tailwind + shadcn/ui</li>
|
|
25
|
+
<li>• Axios + Socket.io</li>
|
|
26
|
+
</ul>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
{/* Estructura */}
|
|
30
|
+
<div className="p-6 border rounded-lg bg-card hover:shadow-lg transition-shadow">
|
|
31
|
+
<div className="flex items-center gap-2 mb-4">
|
|
32
|
+
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
|
33
|
+
<h3 className="font-semibold text-lg">📁 Clean Architecture</h3>
|
|
34
|
+
</div>
|
|
35
|
+
<ul className="text-left space-y-2 text-sm text-muted-foreground">
|
|
36
|
+
<li>• <span className="font-mono">domain/</span> (entities, usecases)</li>
|
|
37
|
+
<li>• <span className="font-mono">infrastructure/</span> (api, repos)</li>
|
|
38
|
+
<li>• <span className="font-mono">presentation/</span> (UI, pages)</li>
|
|
39
|
+
<li>• <span className="font-mono">shared/</span> (utils, types)</li>
|
|
40
|
+
</ul>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
{/* Próximos pasos */}
|
|
45
|
+
<div className="mt-12 p-6 border rounded-lg bg-muted/50">
|
|
46
|
+
<h3 className="font-semibold mb-4">🚀 Próximos pasos</h3>
|
|
47
|
+
<div className="space-y-3 text-left">
|
|
48
|
+
<div>
|
|
49
|
+
<p className="text-sm text-muted-foreground mb-1">1. Agregar componentes shadcn:</p>
|
|
50
|
+
<code className="block p-3 bg-background rounded text-sm font-mono">
|
|
51
|
+
npx shadcn-ui@latest add button card form table
|
|
52
|
+
</code>
|
|
53
|
+
</div>
|
|
54
|
+
<div>
|
|
55
|
+
<p className="text-sm text-muted-foreground mb-1">2. Crear tus entidades en:</p>
|
|
56
|
+
<code className="block p-3 bg-background rounded text-sm font-mono">
|
|
57
|
+
src/domain/entities/
|
|
58
|
+
</code>
|
|
59
|
+
</div>
|
|
60
|
+
<div>
|
|
61
|
+
<p className="text-sm text-muted-foreground mb-1">3. Implementar tus use cases:</p>
|
|
62
|
+
<code className="block p-3 bg-background rounded text-sm font-mono">
|
|
63
|
+
src/domain/usecases/
|
|
64
|
+
</code>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div className="mt-8 text-sm text-muted-foreground">
|
|
70
|
+
<p>📚 Documentación completa en <span className="font-mono">README.md</span></p>
|
|
71
|
+
<p className="mt-2">Creado con ❤️ usando Clean Architecture</p>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
};
|