@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.
@@ -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.