moicle 1.3.1 → 1.6.0
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 +17 -5
- package/assets/agents/developers/nodejs-backend-dev.md +92 -0
- package/assets/agents/developers/react-frontend-dev.md +32 -19
- package/assets/architecture/ddd-architecture.md +337 -0
- package/assets/architecture/go-backend.md +770 -693
- package/assets/architecture/laravel-backend.md +388 -156
- package/assets/architecture/nodejs-nestjs.md +949 -0
- package/assets/architecture/react-frontend.md +216 -145
- package/assets/skills/architect-review/SKILL.md +292 -372
- package/assets/skills/deep-debug/SKILL.md +114 -0
- package/assets/skills/new-feature/SKILL.md +232 -252
- package/assets/skills/refactor/SKILL.md +261 -679
- package/assets/skills/research/SKILL.md +124 -0
- package/assets/skills/review-changes/SKILL.md +312 -0
- package/assets/skills/sync-docs/SKILL.md +115 -74
- package/assets/templates/go-gin/CLAUDE.md +671 -121
- package/assets/templates/react-vite/CLAUDE.md +204 -128
- package/package.json +1 -1
- package/assets/architecture/clean-architecture.md +0 -143
- package/assets/skills/go-module/SKILL.md +0 -77
- package/assets/skills/ship/SKILL.md +0 -297
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# React Frontend Structure
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Module-based architecture with hooks + services. Idiomatic React — no MVVM ceremony.
|
|
4
4
|
|
|
5
5
|
## Project Structure
|
|
6
6
|
|
|
@@ -13,26 +13,21 @@
|
|
|
13
13
|
│ │ ├── app.config.ts
|
|
14
14
|
│ │ └── routes.config.ts
|
|
15
15
|
│ ├── core/
|
|
16
|
-
│ │ ├──
|
|
17
|
-
│ │
|
|
18
|
-
│ │ ├── context/ # Global contexts
|
|
19
|
-
│ │ ├── hooks/ # Core hooks
|
|
20
|
-
│ │ ├── interfaces/ # Core interfaces
|
|
21
|
-
│ │ ├── enums/ # Core enums
|
|
16
|
+
│ │ ├── context/ # Global contexts (auth, theme)
|
|
17
|
+
│ │ ├── hooks/ # Core reusable hooks
|
|
22
18
|
│ │ ├── errors/ # Error handling
|
|
23
19
|
│ │ ├── utils/ # Core utilities
|
|
24
|
-
│ │ ├──
|
|
20
|
+
│ │ ├── http-client.ts # API client
|
|
25
21
|
│ │ └── bootstrap.ts # App initialization
|
|
26
22
|
│ ├── modules/
|
|
27
23
|
│ │ └── {module}/
|
|
28
|
-
│ │ ├──
|
|
29
|
-
│ │ ├── view-models/ # Hooks (state management)
|
|
24
|
+
│ │ ├── types/ # Types, interfaces, DTOs, schemas
|
|
30
25
|
│ │ ├── services/ # API calls
|
|
26
|
+
│ │ ├── hooks/ # Module hooks (state + data fetching)
|
|
31
27
|
│ │ ├── components/ # Module components
|
|
32
28
|
│ │ ├── pages/ # Page components
|
|
33
29
|
│ │ └── index.ts
|
|
34
30
|
│ ├── lib/ # Third-party integrations
|
|
35
|
-
│ ├── services/ # Shared services
|
|
36
31
|
│ ├── utils/ # Shared utilities
|
|
37
32
|
│ ├── router.tsx # Route definitions
|
|
38
33
|
│ ├── index.tsx # Entry point
|
|
@@ -46,56 +41,60 @@
|
|
|
46
41
|
└── tailwind.config.js
|
|
47
42
|
```
|
|
48
43
|
|
|
49
|
-
##
|
|
44
|
+
## Layering
|
|
50
45
|
|
|
51
46
|
```
|
|
52
47
|
┌─────────────────────────────────────────────────┐
|
|
53
|
-
│
|
|
54
|
-
│
|
|
48
|
+
│ Pages / Components │
|
|
49
|
+
│ (render UI) │
|
|
55
50
|
├─────────────────────────────────────────────────┤
|
|
56
|
-
│
|
|
57
|
-
│
|
|
51
|
+
│ Hooks │
|
|
52
|
+
│ (state, queries, mutations, business logic) │
|
|
58
53
|
├─────────────────────────────────────────────────┤
|
|
59
|
-
│
|
|
60
|
-
│
|
|
54
|
+
│ Services │
|
|
55
|
+
│ (pure API calls) │
|
|
56
|
+
├─────────────────────────────────────────────────┤
|
|
57
|
+
│ Types │
|
|
58
|
+
│ (models, DTOs, Zod schemas) │
|
|
61
59
|
└─────────────────────────────────────────────────┘
|
|
62
60
|
```
|
|
63
61
|
|
|
64
|
-
**
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
**Rules:**
|
|
63
|
+
- Components consume hooks, not services directly
|
|
64
|
+
- Hooks orchestrate services + state (TanStack Query, Zustand, useState)
|
|
65
|
+
- Services are pure functions: input → API → output
|
|
66
|
+
- Types are the contract shared across all layers
|
|
69
67
|
|
|
70
68
|
## Module Structure
|
|
71
69
|
|
|
72
70
|
```
|
|
73
71
|
modules/{module}/
|
|
74
|
-
├──
|
|
72
|
+
├── types/
|
|
75
73
|
│ ├── {module}.model.ts # Types & interfaces
|
|
76
74
|
│ ├── {module}.schema.ts # Zod schemas (validation)
|
|
77
75
|
│ └── index.ts
|
|
78
|
-
├── view-models/
|
|
79
|
-
│ ├── use-{module}-list.ts # List ViewModel
|
|
80
|
-
│ ├── use-{module}-form.ts # Form ViewModel (optional)
|
|
81
|
-
│ └── index.ts
|
|
82
76
|
├── services/
|
|
83
77
|
│ ├── {module}.service.ts # API functions
|
|
84
78
|
│ └── index.ts
|
|
79
|
+
├── hooks/
|
|
80
|
+
│ ├── use-{module}-list.ts # List query hook
|
|
81
|
+
│ ├── use-{module}-detail.ts # Detail query hook
|
|
82
|
+
│ ├── use-{module}-mutations.ts # Create/update/delete mutations
|
|
83
|
+
│ └── index.ts
|
|
85
84
|
├── components/
|
|
86
|
-
│ ├── {
|
|
87
|
-
│ ├── {
|
|
85
|
+
│ ├── {module}-table.tsx
|
|
86
|
+
│ ├── {module}-form.tsx
|
|
88
87
|
│ └── index.ts
|
|
89
88
|
├── pages/
|
|
90
|
-
│ ├── {
|
|
89
|
+
│ ├── {module}-list-page.tsx
|
|
91
90
|
│ └── index.ts
|
|
92
91
|
└── index.ts
|
|
93
92
|
```
|
|
94
93
|
|
|
95
94
|
## Key Files
|
|
96
95
|
|
|
97
|
-
###
|
|
98
|
-
```
|
|
96
|
+
### types/user.model.ts
|
|
97
|
+
```ts
|
|
99
98
|
export interface User {
|
|
100
99
|
id: string;
|
|
101
100
|
name: string;
|
|
@@ -110,8 +109,8 @@ export interface CreateUserRequest {
|
|
|
110
109
|
password: string;
|
|
111
110
|
}
|
|
112
111
|
|
|
113
|
-
export interface
|
|
114
|
-
data:
|
|
112
|
+
export interface Paginated<T> {
|
|
113
|
+
data: T[];
|
|
115
114
|
meta: {
|
|
116
115
|
currentPage: number;
|
|
117
116
|
lastPage: number;
|
|
@@ -119,150 +118,222 @@ export interface UserListResponse {
|
|
|
119
118
|
total: number;
|
|
120
119
|
};
|
|
121
120
|
}
|
|
121
|
+
|
|
122
|
+
export interface UserListParams {
|
|
123
|
+
page?: number;
|
|
124
|
+
pageSize?: number;
|
|
125
|
+
search?: string;
|
|
126
|
+
sort?: { field: keyof User; order: 'asc' | 'desc' };
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### types/user.schema.ts
|
|
131
|
+
```ts
|
|
132
|
+
import { z } from 'zod';
|
|
133
|
+
|
|
134
|
+
export const createUserSchema = z.object({
|
|
135
|
+
name: z.string().min(1, 'Name is required'),
|
|
136
|
+
email: z.string().email('Invalid email'),
|
|
137
|
+
password: z.string().min(8, 'Min 8 characters'),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
export type CreateUserFormData = z.infer<typeof createUserSchema>;
|
|
122
141
|
```
|
|
123
142
|
|
|
124
143
|
### services/user.service.ts
|
|
125
|
-
```
|
|
126
|
-
import
|
|
127
|
-
import { User, CreateUserRequest } from '../
|
|
144
|
+
```ts
|
|
145
|
+
import { httpClient } from '@/core/http-client';
|
|
146
|
+
import type { User, CreateUserRequest, Paginated, UserListParams } from '../types/user.model';
|
|
128
147
|
|
|
129
|
-
export const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
148
|
+
export const userService = {
|
|
149
|
+
list: (params: UserListParams) =>
|
|
150
|
+
httpClient.get<Paginated<User>>('/users', { params }),
|
|
151
|
+
|
|
152
|
+
detail: (id: string) =>
|
|
153
|
+
httpClient.get<User>(`/users/${id}`),
|
|
154
|
+
|
|
155
|
+
create: (data: CreateUserRequest) =>
|
|
156
|
+
httpClient.post<User>('/users', data),
|
|
157
|
+
|
|
158
|
+
update: (id: string, data: Partial<CreateUserRequest>) =>
|
|
159
|
+
httpClient.patch<User>(`/users/${id}`, data),
|
|
160
|
+
|
|
161
|
+
remove: (id: string) =>
|
|
162
|
+
httpClient.delete<void>(`/users/${id}`),
|
|
163
|
+
};
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### hooks/use-user-list.ts
|
|
167
|
+
```ts
|
|
168
|
+
import { useQuery } from '@tanstack/react-query';
|
|
169
|
+
import { userService } from '../services/user.service';
|
|
170
|
+
import type { UserListParams } from '../types/user.model';
|
|
171
|
+
|
|
172
|
+
export const userKeys = {
|
|
173
|
+
all: ['users'] as const,
|
|
174
|
+
list: (params: UserListParams) => [...userKeys.all, 'list', params] as const,
|
|
175
|
+
detail: (id: string) => [...userKeys.all, 'detail', id] as const,
|
|
133
176
|
};
|
|
134
177
|
|
|
135
|
-
export const
|
|
136
|
-
|
|
178
|
+
export const useUserList = (params: UserListParams) => {
|
|
179
|
+
return useQuery({
|
|
180
|
+
queryKey: userKeys.list(params),
|
|
181
|
+
queryFn: () => userService.list(params),
|
|
182
|
+
placeholderData: (prev) => prev,
|
|
183
|
+
});
|
|
137
184
|
};
|
|
138
185
|
```
|
|
139
186
|
|
|
140
|
-
###
|
|
141
|
-
```
|
|
142
|
-
import {
|
|
143
|
-
import {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
187
|
+
### hooks/use-user-mutations.ts
|
|
188
|
+
```ts
|
|
189
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
190
|
+
import { userService } from '../services/user.service';
|
|
191
|
+
import { userKeys } from './use-user-list';
|
|
192
|
+
import type { CreateUserRequest } from '../types/user.model';
|
|
193
|
+
|
|
194
|
+
export const useCreateUser = () => {
|
|
195
|
+
const queryClient = useQueryClient();
|
|
196
|
+
|
|
197
|
+
return useMutation({
|
|
198
|
+
mutationFn: (data: CreateUserRequest) => userService.create(data),
|
|
199
|
+
onSuccess: () => {
|
|
200
|
+
queryClient.invalidateQueries({ queryKey: userKeys.all });
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
};
|
|
152
204
|
|
|
153
|
-
|
|
205
|
+
export const useDeleteUser = () => {
|
|
206
|
+
const queryClient = useQueryClient();
|
|
207
|
+
|
|
208
|
+
return useMutation({
|
|
209
|
+
mutationFn: (id: string) => userService.remove(id),
|
|
210
|
+
onSuccess: () => {
|
|
211
|
+
queryClient.invalidateQueries({ queryKey: userKeys.all });
|
|
212
|
+
},
|
|
213
|
+
});
|
|
154
214
|
};
|
|
155
215
|
```
|
|
156
216
|
|
|
157
|
-
### pages/
|
|
217
|
+
### pages/user-list-page.tsx
|
|
158
218
|
```tsx
|
|
159
219
|
import { useState } from 'react';
|
|
160
|
-
import {
|
|
161
|
-
import {
|
|
162
|
-
import { UserTable } from '../components/
|
|
163
|
-
import {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const [
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
handleTableChange,
|
|
174
|
-
reload,
|
|
175
|
-
handleInputSearch,
|
|
176
|
-
} = useUserList();
|
|
177
|
-
|
|
178
|
-
const handleEdit = (record: User) => {
|
|
179
|
-
setSelectedUser(record);
|
|
180
|
-
setModalOpen(true);
|
|
181
|
-
};
|
|
220
|
+
import { useUserList } from '../hooks/use-user-list';
|
|
221
|
+
import { useDeleteUser } from '../hooks/use-user-mutations';
|
|
222
|
+
import { UserTable } from '../components/user-table';
|
|
223
|
+
import { UserFormDialog } from '../components/user-form-dialog';
|
|
224
|
+
import type { User } from '../types/user.model';
|
|
225
|
+
|
|
226
|
+
export default function UserListPage() {
|
|
227
|
+
const [params, setParams] = useState({ page: 1, pageSize: 20 });
|
|
228
|
+
const [editing, setEditing] = useState<User | null>(null);
|
|
229
|
+
const [formOpen, setFormOpen] = useState(false);
|
|
230
|
+
|
|
231
|
+
const { data, isLoading } = useUserList(params);
|
|
232
|
+
const deleteUser = useDeleteUser();
|
|
182
233
|
|
|
183
234
|
return (
|
|
184
|
-
<div>
|
|
185
|
-
<
|
|
186
|
-
|
|
187
|
-
|
|
235
|
+
<div className="container mx-auto p-6">
|
|
236
|
+
<div className="flex justify-between mb-6">
|
|
237
|
+
<h1 className="text-2xl font-bold">Users</h1>
|
|
238
|
+
<Button onClick={() => { setEditing(null); setFormOpen(true); }}>
|
|
239
|
+
Add User
|
|
240
|
+
</Button>
|
|
241
|
+
</div>
|
|
188
242
|
|
|
189
243
|
<UserTable
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
244
|
+
data={data?.data ?? []}
|
|
245
|
+
meta={data?.meta}
|
|
246
|
+
isLoading={isLoading}
|
|
247
|
+
onEdit={(user) => { setEditing(user); setFormOpen(true); }}
|
|
248
|
+
onDelete={(user) => deleteUser.mutate(user.id)}
|
|
249
|
+
onPageChange={(page) => setParams((p) => ({ ...p, page }))}
|
|
196
250
|
/>
|
|
197
251
|
|
|
198
|
-
<
|
|
199
|
-
open={
|
|
200
|
-
onOpenChange={
|
|
201
|
-
user={
|
|
202
|
-
onSuccess={reload}
|
|
252
|
+
<UserFormDialog
|
|
253
|
+
open={formOpen}
|
|
254
|
+
onOpenChange={setFormOpen}
|
|
255
|
+
user={editing}
|
|
203
256
|
/>
|
|
204
257
|
</div>
|
|
205
258
|
);
|
|
206
|
-
}
|
|
259
|
+
}
|
|
207
260
|
```
|
|
208
261
|
|
|
209
|
-
### core/
|
|
210
|
-
```
|
|
262
|
+
### core/http-client.ts
|
|
263
|
+
```ts
|
|
211
264
|
const BASE_URL = import.meta.env.VITE_API_URL;
|
|
212
265
|
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (!res.ok) throw new Error(res.statusText);
|
|
219
|
-
return res.json();
|
|
220
|
-
},
|
|
221
|
-
|
|
222
|
-
post: async <T>(url: string, options?: RequestInit): Promise<T> => {
|
|
223
|
-
const res = await fetch(`${BASE_URL}${url}`, {
|
|
224
|
-
method: 'POST',
|
|
225
|
-
headers: {
|
|
226
|
-
'Content-Type': 'application/json',
|
|
227
|
-
'Authorization': `Bearer ${getToken()}`,
|
|
228
|
-
},
|
|
229
|
-
...options,
|
|
230
|
-
});
|
|
231
|
-
if (!res.ok) throw new Error(res.statusText);
|
|
232
|
-
return res.json();
|
|
233
|
-
},
|
|
234
|
-
|
|
235
|
-
delete: async (url: string): Promise<void> => {
|
|
236
|
-
await fetch(`${BASE_URL}${url}`, {
|
|
237
|
-
method: 'DELETE',
|
|
238
|
-
headers: { 'Authorization': `Bearer ${getToken()}` },
|
|
266
|
+
const buildUrl = (path: string, params?: Record<string, unknown>) => {
|
|
267
|
+
const url = new URL(`${BASE_URL}${path}`);
|
|
268
|
+
if (params) {
|
|
269
|
+
Object.entries(params).forEach(([k, v]) => {
|
|
270
|
+
if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
|
|
239
271
|
});
|
|
240
|
-
}
|
|
272
|
+
}
|
|
273
|
+
return url.toString();
|
|
241
274
|
};
|
|
242
275
|
|
|
243
|
-
|
|
276
|
+
const request = async <T>(path: string, init: RequestInit & { params?: Record<string, unknown> } = {}): Promise<T> => {
|
|
277
|
+
const { params, ...rest } = init;
|
|
278
|
+
const token = getToken();
|
|
279
|
+
const res = await fetch(buildUrl(path, params), {
|
|
280
|
+
...rest,
|
|
281
|
+
headers: {
|
|
282
|
+
'Content-Type': 'application/json',
|
|
283
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
284
|
+
...rest.headers,
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
if (!res.ok) {
|
|
289
|
+
const body = await res.json().catch(() => ({}));
|
|
290
|
+
throw new HttpError(res.status, body?.message ?? res.statusText, body);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return res.status === 204 ? (undefined as T) : res.json();
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
export const httpClient = {
|
|
297
|
+
get: <T>(path: string, opts?: { params?: Record<string, unknown> }) => request<T>(path, { method: 'GET', ...opts }),
|
|
298
|
+
post: <T>(path: string, body?: unknown) => request<T>(path, { method: 'POST', body: JSON.stringify(body) }),
|
|
299
|
+
patch: <T>(path: string, body?: unknown) => request<T>(path, { method: 'PATCH', body: JSON.stringify(body) }),
|
|
300
|
+
put: <T>(path: string, body?: unknown) => request<T>(path, { method: 'PUT', body: JSON.stringify(body) }),
|
|
301
|
+
delete: <T>(path: string) => request<T>(path, { method: 'DELETE' }),
|
|
302
|
+
};
|
|
244
303
|
```
|
|
245
304
|
|
|
305
|
+
## State Management
|
|
306
|
+
|
|
307
|
+
Pick the right tool per scope:
|
|
308
|
+
|
|
309
|
+
| Scope | Tool |
|
|
310
|
+
|-------|------|
|
|
311
|
+
| Server state (API data) | TanStack Query / SWR |
|
|
312
|
+
| Component-local state | `useState` / `useReducer` |
|
|
313
|
+
| Cross-component UI state | Context API |
|
|
314
|
+
| Global client state (cart, filters) | Zustand |
|
|
315
|
+
| Form state | React Hook Form + Zod |
|
|
316
|
+
|
|
317
|
+
Do NOT dump everything into Redux/Zustand — server state belongs in a query cache.
|
|
318
|
+
|
|
246
319
|
## Conventions
|
|
247
320
|
|
|
248
321
|
| Item | Convention | Example |
|
|
249
322
|
|------|------------|---------|
|
|
250
|
-
| Module folder | kebab-case | `
|
|
251
|
-
|
|
|
252
|
-
|
|
|
253
|
-
|
|
|
254
|
-
|
|
|
255
|
-
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
```
|
|
323
|
+
| Module folder | kebab-case | `user-profile/` |
|
|
324
|
+
| Type file | kebab-case.model.ts | `user.model.ts` |
|
|
325
|
+
| Schema file | kebab-case.schema.ts | `user.schema.ts` |
|
|
326
|
+
| Service file | kebab-case.service.ts | `user.service.ts` |
|
|
327
|
+
| Hook file | use-kebab-case.ts | `use-user-list.ts` |
|
|
328
|
+
| Component file | kebab-case.tsx | `user-table.tsx` |
|
|
329
|
+
| Component export | PascalCase named export | `export const UserTable` |
|
|
330
|
+
| Page file | kebab-case-page.tsx | `user-list-page.tsx` |
|
|
331
|
+
| Page export | default export | `export default UserListPage` |
|
|
332
|
+
|
|
333
|
+
## Rules
|
|
334
|
+
|
|
335
|
+
- Components do NOT call services directly — always go through a hook
|
|
336
|
+
- Services are pure: no React, no state, just `fetch` + types
|
|
337
|
+
- Query keys live next to hooks and are exported for invalidation
|
|
338
|
+
- Validate external input at the boundary with Zod — trust types inside the app
|
|
339
|
+
- One module = one feature domain; shared code goes to `core/` or `components/ui/`
|