@create-lft-app/nextjs 3.0.0 → 3.1.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 +549 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
# @create-lft-app/nextjs
|
|
2
|
+
|
|
3
|
+
CLI para crear proyectos Next.js con arquitectura modular, Supabase, y Midday Design System.
|
|
4
|
+
|
|
5
|
+
## Instalación
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @create-lft-app/nextjs mi-proyecto
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Stack Tecnológico
|
|
12
|
+
|
|
13
|
+
| Categoría | Librería | Versión |
|
|
14
|
+
|-----------|----------|---------|
|
|
15
|
+
| Framework | Next.js | ^16 |
|
|
16
|
+
| Lenguaje | TypeScript | ^5 |
|
|
17
|
+
| Estado Global | Zustand | ^5 |
|
|
18
|
+
| Data Fetching | TanStack Query | ^5 |
|
|
19
|
+
| Tablas | TanStack Table | ^8 |
|
|
20
|
+
| UI Components | Radix UI | latest |
|
|
21
|
+
| Animaciones | Framer Motion | ^11 |
|
|
22
|
+
| Auth & Backend | Supabase SSR | ^0.5 |
|
|
23
|
+
| ORM | Drizzle | latest |
|
|
24
|
+
| Validación | Zod | ^3 |
|
|
25
|
+
| Excel | xlsx (SheetJS) | latest |
|
|
26
|
+
| Fechas | dayjs | ^1 |
|
|
27
|
+
| Forms | React Hook Form | ^7 |
|
|
28
|
+
| Package Manager | pnpm | latest |
|
|
29
|
+
|
|
30
|
+
## Estructura de Carpetas
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
template/
|
|
34
|
+
├── proxy.ts # Next.js 16 middleware (protección de rutas)
|
|
35
|
+
├── drizzle.config.ts # Configuración Drizzle ORM
|
|
36
|
+
│
|
|
37
|
+
├── src/
|
|
38
|
+
│ ├── app/
|
|
39
|
+
│ │ ├── layout.tsx # Layout raíz
|
|
40
|
+
│ │ ├── page.tsx # Página principal (redirect)
|
|
41
|
+
│ │ ├── providers.tsx # TanStack Query + providers
|
|
42
|
+
│ │ │
|
|
43
|
+
│ │ ├── api/ # API Routes
|
|
44
|
+
│ │ │ └── webhooks/
|
|
45
|
+
│ │ │ └── route.ts
|
|
46
|
+
│ │ │
|
|
47
|
+
│ │ ├── (auth)/ # Rutas protegidas (requieren sesión)
|
|
48
|
+
│ │ │ ├── layout.tsx
|
|
49
|
+
│ │ │ ├── dashboard/
|
|
50
|
+
│ │ │ └── users/
|
|
51
|
+
│ │ │
|
|
52
|
+
│ │ └── (public)/ # Rutas públicas
|
|
53
|
+
│ │ ├── layout.tsx
|
|
54
|
+
│ │ └── login/
|
|
55
|
+
│ │
|
|
56
|
+
│ ├── components/
|
|
57
|
+
│ │ ├── layout/ # Componentes de layout
|
|
58
|
+
│ │ │ ├── sidebar.tsx
|
|
59
|
+
│ │ │ ├── sidebar-context.tsx
|
|
60
|
+
│ │ │ └── main-content.tsx
|
|
61
|
+
│ │ │
|
|
62
|
+
│ │ ├── tables/ # TanStack Table genéricos
|
|
63
|
+
│ │ │ ├── data-table.tsx
|
|
64
|
+
│ │ │ ├── data-table-column-header.tsx
|
|
65
|
+
│ │ │ ├── data-table-pagination.tsx
|
|
66
|
+
│ │ │ ├── data-table-toolbar.tsx
|
|
67
|
+
│ │ │ └── data-table-view-options.tsx
|
|
68
|
+
│ │ │
|
|
69
|
+
│ │ └── ui/ # Radix + Framer Motion
|
|
70
|
+
│ │ ├── animations/
|
|
71
|
+
│ │ ├── button.tsx
|
|
72
|
+
│ │ ├── card.tsx
|
|
73
|
+
│ │ ├── dialog.tsx
|
|
74
|
+
│ │ ├── sheet.tsx
|
|
75
|
+
│ │ └── ...
|
|
76
|
+
│ │
|
|
77
|
+
│ ├── config/
|
|
78
|
+
│ │ ├── navigation.ts # Configuración de navegación
|
|
79
|
+
│ │ └── site.ts # Metadata del sitio
|
|
80
|
+
│ │
|
|
81
|
+
│ ├── db/ # Drizzle ORM
|
|
82
|
+
│ │ ├── index.ts # Cliente de base de datos
|
|
83
|
+
│ │ ├── seed.ts # Seeds
|
|
84
|
+
│ │ └── schema/ # Schemas de tablas
|
|
85
|
+
│ │
|
|
86
|
+
│ ├── hooks/ # Hooks globales reutilizables
|
|
87
|
+
│ │ ├── useDataTable.ts
|
|
88
|
+
│ │ ├── useDebounce.ts
|
|
89
|
+
│ │ └── useMediaQuery.ts
|
|
90
|
+
│ │
|
|
91
|
+
│ ├── lib/
|
|
92
|
+
│ │ ├── utils.ts # Utilidades (cn, etc.)
|
|
93
|
+
│ │ ├── query-client.ts # TanStack Query config
|
|
94
|
+
│ │ │
|
|
95
|
+
│ │ ├── date/ # Utilidades dayjs
|
|
96
|
+
│ │ │ ├── config.ts
|
|
97
|
+
│ │ │ └── formatters.ts
|
|
98
|
+
│ │ │
|
|
99
|
+
│ │ ├── excel/ # Utilidades xlsx
|
|
100
|
+
│ │ │ ├── exporter.ts
|
|
101
|
+
│ │ │ └── parser.ts
|
|
102
|
+
│ │ │
|
|
103
|
+
│ │ ├── supabase/ # Clientes Supabase
|
|
104
|
+
│ │ │ ├── client.ts # Browser
|
|
105
|
+
│ │ │ ├── server.ts # Server
|
|
106
|
+
│ │ │ └── proxy.ts # Session middleware
|
|
107
|
+
│ │ │
|
|
108
|
+
│ │ └── validations/ # Zod schemas comunes
|
|
109
|
+
│ │
|
|
110
|
+
│ ├── modules/ # ⭐ MÓDULOS (feature-based)
|
|
111
|
+
│ │ ├── auth/
|
|
112
|
+
│ │ └── users/
|
|
113
|
+
│ │
|
|
114
|
+
│ ├── stores/ # Zustand stores globales
|
|
115
|
+
│ │ └── useUiStore.ts
|
|
116
|
+
│ │
|
|
117
|
+
│ └── types/ # TypeScript types globales
|
|
118
|
+
│
|
|
119
|
+
└── supabase/
|
|
120
|
+
├── config.toml
|
|
121
|
+
└── functions/ # Edge Functions (Deno)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Arquitectura Modular
|
|
125
|
+
|
|
126
|
+
### Patrón Obligatorio
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
Store (Zustand) → Queries (TanStack) → Mutations → Hook Unificado → Componente
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Regla fundamental:** Los componentes **SOLO** importan el hook unificado, nunca queries, mutations o stores directamente.
|
|
133
|
+
|
|
134
|
+
### Estructura de un Módulo
|
|
135
|
+
|
|
136
|
+
Cada módulo es **self-contained** y sigue esta estructura:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
src/modules/[nombre]/
|
|
140
|
+
├── index.ts # Barrel exports
|
|
141
|
+
├── columns.tsx # Columnas TanStack Table (si aplica)
|
|
142
|
+
│
|
|
143
|
+
├── actions/
|
|
144
|
+
│ └── [nombre]-actions.ts # Server actions
|
|
145
|
+
│
|
|
146
|
+
├── components/
|
|
147
|
+
│ └── [nombre]-list.tsx # Componentes del módulo
|
|
148
|
+
│
|
|
149
|
+
├── hooks/
|
|
150
|
+
│ ├── use[Nombre].ts # ⭐ Hook unificado (ÚNICO import en componentes)
|
|
151
|
+
│ ├── use[Nombre]Queries.ts # Queries TanStack
|
|
152
|
+
│ └── use[Nombre]Mutations.ts # Mutations TanStack
|
|
153
|
+
│
|
|
154
|
+
├── schemas/
|
|
155
|
+
│ └── [nombre].schema.ts # Zod schemas + tipos
|
|
156
|
+
│
|
|
157
|
+
└── stores/
|
|
158
|
+
└── use[Nombre]Store.ts # Zustand store del módulo
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Crear un Nuevo Módulo
|
|
162
|
+
|
|
163
|
+
### Paso 1: Crear estructura de carpetas
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
mkdir -p src/modules/products/{actions,components,hooks,schemas,stores}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Paso 2: Schema (Zod + tipos)
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
// src/modules/products/schemas/products.schema.ts
|
|
173
|
+
import { z } from 'zod'
|
|
174
|
+
|
|
175
|
+
export const productSchema = z.object({
|
|
176
|
+
id: z.string().uuid(),
|
|
177
|
+
name: z.string().min(1, 'Nombre requerido'),
|
|
178
|
+
price: z.number().positive(),
|
|
179
|
+
stock: z.number().int().min(0),
|
|
180
|
+
created_at: z.string(),
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
export const createProductSchema = productSchema.omit({ id: true, created_at: true })
|
|
184
|
+
export const updateProductSchema = createProductSchema.partial()
|
|
185
|
+
|
|
186
|
+
export type Product = z.infer<typeof productSchema>
|
|
187
|
+
export type CreateProductInput = z.infer<typeof createProductSchema>
|
|
188
|
+
export type UpdateProductInput = z.infer<typeof updateProductSchema>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Paso 3: Server Actions
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
// src/modules/products/actions/products-actions.ts
|
|
195
|
+
'use server'
|
|
196
|
+
|
|
197
|
+
import { createClient } from '@/lib/supabase/server'
|
|
198
|
+
import { createProductSchema, type CreateProductInput } from '../schemas/products.schema'
|
|
199
|
+
|
|
200
|
+
export async function getProducts() {
|
|
201
|
+
const supabase = await createClient()
|
|
202
|
+
const { data: { user } } = await supabase.auth.getUser()
|
|
203
|
+
if (!user) throw new Error('Unauthorized')
|
|
204
|
+
|
|
205
|
+
const { data, error } = await supabase
|
|
206
|
+
.from('products')
|
|
207
|
+
.select('*')
|
|
208
|
+
.order('created_at', { ascending: false })
|
|
209
|
+
|
|
210
|
+
if (error) throw error
|
|
211
|
+
return data
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function createProduct(input: CreateProductInput) {
|
|
215
|
+
const parsed = createProductSchema.safeParse(input)
|
|
216
|
+
if (!parsed.success) throw new Error(parsed.error.errors[0].message)
|
|
217
|
+
|
|
218
|
+
const supabase = await createClient()
|
|
219
|
+
const { data: { user } } = await supabase.auth.getUser()
|
|
220
|
+
if (!user) throw new Error('Unauthorized')
|
|
221
|
+
|
|
222
|
+
const { data, error } = await supabase
|
|
223
|
+
.from('products')
|
|
224
|
+
.insert(parsed.data)
|
|
225
|
+
.select()
|
|
226
|
+
.single()
|
|
227
|
+
|
|
228
|
+
if (error) throw error
|
|
229
|
+
return data
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Paso 4: Store (Zustand)
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
// src/modules/products/stores/useProductsStore.ts
|
|
237
|
+
import { create } from 'zustand'
|
|
238
|
+
import { useShallow } from 'zustand/shallow'
|
|
239
|
+
|
|
240
|
+
interface ProductsState {
|
|
241
|
+
selectedProductId: string | null
|
|
242
|
+
isCreateDialogOpen: boolean
|
|
243
|
+
filters: { search: string; category: string | null }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
interface ProductsActions {
|
|
247
|
+
setSelectedProductId: (id: string | null) => void
|
|
248
|
+
setIsCreateDialogOpen: (open: boolean) => void
|
|
249
|
+
setFilters: (filters: Partial<ProductsState['filters']>) => void
|
|
250
|
+
reset: () => void
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const initialState: ProductsState = {
|
|
254
|
+
selectedProductId: null,
|
|
255
|
+
isCreateDialogOpen: false,
|
|
256
|
+
filters: { search: '', category: null },
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export const useProductsStore = create<ProductsState & ProductsActions>((set) => ({
|
|
260
|
+
...initialState,
|
|
261
|
+
setSelectedProductId: (id) => set({ selectedProductId: id }),
|
|
262
|
+
setIsCreateDialogOpen: (open) => set({ isCreateDialogOpen: open }),
|
|
263
|
+
setFilters: (filters) => set((state) => ({
|
|
264
|
+
filters: { ...state.filters, ...filters }
|
|
265
|
+
})),
|
|
266
|
+
reset: () => set(initialState),
|
|
267
|
+
}))
|
|
268
|
+
|
|
269
|
+
// Selector optimizado con useShallow
|
|
270
|
+
export const useProductsFilters = () =>
|
|
271
|
+
useProductsStore(useShallow((state) => state.filters))
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Paso 5: Queries (TanStack Query)
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
// src/modules/products/hooks/useProductsQueries.ts
|
|
278
|
+
'use client'
|
|
279
|
+
|
|
280
|
+
import { useQuery } from '@tanstack/react-query'
|
|
281
|
+
import { getProducts, getProductById } from '../actions/products-actions'
|
|
282
|
+
|
|
283
|
+
export const productsKeys = {
|
|
284
|
+
all: ['products'] as const,
|
|
285
|
+
lists: () => [...productsKeys.all, 'list'] as const,
|
|
286
|
+
list: (filters: Record<string, unknown>) => [...productsKeys.lists(), filters] as const,
|
|
287
|
+
details: () => [...productsKeys.all, 'detail'] as const,
|
|
288
|
+
detail: (id: string) => [...productsKeys.details(), id] as const,
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function useProductsQueries(productId?: string) {
|
|
292
|
+
const productsQuery = useQuery({
|
|
293
|
+
queryKey: productsKeys.lists(),
|
|
294
|
+
queryFn: () => getProducts(),
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
const productQuery = useQuery({
|
|
298
|
+
queryKey: productsKeys.detail(productId!),
|
|
299
|
+
queryFn: () => getProductById(productId!),
|
|
300
|
+
enabled: !!productId,
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
products: productsQuery.data ?? [],
|
|
305
|
+
product: productQuery.data,
|
|
306
|
+
isLoading: productsQuery.isLoading,
|
|
307
|
+
error: productsQuery.error,
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Paso 6: Mutations (TanStack Query)
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
// src/modules/products/hooks/useProductsMutations.ts
|
|
316
|
+
'use client'
|
|
317
|
+
|
|
318
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
319
|
+
import { createProduct, updateProduct, deleteProduct } from '../actions/products-actions'
|
|
320
|
+
import { productsKeys } from './useProductsQueries'
|
|
321
|
+
import type { CreateProductInput, UpdateProductInput } from '../schemas/products.schema'
|
|
322
|
+
|
|
323
|
+
export function useProductsMutations() {
|
|
324
|
+
const queryClient = useQueryClient()
|
|
325
|
+
|
|
326
|
+
const createMutation = useMutation({
|
|
327
|
+
mutationFn: (data: CreateProductInput) => createProduct(data),
|
|
328
|
+
onSuccess: () => {
|
|
329
|
+
queryClient.invalidateQueries({ queryKey: productsKeys.lists() })
|
|
330
|
+
},
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
const updateMutation = useMutation({
|
|
334
|
+
mutationFn: ({ id, data }: { id: string; data: UpdateProductInput }) =>
|
|
335
|
+
updateProduct(id, data),
|
|
336
|
+
onSuccess: () => {
|
|
337
|
+
queryClient.invalidateQueries({ queryKey: productsKeys.all })
|
|
338
|
+
},
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
const deleteMutation = useMutation({
|
|
342
|
+
mutationFn: (id: string) => deleteProduct(id),
|
|
343
|
+
onSuccess: () => {
|
|
344
|
+
queryClient.invalidateQueries({ queryKey: productsKeys.lists() })
|
|
345
|
+
},
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
createProduct: createMutation.mutateAsync,
|
|
350
|
+
updateProduct: updateMutation.mutateAsync,
|
|
351
|
+
deleteProduct: deleteMutation.mutateAsync,
|
|
352
|
+
isCreating: createMutation.isPending,
|
|
353
|
+
isUpdating: updateMutation.isPending,
|
|
354
|
+
isDeleting: deleteMutation.isPending,
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Paso 7: Hook Unificado ⭐
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
// src/modules/products/hooks/useProducts.ts
|
|
363
|
+
'use client'
|
|
364
|
+
|
|
365
|
+
import { useProductsQueries } from './useProductsQueries'
|
|
366
|
+
import { useProductsMutations } from './useProductsMutations'
|
|
367
|
+
import { useProductsStore, useProductsFilters } from '../stores/useProductsStore'
|
|
368
|
+
|
|
369
|
+
export function useProducts(productId?: string) {
|
|
370
|
+
// Queries
|
|
371
|
+
const { products, product, isLoading, error } = useProductsQueries(productId)
|
|
372
|
+
|
|
373
|
+
// Mutations
|
|
374
|
+
const {
|
|
375
|
+
createProduct,
|
|
376
|
+
updateProduct,
|
|
377
|
+
deleteProduct,
|
|
378
|
+
isCreating,
|
|
379
|
+
isUpdating,
|
|
380
|
+
isDeleting
|
|
381
|
+
} = useProductsMutations()
|
|
382
|
+
|
|
383
|
+
// Store
|
|
384
|
+
const filters = useProductsFilters()
|
|
385
|
+
const {
|
|
386
|
+
selectedProductId,
|
|
387
|
+
isCreateDialogOpen,
|
|
388
|
+
setSelectedProductId,
|
|
389
|
+
setIsCreateDialogOpen,
|
|
390
|
+
setFilters,
|
|
391
|
+
} = useProductsStore()
|
|
392
|
+
|
|
393
|
+
// Datos filtrados
|
|
394
|
+
const filteredProducts = products.filter((p) =>
|
|
395
|
+
p.name.toLowerCase().includes(filters.search.toLowerCase())
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
// Data
|
|
400
|
+
products: filteredProducts,
|
|
401
|
+
product,
|
|
402
|
+
|
|
403
|
+
// Loading states
|
|
404
|
+
isLoading,
|
|
405
|
+
isCreating,
|
|
406
|
+
isUpdating,
|
|
407
|
+
isDeleting,
|
|
408
|
+
error,
|
|
409
|
+
|
|
410
|
+
// Actions
|
|
411
|
+
createProduct,
|
|
412
|
+
updateProduct,
|
|
413
|
+
deleteProduct,
|
|
414
|
+
|
|
415
|
+
// UI State
|
|
416
|
+
selectedProductId,
|
|
417
|
+
isCreateDialogOpen,
|
|
418
|
+
filters,
|
|
419
|
+
setSelectedProductId,
|
|
420
|
+
setIsCreateDialogOpen,
|
|
421
|
+
setFilters,
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### Paso 8: Componentes
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
// src/modules/products/components/products-list.tsx
|
|
430
|
+
'use client'
|
|
431
|
+
|
|
432
|
+
import { DataTable } from '@/components/tables/data-table'
|
|
433
|
+
import { useProducts } from '../hooks/useProducts'
|
|
434
|
+
import { columns } from '../columns'
|
|
435
|
+
|
|
436
|
+
export function ProductsList() {
|
|
437
|
+
const { products, isLoading } = useProducts()
|
|
438
|
+
|
|
439
|
+
if (isLoading) {
|
|
440
|
+
return <div>Cargando productos...</div>
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return (
|
|
444
|
+
<DataTable
|
|
445
|
+
columns={columns}
|
|
446
|
+
data={products}
|
|
447
|
+
searchKey="name"
|
|
448
|
+
searchPlaceholder="Buscar por nombre..."
|
|
449
|
+
/>
|
|
450
|
+
)
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Paso 9: Barrel Export
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
// src/modules/products/index.ts
|
|
458
|
+
export * from './components/products-list'
|
|
459
|
+
export * from './hooks/useProducts'
|
|
460
|
+
export * from './schemas/products.schema'
|
|
461
|
+
export { columns as productColumns } from './columns'
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### Paso 10: Crear página
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
// src/app/(auth)/products/page.tsx
|
|
468
|
+
import { createClient } from '@/lib/supabase/server'
|
|
469
|
+
import { ProductsContent } from './products-content'
|
|
470
|
+
|
|
471
|
+
export default async function ProductsPage() {
|
|
472
|
+
const supabase = await createClient()
|
|
473
|
+
const { data: { user } } = await supabase.auth.getUser()
|
|
474
|
+
|
|
475
|
+
return <ProductsContent userEmail={user?.email ?? ''} />
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
// src/app/(auth)/products/products-content.tsx
|
|
481
|
+
'use client'
|
|
482
|
+
|
|
483
|
+
import { PageTransition, PageHeader, PageTitle } from '@/components/ui/animations'
|
|
484
|
+
import { ProductsList } from '@/modules/products'
|
|
485
|
+
|
|
486
|
+
export function ProductsContent({ userEmail }: { userEmail: string }) {
|
|
487
|
+
return (
|
|
488
|
+
<PageTransition className="flex flex-1 flex-col gap-6 p-6">
|
|
489
|
+
<PageHeader>
|
|
490
|
+
<PageTitle>Productos</PageTitle>
|
|
491
|
+
</PageHeader>
|
|
492
|
+
<ProductsList />
|
|
493
|
+
</PageTransition>
|
|
494
|
+
)
|
|
495
|
+
}
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### Paso 11: Agregar al Sidebar
|
|
499
|
+
|
|
500
|
+
```typescript
|
|
501
|
+
// src/components/layout/sidebar.tsx
|
|
502
|
+
const navItems: NavItem[] = [
|
|
503
|
+
// ...otros items
|
|
504
|
+
{
|
|
505
|
+
path: '/products',
|
|
506
|
+
name: 'Productos',
|
|
507
|
+
icon: 'Package', // Agregar icono en icons.tsx si no existe
|
|
508
|
+
},
|
|
509
|
+
]
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### Paso 12: Proteger ruta (si es necesario)
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
// proxy.ts
|
|
516
|
+
const protectedPaths = ['/dashboard', '/users', '/products', '/settings']
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
## Variables de Entorno
|
|
520
|
+
|
|
521
|
+
```bash
|
|
522
|
+
# .env.local
|
|
523
|
+
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
524
|
+
NEXT_PUBLIC_SUPABASE_URL=tu-supabase-url
|
|
525
|
+
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=tu-supabase-key
|
|
526
|
+
DATABASE_URL=postgresql://user:password@host:5432/database
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
## Comandos
|
|
530
|
+
|
|
531
|
+
```bash
|
|
532
|
+
# Desarrollo
|
|
533
|
+
pnpm dev
|
|
534
|
+
|
|
535
|
+
# Build
|
|
536
|
+
pnpm build
|
|
537
|
+
|
|
538
|
+
# Linting
|
|
539
|
+
pnpm lint
|
|
540
|
+
|
|
541
|
+
# Base de datos
|
|
542
|
+
pnpm db:generate # Generar migraciones
|
|
543
|
+
pnpm db:migrate # Aplicar migraciones
|
|
544
|
+
pnpm db:seed # Ejecutar seeds
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
## Licencia
|
|
548
|
+
|
|
549
|
+
MIT
|