@create-lft-app/nextjs
v3.1.0
Published
Next.js template para proyectos LFT con Midday Design System
Readme
@create-lft-app/nextjs
CLI para crear proyectos Next.js con arquitectura modular, Supabase, y Midday Design System.
Instalación
npx @create-lft-app/nextjs mi-proyectoStack Tecnológico
| Categoría | Librería | Versión | |-----------|----------|---------| | Framework | Next.js | ^16 | | Lenguaje | TypeScript | ^5 | | Estado Global | Zustand | ^5 | | Data Fetching | TanStack Query | ^5 | | Tablas | TanStack Table | ^8 | | UI Components | Radix UI | latest | | Animaciones | Framer Motion | ^11 | | Auth & Backend | Supabase SSR | ^0.5 | | ORM | Drizzle | latest | | Validación | Zod | ^3 | | Excel | xlsx (SheetJS) | latest | | Fechas | dayjs | ^1 | | Forms | React Hook Form | ^7 | | Package Manager | pnpm | latest |
Estructura de Carpetas
template/
├── proxy.ts # Next.js 16 middleware (protección de rutas)
├── drizzle.config.ts # Configuración Drizzle ORM
│
├── src/
│ ├── app/
│ │ ├── layout.tsx # Layout raíz
│ │ ├── page.tsx # Página principal (redirect)
│ │ ├── providers.tsx # TanStack Query + providers
│ │ │
│ │ ├── api/ # API Routes
│ │ │ └── webhooks/
│ │ │ └── route.ts
│ │ │
│ │ ├── (auth)/ # Rutas protegidas (requieren sesión)
│ │ │ ├── layout.tsx
│ │ │ ├── dashboard/
│ │ │ └── users/
│ │ │
│ │ └── (public)/ # Rutas públicas
│ │ ├── layout.tsx
│ │ └── login/
│ │
│ ├── components/
│ │ ├── layout/ # Componentes de layout
│ │ │ ├── sidebar.tsx
│ │ │ ├── sidebar-context.tsx
│ │ │ └── main-content.tsx
│ │ │
│ │ ├── tables/ # TanStack Table genéricos
│ │ │ ├── data-table.tsx
│ │ │ ├── data-table-column-header.tsx
│ │ │ ├── data-table-pagination.tsx
│ │ │ ├── data-table-toolbar.tsx
│ │ │ └── data-table-view-options.tsx
│ │ │
│ │ └── ui/ # Radix + Framer Motion
│ │ ├── animations/
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── dialog.tsx
│ │ ├── sheet.tsx
│ │ └── ...
│ │
│ ├── config/
│ │ ├── navigation.ts # Configuración de navegación
│ │ └── site.ts # Metadata del sitio
│ │
│ ├── db/ # Drizzle ORM
│ │ ├── index.ts # Cliente de base de datos
│ │ ├── seed.ts # Seeds
│ │ └── schema/ # Schemas de tablas
│ │
│ ├── hooks/ # Hooks globales reutilizables
│ │ ├── useDataTable.ts
│ │ ├── useDebounce.ts
│ │ └── useMediaQuery.ts
│ │
│ ├── lib/
│ │ ├── utils.ts # Utilidades (cn, etc.)
│ │ ├── query-client.ts # TanStack Query config
│ │ │
│ │ ├── date/ # Utilidades dayjs
│ │ │ ├── config.ts
│ │ │ └── formatters.ts
│ │ │
│ │ ├── excel/ # Utilidades xlsx
│ │ │ ├── exporter.ts
│ │ │ └── parser.ts
│ │ │
│ │ ├── supabase/ # Clientes Supabase
│ │ │ ├── client.ts # Browser
│ │ │ ├── server.ts # Server
│ │ │ └── proxy.ts # Session middleware
│ │ │
│ │ └── validations/ # Zod schemas comunes
│ │
│ ├── modules/ # ⭐ MÓDULOS (feature-based)
│ │ ├── auth/
│ │ └── users/
│ │
│ ├── stores/ # Zustand stores globales
│ │ └── useUiStore.ts
│ │
│ └── types/ # TypeScript types globales
│
└── supabase/
├── config.toml
└── functions/ # Edge Functions (Deno)Arquitectura Modular
Patrón Obligatorio
Store (Zustand) → Queries (TanStack) → Mutations → Hook Unificado → ComponenteRegla fundamental: Los componentes SOLO importan el hook unificado, nunca queries, mutations o stores directamente.
Estructura de un Módulo
Cada módulo es self-contained y sigue esta estructura:
src/modules/[nombre]/
├── index.ts # Barrel exports
├── columns.tsx # Columnas TanStack Table (si aplica)
│
├── actions/
│ └── [nombre]-actions.ts # Server actions
│
├── components/
│ └── [nombre]-list.tsx # Componentes del módulo
│
├── hooks/
│ ├── use[Nombre].ts # ⭐ Hook unificado (ÚNICO import en componentes)
│ ├── use[Nombre]Queries.ts # Queries TanStack
│ └── use[Nombre]Mutations.ts # Mutations TanStack
│
├── schemas/
│ └── [nombre].schema.ts # Zod schemas + tipos
│
└── stores/
└── use[Nombre]Store.ts # Zustand store del móduloCrear un Nuevo Módulo
Paso 1: Crear estructura de carpetas
mkdir -p src/modules/products/{actions,components,hooks,schemas,stores}Paso 2: Schema (Zod + tipos)
// src/modules/products/schemas/products.schema.ts
import { z } from 'zod'
export const productSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1, 'Nombre requerido'),
price: z.number().positive(),
stock: z.number().int().min(0),
created_at: z.string(),
})
export const createProductSchema = productSchema.omit({ id: true, created_at: true })
export const updateProductSchema = createProductSchema.partial()
export type Product = z.infer<typeof productSchema>
export type CreateProductInput = z.infer<typeof createProductSchema>
export type UpdateProductInput = z.infer<typeof updateProductSchema>Paso 3: Server Actions
// src/modules/products/actions/products-actions.ts
'use server'
import { createClient } from '@/lib/supabase/server'
import { createProductSchema, type CreateProductInput } from '../schemas/products.schema'
export async function getProducts() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) throw new Error('Unauthorized')
const { data, error } = await supabase
.from('products')
.select('*')
.order('created_at', { ascending: false })
if (error) throw error
return data
}
export async function createProduct(input: CreateProductInput) {
const parsed = createProductSchema.safeParse(input)
if (!parsed.success) throw new Error(parsed.error.errors[0].message)
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) throw new Error('Unauthorized')
const { data, error } = await supabase
.from('products')
.insert(parsed.data)
.select()
.single()
if (error) throw error
return data
}Paso 4: Store (Zustand)
// src/modules/products/stores/useProductsStore.ts
import { create } from 'zustand'
import { useShallow } from 'zustand/shallow'
interface ProductsState {
selectedProductId: string | null
isCreateDialogOpen: boolean
filters: { search: string; category: string | null }
}
interface ProductsActions {
setSelectedProductId: (id: string | null) => void
setIsCreateDialogOpen: (open: boolean) => void
setFilters: (filters: Partial<ProductsState['filters']>) => void
reset: () => void
}
const initialState: ProductsState = {
selectedProductId: null,
isCreateDialogOpen: false,
filters: { search: '', category: null },
}
export const useProductsStore = create<ProductsState & ProductsActions>((set) => ({
...initialState,
setSelectedProductId: (id) => set({ selectedProductId: id }),
setIsCreateDialogOpen: (open) => set({ isCreateDialogOpen: open }),
setFilters: (filters) => set((state) => ({
filters: { ...state.filters, ...filters }
})),
reset: () => set(initialState),
}))
// Selector optimizado con useShallow
export const useProductsFilters = () =>
useProductsStore(useShallow((state) => state.filters))Paso 5: Queries (TanStack Query)
// src/modules/products/hooks/useProductsQueries.ts
'use client'
import { useQuery } from '@tanstack/react-query'
import { getProducts, getProductById } from '../actions/products-actions'
export const productsKeys = {
all: ['products'] as const,
lists: () => [...productsKeys.all, 'list'] as const,
list: (filters: Record<string, unknown>) => [...productsKeys.lists(), filters] as const,
details: () => [...productsKeys.all, 'detail'] as const,
detail: (id: string) => [...productsKeys.details(), id] as const,
}
export function useProductsQueries(productId?: string) {
const productsQuery = useQuery({
queryKey: productsKeys.lists(),
queryFn: () => getProducts(),
})
const productQuery = useQuery({
queryKey: productsKeys.detail(productId!),
queryFn: () => getProductById(productId!),
enabled: !!productId,
})
return {
products: productsQuery.data ?? [],
product: productQuery.data,
isLoading: productsQuery.isLoading,
error: productsQuery.error,
}
}Paso 6: Mutations (TanStack Query)
// src/modules/products/hooks/useProductsMutations.ts
'use client'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { createProduct, updateProduct, deleteProduct } from '../actions/products-actions'
import { productsKeys } from './useProductsQueries'
import type { CreateProductInput, UpdateProductInput } from '../schemas/products.schema'
export function useProductsMutations() {
const queryClient = useQueryClient()
const createMutation = useMutation({
mutationFn: (data: CreateProductInput) => createProduct(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: productsKeys.lists() })
},
})
const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: string; data: UpdateProductInput }) =>
updateProduct(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: productsKeys.all })
},
})
const deleteMutation = useMutation({
mutationFn: (id: string) => deleteProduct(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: productsKeys.lists() })
},
})
return {
createProduct: createMutation.mutateAsync,
updateProduct: updateMutation.mutateAsync,
deleteProduct: deleteMutation.mutateAsync,
isCreating: createMutation.isPending,
isUpdating: updateMutation.isPending,
isDeleting: deleteMutation.isPending,
}
}Paso 7: Hook Unificado ⭐
// src/modules/products/hooks/useProducts.ts
'use client'
import { useProductsQueries } from './useProductsQueries'
import { useProductsMutations } from './useProductsMutations'
import { useProductsStore, useProductsFilters } from '../stores/useProductsStore'
export function useProducts(productId?: string) {
// Queries
const { products, product, isLoading, error } = useProductsQueries(productId)
// Mutations
const {
createProduct,
updateProduct,
deleteProduct,
isCreating,
isUpdating,
isDeleting
} = useProductsMutations()
// Store
const filters = useProductsFilters()
const {
selectedProductId,
isCreateDialogOpen,
setSelectedProductId,
setIsCreateDialogOpen,
setFilters,
} = useProductsStore()
// Datos filtrados
const filteredProducts = products.filter((p) =>
p.name.toLowerCase().includes(filters.search.toLowerCase())
)
return {
// Data
products: filteredProducts,
product,
// Loading states
isLoading,
isCreating,
isUpdating,
isDeleting,
error,
// Actions
createProduct,
updateProduct,
deleteProduct,
// UI State
selectedProductId,
isCreateDialogOpen,
filters,
setSelectedProductId,
setIsCreateDialogOpen,
setFilters,
}
}Paso 8: Componentes
// src/modules/products/components/products-list.tsx
'use client'
import { DataTable } from '@/components/tables/data-table'
import { useProducts } from '../hooks/useProducts'
import { columns } from '../columns'
export function ProductsList() {
const { products, isLoading } = useProducts()
if (isLoading) {
return <div>Cargando productos...</div>
}
return (
<DataTable
columns={columns}
data={products}
searchKey="name"
searchPlaceholder="Buscar por nombre..."
/>
)
}Paso 9: Barrel Export
// src/modules/products/index.ts
export * from './components/products-list'
export * from './hooks/useProducts'
export * from './schemas/products.schema'
export { columns as productColumns } from './columns'Paso 10: Crear página
// src/app/(auth)/products/page.tsx
import { createClient } from '@/lib/supabase/server'
import { ProductsContent } from './products-content'
export default async function ProductsPage() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
return <ProductsContent userEmail={user?.email ?? ''} />
}// src/app/(auth)/products/products-content.tsx
'use client'
import { PageTransition, PageHeader, PageTitle } from '@/components/ui/animations'
import { ProductsList } from '@/modules/products'
export function ProductsContent({ userEmail }: { userEmail: string }) {
return (
<PageTransition className="flex flex-1 flex-col gap-6 p-6">
<PageHeader>
<PageTitle>Productos</PageTitle>
</PageHeader>
<ProductsList />
</PageTransition>
)
}Paso 11: Agregar al Sidebar
// src/components/layout/sidebar.tsx
const navItems: NavItem[] = [
// ...otros items
{
path: '/products',
name: 'Productos',
icon: 'Package', // Agregar icono en icons.tsx si no existe
},
]Paso 12: Proteger ruta (si es necesario)
// proxy.ts
const protectedPaths = ['/dashboard', '/users', '/products', '/settings']Variables de Entorno
# .env.local
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_SUPABASE_URL=tu-supabase-url
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=tu-supabase-key
DATABASE_URL=postgresql://user:password@host:5432/databaseComandos
# Desarrollo
pnpm dev
# Build
pnpm build
# Linting
pnpm lint
# Base de datos
pnpm db:generate # Generar migraciones
pnpm db:migrate # Aplicar migraciones
pnpm db:seed # Ejecutar seedsLicencia
MIT
