@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,108 @@
|
|
|
1
|
+
# Checklists
|
|
2
|
+
|
|
3
|
+
> **Related:** [README](./README.md)
|
|
4
|
+
|
|
5
|
+
## After Writing Code
|
|
6
|
+
|
|
7
|
+
> **IMPORTANT**: Always run these commands after writing/modifying code:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# 1. Type check
|
|
11
|
+
npm run typecheck
|
|
12
|
+
|
|
13
|
+
# 2. Lint check
|
|
14
|
+
npm run lint
|
|
15
|
+
|
|
16
|
+
# Or combined
|
|
17
|
+
npm run typecheck && npm run lint
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Adding New Resource
|
|
23
|
+
|
|
24
|
+
When adding a new resource (e.g., `posts`), follow these steps:
|
|
25
|
+
|
|
26
|
+
### 1. Service Layer (Always in `services/`)
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Create: services/posts.ts
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- [ ] Import types: `import type { Post, PostCreate, PostUpdate } from "@/types/model"`
|
|
33
|
+
- [ ] Define only `PostListParams` (Create/Update come from Omnify)
|
|
34
|
+
- [ ] Create `postService` object with CRUD methods
|
|
35
|
+
- [ ] Add JSDoc comments for each method
|
|
36
|
+
|
|
37
|
+
### 2. Query Keys
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Update: lib/queryKeys.ts
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- [ ] Add `posts` object to `queryKeys`
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
posts: {
|
|
47
|
+
all: ["posts"] as const,
|
|
48
|
+
lists: () => [...queryKeys.posts.all, "list"] as const,
|
|
49
|
+
list: (params?: PostListParams) => [...queryKeys.posts.lists(), params] as const,
|
|
50
|
+
details: () => [...queryKeys.posts.all, "detail"] as const,
|
|
51
|
+
detail: (id: number) => [...queryKeys.posts.details(), id] as const,
|
|
52
|
+
},
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 3. Feature Components (in `features/posts/`)
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Create: features/posts/
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- [ ] `PostTable.tsx` - Table component
|
|
62
|
+
- [ ] `PostForm.tsx` - Form component
|
|
63
|
+
- [ ] `usePostFilters.ts` - Feature-specific hooks (if needed)
|
|
64
|
+
|
|
65
|
+
### 4. Pages
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# Create pages in app/(dashboard)/posts/
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- [ ] `page.tsx` - List page (imports from `features/posts/`)
|
|
72
|
+
- [ ] `new/page.tsx` - Create form
|
|
73
|
+
- [ ] `[id]/page.tsx` - Detail view
|
|
74
|
+
- [ ] `[id]/edit/page.tsx` - Edit form
|
|
75
|
+
|
|
76
|
+
### 5. Shared Components (only if reused)
|
|
77
|
+
|
|
78
|
+
- [ ] If component used in 2+ features → move to `components/common/`
|
|
79
|
+
|
|
80
|
+
### 6. Translations
|
|
81
|
+
|
|
82
|
+
- [ ] Add labels to `src/i18n/messages/*.json` if needed
|
|
83
|
+
|
|
84
|
+
### 7. Final Check
|
|
85
|
+
|
|
86
|
+
- [ ] Run `npm run typecheck && npm run lint`
|
|
87
|
+
- [ ] Test create, read, update, delete operations
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Adding New Language
|
|
92
|
+
|
|
93
|
+
- [ ] Create message file: `src/i18n/messages/{locale}.json`
|
|
94
|
+
- [ ] Add locale to `src/i18n/config.ts`
|
|
95
|
+
- [ ] Import Ant Design locale in `src/components/AntdThemeProvider.tsx`
|
|
96
|
+
- [ ] Test with `LocaleSwitcher` component
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Before Commit
|
|
101
|
+
|
|
102
|
+
- [ ] `npm run typecheck` passes
|
|
103
|
+
- [ ] `npm run lint` passes
|
|
104
|
+
- [ ] No console warnings about deprecated props
|
|
105
|
+
- [ ] No hardcoded strings (use i18n)
|
|
106
|
+
- [ ] Forms handle loading state (`isPending`)
|
|
107
|
+
- [ ] Forms handle validation errors (`getFormErrors`)
|
|
108
|
+
- [ ] Mutations invalidate related queries
|
|
@@ -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 (`@/types/model`) |
|
|
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.
|