@create-lft-app/nextjs 3.1.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.
- package/package.json +1 -1
- package/template/.claude/skills/anti-patterns.md +150 -0
- package/template/.claude/skills/drizzle-schema.md +178 -0
- package/template/.claude/skills/formatting.md +56 -0
- package/template/.claude/skills/module-architecture.md +143 -0
- package/template/.claude/skills/supabase-server-actions.md +199 -0
- package/template/.claude/skills/ui-patterns.md +161 -0
- package/template/CLAUDE.md +74 -239
- package/template/src/components/layout/sidebar.tsx +4 -9
- package/template/src/components/tables/data-table-date-filter.tsx +203 -0
- package/template/src/components/tables/data-table-faceted-filter.tsx +185 -0
- package/template/src/components/tables/data-table-filters-dropdown.tsx +130 -0
- package/template/src/components/tables/data-table-number-filter.tsx +295 -0
- package/template/src/components/tables/data-table-toolbar.tsx +115 -25
- package/template/src/components/tables/data-table-view-options.tsx +10 -6
- package/template/src/components/tables/data-table.tsx +41 -21
- package/template/src/components/tables/index.ts +5 -1
- package/template/src/components/ui/alert-dialog.tsx +1 -1
- package/template/src/components/ui/avatar.tsx +2 -2
- package/template/src/components/ui/badge.tsx +2 -2
- package/template/src/components/ui/button.tsx +1 -1
- package/template/src/components/ui/card.tsx +1 -1
- package/template/src/components/ui/command.tsx +4 -4
- package/template/src/components/ui/dropdown-menu.tsx +4 -4
- package/template/src/components/ui/form.tsx +1 -1
- package/template/src/components/ui/icons.tsx +1 -1
- package/template/src/components/ui/popover.tsx +1 -1
- package/template/src/components/ui/progress.tsx +1 -1
- package/template/src/components/ui/select.tsx +3 -3
- package/template/src/components/ui/sonner.tsx +1 -1
- package/template/src/components/ui/spinner.tsx +1 -1
- package/template/src/components/ui/table.tsx +3 -3
- package/template/src/components/ui/tooltip.tsx +2 -2
- package/template/src/config/navigation.ts +1 -11
- package/template/src/config/roles.ts +27 -0
- package/template/src/lib/date/config.ts +4 -2
- package/template/src/lib/date/formatters.ts +7 -0
- package/template/src/lib/date/index.ts +8 -1
- package/template/src/lib/supabase/admin.ts +23 -0
- package/template/src/lib/supabase/proxy.ts +1 -1
- package/template/src/modules/users/actions/users-actions.ts +106 -34
- package/template/src/modules/users/columns.tsx +29 -9
- package/template/src/modules/users/components/users-list.tsx +27 -1
- package/template/src/modules/users/hooks/useUsersMutations.ts +3 -3
- package/template/src/modules/users/index.ts +20 -2
- package/template/src/modules/users/schemas/users.schema.ts +29 -1
- package/template/src/modules/users/types/auth-user.types.ts +42 -0
- package/template/src/modules/users/utils/user-mapper.ts +32 -0
- package/template/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -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
|
+
```
|