@famgia/omnify-laravel 0.0.119 → 0.0.120
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,211 @@
|
|
|
1
|
+
# Internationalization (i18n) Guide
|
|
2
|
+
|
|
3
|
+
> **Related:** [README](./README.md) | [Ant Design](./antd-guide.md)
|
|
4
|
+
|
|
5
|
+
This project uses `next-intl` for internationalization.
|
|
6
|
+
|
|
7
|
+
## Current Locales
|
|
8
|
+
|
|
9
|
+
| Code | Language | Default |
|
|
10
|
+
| ---- | ---------- | ------- |
|
|
11
|
+
| ja | 日本語 | ✅ |
|
|
12
|
+
| en | English | |
|
|
13
|
+
| vi | Tiếng Việt | |
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
### Basic Translation
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { useTranslations } from "next-intl";
|
|
23
|
+
|
|
24
|
+
function MyComponent() {
|
|
25
|
+
const t = useTranslations();
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Button>{t("common.save")}</Button>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### With Namespace
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
const t = useTranslations("messages");
|
|
37
|
+
t("created") // "作成しました" (ja) | "Created successfully" (en)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### With Parameters
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
t("validation.minLength", { field: "Password", min: 8 })
|
|
44
|
+
// "Password must be at least 8 characters"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### LocaleSwitcher Component
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import LocaleSwitcher from "@/components/LocaleSwitcher";
|
|
51
|
+
|
|
52
|
+
<LocaleSwitcher /> // Dropdown to switch language
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Get Current Locale
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { useLocale } from "@/hooks/useLocale";
|
|
59
|
+
|
|
60
|
+
function MyComponent() {
|
|
61
|
+
const { locale, setLocale, localeNames } = useLocale();
|
|
62
|
+
// locale: "ja" | "en" | "vi"
|
|
63
|
+
// setLocale: (locale) => void
|
|
64
|
+
// localeNames: { ja: "日本語", en: "English", vi: "Tiếng Việt" }
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Adding New Language
|
|
71
|
+
|
|
72
|
+
### Step 1: Create Message File
|
|
73
|
+
|
|
74
|
+
Create `src/i18n/messages/{locale}.json`:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"common": {
|
|
79
|
+
"save": "저장",
|
|
80
|
+
"cancel": "취소",
|
|
81
|
+
"delete": "삭제",
|
|
82
|
+
"edit": "편집",
|
|
83
|
+
"create": "만들기",
|
|
84
|
+
"search": "검색",
|
|
85
|
+
"loading": "로딩 중...",
|
|
86
|
+
"noData": "데이터 없음",
|
|
87
|
+
"confirm": "확인",
|
|
88
|
+
"back": "뒤로",
|
|
89
|
+
"next": "다음",
|
|
90
|
+
"previous": "이전",
|
|
91
|
+
"submit": "제출",
|
|
92
|
+
"reset": "초기화",
|
|
93
|
+
"close": "닫기",
|
|
94
|
+
"yes": "예",
|
|
95
|
+
"no": "아니오"
|
|
96
|
+
},
|
|
97
|
+
"messages": {
|
|
98
|
+
"created": "생성되었습니다",
|
|
99
|
+
"updated": "업데이트되었습니다",
|
|
100
|
+
"deleted": "삭제되었습니다",
|
|
101
|
+
"saved": "저장되었습니다",
|
|
102
|
+
"error": "오류가 발생했습니다",
|
|
103
|
+
"confirmDelete": "정말 삭제하시겠습니까?",
|
|
104
|
+
"networkError": "네트워크 오류",
|
|
105
|
+
"serverError": "서버 오류",
|
|
106
|
+
"unauthorized": "로그인해 주세요",
|
|
107
|
+
"forbidden": "접근 권한이 없습니다",
|
|
108
|
+
"notFound": "찾을 수 없습니다",
|
|
109
|
+
"sessionExpired": "세션이 만료되었습니다. 페이지를 새로고침해 주세요",
|
|
110
|
+
"tooManyRequests": "요청이 너무 많습니다. 잠시 기다려 주세요"
|
|
111
|
+
},
|
|
112
|
+
"auth": {
|
|
113
|
+
"login": "로그인",
|
|
114
|
+
"logout": "로그아웃",
|
|
115
|
+
"register": "회원가입",
|
|
116
|
+
"email": "이메일",
|
|
117
|
+
"password": "비밀번호",
|
|
118
|
+
"passwordConfirm": "비밀번호 확인",
|
|
119
|
+
"rememberMe": "로그인 상태 유지",
|
|
120
|
+
"forgotPassword": "비밀번호를 잊으셨나요?",
|
|
121
|
+
"resetPassword": "비밀번호 재설정",
|
|
122
|
+
"loginSuccess": "로그인되었습니다",
|
|
123
|
+
"logoutSuccess": "로그아웃되었습니다",
|
|
124
|
+
"registerSuccess": "가입이 완료되었습니다"
|
|
125
|
+
},
|
|
126
|
+
"validation": {
|
|
127
|
+
"required": "{field}은(는) 필수입니다",
|
|
128
|
+
"email": "유효한 이메일 주소를 입력해 주세요",
|
|
129
|
+
"minLength": "{field}은(는) {min}자 이상이어야 합니다",
|
|
130
|
+
"maxLength": "{field}은(는) {max}자 이하여야 합니다",
|
|
131
|
+
"passwordMatch": "비밀번호가 일치하지 않습니다"
|
|
132
|
+
},
|
|
133
|
+
"nav": {
|
|
134
|
+
"home": "홈",
|
|
135
|
+
"dashboard": "대시보드",
|
|
136
|
+
"users": "사용자",
|
|
137
|
+
"settings": "설정",
|
|
138
|
+
"profile": "프로필"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Step 2: Update Config
|
|
144
|
+
|
|
145
|
+
Edit `src/i18n/config.ts`:
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
export const locales = ["ja", "en", "vi", "ko"] as const; // Add new locale
|
|
149
|
+
|
|
150
|
+
export const localeNames: Record<Locale, string> = {
|
|
151
|
+
ja: "日本語",
|
|
152
|
+
en: "English",
|
|
153
|
+
vi: "Tiếng Việt",
|
|
154
|
+
ko: "한국어", // Add display name
|
|
155
|
+
};
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Step 3: Add Ant Design Locale
|
|
159
|
+
|
|
160
|
+
Edit `src/components/AntdThemeProvider.tsx`:
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
import jaJP from "antd/locale/ja_JP";
|
|
164
|
+
import enUS from "antd/locale/en_US";
|
|
165
|
+
import viVN from "antd/locale/vi_VN";
|
|
166
|
+
import koKR from "antd/locale/ko_KR"; // Add import
|
|
167
|
+
|
|
168
|
+
const antdLocales = {
|
|
169
|
+
ja: jaJP,
|
|
170
|
+
en: enUS,
|
|
171
|
+
vi: viVN,
|
|
172
|
+
ko: koKR, // Add mapping
|
|
173
|
+
};
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## File Structure
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
src/i18n/
|
|
182
|
+
├── config.ts # Locales configuration
|
|
183
|
+
├── request.ts # Server-side locale detection
|
|
184
|
+
├── index.ts # Exports
|
|
185
|
+
└── messages/
|
|
186
|
+
├── ja.json # Japanese translations
|
|
187
|
+
├── en.json # English translations
|
|
188
|
+
└── vi.json # Vietnamese translations
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Best Practices
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
// ✅ DO: Use translation keys
|
|
197
|
+
<Button>{t("common.save")}</Button>
|
|
198
|
+
|
|
199
|
+
// ❌ DON'T: Hardcode strings
|
|
200
|
+
<Button>保存</Button>
|
|
201
|
+
|
|
202
|
+
// ✅ DO: Use namespaces for context
|
|
203
|
+
const tAuth = useTranslations("auth");
|
|
204
|
+
const tMessages = useTranslations("messages");
|
|
205
|
+
|
|
206
|
+
// ✅ DO: Use parameters for dynamic content
|
|
207
|
+
t("validation.minLength", { field: t("auth.password"), min: 8 })
|
|
208
|
+
|
|
209
|
+
// ❌ DON'T: Concatenate strings
|
|
210
|
+
`${t("auth.password")} must be at least 8 characters`
|
|
211
|
+
```
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Laravel Integration
|
|
2
|
+
|
|
3
|
+
> **Related:** [README](./README.md) | [Service Pattern](./service-pattern.md)
|
|
4
|
+
|
|
5
|
+
## Sanctum Authentication Flow
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
// Step 1: Get CSRF cookie (required before POST requests)
|
|
9
|
+
await api.get("/sanctum/csrf-cookie");
|
|
10
|
+
|
|
11
|
+
// Step 2: Login
|
|
12
|
+
await api.post("/login", { email, password });
|
|
13
|
+
// Cookie is now set automatically
|
|
14
|
+
|
|
15
|
+
// Step 3: Access protected routes
|
|
16
|
+
await api.get("/api/user"); // Works! Cookie sent automatically
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Error Handling Map
|
|
22
|
+
|
|
23
|
+
| HTTP Status | Laravel Meaning | Frontend Action |
|
|
24
|
+
| ----------- | ------------------- | ------------------------- |
|
|
25
|
+
| 200 | Success | Process response |
|
|
26
|
+
| 201 | Created | Process response |
|
|
27
|
+
| 204 | No Content | Success (no body) |
|
|
28
|
+
| 401 | Unauthenticated | Redirect to `/login` |
|
|
29
|
+
| 403 | Forbidden | Show error message |
|
|
30
|
+
| 404 | Not Found | Show error message |
|
|
31
|
+
| 419 | CSRF Token Mismatch | Refresh page |
|
|
32
|
+
| 422 | Validation Error | Display in form fields |
|
|
33
|
+
| 429 | Too Many Requests | Show rate limit message |
|
|
34
|
+
| 500+ | Server Error | Show server error message |
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## API Response Types
|
|
39
|
+
|
|
40
|
+
### Laravel Pagination Response
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
interface PaginatedResponse<T> {
|
|
44
|
+
data: T[];
|
|
45
|
+
links: {
|
|
46
|
+
first: string | null;
|
|
47
|
+
last: string | null;
|
|
48
|
+
prev: string | null;
|
|
49
|
+
next: string | null;
|
|
50
|
+
};
|
|
51
|
+
meta: {
|
|
52
|
+
current_page: number;
|
|
53
|
+
from: number | null;
|
|
54
|
+
last_page: number;
|
|
55
|
+
per_page: number;
|
|
56
|
+
to: number | null;
|
|
57
|
+
total: number;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Laravel API Resource Response (single item)
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
interface ResourceResponse<T> {
|
|
66
|
+
data: T;
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Laravel Validation Error Response (422)
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
interface ValidationErrorResponse {
|
|
74
|
+
message: string;
|
|
75
|
+
errors: Record<string, string[]>;
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Handling Laravel Responses
|
|
82
|
+
|
|
83
|
+
### In Service Layer
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
const userService = {
|
|
87
|
+
// Paginated list
|
|
88
|
+
list: async (params?: UserListParams): Promise<PaginatedResponse<User>> => {
|
|
89
|
+
const { data } = await api.get("/api/users", { params });
|
|
90
|
+
return data; // Already typed as PaginatedResponse
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
// Single resource (handle { data: ... } wrapper)
|
|
94
|
+
get: async (id: number): Promise<User> => {
|
|
95
|
+
const { data } = await api.get(`/api/users/${id}`);
|
|
96
|
+
return data.data ?? data; // Handle both wrapped and unwrapped
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### In Components (Form Validation)
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { getFormErrors } from "@/lib/api";
|
|
105
|
+
|
|
106
|
+
const mutation = useMutation({
|
|
107
|
+
mutationFn: userService.create,
|
|
108
|
+
onError: (error) => {
|
|
109
|
+
// Transform Laravel 422 errors to Ant Design format
|
|
110
|
+
form.setFields(getFormErrors(error));
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Axios Instance Configuration
|
|
118
|
+
|
|
119
|
+
The `lib/api.ts` is pre-configured for Laravel Sanctum:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
const api = axios.create({
|
|
123
|
+
baseURL: process.env.NEXT_PUBLIC_API_URL,
|
|
124
|
+
timeout: 30000,
|
|
125
|
+
headers: {
|
|
126
|
+
"Content-Type": "application/json",
|
|
127
|
+
Accept: "application/json",
|
|
128
|
+
},
|
|
129
|
+
withCredentials: true, // Required for Sanctum cookies
|
|
130
|
+
withXSRFToken: true, // Auto send XSRF-TOKEN cookie as header
|
|
131
|
+
xsrfCookieName: "XSRF-TOKEN",
|
|
132
|
+
xsrfHeaderName: "X-XSRF-TOKEN",
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Common Patterns
|
|
139
|
+
|
|
140
|
+
### Login Flow
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import { csrf } from "@/lib/api";
|
|
144
|
+
|
|
145
|
+
const login = async (email: string, password: string) => {
|
|
146
|
+
// 1. Get CSRF cookie first
|
|
147
|
+
await csrf();
|
|
148
|
+
|
|
149
|
+
// 2. Login
|
|
150
|
+
await api.post("/login", { email, password });
|
|
151
|
+
|
|
152
|
+
// 3. Get user data
|
|
153
|
+
const { data } = await api.get("/api/user");
|
|
154
|
+
return data;
|
|
155
|
+
};
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Protected API Call
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
// No special handling needed - cookies are sent automatically
|
|
162
|
+
const users = await userService.list();
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Handling 401 (Unauthenticated)
|
|
166
|
+
|
|
167
|
+
The interceptor in `lib/api.ts` automatically redirects to `/login` on 401:
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
api.interceptors.response.use(
|
|
171
|
+
(response) => response,
|
|
172
|
+
(error) => {
|
|
173
|
+
if (error.response?.status === 401) {
|
|
174
|
+
if (!window.location.pathname.includes("/login")) {
|
|
175
|
+
window.location.href = "/login";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return Promise.reject(error);
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
```
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Service Layer Pattern
|
|
2
|
+
|
|
3
|
+
> **Related:** [README](./README.md) | [TanStack Query](./tanstack-query.md) | [Laravel Integration](./laravel-integration.md)
|
|
4
|
+
|
|
5
|
+
## Types Rule
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
// ✅ DO: Import Model + Create/Update types from Omnify
|
|
9
|
+
import type { User, UserCreate, UserUpdate } from "@/types/model";
|
|
10
|
+
|
|
11
|
+
// ✅ DO: Only define query params locally (not generated by Omnify)
|
|
12
|
+
export interface UserListParams {
|
|
13
|
+
search?: string;
|
|
14
|
+
page?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ❌ DON'T: Define types that Omnify already generates
|
|
18
|
+
export interface UserCreateInput { ... } // WRONG - use UserCreate from Omnify
|
|
19
|
+
export interface User { ... } // WRONG - use User from Omnify
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Service Template
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
/**
|
|
28
|
+
* Users Service
|
|
29
|
+
*
|
|
30
|
+
* CRUD operations for User resource.
|
|
31
|
+
* Maps to Laravel UserController.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import api, { PaginatedResponse } from "@/lib/api";
|
|
35
|
+
import type { User, UserCreate, UserUpdate } from "@/types/model";
|
|
36
|
+
|
|
37
|
+
// =============================================================================
|
|
38
|
+
// Types - Only query params (Create/Update come from Omnify)
|
|
39
|
+
// =============================================================================
|
|
40
|
+
|
|
41
|
+
/** Query params for listing users */
|
|
42
|
+
export interface UserListParams {
|
|
43
|
+
search?: string;
|
|
44
|
+
page?: number;
|
|
45
|
+
per_page?: number;
|
|
46
|
+
sort_by?: keyof User;
|
|
47
|
+
sort_order?: "asc" | "desc";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// =============================================================================
|
|
51
|
+
// Service
|
|
52
|
+
// =============================================================================
|
|
53
|
+
|
|
54
|
+
const BASE_URL = "/api/users";
|
|
55
|
+
|
|
56
|
+
export const userService = {
|
|
57
|
+
/**
|
|
58
|
+
* Get paginated list of users
|
|
59
|
+
* GET /api/users
|
|
60
|
+
*/
|
|
61
|
+
list: async (params?: UserListParams): Promise<PaginatedResponse<User>> => {
|
|
62
|
+
const { data } = await api.get(BASE_URL, { params });
|
|
63
|
+
return data;
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get single user by ID
|
|
68
|
+
* GET /api/users/:id
|
|
69
|
+
*/
|
|
70
|
+
get: async (id: number): Promise<User> => {
|
|
71
|
+
const { data } = await api.get(`${BASE_URL}/${id}`);
|
|
72
|
+
return data.data ?? data;
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Create new user
|
|
77
|
+
* POST /api/users
|
|
78
|
+
*/
|
|
79
|
+
create: async (input: UserCreate): Promise<User> => {
|
|
80
|
+
const { data } = await api.post(BASE_URL, input);
|
|
81
|
+
return data.data ?? data;
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Update existing user
|
|
86
|
+
* PUT /api/users/:id
|
|
87
|
+
*/
|
|
88
|
+
update: async (id: number, input: UserUpdate): Promise<User> => {
|
|
89
|
+
const { data } = await api.put(`${BASE_URL}/${id}`, input);
|
|
90
|
+
return data.data ?? data;
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Delete user
|
|
95
|
+
* DELETE /api/users/:id
|
|
96
|
+
*/
|
|
97
|
+
delete: async (id: number): Promise<void> => {
|
|
98
|
+
await api.delete(`${BASE_URL}/${id}`);
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Service Rules
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
// ✅ DO: Import all types from Omnify
|
|
109
|
+
import type { User, UserCreate, UserUpdate } from "@/types/model";
|
|
110
|
+
|
|
111
|
+
// ❌ DON'T: Define types that Omnify generates
|
|
112
|
+
export interface UserCreate { ... } // WRONG!
|
|
113
|
+
export interface User { ... } // WRONG!
|
|
114
|
+
|
|
115
|
+
// ✅ DO: Only define query params locally
|
|
116
|
+
export interface UserListParams { ... } // OK - not in Omnify
|
|
117
|
+
|
|
118
|
+
// ✅ DO: Keep services pure (no React hooks)
|
|
119
|
+
export const userService = {
|
|
120
|
+
get: (id) => api.get(`/api/users/${id}`).then(r => r.data.data ?? r.data),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// ❌ DON'T: Use React hooks in services
|
|
124
|
+
export const userService = {
|
|
125
|
+
get: (id) => {
|
|
126
|
+
const [data, setData] = useState(); // WRONG!
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// ✅ DO: Handle Laravel's { data: ... } wrapper
|
|
131
|
+
get: (id) => api.get(`/api/users/${id}`).then(r => r.data.data ?? r.data),
|
|
132
|
+
|
|
133
|
+
// ❌ DON'T: Return raw axios response
|
|
134
|
+
get: (id) => api.get(`/api/users/${id}`), // Returns AxiosResponse, not data
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Using Validation Rules
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { Form, Input } from "antd";
|
|
143
|
+
import { useLocale } from "next-intl";
|
|
144
|
+
import { userSchemas, getUserFieldLabel } from "@/types/model/User";
|
|
145
|
+
import { zodRule } from "@/lib/form-validation";
|
|
146
|
+
|
|
147
|
+
function UserForm() {
|
|
148
|
+
const locale = useLocale();
|
|
149
|
+
const label = (key: string) => getUserFieldLabel(key, locale);
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<Form>
|
|
153
|
+
{/* Name */}
|
|
154
|
+
<Form.Item
|
|
155
|
+
name="name"
|
|
156
|
+
label={label("name")}
|
|
157
|
+
rules={[zodRule(userSchemas.name, label("name"))]}
|
|
158
|
+
>
|
|
159
|
+
<Input />
|
|
160
|
+
</Form.Item>
|
|
161
|
+
</Form>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Types Summary
|
|
169
|
+
|
|
170
|
+
| Type | Source | Example |
|
|
171
|
+
| -------- | ---------------------------- | ------------------------------------------ |
|
|
172
|
+
| Model | `@/types/model` (Omnify) | `User` |
|
|
173
|
+
| Create | `@/types/model` (Omnify) | `UserCreate` |
|
|
174
|
+
| Update | `@/types/model` (Omnify) | `UserUpdate` |
|
|
175
|
+
| Schemas | `@/types/model` (Omnify) | `userSchemas.email` |
|
|
176
|
+
| Params | Service file (manual) | `UserListParams` |
|
|
177
|
+
| Response | `@/lib/api.ts` | `PaginatedResponse<T>` |
|
|
178
|
+
| Rules | `@/lib/form-validation` | `zodRule(schema, label)` |
|
|
179
|
+
|
|
180
|
+
See [Types Guide](./types-guide.md) for complete details.
|