@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.
Files changed (2) hide show
  1. package/README.md +549 -0
  2. 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@create-lft-app/nextjs",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Next.js template para proyectos LFT con Midday Design System",
5
5
  "type": "module",
6
6
  "bin": {