@create-lft-app/nextjs 3.2.0 → 3.3.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 (133) hide show
  1. package/README.md +549 -549
  2. package/package.json +48 -48
  3. package/template/.claude/skills/anti-patterns.md +150 -0
  4. package/template/.claude/skills/drizzle-schema.md +178 -0
  5. package/template/.claude/skills/formatting.md +56 -0
  6. package/template/.claude/skills/module-architecture.md +143 -0
  7. package/template/.claude/skills/supabase-server-actions.md +199 -0
  8. package/template/.claude/skills/ui-patterns.md +161 -0
  9. package/template/CLAUDE.md +114 -1239
  10. package/template/drizzle.config.ts +12 -12
  11. package/template/eslint.config.mjs +16 -16
  12. package/template/gitignore +36 -36
  13. package/template/next.config.ts +7 -7
  14. package/template/package.json +86 -86
  15. package/template/postcss.config.mjs +7 -7
  16. package/template/proxy.ts +12 -12
  17. package/template/public/logolft.svg +11 -11
  18. package/template/src/app/(auth)/dashboard/dashboard-content.tsx +124 -124
  19. package/template/src/app/(auth)/dashboard/page.tsx +9 -9
  20. package/template/src/app/(auth)/layout.tsx +7 -7
  21. package/template/src/app/(auth)/users/page.tsx +9 -9
  22. package/template/src/app/(auth)/users/users-content.tsx +26 -26
  23. package/template/src/app/(public)/layout.tsx +7 -7
  24. package/template/src/app/(public)/login/page.tsx +17 -17
  25. package/template/src/app/api/webhooks/route.ts +20 -20
  26. package/template/src/app/globals.css +249 -249
  27. package/template/src/app/layout.tsx +37 -37
  28. package/template/src/app/page.tsx +5 -5
  29. package/template/src/app/providers.tsx +27 -27
  30. package/template/src/components/layout/main-content.tsx +28 -28
  31. package/template/src/components/layout/sidebar-context.tsx +33 -33
  32. package/template/src/components/layout/sidebar.tsx +141 -141
  33. package/template/src/components/tables/data-table-column-header.tsx +68 -68
  34. package/template/src/components/tables/data-table-date-filter.tsx +203 -203
  35. package/template/src/components/tables/data-table-faceted-filter.tsx +185 -185
  36. package/template/src/components/tables/data-table-filters-dropdown.tsx +130 -130
  37. package/template/src/components/tables/data-table-number-filter.tsx +295 -295
  38. package/template/src/components/tables/data-table-pagination.tsx +99 -99
  39. package/template/src/components/tables/data-table-toolbar.tsx +140 -140
  40. package/template/src/components/tables/data-table-view-options.tsx +63 -63
  41. package/template/src/components/tables/data-table.tsx +148 -148
  42. package/template/src/components/tables/index.ts +9 -9
  43. package/template/src/components/ui/accordion.tsx +58 -58
  44. package/template/src/components/ui/alert-dialog.tsx +165 -165
  45. package/template/src/components/ui/alert.tsx +66 -66
  46. package/template/src/components/ui/animations/index.ts +44 -44
  47. package/template/src/components/ui/avatar.tsx +55 -55
  48. package/template/src/components/ui/badge.tsx +50 -50
  49. package/template/src/components/ui/button.tsx +118 -118
  50. package/template/src/components/ui/calendar.tsx +220 -220
  51. package/template/src/components/ui/card.tsx +113 -113
  52. package/template/src/components/ui/checkbox.tsx +38 -38
  53. package/template/src/components/ui/collapsible.tsx +33 -33
  54. package/template/src/components/ui/command.tsx +196 -196
  55. package/template/src/components/ui/dialog.tsx +156 -156
  56. package/template/src/components/ui/dropdown-menu.tsx +280 -280
  57. package/template/src/components/ui/form.tsx +171 -171
  58. package/template/src/components/ui/icons.tsx +167 -167
  59. package/template/src/components/ui/input.tsx +28 -28
  60. package/template/src/components/ui/label.tsx +25 -25
  61. package/template/src/components/ui/motion.tsx +197 -197
  62. package/template/src/components/ui/page-transition.tsx +166 -166
  63. package/template/src/components/ui/popover.tsx +59 -59
  64. package/template/src/components/ui/progress.tsx +32 -32
  65. package/template/src/components/ui/radio-group.tsx +45 -45
  66. package/template/src/components/ui/scroll-area.tsx +63 -63
  67. package/template/src/components/ui/select.tsx +208 -208
  68. package/template/src/components/ui/separator.tsx +28 -28
  69. package/template/src/components/ui/sheet.tsx +170 -170
  70. package/template/src/components/ui/sidebar.tsx +726 -726
  71. package/template/src/components/ui/skeleton.tsx +15 -15
  72. package/template/src/components/ui/slider.tsx +58 -58
  73. package/template/src/components/ui/sonner.tsx +47 -47
  74. package/template/src/components/ui/spinner.tsx +27 -27
  75. package/template/src/components/ui/submit-button.tsx +47 -47
  76. package/template/src/components/ui/switch.tsx +31 -31
  77. package/template/src/components/ui/table.tsx +120 -120
  78. package/template/src/components/ui/tabs.tsx +75 -75
  79. package/template/src/components/ui/textarea.tsx +26 -26
  80. package/template/src/components/ui/tooltip.tsx +70 -70
  81. package/template/src/config/navigation.ts +59 -59
  82. package/template/src/config/roles.ts +27 -27
  83. package/template/src/config/site.ts +12 -12
  84. package/template/src/db/index.ts +12 -12
  85. package/template/src/db/schema/index.ts +1 -1
  86. package/template/src/db/schema/users.ts +16 -16
  87. package/template/src/db/seed.ts +39 -39
  88. package/template/src/hooks/index.ts +3 -3
  89. package/template/src/hooks/use-mobile.ts +21 -21
  90. package/template/src/hooks/useDataTable.ts +82 -82
  91. package/template/src/hooks/useDebounce.ts +49 -49
  92. package/template/src/hooks/useMediaQuery.ts +36 -36
  93. package/template/src/lib/date/config.ts +36 -36
  94. package/template/src/lib/date/formatters.ts +127 -127
  95. package/template/src/lib/date/index.ts +26 -26
  96. package/template/src/lib/excel/exporter.ts +89 -89
  97. package/template/src/lib/excel/index.ts +14 -14
  98. package/template/src/lib/excel/parser.ts +96 -96
  99. package/template/src/lib/query-client.ts +35 -35
  100. package/template/src/lib/supabase/admin.ts +23 -23
  101. package/template/src/lib/supabase/client.ts +11 -11
  102. package/template/src/lib/supabase/proxy.ts +67 -67
  103. package/template/src/lib/supabase/server.ts +38 -38
  104. package/template/src/lib/supabase/types.ts +53 -53
  105. package/template/src/lib/utils.ts +6 -6
  106. package/template/src/lib/validations/common.ts +75 -75
  107. package/template/src/lib/validations/index.ts +20 -20
  108. package/template/src/modules/auth/actions/auth-actions.ts +59 -59
  109. package/template/src/modules/auth/components/login-form.tsx +68 -68
  110. package/template/src/modules/auth/hooks/useAuth.ts +38 -38
  111. package/template/src/modules/auth/hooks/useAuthMutations.ts +43 -43
  112. package/template/src/modules/auth/hooks/useAuthQueries.ts +43 -43
  113. package/template/src/modules/auth/index.ts +12 -12
  114. package/template/src/modules/auth/schemas/auth.schema.ts +32 -32
  115. package/template/src/modules/auth/stores/useAuthStore.ts +37 -37
  116. package/template/src/modules/users/actions/users-actions.ts +166 -166
  117. package/template/src/modules/users/columns.tsx +106 -106
  118. package/template/src/modules/users/components/users-list.tsx +48 -48
  119. package/template/src/modules/users/hooks/useUsers.ts +39 -39
  120. package/template/src/modules/users/hooks/useUsersMutations.ts +55 -55
  121. package/template/src/modules/users/hooks/useUsersQueries.ts +35 -35
  122. package/template/src/modules/users/index.ts +30 -30
  123. package/template/src/modules/users/schemas/users.schema.ts +51 -51
  124. package/template/src/modules/users/stores/useUsersStore.ts +60 -60
  125. package/template/src/modules/users/types/auth-user.types.ts +42 -42
  126. package/template/src/modules/users/utils/user-mapper.ts +32 -32
  127. package/template/src/stores/index.ts +1 -1
  128. package/template/src/stores/useUiStore.ts +55 -55
  129. package/template/src/types/api.ts +28 -28
  130. package/template/src/types/index.ts +2 -2
  131. package/template/src/types/table.ts +34 -34
  132. package/template/supabase/config.toml +94 -94
  133. package/template/tsconfig.json +42 -42
package/package.json CHANGED
@@ -1,48 +1,48 @@
1
- {
2
- "name": "@create-lft-app/nextjs",
3
- "version": "3.2.0",
4
- "description": "Next.js template para proyectos LFT con Midday Design System",
5
- "type": "module",
6
- "bin": {
7
- "create-lft-nextjs": "./bin/cli.js"
8
- },
9
- "files": [
10
- "bin",
11
- "template",
12
- "!template/node_modules",
13
- "!template/.next",
14
- "!template/package-lock.json",
15
- "!template/pnpm-lock.yaml",
16
- "!template/next-env.d.ts",
17
- "!template/.env*"
18
- ],
19
- "scripts": {
20
- "build": "tsc",
21
- "prepublishOnly": "npm run build"
22
- },
23
- "keywords": [
24
- "nextjs",
25
- "template",
26
- "lft",
27
- "midday",
28
- "dashboard"
29
- ],
30
- "author": "LFT",
31
- "license": "MIT",
32
- "repository": {
33
- "type": "git",
34
- "url": "https://github.com/somosdcg/next-boiler"
35
- },
36
- "publishConfig": {
37
- "access": "public"
38
- },
39
- "devDependencies": {
40
- "@types/fs-extra": "^11.0.0",
41
- "@types/node": "^20.0.0",
42
- "typescript": "^5.0.0"
43
- },
44
- "dependencies": {
45
- "commander": "^12.0.0",
46
- "fs-extra": "^11.0.0"
47
- }
48
- }
1
+ {
2
+ "name": "@create-lft-app/nextjs",
3
+ "version": "3.3.0",
4
+ "description": "Next.js template para proyectos LFT con Midday Design System",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-lft-nextjs": "./bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "template",
12
+ "!template/node_modules",
13
+ "!template/.next",
14
+ "!template/package-lock.json",
15
+ "!template/pnpm-lock.yaml",
16
+ "!template/next-env.d.ts",
17
+ "!template/.env*"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "prepublishOnly": "npm run build"
22
+ },
23
+ "keywords": [
24
+ "nextjs",
25
+ "template",
26
+ "lft",
27
+ "midday",
28
+ "dashboard"
29
+ ],
30
+ "author": "LFT",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/somosdcg/next-boiler"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "devDependencies": {
40
+ "@types/fs-extra": "^11.0.0",
41
+ "@types/node": "^20.0.0",
42
+ "typescript": "^5.0.0"
43
+ },
44
+ "dependencies": {
45
+ "commander": "^12.0.0",
46
+ "fs-extra": "^11.0.0"
47
+ }
48
+ }
@@ -0,0 +1,150 @@
1
+ # Anti-Patterns & Correct Patterns
2
+
3
+ ## NEVER: Import queries or mutations directly in components
4
+
5
+ ```typescript
6
+ // WRONG
7
+ import { useEntityMutations } from '@/modules/[entity]/hooks/useEntityMutations'
8
+ import { useEntityQueries } from '@/modules/[entity]/hooks/useEntityQueries'
9
+
10
+ export function MyComponent() {
11
+ const { createEntity } = useEntityMutations()
12
+ const { entities } = useEntityQueries()
13
+ }
14
+ ```
15
+
16
+ ```typescript
17
+ // CORRECT — Always use the unified hook
18
+ import { useEntity } from '@/modules/[entity]/hooks/useEntity'
19
+
20
+ export function MyComponent() {
21
+ const { createEntity, entities } = useEntity()
22
+ }
23
+ ```
24
+
25
+ ## NEVER: Consume the entire Zustand store
26
+
27
+ ```typescript
28
+ // WRONG — Causes unnecessary re-renders
29
+ const store = useEntityStore()
30
+ ```
31
+
32
+ ```typescript
33
+ // CORRECT — Use separate selectors with useShallow
34
+ import { useShallow } from 'zustand/react/shallow'
35
+
36
+ const state = useEntityStore(useShallow((s) => s.state))
37
+ const actions = useEntityStore(useShallow((s) => s.actions))
38
+ ```
39
+
40
+ ## NEVER: Manual data fetching in useEffect
41
+
42
+ ```typescript
43
+ // WRONG
44
+ useEffect(() => {
45
+ fetch('/api/entities').then(setData)
46
+ }, [])
47
+ ```
48
+
49
+ ```typescript
50
+ // CORRECT — Use TanStack Query
51
+ const { data } = useQuery({
52
+ queryKey: ['entities'],
53
+ queryFn: fetchEntities,
54
+ })
55
+ ```
56
+
57
+ ## NEVER: Server actions without authenticated user context
58
+
59
+ ```typescript
60
+ // WRONG — Anonymous client, RLS will fail
61
+ export async function deleteEntity(id: string) {
62
+ const supabase = createClient()
63
+ await supabase.from('entities').delete().eq('id', id)
64
+ }
65
+ ```
66
+
67
+ ```typescript
68
+ // CORRECT — Authenticated client with user verification
69
+ 'use server'
70
+ import { createClient } from '@/lib/supabase/server'
71
+
72
+ export async function deleteEntity(id: string) {
73
+ const supabase = await createClient()
74
+ const { data: { user } } = await supabase.auth.getUser()
75
+ if (!user) throw new Error('Unauthorized')
76
+
77
+ const { error } = await supabase.from('entities').delete().eq('id', id)
78
+ if (error) throw error
79
+ }
80
+ ```
81
+
82
+ ## NEVER: Use Drizzle db directly in Server Actions
83
+
84
+ ```typescript
85
+ // WRONG — db queries must go through repository
86
+ 'use server'
87
+ export async function getProducts() {
88
+ return await db.select().from(products) // VIOLATION
89
+ }
90
+ ```
91
+
92
+ ```typescript
93
+ // CORRECT — Server actions use repository
94
+ 'use server'
95
+ import { productRepository } from '../repositories/products.repository'
96
+
97
+ export async function getProducts() {
98
+ await requireAuth()
99
+ return productRepository.findAll()
100
+ }
101
+ ```
102
+
103
+ **RULE**: If you see `db.select()`, `db.insert()`, `db.update()`, or `db.delete()` outside a `.repository.ts` file, it's an architectural violation.
104
+
105
+ ## NEVER: Put server/fetched data in Zustand stores
106
+
107
+ Zustand stores are for **UI state only** (selections, filters, modal states). Server data belongs in TanStack Query cache.
108
+
109
+ ## NEVER: Use modals/dialogs for CRUD operations
110
+
111
+ ```typescript
112
+ // WRONG — Modal for create/edit
113
+ <Dialog>
114
+ <DialogTrigger>Create User</DialogTrigger>
115
+ <DialogContent><UserForm /></DialogContent>
116
+ </Dialog>
117
+ ```
118
+
119
+ ```typescript
120
+ // CORRECT — Navigate to full pages
121
+ router.push('/users/new') // Create
122
+ router.push(`/users/${id}/edit`) // Edit
123
+ router.push(`/users/${id}`) // View detail
124
+ // Only exception: AlertDialog for delete confirmation
125
+ ```
126
+
127
+ ## NEVER: Hardcode locale for dates or numbers
128
+
129
+ ```typescript
130
+ // WRONG
131
+ date.toLocaleDateString('es-ES')
132
+ num.toLocaleString('es-AR')
133
+ ```
134
+
135
+ ```typescript
136
+ // CORRECT — Use centralized config (see skill: formatting)
137
+ import { formatDate } from '@/lib/date'
138
+ import { DEFAULT_LOCALE } from '@/lib/date'
139
+
140
+ formatDate(value)
141
+ num.toLocaleString(DEFAULT_LOCALE)
142
+ ```
143
+
144
+ ## NEVER: Use revalidatePath when data is consumed via TanStack Query
145
+
146
+ Cache invalidation is handled by `invalidateQueries` in the mutations hook. Only use `revalidatePath` if a Server Component also consumes the same data outside TanStack Query.
147
+
148
+ ## NEVER: Skip the implementation order
149
+
150
+ Always follow the checklist in skill: module-architecture.
@@ -0,0 +1,178 @@
1
+ # Drizzle ORM & Repository Pattern
2
+
3
+ ## File Structure
4
+
5
+ ```
6
+ src/db/
7
+ ├── index.ts # Drizzle client (exports `db`)
8
+ ├── schema/
9
+ │ ├── index.ts # Barrel export of all schemas — ALWAYS export here
10
+ │ └── [entity].ts # Schema per entity
11
+ ├── migrations/ # Auto-generated migrations
12
+ └── seed.ts # Seed script
13
+ ```
14
+
15
+ ## Drizzle Client
16
+
17
+ ```typescript
18
+ // src/db/index.ts
19
+ import { drizzle } from 'drizzle-orm/postgres-js'
20
+ import postgres from 'postgres'
21
+ import * as schema from './schema'
22
+
23
+ const connectionString = process.env.DATABASE_URL!
24
+ const client = postgres(connectionString, { prepare: false })
25
+
26
+ export const db = drizzle(client, { schema })
27
+ export type Database = typeof db
28
+ ```
29
+
30
+ ## Schema Definition
31
+
32
+ ```typescript
33
+ // src/db/schema/products.ts
34
+ import { pgTable, uuid, varchar, text, timestamp, pgEnum, numeric, integer } from 'drizzle-orm/pg-core'
35
+
36
+ export const products = pgTable('products', {
37
+ id: uuid('id').primaryKey().defaultRandom(),
38
+ name: varchar('name', { length: 255 }).notNull(),
39
+ description: text('description'),
40
+ price: numeric('price', { precision: 10, scale: 2 }).notNull(),
41
+ stock: integer('stock').default(0).notNull(),
42
+ createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
43
+ updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
44
+ })
45
+
46
+ // Inferred types
47
+ export type Product = typeof products.$inferSelect
48
+ export type NewProduct = typeof products.$inferInsert
49
+ ```
50
+
51
+ **ALWAYS export each schema in `src/db/schema/index.ts`:**
52
+
53
+ ```typescript
54
+ // src/db/schema/index.ts
55
+ export * from './products'
56
+ // Add new entities here
57
+ ```
58
+
59
+ ### Naming Conventions
60
+
61
+ - Table names: **snake_case plural** (`products`, `order_items`)
62
+ - Column names: **snake_case** in DB (`created_at`), **camelCase** in TypeScript (`createdAt`)
63
+ - Enum types: **snake_case** (`user_role`, `order_status`)
64
+ - Always include `id`, `createdAt`, `updatedAt` on every table
65
+ - Use `uuid` for primary keys with `defaultRandom()`
66
+ - Use `timestamp('...', { withTimezone: true })` for dates
67
+
68
+ ## Repository Pattern (MANDATORY for own tables)
69
+
70
+ Every entity with its own table MUST have a repository. Server actions NEVER use `db` directly.
71
+
72
+ ```typescript
73
+ // modules/[entity]/repositories/[entity].repository.ts
74
+ import { db } from '@/db'
75
+ import { products, type Product, type NewProduct } from '@/db/schema'
76
+ import { eq } from 'drizzle-orm'
77
+
78
+ export interface ProductRepository {
79
+ findAll(): Promise<Product[]>
80
+ findById(id: string): Promise<Product | null>
81
+ create(data: NewProduct): Promise<Product>
82
+ update(id: string, data: Partial<NewProduct>): Promise<Product>
83
+ delete(id: string): Promise<void>
84
+ }
85
+
86
+ export const productRepository: ProductRepository = {
87
+ async findAll() {
88
+ return await db.select().from(products)
89
+ },
90
+
91
+ async findById(id: string) {
92
+ const [product] = await db
93
+ .select()
94
+ .from(products)
95
+ .where(eq(products.id, id))
96
+ return product ?? null
97
+ },
98
+
99
+ async create(data: NewProduct) {
100
+ const [product] = await db.insert(products).values(data).returning()
101
+ return product
102
+ },
103
+
104
+ async update(id: string, data: Partial<NewProduct>) {
105
+ const [product] = await db
106
+ .update(products)
107
+ .set({ ...data, updatedAt: new Date() })
108
+ .where(eq(products.id, id))
109
+ .returning()
110
+ return product
111
+ },
112
+
113
+ async delete(id: string) {
114
+ await db.delete(products).where(eq(products.id, id))
115
+ },
116
+ }
117
+ ```
118
+
119
+ ## Drizzle Commands
120
+
121
+ | Command | When to use |
122
+ |---------|-------------|
123
+ | `pnpm db:generate` | After modifying schemas to generate migrations |
124
+ | `pnpm db:migrate` | To apply pending migrations |
125
+ | `pnpm db:push` | Dev only — sync schema directly without migration |
126
+ | `pnpm db:studio` | To inspect data visually |
127
+ | `pnpm db:seed` | To populate database with seed data |
128
+
129
+ ## Schema Change Workflow
130
+
131
+ 1. Modify or create schema in `src/db/schema/[entity].ts`
132
+ 2. Export in `src/db/schema/index.ts`
133
+ 3. Run: `pnpm db:generate`
134
+ 4. Review generated migration in `src/db/migrations/`
135
+ 5. Run: `pnpm db:migrate`
136
+
137
+ ## Seed Pattern
138
+
139
+ ```typescript
140
+ // src/db/seed.ts
141
+ import { db } from './index'
142
+ import { products } from './schema'
143
+
144
+ async function seed() {
145
+ console.log('Seeding database...')
146
+ await db.delete(products)
147
+ await db.insert(products).values([
148
+ { name: 'Product A', price: '10.00' },
149
+ { name: 'Product B', price: '20.00' },
150
+ ])
151
+ console.log('Database seeded!')
152
+ }
153
+
154
+ seed().catch(console.error).finally(() => process.exit())
155
+ ```
156
+
157
+ ## Drizzle vs Supabase Admin
158
+
159
+ | Entity | Must use | Reason |
160
+ |--------|----------|--------|
161
+ | `auth.users` | Supabase Admin API | Auth users (see skill: supabase-server-actions) |
162
+ | Own tables (`products`, `orders`, etc.) | Drizzle ORM + Repository | RLS + typed queries |
163
+
164
+ ## Drizzle Config
165
+
166
+ ```typescript
167
+ // drizzle.config.ts
168
+ import { defineConfig } from 'drizzle-kit'
169
+
170
+ export default defineConfig({
171
+ schema: './src/db/schema/index.ts',
172
+ out: './src/db/migrations',
173
+ dialect: 'postgresql',
174
+ dbCredentials: {
175
+ url: process.env.DATABASE_URL!,
176
+ },
177
+ })
178
+ ```
@@ -0,0 +1,56 @@
1
+ # Date & Number Formatting
2
+
3
+ ## Rule: ALWAYS use `@/lib/date` for dates, NEVER native Date methods
4
+
5
+ ```typescript
6
+ // WRONG
7
+ const date = new Date(value)
8
+ return date.toLocaleDateString('es-ES')
9
+
10
+ // CORRECT
11
+ import { formatDate } from '@/lib/date'
12
+ return formatDate(value)
13
+ ```
14
+
15
+ ## Available Functions (`@/lib/date`)
16
+
17
+ | Function | Output example | Use case |
18
+ |----------|---------------|----------|
19
+ | `formatDate(date)` | `"15/01/2024"` | Standard date |
20
+ | `formatDateShort(date)` | `"15 ene"` | Filters, chips |
21
+ | `formatDateLong(date)` | `"15 de enero de 2024"` | Headers, titles |
22
+ | `formatTime(date)` | `"14:30"` | Time only |
23
+ | `formatDateTime(date)` | `"15/01/2024 14:30"` | Date and time |
24
+ | `formatRelative(date)` | `"hace 2 horas"` | Relative time |
25
+
26
+ ## Number Formatting
27
+
28
+ ALWAYS use `DEFAULT_LOCALE` from `@/lib/date` for numbers:
29
+
30
+ ```typescript
31
+ // WRONG — hardcoded locale
32
+ num.toLocaleString('es-AR')
33
+
34
+ // CORRECT — centralized config
35
+ import { DEFAULT_LOCALE, DEFAULT_CURRENCY } from '@/lib/date'
36
+ num.toLocaleString(DEFAULT_LOCALE)
37
+
38
+ // Currency
39
+ new Intl.NumberFormat(DEFAULT_LOCALE, {
40
+ style: 'currency',
41
+ currency: DEFAULT_CURRENCY,
42
+ }).format(amount)
43
+ ```
44
+
45
+ ## Configuration
46
+
47
+ All i18n config is centralized in `lib/date/config.ts`:
48
+
49
+ ```typescript
50
+ // lib/date/config.ts
51
+ export const DEFAULT_LOCALE = 'es-AR'
52
+ export const DEFAULT_TIMEZONE = 'America/Argentina/Buenos_Aires'
53
+ export const DEFAULT_CURRENCY = 'ARS'
54
+ ```
55
+
56
+ Changing these values affects the entire project (dates and numbers).
@@ -0,0 +1,143 @@
1
+ # Module Architecture Pattern
2
+
3
+ ## Mandatory Flow
4
+
5
+ ```
6
+ Repository → Server Actions → Queries → Mutations → Unified Hook → Component
7
+ ```
8
+
9
+ Every feature module MUST follow this exact structure. No exceptions.
10
+
11
+ ## Module Folder Structure
12
+
13
+ ```
14
+ modules/
15
+ └── [entity]/
16
+ ├── repositories/
17
+ │ └── [entity].repository.ts # Data access with Drizzle (required for own tables)
18
+ ├── actions/
19
+ │ └── [entity]-actions.ts # Server actions (uses repository, NEVER db direct)
20
+ ├── components/
21
+ │ ├── [entity]-list.tsx # Component imports ONLY unified hook
22
+ │ ├── [entity]-form.tsx
23
+ │ └── [entity]-detail.tsx
24
+ ├── hooks/
25
+ │ ├── use[Entity]Queries.ts # TanStack Query (read operations)
26
+ │ ├── use[Entity]Mutations.ts # TanStack Mutations (write operations)
27
+ │ └── use[Entity].ts # UNIFIED HOOK (components use THIS)
28
+ ├── stores/
29
+ │ └── use[Entity]Store.ts # Zustand store (UI state only)
30
+ ├── schemas/
31
+ │ └── [entity].schema.ts # Zod validation schemas
32
+ ├── utils/
33
+ │ └── [entity]-mapper.ts # Mappers if needed (e.g., auth.users → User)
34
+ ├── columns.tsx # TanStack Table column definitions (if needed)
35
+ └── index.ts # Barrel export
36
+ ```
37
+
38
+ ## Layer Architecture
39
+
40
+ ```
41
+ UI/Presentation Layer
42
+ ↓ (components use unified hook)
43
+ Service/Business Layer (hooks: use[Entity].ts)
44
+ ↓ (calls server actions)
45
+ Action Layer (actions: [entity]-actions.ts)
46
+ ↓ (calls repository)
47
+ Data Access Layer (repositories: [entity].repository.ts)
48
+ ↓ (uses Drizzle)
49
+ Data Source (Supabase/PostgreSQL)
50
+ ```
51
+
52
+ ## Implementation Checklist
53
+
54
+ When creating or modifying a module, follow these steps in order:
55
+
56
+ 1. **Drizzle schema** — `db/schema/[entity].ts` + export in `db/schema/index.ts` (see skill: drizzle-schema)
57
+ 2. **Repository** — `modules/[entity]/repositories/[entity].repository.ts` (required for own tables, see skill: drizzle-schema)
58
+ 3. **Zod schemas** — `modules/[entity]/schemas/[entity].schema.ts`
59
+ 4. **Server actions** — `modules/[entity]/actions/[entity]-actions.ts` (uses repository, NEVER db direct; see skill: supabase-server-actions)
60
+ 5. **Zustand store** — `modules/[entity]/stores/use[Entity]Store.ts` (UI state only, consumed with `useShallow`)
61
+ 6. **Queries hook** — `modules/[entity]/hooks/use[Entity]Queries.ts` (TanStack Query, read operations)
62
+ 7. **Mutations hook** — `modules/[entity]/hooks/use[Entity]Mutations.ts` (TanStack Mutations with cache invalidation)
63
+ 8. **Unified hook** — `modules/[entity]/hooks/use[Entity].ts` (combines store + queries + mutations)
64
+ 9. **Components** — Import ONLY the unified hook, never queries/mutations directly
65
+ 10. **Pages** — Create routes: `/[entity]`, `/[entity]/new`, `/[entity]/[id]`, `/[entity]/[id]/edit` (see skill: ui-patterns)
66
+ 11. **Verify** — Run `pnpm typecheck && pnpm lint`
67
+
68
+ **Note:** For modules using `auth.users` (like the users module), skip steps 1-2 and use Supabase Admin API instead (see skill: supabase-server-actions).
69
+
70
+ ## Unified Hook Pattern
71
+
72
+ The unified hook is the single entry point for components. It combines store state, queries, and mutations:
73
+
74
+ ```typescript
75
+ // modules/[entity]/hooks/use[Entity].ts
76
+ import { useShallow } from 'zustand/react/shallow'
77
+ import { use[Entity]Store } from '../stores/use[Entity]Store'
78
+ import { use[Entity]Queries } from './use[Entity]Queries'
79
+ import { use[Entity]Mutations } from './use[Entity]Mutations'
80
+
81
+ export function use[Entity]() {
82
+ const state = use[Entity]Store(useShallow((s) => s.state))
83
+ const actions = use[Entity]Store(useShallow((s) => s.actions))
84
+ const queries = use[Entity]Queries()
85
+ const mutations = use[Entity]Mutations()
86
+
87
+ return {
88
+ // Store
89
+ ...state,
90
+ ...actions,
91
+ // Queries
92
+ ...queries,
93
+ // Mutations
94
+ ...mutations,
95
+ }
96
+ }
97
+ // IMPORTANT: Avoid naming collisions across store/queries/mutations.
98
+ // Use prefixed names (e.g., `isCreating`, `isDeleting` instead of generic `isPending`).
99
+ ```
100
+
101
+ ## Component Usage
102
+
103
+ ```typescript
104
+ // modules/[entity]/components/[entity]-list.tsx
105
+ import { use[Entity] } from '../hooks/use[Entity]'
106
+
107
+ export function EntityList() {
108
+ const { entities, isLoading, createEntity, selectedId, setSelectedId } = use[Entity]()
109
+
110
+ // All state, queries, and mutations available from a single hook
111
+ }
112
+ ```
113
+
114
+ ## Zustand Store Pattern
115
+
116
+ Stores hold **UI state only** (selections, filters, modals). Never put server data in stores.
117
+
118
+ ```typescript
119
+ // modules/[entity]/stores/use[Entity]Store.ts
120
+ import { create } from 'zustand'
121
+
122
+ interface EntityState {
123
+ state: {
124
+ selectedId: string | null
125
+ isFormOpen: boolean
126
+ }
127
+ actions: {
128
+ setSelectedId: (id: string | null) => void
129
+ setFormOpen: (open: boolean) => void
130
+ }
131
+ }
132
+
133
+ export const use[Entity]Store = create<EntityState>((set) => ({
134
+ state: {
135
+ selectedId: null,
136
+ isFormOpen: false,
137
+ },
138
+ actions: {
139
+ setSelectedId: (id) => set((s) => ({ state: { ...s.state, selectedId: id } })),
140
+ setFormOpen: (open) => set((s) => ({ state: { ...s.state, isFormOpen: open } })),
141
+ },
142
+ }))
143
+ ```