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,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,8 @@
1
+ import { BaseEntity } from '@/shared/types';
2
+
3
+ export interface User extends BaseEntity {
4
+ email: string;
5
+ nombre: string;
6
+ rol: string;
7
+ activo: boolean;
8
+ }
@@ -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
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import '@/styles/index.css'
4
+ import App from '@/app/App.tsx'
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
@@ -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
+ };