@famgia/omnify-ai-guides 2.0.15
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/README.md +105 -0
- package/dist/chunk-RCTEXK7C.js +549 -0
- package/dist/chunk-RCTEXK7C.js.map +1 -0
- package/dist/config/rules.yaml +524 -0
- package/dist/index.cjs +587 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +55 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/knowledge/agents/architect.md.stub +150 -0
- package/dist/knowledge/agents/developer.md.stub +190 -0
- package/dist/knowledge/agents/reviewer.md.stub +134 -0
- package/dist/knowledge/agents/tester.md.stub +196 -0
- package/dist/knowledge/checklists/backend.md.stub +112 -0
- package/dist/knowledge/checklists/react.md.stub +108 -0
- package/dist/knowledge/claude-rules/laravel-controllers.md.stub +57 -0
- package/dist/knowledge/claude-rules/laravel-migrations.md.stub +47 -0
- package/dist/knowledge/claude-rules/laravel-tests.md.stub +52 -0
- package/dist/knowledge/claude-rules/naming.md.stub +369 -0
- package/dist/knowledge/claude-rules/performance.md.stub +256 -0
- package/dist/knowledge/claude-rules/php-standards.md.stub +305 -0
- package/dist/knowledge/claude-rules/react-components.md.stub +67 -0
- package/dist/knowledge/claude-rules/schema-yaml.md.stub +83 -0
- package/dist/knowledge/claude-rules/security.md.stub +164 -0
- package/dist/knowledge/cursor-rules/antd-deprecations.mdc.stub +62 -0
- package/dist/knowledge/cursor-rules/basemodel-readonly.mdc.stub +66 -0
- package/dist/knowledge/cursor-rules/baserequest-readonly.mdc.stub +74 -0
- package/dist/knowledge/cursor-rules/baseresource-readonly.mdc.stub +78 -0
- package/dist/knowledge/cursor-rules/laravel-controller.mdc.stub +421 -0
- package/dist/knowledge/cursor-rules/laravel-request.mdc.stub +112 -0
- package/dist/knowledge/cursor-rules/laravel-resource.mdc.stub +73 -0
- package/dist/knowledge/cursor-rules/laravel-review.mdc.stub +69 -0
- package/dist/knowledge/cursor-rules/laravel-testing.mdc.stub +138 -0
- package/dist/knowledge/cursor-rules/laravel.mdc.stub +138 -0
- package/dist/knowledge/cursor-rules/migrations-workflow.mdc.stub +224 -0
- package/dist/knowledge/cursor-rules/model-editable.mdc.stub +120 -0
- package/dist/knowledge/cursor-rules/omnify-migrations.mdc.stub +109 -0
- package/dist/knowledge/cursor-rules/omnify-schema.mdc.stub +358 -0
- package/dist/knowledge/cursor-rules/omnify.mdc.stub +58 -0
- package/dist/knowledge/cursor-rules/react-design.mdc.stub +693 -0
- package/dist/knowledge/cursor-rules/react-form.mdc.stub +292 -0
- package/dist/knowledge/cursor-rules/react-services.mdc.stub +304 -0
- package/dist/knowledge/cursor-rules/react.mdc.stub +336 -0
- package/dist/knowledge/cursor-rules/request-editable.mdc.stub +111 -0
- package/dist/knowledge/cursor-rules/resource-editable.mdc.stub +125 -0
- package/dist/knowledge/cursor-rules/schema-create.mdc.stub +440 -0
- package/dist/knowledge/cursor-rules/validation-rules.mdc.stub +181 -0
- package/dist/knowledge/laravel/README.md.stub +59 -0
- package/dist/knowledge/laravel/architecture.md.stub +424 -0
- package/dist/knowledge/laravel/authentication.md.stub +588 -0
- package/dist/knowledge/laravel/controller.md.stub +484 -0
- package/dist/knowledge/laravel/datetime.md.stub +334 -0
- package/dist/knowledge/laravel/migrations-team.md.stub +376 -0
- package/dist/knowledge/laravel/openapi.md.stub +449 -0
- package/dist/knowledge/laravel/request.md.stub +450 -0
- package/dist/knowledge/laravel/resource.md.stub +516 -0
- package/dist/knowledge/laravel/service.md.stub +503 -0
- package/dist/knowledge/laravel/testing.md.stub +1504 -0
- package/dist/knowledge/omnify/antdesign-guide.md.stub +401 -0
- package/dist/knowledge/omnify/config-guide.md.stub +405 -0
- package/dist/knowledge/omnify/japan-guide.md.stub +186 -0
- package/dist/knowledge/omnify/laravel-guide.md.stub +61 -0
- package/dist/knowledge/omnify/partial-schema-guide.md.stub +353 -0
- package/dist/knowledge/omnify/react-form-guide.md.stub +225 -0
- package/dist/knowledge/omnify/schema-guide.md.stub +144 -0
- package/dist/knowledge/omnify/typescript-guide.md.stub +337 -0
- package/dist/knowledge/react/README.md.stub +221 -0
- package/dist/knowledge/react/antd-guide.md +528 -0
- package/dist/knowledge/react/antd-guide.md.stub +528 -0
- package/dist/knowledge/react/checklist.md.stub +108 -0
- package/dist/knowledge/react/datetime-guide.md.stub +137 -0
- package/dist/knowledge/react/design-philosophy.md.stub +363 -0
- package/dist/knowledge/react/i18n-guide.md.stub +211 -0
- package/dist/knowledge/react/laravel-integration.md.stub +181 -0
- package/dist/knowledge/react/service-pattern.md.stub +180 -0
- package/dist/knowledge/react/tanstack-query.md.stub +339 -0
- package/dist/knowledge/react/types-guide.md +669 -0
- package/dist/knowledge/react/types-guide.md.stub +669 -0
- package/dist/knowledge/workflows/bug-fix.md.stub +201 -0
- package/dist/knowledge/workflows/code-review.md.stub +164 -0
- package/dist/knowledge/workflows/new-feature.md.stub +327 -0
- package/dist/plugin-M95GyBll.d.cts +191 -0
- package/dist/plugin-M95GyBll.d.ts +191 -0
- package/dist/plugin.cjs +573 -0
- package/dist/plugin.cjs.map +1 -0
- package/dist/plugin.d.cts +2 -0
- package/dist/plugin.d.ts +2 -0
- package/dist/plugin.js +15 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +53 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# DateTime Handling Guide (Frontend)
|
|
2
|
+
|
|
3
|
+
> **Related:** [README](./README.md) | [Service Pattern](./service-pattern.md) | [Laravel Integration](./laravel-integration.md)
|
|
4
|
+
|
|
5
|
+
## Golden Rule: "Store UTC, Display Local"
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
API Response (UTC) → Day.js → Display (Local Timezone)
|
|
9
|
+
User Input (Local) → Day.js → API Request (UTC)
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Setup
|
|
15
|
+
|
|
16
|
+
Day.js is already configured in this project. Import from `@/lib/dayjs`:
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import dayjs, { formatDateTime, toUTCString } from "@/lib/dayjs";
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
> **Rule:** Always import from `@/lib/dayjs`, never from `dayjs` directly.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Usage Patterns
|
|
27
|
+
|
|
28
|
+
### Display UTC from API
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import dayjs, { formatDateTime, formatRelative } from "@/lib/dayjs";
|
|
32
|
+
|
|
33
|
+
// API returns: "2024-01-15T10:30:00Z" (UTC)
|
|
34
|
+
const createdAt = "2024-01-15T10:30:00Z";
|
|
35
|
+
|
|
36
|
+
// Using helper functions (recommended)
|
|
37
|
+
formatDateTime(createdAt); // "2024/01/15 19:30"
|
|
38
|
+
formatRelative(createdAt); // "3日前"
|
|
39
|
+
|
|
40
|
+
// Using dayjs directly
|
|
41
|
+
dayjs(createdAt).format("LL"); // "2024年1月15日" (localized)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Send Local Input as UTC
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import dayjs, { toUTCString } from "@/lib/dayjs";
|
|
48
|
+
|
|
49
|
+
// User selects: 2024/01/15 19:30 (local time)
|
|
50
|
+
const localDate = dayjs("2024-01-15 19:30");
|
|
51
|
+
|
|
52
|
+
// Convert to UTC for API
|
|
53
|
+
const utcString = toUTCString(localDate);
|
|
54
|
+
// "2024-01-15T10:30:00.000Z"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### With Ant Design DatePicker
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { DatePicker, Form } from "antd";
|
|
61
|
+
import dayjs, { toUTCString, fromUTCString } from "@/lib/dayjs";
|
|
62
|
+
import type { Dayjs } from "@/lib/dayjs";
|
|
63
|
+
|
|
64
|
+
function MyForm() {
|
|
65
|
+
const handleSubmit = (values: { date: Dayjs }) => {
|
|
66
|
+
api.post("/events", { date: toUTCString(values.date) });
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Form onFinish={handleSubmit}>
|
|
71
|
+
<Form.Item name="date">
|
|
72
|
+
<DatePicker showTime />
|
|
73
|
+
</Form.Item>
|
|
74
|
+
</Form>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Display API date in DatePicker
|
|
79
|
+
<DatePicker defaultValue={fromUTCString(event.scheduled_at)} />
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Available Functions
|
|
85
|
+
|
|
86
|
+
| Function | Description | Example Output |
|
|
87
|
+
| ------------------------------ | --------------------- | ---------------------------- |
|
|
88
|
+
| `formatDate(utc)` | Date only | `"2024/01/15"` |
|
|
89
|
+
| `formatDateTime(utc)` | Date + time | `"2024/01/15 19:30"` |
|
|
90
|
+
| `formatDateLocalized(utc)` | Localized date | `"2024年1月15日"` |
|
|
91
|
+
| `formatDateTimeLocalized(utc)` | Localized date + time | `"2024年1月15日 19:30"` |
|
|
92
|
+
| `formatRelative(utc)` | Relative time | `"3日前"` |
|
|
93
|
+
| `toUTCString(dayjs)` | Convert to UTC ISO | `"2024-01-15T10:30:00.000Z"` |
|
|
94
|
+
| `fromUTCString(utc)` | Parse to Dayjs | `Dayjs object` |
|
|
95
|
+
| `nowUTC()` | Current UTC time | `Dayjs object` |
|
|
96
|
+
| `isPast(utc)` | Check if past | `true/false` |
|
|
97
|
+
| `isFuture(utc)` | Check if future | `true/false` |
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Anti-Patterns ❌
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// ❌ Import dayjs directly
|
|
105
|
+
import dayjs from "dayjs";
|
|
106
|
+
|
|
107
|
+
// ❌ Use native Date
|
|
108
|
+
new Date("2024-01-15");
|
|
109
|
+
|
|
110
|
+
// ❌ Send local date string to API
|
|
111
|
+
api.post({ date: "2024/01/15 19:30" });
|
|
112
|
+
|
|
113
|
+
// ❌ Use moment.js
|
|
114
|
+
import moment from "moment";
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Correct Patterns ✅
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
// ✅ Import from lib
|
|
121
|
+
import dayjs, { formatDateTime, toUTCString } from "@/lib/dayjs";
|
|
122
|
+
|
|
123
|
+
// ✅ Use helper functions
|
|
124
|
+
formatDateTime(user.created_at);
|
|
125
|
+
|
|
126
|
+
// ✅ Send UTC to API
|
|
127
|
+
api.post({ date: toUTCString(localDate) });
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Checklist
|
|
133
|
+
|
|
134
|
+
- [ ] Import from `@/lib/dayjs`, not `dayjs`
|
|
135
|
+
- [ ] Use helper functions for display
|
|
136
|
+
- [ ] Use `toUTCString()` when sending to API
|
|
137
|
+
- [ ] Use `fromUTCString()` for form default values
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
# Design Philosophy
|
|
2
|
+
|
|
3
|
+
> This document explains the architectural decisions and design principles for this frontend project.
|
|
4
|
+
|
|
5
|
+
## Architecture: Clean Architecture + Service Layer
|
|
6
|
+
|
|
7
|
+
```mermaid
|
|
8
|
+
flowchart TD
|
|
9
|
+
subgraph UI["UI Layer"]
|
|
10
|
+
Page["Page (Container)"]
|
|
11
|
+
Component["Component (Presentational)"]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
subgraph Hooks["Hooks Layer"]
|
|
15
|
+
Query["TanStack Query"]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
subgraph Services["Service Layer"]
|
|
19
|
+
Service["userService, postService"]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
subgraph Infra["Infrastructure Layer"]
|
|
23
|
+
Axios["Axios (lib/api.ts)"]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
Page --> Query
|
|
27
|
+
Component --> Page
|
|
28
|
+
Query --> Service
|
|
29
|
+
Service --> Axios
|
|
30
|
+
Axios -->|HTTP| API[Laravel API]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
| Layer | Responsibility | Rules |
|
|
34
|
+
| -------------- | --------------------------------- | ------------------------ |
|
|
35
|
+
| UI | Display data, handle interactions | No direct API calls |
|
|
36
|
+
| Hooks | Server state, caching, sync | Uses services |
|
|
37
|
+
| Service | API communication | No React, pure functions |
|
|
38
|
+
| Infrastructure | HTTP client, interceptors | Framework-agnostic |
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Why This Architecture?
|
|
43
|
+
|
|
44
|
+
### 1. Separation of Concerns
|
|
45
|
+
|
|
46
|
+
Each layer has a single responsibility:
|
|
47
|
+
|
|
48
|
+
| Layer | Knows About | Doesn't Know About |
|
|
49
|
+
| -------------- | ----------------------- | ---------------------- |
|
|
50
|
+
| UI | Components, hooks | HTTP, API endpoints |
|
|
51
|
+
| Hooks | Services, query keys | Axios, response format |
|
|
52
|
+
| Services | API instance, endpoints | React, components |
|
|
53
|
+
| Infrastructure | HTTP protocol | Business logic |
|
|
54
|
+
|
|
55
|
+
**Benefit**: Change one layer without affecting others.
|
|
56
|
+
|
|
57
|
+
### 2. Testability
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// Services are pure functions - easy to test
|
|
61
|
+
test("userService.get returns user", async () => {
|
|
62
|
+
const user = await userService.get(1);
|
|
63
|
+
expect(user.id).toBe(1);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Components receive data via props - easy to mock
|
|
67
|
+
test("UserTable renders users", () => {
|
|
68
|
+
render(<UserTable users={mockUsers} loading={false} />);
|
|
69
|
+
expect(screen.getByText("John")).toBeInTheDocument();
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 3. Reusability
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// Services can be used anywhere
|
|
77
|
+
const user = await userService.get(1); // In a script
|
|
78
|
+
const { data } = useQuery({ queryFn: () => userService.get(1) }); // In React
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 4. Type Safety
|
|
82
|
+
|
|
83
|
+
Types flow through layers:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// Omnify generates Model types (synced with DB)
|
|
87
|
+
type User = { id: number; name: string; email: string; }
|
|
88
|
+
|
|
89
|
+
// Services use Model types + define Input types
|
|
90
|
+
const userService = {
|
|
91
|
+
get: (id: number): Promise<User> => ...,
|
|
92
|
+
create: (input: UserCreateInput): Promise<User> => ...,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Hooks inherit types from services
|
|
96
|
+
const { data } = useQuery<User>({ ... });
|
|
97
|
+
|
|
98
|
+
// Components receive typed props
|
|
99
|
+
function UserCard({ user }: { user: User }) { ... }
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Why TanStack Query?
|
|
105
|
+
|
|
106
|
+
### Problem: Server State is Different
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
// ❌ Client state (useState) - you control it
|
|
110
|
+
const [count, setCount] = useState(0);
|
|
111
|
+
setCount(1); // Immediately updates
|
|
112
|
+
|
|
113
|
+
// ❌ Server state - you DON'T control it
|
|
114
|
+
const [users, setUsers] = useState([]);
|
|
115
|
+
// What if another user adds a new user?
|
|
116
|
+
// What if the network fails?
|
|
117
|
+
// What if the data is stale?
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Solution: TanStack Query
|
|
121
|
+
|
|
122
|
+
| Feature | Without TanStack | With TanStack |
|
|
123
|
+
| ------------------ | -------------------- | -------------------------------- |
|
|
124
|
+
| Loading state | Manual `useState` | Automatic `isLoading` |
|
|
125
|
+
| Error handling | Try/catch everywhere | Automatic `error` |
|
|
126
|
+
| Caching | None or manual | Automatic, configurable |
|
|
127
|
+
| Refetching | Manual | Automatic on focus/reconnect |
|
|
128
|
+
| Deduplication | None | Automatic (same key = 1 request) |
|
|
129
|
+
| Optimistic updates | Complex | Built-in |
|
|
130
|
+
|
|
131
|
+
### TanStack Query vs SWR
|
|
132
|
+
|
|
133
|
+
| Feature | TanStack Query | SWR |
|
|
134
|
+
| ------------------ | --------------------------- | --------------- |
|
|
135
|
+
| Mutations | ✅ First-class `useMutation` | ⚠️ Manual |
|
|
136
|
+
| Devtools | ✅ Excellent | ⚠️ Basic |
|
|
137
|
+
| Query invalidation | ✅ Granular control | ⚠️ Limited |
|
|
138
|
+
| Optimistic updates | ✅ Built-in rollback | ⚠️ Manual |
|
|
139
|
+
| Bundle size | ~13KB | ~4KB |
|
|
140
|
+
| Best for | Complex apps | Simple fetching |
|
|
141
|
+
|
|
142
|
+
**We chose TanStack Query** because:
|
|
143
|
+
- Better mutation support (CRUD apps need this)
|
|
144
|
+
- Granular cache invalidation (important for data consistency)
|
|
145
|
+
- Better devtools for debugging
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Why Service Layer?
|
|
150
|
+
|
|
151
|
+
### Problem: API Calls Scattered
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
// ❌ API calls in components
|
|
155
|
+
function UserList() {
|
|
156
|
+
const { data } = useQuery({
|
|
157
|
+
queryFn: () => axios.get("/api/users").then(r => r.data),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function UserDetail({ id }) {
|
|
162
|
+
const { data } = useQuery({
|
|
163
|
+
queryFn: () => axios.get(`/api/users/${id}`).then(r => r.data.data),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
// Problems:
|
|
167
|
+
// - Duplicated logic (response unwrapping)
|
|
168
|
+
// - Hard to change API (update every component)
|
|
169
|
+
// - Hard to test (need to mock axios in every test)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Solution: Service Layer
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// ✅ Centralized in service
|
|
176
|
+
const userService = {
|
|
177
|
+
list: () => api.get("/api/users").then(r => r.data),
|
|
178
|
+
get: (id) => api.get(`/api/users/${id}`).then(r => r.data.data ?? r.data),
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// Components just use services
|
|
182
|
+
function UserList() {
|
|
183
|
+
const { data } = useQuery({ queryFn: userService.list });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function UserDetail({ id }) {
|
|
187
|
+
const { data } = useQuery({ queryFn: () => userService.get(id) });
|
|
188
|
+
}
|
|
189
|
+
// Benefits:
|
|
190
|
+
// - Single source of truth for API logic
|
|
191
|
+
// - Easy to change (update one file)
|
|
192
|
+
// - Easy to test (mock service, not axios)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Design Principles
|
|
198
|
+
|
|
199
|
+
### 1. Single Source of Truth
|
|
200
|
+
|
|
201
|
+
| Data Type | Source |
|
|
202
|
+
| -------------------------- | ------------------------------ |
|
|
203
|
+
| Server data (users, posts) | TanStack Query |
|
|
204
|
+
| Model types | Omnify (`@omnify/schemas`) |
|
|
205
|
+
| Form state | Ant Design Form |
|
|
206
|
+
| UI state (modal open) | Local `useState` |
|
|
207
|
+
| Global client state | Context or Zustand (if needed) |
|
|
208
|
+
|
|
209
|
+
### 2. Colocation
|
|
210
|
+
|
|
211
|
+
Keep related code together:
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
// ✅ Good: Related files together
|
|
215
|
+
services/
|
|
216
|
+
users.ts # Service + Input types
|
|
217
|
+
posts.ts
|
|
218
|
+
|
|
219
|
+
// ❌ Bad: Types scattered
|
|
220
|
+
types/
|
|
221
|
+
UserCreateInput.ts
|
|
222
|
+
UserUpdateInput.ts
|
|
223
|
+
PostCreateInput.ts
|
|
224
|
+
services/
|
|
225
|
+
users.ts
|
|
226
|
+
posts.ts
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### 3. Explicit Over Implicit
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
// ✅ Explicit: Clear what this does
|
|
233
|
+
const { data: users, isLoading, error } = useQuery({
|
|
234
|
+
queryKey: queryKeys.users.list(filters),
|
|
235
|
+
queryFn: () => userService.list(filters),
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// ❌ Implicit: Magic happening somewhere
|
|
239
|
+
const users = useUsers(filters); // What's inside? Caching? Error handling?
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### 4. Fail Fast
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
// ✅ Types catch errors at compile time
|
|
246
|
+
const user: User = { id: "1" }; // Error: id should be number
|
|
247
|
+
|
|
248
|
+
// ✅ ESLint catches bad patterns
|
|
249
|
+
useEffect(() => { fetchData() }, []); // Warning: use useQuery
|
|
250
|
+
|
|
251
|
+
// ✅ Runtime errors shown to user
|
|
252
|
+
onError: (error) => form.setFields(getFormErrors(error));
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### 5. Composition Over Inheritance
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
// ✅ Compose small pieces
|
|
259
|
+
function UserPage() {
|
|
260
|
+
return (
|
|
261
|
+
<PageLayout>
|
|
262
|
+
<UserFilters />
|
|
263
|
+
<UserTable />
|
|
264
|
+
<UserPagination />
|
|
265
|
+
</PageLayout>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ❌ Don't create giant "smart" components
|
|
270
|
+
function UserPage() {
|
|
271
|
+
// 500 lines of mixed concerns
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Data Flow
|
|
278
|
+
|
|
279
|
+
```mermaid
|
|
280
|
+
sequenceDiagram
|
|
281
|
+
participant User
|
|
282
|
+
participant Component as Component (UI)
|
|
283
|
+
participant Mutation as Mutation (TanStack)
|
|
284
|
+
participant Service as Service
|
|
285
|
+
participant Axios as Axios (Infra)
|
|
286
|
+
participant API as Laravel API
|
|
287
|
+
|
|
288
|
+
User->>Component: clicks "Save"
|
|
289
|
+
Component->>Mutation: mutation.mutate(values)
|
|
290
|
+
Mutation->>Service: userService.create(values)
|
|
291
|
+
Service->>Axios: api.post("/api/users", values)
|
|
292
|
+
Axios->>API: HTTP POST
|
|
293
|
+
API-->>Axios: Response
|
|
294
|
+
Axios-->>Service: data
|
|
295
|
+
Service-->>Mutation: User
|
|
296
|
+
Mutation->>Mutation: invalidateQueries
|
|
297
|
+
Mutation-->>Component: onSuccess
|
|
298
|
+
Component-->>User: UI updates
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## Coding Style: Centralized Configuration
|
|
304
|
+
|
|
305
|
+
### Rule: One File Per Concern in `lib/`
|
|
306
|
+
|
|
307
|
+
```
|
|
308
|
+
lib/
|
|
309
|
+
├── api.ts # Axios config + interceptors + error handling
|
|
310
|
+
├── query.tsx # QueryClient config + provider
|
|
311
|
+
├── queryKeys.ts # All query keys
|
|
312
|
+
└── dayjs.ts # Day.js config + plugins + utilities
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
**Why single file?**
|
|
316
|
+
- **Consistency**: One place to find all related code
|
|
317
|
+
- **Maintainability**: Easy to update
|
|
318
|
+
- **Discoverability**: Clear what's available
|
|
319
|
+
- **No over-engineering**: Avoid splitting into multiple files unnecessarily
|
|
320
|
+
|
|
321
|
+
### Anti-Pattern ❌
|
|
322
|
+
|
|
323
|
+
```
|
|
324
|
+
// Don't split into multiple files unnecessarily
|
|
325
|
+
lib/
|
|
326
|
+
├── dayjs/
|
|
327
|
+
│ ├── config.ts
|
|
328
|
+
│ ├── plugins.ts
|
|
329
|
+
│ ├── locale.ts
|
|
330
|
+
│ └── utils.ts
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Correct Pattern ✅
|
|
334
|
+
|
|
335
|
+
```
|
|
336
|
+
// Keep related code in one file
|
|
337
|
+
lib/
|
|
338
|
+
└── dayjs.ts # Config + plugins + locale + utils (all in one)
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### When to Split Files
|
|
342
|
+
|
|
343
|
+
Only split when:
|
|
344
|
+
1. File exceeds ~300-400 lines
|
|
345
|
+
2. Clear separate concerns (e.g., `api.ts` vs `queryKeys.ts`)
|
|
346
|
+
3. Different import patterns needed
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## Summary
|
|
351
|
+
|
|
352
|
+
| Aspect | Choice | Reason |
|
|
353
|
+
| ------------ | ------------------- | ----------------------------------- |
|
|
354
|
+
| Architecture | Clean Architecture | Separation of concerns, testability |
|
|
355
|
+
| Server State | TanStack Query | Caching, mutations, devtools |
|
|
356
|
+
| HTTP Client | Axios | Interceptors, Laravel integration |
|
|
357
|
+
| UI Library | Ant Design | Comprehensive, consistent |
|
|
358
|
+
| Types | TypeScript + Omnify | Type safety, DB sync |
|
|
359
|
+
| Styling | Tailwind CSS | Utility-first, fast |
|
|
360
|
+
| i18n | next-intl | Server components support |
|
|
361
|
+
| Date/Time | Day.js | Ant Design compatible, lightweight |
|
|
362
|
+
|
|
363
|
+
**Philosophy**: Keep it simple, explicit, and type-safe. Each layer does one thing well.
|
|
@@ -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
|
+
```
|