@famgia/omnify-laravel 0.0.119 → 0.0.121
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/dist/{chunk-7I6UNXOD.js → chunk-NMX3TLZT.js} +8 -1
- package/dist/{chunk-7I6UNXOD.js.map → chunk-NMX3TLZT.js.map} +1 -1
- package/dist/index.cjs +7 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin.cjs +7 -0
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.js +1 -1
- package/package.json +4 -4
- package/stubs/ai-guides/claude-checklists/react.md.stub +108 -0
- package/stubs/ai-guides/cursor/omnify-schema.mdc.stub +339 -0
- package/stubs/ai-guides/cursor/react-design.mdc.stub +693 -0
- package/stubs/ai-guides/cursor/react-form.mdc.stub +277 -0
- package/stubs/ai-guides/cursor/react-services.mdc.stub +304 -0
- package/stubs/ai-guides/cursor/react.mdc.stub +336 -0
- package/stubs/ai-guides/cursor/schema-create.mdc.stub +344 -0
- package/stubs/ai-guides/react/README.md.stub +221 -0
- package/stubs/ai-guides/react/antd-guide.md.stub +457 -0
- package/stubs/ai-guides/react/checklist.md.stub +108 -0
- package/stubs/ai-guides/react/datetime-guide.md.stub +137 -0
- package/stubs/ai-guides/react/design-philosophy.md.stub +363 -0
- package/stubs/ai-guides/react/i18n-guide.md.stub +211 -0
- package/stubs/ai-guides/react/laravel-integration.md.stub +181 -0
- package/stubs/ai-guides/react/service-pattern.md.stub +180 -0
- package/stubs/ai-guides/react/tanstack-query.md.stub +339 -0
- package/stubs/ai-guides/react/types-guide.md.stub +671 -0
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
# TypeScript Types Guide
|
|
2
|
+
|
|
3
|
+
> This guide defines where and how to define types in this project.
|
|
4
|
+
|
|
5
|
+
## Type Categories
|
|
6
|
+
|
|
7
|
+
| Category | Location | Generated | Example |
|
|
8
|
+
| ------------------- | -------------------- | --------- | ----------------------------- |
|
|
9
|
+
| **Model** | `@/types/model` | ✅ Omnify | `User`, `Post` |
|
|
10
|
+
| **Create/Update** | `@/types/model` | ✅ Omnify | `UserCreate`, `UserUpdate` |
|
|
11
|
+
| **Common** | `@/types/model` | ✅ Omnify | `DateTimeString`, `LocaleMap` |
|
|
12
|
+
| **Validation** | `@/types/model` | ✅ Omnify | `getUserRules(locale)` |
|
|
13
|
+
| **Enum** | `@/types/model/enum` | ✅ Omnify | `PostStatus`, `UserRole` |
|
|
14
|
+
| **API Params** | Service file | ❌ Manual | `UserListParams` |
|
|
15
|
+
| **API Response** | `@/lib/api.ts` | ❌ Manual | `PaginatedResponse<T>` |
|
|
16
|
+
| **Component Props** | Component file | ❌ Manual | `UserTableProps` |
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 1. Model Types (Omnify)
|
|
21
|
+
|
|
22
|
+
**Location**: `src/types/model/`
|
|
23
|
+
|
|
24
|
+
**Source**: Auto-generated from `.omnify/schemas/`
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// ✅ Import from @/types/model
|
|
28
|
+
import type { User, UserCreate, UserUpdate } from "@/types/model";
|
|
29
|
+
import type { DateTimeString } from "@/types/model";
|
|
30
|
+
import { getUserRules } from "@/types/model";
|
|
31
|
+
|
|
32
|
+
// ❌ DON'T define model types manually
|
|
33
|
+
interface User { ... } // WRONG - already generated
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Structure
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
src/types/model/
|
|
40
|
+
├── common.ts ❌ DO NOT EDIT
|
|
41
|
+
│ # LocaleMap, ValidationRule, DateTimeString
|
|
42
|
+
├── base/ ❌ DO NOT EDIT
|
|
43
|
+
│ └── User.ts # User + UserCreate + UserUpdate
|
|
44
|
+
├── rules/ ❌ DO NOT EDIT
|
|
45
|
+
│ └── User.rules.ts # getUserRules(), getUserDisplayName()
|
|
46
|
+
├── enum/ ❌ DO NOT EDIT (if exists)
|
|
47
|
+
│ └── PostStatus.ts
|
|
48
|
+
├── index.ts ❌ DO NOT EDIT (re-exports)
|
|
49
|
+
└── User.ts ✅ CAN EDIT (extension)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Generated Types Per Model
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// Auto-generated in base/User.ts:
|
|
56
|
+
interface User {
|
|
57
|
+
id: number;
|
|
58
|
+
name: string;
|
|
59
|
+
email: string;
|
|
60
|
+
created_at?: DateTimeString; // Uses DateTimeString
|
|
61
|
+
updated_at?: DateTimeString;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type UserCreate = Omit<User, 'id' | 'created_at' | 'updated_at'>;
|
|
65
|
+
type UserUpdate = Partial<UserCreate>;
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Extending Model Types
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// src/types/model/User.ts (safe to edit)
|
|
72
|
+
import type { User as UserBase } from "./base/User";
|
|
73
|
+
|
|
74
|
+
export interface User extends UserBase {
|
|
75
|
+
// Frontend-only computed properties
|
|
76
|
+
fullName?: string;
|
|
77
|
+
|
|
78
|
+
// UI state
|
|
79
|
+
isSelected?: boolean;
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 2. Enum Types (CRITICAL)
|
|
86
|
+
|
|
87
|
+
**Location**: `@/omnify/enum/` or `@/types/model/enum/`
|
|
88
|
+
|
|
89
|
+
**ALWAYS use generated Enums - NEVER inline union types!**
|
|
90
|
+
|
|
91
|
+
### Generated Enum Structure
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// @/omnify/enum/ApprovalStatus.ts (auto-generated)
|
|
95
|
+
|
|
96
|
+
// 1. Enum type
|
|
97
|
+
export enum ApprovalStatus {
|
|
98
|
+
Pending = "pending",
|
|
99
|
+
Approved = "approved",
|
|
100
|
+
Rejected = "rejected",
|
|
101
|
+
Cancelled = "cancelled",
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 2. Values array (for iteration)
|
|
105
|
+
export const ApprovalStatusValues = [
|
|
106
|
+
ApprovalStatus.Pending,
|
|
107
|
+
ApprovalStatus.Approved,
|
|
108
|
+
ApprovalStatus.Rejected,
|
|
109
|
+
ApprovalStatus.Cancelled,
|
|
110
|
+
] as const;
|
|
111
|
+
|
|
112
|
+
// 3. Type guard
|
|
113
|
+
export function isApprovalStatus(value: unknown): value is ApprovalStatus {
|
|
114
|
+
return ApprovalStatusValues.includes(value as ApprovalStatus);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 4. i18n label getter
|
|
118
|
+
export function getApprovalStatusLabel(value: ApprovalStatus, locale: string): string {
|
|
119
|
+
// Returns localized label
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### ❌ FORBIDDEN Patterns
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// ❌ Complex type extraction - FORBIDDEN!
|
|
127
|
+
const status = value as NonNullable<ListParams["filter"]>["status"];
|
|
128
|
+
|
|
129
|
+
// ❌ Hardcoded union type - NOT DRY!
|
|
130
|
+
const status: "pending" | "approved" | "rejected" = "pending";
|
|
131
|
+
|
|
132
|
+
// ❌ String type - NO TYPE SAFETY!
|
|
133
|
+
const [status, setStatus] = useState<string>("");
|
|
134
|
+
|
|
135
|
+
// ❌ Hardcoded string comparisons
|
|
136
|
+
if (status === "pending") { ... }
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### ✅ REQUIRED Patterns
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// ✅ Import from generated enum file
|
|
143
|
+
import {
|
|
144
|
+
ApprovalStatus,
|
|
145
|
+
ApprovalStatusValues,
|
|
146
|
+
isApprovalStatus,
|
|
147
|
+
getApprovalStatusLabel
|
|
148
|
+
} from "@/omnify/enum/ApprovalStatus";
|
|
149
|
+
|
|
150
|
+
// ✅ Type assertions
|
|
151
|
+
const status = unknownValue as ApprovalStatus;
|
|
152
|
+
|
|
153
|
+
// ✅ State with Enum
|
|
154
|
+
const [status, setStatus] = useState<ApprovalStatus | "">("");
|
|
155
|
+
const [status, setStatus] = useState<ApprovalStatus>(ApprovalStatus.Pending);
|
|
156
|
+
|
|
157
|
+
// ✅ Enum comparisons
|
|
158
|
+
if (status === ApprovalStatus.Pending) { ... }
|
|
159
|
+
|
|
160
|
+
// ✅ Iterate with Values array
|
|
161
|
+
const options = ApprovalStatusValues.map(value => ({
|
|
162
|
+
value,
|
|
163
|
+
label: getApprovalStatusLabel(value, locale)
|
|
164
|
+
}));
|
|
165
|
+
|
|
166
|
+
// ✅ Type guard
|
|
167
|
+
if (isApprovalStatus(value)) {
|
|
168
|
+
// value is ApprovalStatus
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Select/Filter Options Pattern
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
import {
|
|
176
|
+
ApprovalStatus,
|
|
177
|
+
ApprovalStatusValues,
|
|
178
|
+
getApprovalStatusLabel
|
|
179
|
+
} from "@/omnify/enum/ApprovalStatus";
|
|
180
|
+
import { useLocale } from "next-intl";
|
|
181
|
+
|
|
182
|
+
function StatusFilter() {
|
|
183
|
+
const locale = useLocale();
|
|
184
|
+
|
|
185
|
+
// ✅ Build options from generated enum
|
|
186
|
+
const statusOptions = ApprovalStatusValues.map(value => ({
|
|
187
|
+
value,
|
|
188
|
+
label: getApprovalStatusLabel(value, locale)
|
|
189
|
+
}));
|
|
190
|
+
|
|
191
|
+
// ✅ State with Enum type
|
|
192
|
+
const [status, setStatus] = useState<ApprovalStatus | "all">("all");
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<Select
|
|
196
|
+
value={status}
|
|
197
|
+
onChange={setStatus}
|
|
198
|
+
options={[
|
|
199
|
+
{ value: "all", label: t("common.all") },
|
|
200
|
+
...statusOptions
|
|
201
|
+
]}
|
|
202
|
+
/>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Filter Params with Enum
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
import { ApprovalStatus } from "@/omnify/enum/ApprovalStatus";
|
|
211
|
+
|
|
212
|
+
interface AttendanceListParams {
|
|
213
|
+
filter?: {
|
|
214
|
+
approval_status?: ApprovalStatus; // ✅ Use Enum type
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ✅ Type assertion with Enum
|
|
219
|
+
const handleStatusChange = (value: string) => {
|
|
220
|
+
setFilters(prev => ({
|
|
221
|
+
...prev,
|
|
222
|
+
filter: {
|
|
223
|
+
...prev.filter,
|
|
224
|
+
approval_status: value as ApprovalStatus // ✅ Not inline union type!
|
|
225
|
+
}
|
|
226
|
+
}));
|
|
227
|
+
};
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## 3. Using Generated Types
|
|
233
|
+
|
|
234
|
+
### Create/Update Types
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
// ✅ Use Omnify-generated types
|
|
238
|
+
import type { User, UserCreate, UserUpdate } from "@/types/model";
|
|
239
|
+
|
|
240
|
+
const userService = {
|
|
241
|
+
create: (input: UserCreate) => api.post("/api/users", input),
|
|
242
|
+
update: (id: number, input: UserUpdate) => api.put(`/api/users/${id}`, input),
|
|
243
|
+
};
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Validation Rules with Ant Design Form.Item
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
import { userSchemas, getCustomerFieldLabel } from "@/types/model/User";
|
|
250
|
+
import { zodRule } from "@/lib/form-validation";
|
|
251
|
+
import { useLocale } from "next-intl";
|
|
252
|
+
|
|
253
|
+
function UserForm() {
|
|
254
|
+
const locale = useLocale();
|
|
255
|
+
const label = (key: string) => getCustomerFieldLabel(key, locale);
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<Form>
|
|
259
|
+
{/* Name */}
|
|
260
|
+
<Form.Item
|
|
261
|
+
name="name"
|
|
262
|
+
label={label("name")}
|
|
263
|
+
rules={[zodRule(userSchemas.name, label("name"))]}
|
|
264
|
+
>
|
|
265
|
+
<Input />
|
|
266
|
+
</Form.Item>
|
|
267
|
+
|
|
268
|
+
{/* Email */}
|
|
269
|
+
<Form.Item
|
|
270
|
+
name="email"
|
|
271
|
+
label={label("email")}
|
|
272
|
+
rules={[zodRule(userSchemas.email, label("email"))]}
|
|
273
|
+
>
|
|
274
|
+
<Input />
|
|
275
|
+
</Form.Item>
|
|
276
|
+
</Form>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**Key Points:**
|
|
282
|
+
- Import `{model}Schemas` for Zod validation schemas
|
|
283
|
+
- Import `zodRule` from `@/lib/form-validation`
|
|
284
|
+
- Use `zodRule(schema, displayName)` in Form.Item rules
|
|
285
|
+
- Comment `{/* Field Name */}` before each Form.Item for clarity
|
|
286
|
+
|
|
287
|
+
### DateTimeString
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
import type { DateTimeString } from "@/types/model";
|
|
291
|
+
import { formatDateTime } from "@/lib/dayjs";
|
|
292
|
+
|
|
293
|
+
interface Event {
|
|
294
|
+
scheduled_at: DateTimeString; // ISO 8601 UTC string
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Display
|
|
298
|
+
formatDateTime(event.scheduled_at); // "2024/01/15 19:30"
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## 3. API Params Types (Manual)
|
|
304
|
+
|
|
305
|
+
**Location**: Service file (colocated)
|
|
306
|
+
|
|
307
|
+
**Only define query params (not in Omnify):**
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
// services/users.ts
|
|
311
|
+
import type { User, UserCreate, UserUpdate } from "@/types/model";
|
|
312
|
+
|
|
313
|
+
/** Query params for listing users (GET /api/users) */
|
|
314
|
+
export interface UserListParams {
|
|
315
|
+
search?: string;
|
|
316
|
+
role?: string;
|
|
317
|
+
page?: number;
|
|
318
|
+
per_page?: number;
|
|
319
|
+
sort_by?: keyof User;
|
|
320
|
+
sort_order?: "asc" | "desc";
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export const userService = {
|
|
324
|
+
list: (params?: UserListParams) => ...,
|
|
325
|
+
create: (input: UserCreate) => ..., // ← Use Omnify type
|
|
326
|
+
update: (id: number, input: UserUpdate) => ..., // ← Use Omnify type
|
|
327
|
+
};
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## 4. API Response Types
|
|
333
|
+
|
|
334
|
+
**Location**: `src/lib/api.ts`
|
|
335
|
+
|
|
336
|
+
**Naming**: `{Name}Response`, `Paginated{Name}`
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
// lib/api.ts
|
|
340
|
+
|
|
341
|
+
/** Laravel paginated response */
|
|
342
|
+
export interface PaginatedResponse<T> {
|
|
343
|
+
data: T[];
|
|
344
|
+
links: {
|
|
345
|
+
first: string | null;
|
|
346
|
+
last: string | null;
|
|
347
|
+
prev: string | null;
|
|
348
|
+
next: string | null;
|
|
349
|
+
};
|
|
350
|
+
meta: {
|
|
351
|
+
current_page: number;
|
|
352
|
+
from: number | null;
|
|
353
|
+
last_page: number;
|
|
354
|
+
per_page: number;
|
|
355
|
+
to: number | null;
|
|
356
|
+
total: number;
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Laravel single resource response */
|
|
361
|
+
export interface ResourceResponse<T> {
|
|
362
|
+
data: T;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Laravel validation error (422) */
|
|
366
|
+
export interface ValidationError {
|
|
367
|
+
message: string;
|
|
368
|
+
errors: Record<string, string[]>;
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Usage in Service
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
import api, { PaginatedResponse } from "@/lib/api";
|
|
376
|
+
import type { User } from "@/types/model";
|
|
377
|
+
|
|
378
|
+
export const userService = {
|
|
379
|
+
list: async (params?: UserListParams): Promise<PaginatedResponse<User>> => {
|
|
380
|
+
const { data } = await api.get("/api/users", { params });
|
|
381
|
+
return data;
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
## 4. Component Props Types
|
|
389
|
+
|
|
390
|
+
**Location**: Same file as component (inline)
|
|
391
|
+
|
|
392
|
+
**Naming**: `{Component}Props`
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
// components/tables/UserTable.tsx
|
|
396
|
+
|
|
397
|
+
import type { User } from "@/types/model";
|
|
398
|
+
import type { PaginatedResponse } from "@/lib/api";
|
|
399
|
+
|
|
400
|
+
// ─────────────────────────────────────────────────────────────────
|
|
401
|
+
// Props - Define at top of file
|
|
402
|
+
// ─────────────────────────────────────────────────────────────────
|
|
403
|
+
|
|
404
|
+
interface UserTableProps {
|
|
405
|
+
users: User[];
|
|
406
|
+
loading?: boolean;
|
|
407
|
+
pagination?: PaginatedResponse<User>["meta"];
|
|
408
|
+
onPageChange?: (page: number) => void;
|
|
409
|
+
onEdit?: (user: User) => void;
|
|
410
|
+
onDelete?: (user: User) => void;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ─────────────────────────────────────────────────────────────────
|
|
414
|
+
// Component
|
|
415
|
+
// ─────────────────────────────────────────────────────────────────
|
|
416
|
+
|
|
417
|
+
export function UserTable({
|
|
418
|
+
users,
|
|
419
|
+
loading = false,
|
|
420
|
+
pagination,
|
|
421
|
+
onPageChange,
|
|
422
|
+
onEdit,
|
|
423
|
+
onDelete,
|
|
424
|
+
}: UserTableProps) {
|
|
425
|
+
return <Table ... />;
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### When to Export Props
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
// ✅ Export if other components need it
|
|
433
|
+
export interface UserTableProps { ... }
|
|
434
|
+
|
|
435
|
+
// ✅ Don't export if only used internally
|
|
436
|
+
interface UserTableProps { ... }
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## 5. Hook Types
|
|
442
|
+
|
|
443
|
+
**Location**: Hook file (inline or inferred)
|
|
444
|
+
|
|
445
|
+
**Approach**: Let TypeScript infer return types when possible
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
// hooks/useUsers.ts
|
|
449
|
+
|
|
450
|
+
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
451
|
+
import { userService, UserCreateInput } from "@/services/users";
|
|
452
|
+
import { queryKeys } from "@/lib/queryKeys";
|
|
453
|
+
|
|
454
|
+
export function useUsers(params?: UserListParams) {
|
|
455
|
+
// Return type is inferred from userService.list
|
|
456
|
+
return useQuery({
|
|
457
|
+
queryKey: queryKeys.users.list(params),
|
|
458
|
+
queryFn: () => userService.list(params),
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export function useCreateUser() {
|
|
463
|
+
const queryClient = useQueryClient();
|
|
464
|
+
|
|
465
|
+
// Return type is inferred from useMutation
|
|
466
|
+
return useMutation({
|
|
467
|
+
mutationFn: (input: UserCreateInput) => userService.create(input),
|
|
468
|
+
onSuccess: () => {
|
|
469
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.users.all });
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### When to Define Return Type
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
// ✅ Let TypeScript infer (simpler, less maintenance)
|
|
479
|
+
export function useUsers(params?: UserListParams) {
|
|
480
|
+
return useQuery({ ... });
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ✅ Define explicitly if complex or for documentation
|
|
484
|
+
export function useAuth(): {
|
|
485
|
+
user: User | undefined;
|
|
486
|
+
isLoading: boolean;
|
|
487
|
+
login: (input: LoginInput) => Promise<void>;
|
|
488
|
+
logout: () => Promise<void>;
|
|
489
|
+
} {
|
|
490
|
+
...
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
---
|
|
495
|
+
|
|
496
|
+
## 6. Shared/Utility Types
|
|
497
|
+
|
|
498
|
+
**Location**: `src/types/index.ts` (only if used across many files)
|
|
499
|
+
|
|
500
|
+
```typescript
|
|
501
|
+
// types/index.ts
|
|
502
|
+
|
|
503
|
+
/** Common ID type */
|
|
504
|
+
export type ID = number;
|
|
505
|
+
|
|
506
|
+
/** Nullable type helper */
|
|
507
|
+
export type Nullable<T> = T | null;
|
|
508
|
+
|
|
509
|
+
/** Make specific keys optional */
|
|
510
|
+
export type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
|
511
|
+
|
|
512
|
+
/** Extract array element type */
|
|
513
|
+
export type ArrayElement<T> = T extends (infer U)[] ? U : never;
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
### When to Use Shared Types
|
|
517
|
+
|
|
518
|
+
```typescript
|
|
519
|
+
// ✅ Use shared types for truly common patterns
|
|
520
|
+
import type { ID, Nullable } from "@/types";
|
|
521
|
+
|
|
522
|
+
interface Post {
|
|
523
|
+
id: ID;
|
|
524
|
+
author_id: ID;
|
|
525
|
+
published_at: Nullable<string>;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// ❌ Don't over-abstract
|
|
529
|
+
// Bad: Creating shared types for every little thing
|
|
530
|
+
export type UserName = string; // Just use string
|
|
531
|
+
export type UserId = number; // Just use number
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
---
|
|
535
|
+
|
|
536
|
+
## Type Definition Checklist
|
|
537
|
+
|
|
538
|
+
### Before Creating a Type
|
|
539
|
+
|
|
540
|
+
1. **Is it a Model?** → Use `@/types/model` (Omnify)
|
|
541
|
+
2. **Is it API input?** → Define in service file
|
|
542
|
+
3. **Is it API response?** → Use/extend types in `lib/api.ts`
|
|
543
|
+
4. **Is it component props?** → Define in component file
|
|
544
|
+
5. **Is it used in 3+ places?** → Consider `types/index.ts`
|
|
545
|
+
|
|
546
|
+
### Type Naming Conventions
|
|
547
|
+
|
|
548
|
+
| Type | Pattern | Example |
|
|
549
|
+
| ------------ | -------------------- | ------------------- |
|
|
550
|
+
| Model | PascalCase | `User`, `Post` |
|
|
551
|
+
| Create Input | `{Model}CreateInput` | `UserCreateInput` |
|
|
552
|
+
| Update Input | `{Model}UpdateInput` | `UserUpdateInput` |
|
|
553
|
+
| List Params | `{Model}ListParams` | `UserListParams` |
|
|
554
|
+
| Props | `{Component}Props` | `UserTableProps` |
|
|
555
|
+
| Response | `{Name}Response` | `PaginatedResponse` |
|
|
556
|
+
|
|
557
|
+
---
|
|
558
|
+
|
|
559
|
+
## Complete Example
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
563
|
+
// types/model/User.ts (Omnify extension)
|
|
564
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
565
|
+
import type { User as UserBase } from "./base/User";
|
|
566
|
+
|
|
567
|
+
export interface User extends UserBase {
|
|
568
|
+
// Add frontend-only properties if needed
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
572
|
+
// services/users.ts
|
|
573
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
574
|
+
import api, { PaginatedResponse } from "@/lib/api";
|
|
575
|
+
import type { User } from "@/types/model";
|
|
576
|
+
|
|
577
|
+
export interface UserCreateInput {
|
|
578
|
+
name: string;
|
|
579
|
+
email: string;
|
|
580
|
+
password: string;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
export interface UserUpdateInput {
|
|
584
|
+
name?: string;
|
|
585
|
+
email?: string;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export interface UserListParams {
|
|
589
|
+
search?: string;
|
|
590
|
+
page?: number;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export const userService = {
|
|
594
|
+
list: async (params?: UserListParams): Promise<PaginatedResponse<User>> => {
|
|
595
|
+
const { data } = await api.get("/api/users", { params });
|
|
596
|
+
return data;
|
|
597
|
+
},
|
|
598
|
+
get: async (id: number): Promise<User> => {
|
|
599
|
+
const { data } = await api.get(`/api/users/${id}`);
|
|
600
|
+
return data.data ?? data;
|
|
601
|
+
},
|
|
602
|
+
create: async (input: UserCreateInput): Promise<User> => {
|
|
603
|
+
const { data } = await api.post("/api/users", input);
|
|
604
|
+
return data.data ?? data;
|
|
605
|
+
},
|
|
606
|
+
update: async (id: number, input: UserUpdateInput): Promise<User> => {
|
|
607
|
+
const { data } = await api.put(`/api/users/${id}`, input);
|
|
608
|
+
return data.data ?? data;
|
|
609
|
+
},
|
|
610
|
+
delete: async (id: number): Promise<void> => {
|
|
611
|
+
await api.delete(`/api/users/${id}`);
|
|
612
|
+
},
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
616
|
+
// components/tables/UserTable.tsx
|
|
617
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
618
|
+
import type { User } from "@/types/model";
|
|
619
|
+
|
|
620
|
+
interface UserTableProps {
|
|
621
|
+
users: User[];
|
|
622
|
+
loading?: boolean;
|
|
623
|
+
onEdit?: (user: User) => void;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
export function UserTable({ users, loading, onEdit }: UserTableProps) {
|
|
627
|
+
return <Table dataSource={users} loading={loading} ... />;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
631
|
+
// app/(dashboard)/users/page.tsx
|
|
632
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
633
|
+
"use client";
|
|
634
|
+
|
|
635
|
+
import { useQuery } from "@tanstack/react-query";
|
|
636
|
+
import { userService, UserListParams } from "@/services/users";
|
|
637
|
+
import { UserTable } from "@/components/tables/UserTable";
|
|
638
|
+
import { queryKeys } from "@/lib/queryKeys";
|
|
639
|
+
|
|
640
|
+
export default function UsersPage() {
|
|
641
|
+
const [params, setParams] = useState<UserListParams>({ page: 1 });
|
|
642
|
+
|
|
643
|
+
const { data, isLoading } = useQuery({
|
|
644
|
+
queryKey: queryKeys.users.list(params),
|
|
645
|
+
queryFn: () => userService.list(params),
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
return (
|
|
649
|
+
<UserTable
|
|
650
|
+
users={data?.data ?? []}
|
|
651
|
+
loading={isLoading}
|
|
652
|
+
onEdit={(user) => router.push(`/users/${user.id}/edit`)}
|
|
653
|
+
/>
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
---
|
|
659
|
+
|
|
660
|
+
## Summary
|
|
661
|
+
|
|
662
|
+
| Type | Location | Why |
|
|
663
|
+
| -------- | -------------------- | ------------------------- |
|
|
664
|
+
| Model | `@/types/model` | Synced with DB via Omnify |
|
|
665
|
+
| Input | Service file | Colocated with API logic |
|
|
666
|
+
| Response | `lib/api.ts` | Shared Laravel patterns |
|
|
667
|
+
| Props | Component file | Colocated with component |
|
|
668
|
+
| Hook | Hook file (inferred) | TypeScript handles it |
|
|
669
|
+
| Utility | `types/index.ts` | Only if widely used |
|
|
670
|
+
|
|
671
|
+
**Philosophy**: Keep types close to their usage. Don't over-organize.
|