moicle 1.4.0 → 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 +15 -4
- package/assets/agents/developers/nodejs-backend-dev.md +92 -0
- package/assets/agents/developers/react-frontend-dev.md +32 -19
- package/assets/architecture/nodejs-nestjs.md +949 -0
- package/assets/architecture/react-frontend.md +216 -145
- package/assets/skills/deep-debug/SKILL.md +62 -62
- package/assets/skills/research/SKILL.md +124 -0
- package/assets/skills/review-changes/SKILL.md +312 -0
- package/assets/templates/react-vite/CLAUDE.md +204 -128
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@ Frontend application built with:
|
|
|
10
10
|
- **shadcn/ui** - UI component library
|
|
11
11
|
- **React Router 7** - Client-side routing
|
|
12
12
|
- **TanStack Query** - Server state management
|
|
13
|
+
- **React Hook Form + Zod** - Forms & validation
|
|
13
14
|
|
|
14
15
|
## Quick Start
|
|
15
16
|
|
|
@@ -42,12 +43,13 @@ pnpm lint
|
|
|
42
43
|
│ │ ├── layouts/ # Layout components
|
|
43
44
|
│ │ │ ├── root-layout.tsx
|
|
44
45
|
│ │ │ └── sidebar-layout.tsx
|
|
45
|
-
│ │ ├── modules/ # Feature modules
|
|
46
|
+
│ │ ├── modules/ # Feature modules
|
|
46
47
|
│ │ │ └── {module}/
|
|
47
|
-
│ │ │ ├──
|
|
48
|
-
│ │ │ ├──
|
|
49
|
-
│ │ │ ├──
|
|
50
|
-
│ │ │
|
|
48
|
+
│ │ │ ├── types/ # Models, DTOs, Zod schemas
|
|
49
|
+
│ │ │ ├── services/ # Pure API functions
|
|
50
|
+
│ │ │ ├── hooks/ # Query/mutation hooks
|
|
51
|
+
│ │ │ ├── components/ # Feature components
|
|
52
|
+
│ │ │ └── pages/ # Page components
|
|
51
53
|
│ │ ├── shared/ # Shared utilities
|
|
52
54
|
│ │ │ ├── components/
|
|
53
55
|
│ │ │ ├── contexts/
|
|
@@ -55,7 +57,7 @@ pnpm lint
|
|
|
55
57
|
│ │ └── router.tsx # Router setup
|
|
56
58
|
│ ├── components/ui/ # shadcn/ui components
|
|
57
59
|
│ ├── lib/
|
|
58
|
-
│ │ ├──
|
|
60
|
+
│ │ ├── http-client.ts # HTTP client
|
|
59
61
|
│ │ └── utils.ts # Utility functions
|
|
60
62
|
│ ├── main.tsx # App entry
|
|
61
63
|
│ └── index.css # Global styles
|
|
@@ -72,63 +74,130 @@ pnpm lint
|
|
|
72
74
|
### File Naming
|
|
73
75
|
- Use `kebab-case.tsx` for components
|
|
74
76
|
- Use `use-*.ts` for hooks
|
|
75
|
-
- Use `*.model.ts` for
|
|
77
|
+
- Use `*.model.ts` for types
|
|
76
78
|
- Use `*.schema.ts` for Zod schemas
|
|
79
|
+
- Use `*.service.ts` for API service functions
|
|
77
80
|
|
|
78
|
-
###
|
|
81
|
+
### Module Layering
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
pages / components → hooks → services → types
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
- **Components** consume hooks only — never call services directly
|
|
88
|
+
- **Hooks** orchestrate queries/mutations and local state
|
|
89
|
+
- **Services** are pure: input → API → typed output
|
|
90
|
+
- **Types** are shared contracts
|
|
91
|
+
|
|
92
|
+
### Types (models + schemas)
|
|
79
93
|
|
|
80
|
-
**Model** - Data types and API functions:
|
|
81
94
|
```typescript
|
|
82
|
-
//
|
|
95
|
+
// modules/entity/types/entity.model.ts
|
|
83
96
|
export interface Entity {
|
|
84
97
|
id: string;
|
|
85
98
|
name: string;
|
|
86
99
|
status: 'active' | 'inactive';
|
|
87
100
|
}
|
|
88
101
|
|
|
89
|
-
export
|
|
90
|
-
|
|
102
|
+
export interface CreateEntityDTO {
|
|
103
|
+
name: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface Paginated<T> {
|
|
107
|
+
data: T[];
|
|
108
|
+
meta: { page: number; perPage: number; total: number };
|
|
91
109
|
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// modules/entity/types/entity.schema.ts
|
|
114
|
+
import { z } from 'zod';
|
|
115
|
+
|
|
116
|
+
export const createEntitySchema = z.object({
|
|
117
|
+
name: z.string().min(1, 'Name is required'),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
export type CreateEntityFormData = z.infer<typeof createEntitySchema>;
|
|
121
|
+
```
|
|
92
122
|
|
|
93
|
-
|
|
94
|
-
|
|
123
|
+
### Services (pure API calls)
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// modules/entity/services/entity.service.ts
|
|
127
|
+
import { httpClient } from '@/lib/http-client';
|
|
128
|
+
import type { Entity, CreateEntityDTO, Paginated } from '../types/entity.model';
|
|
129
|
+
|
|
130
|
+
export const entityService = {
|
|
131
|
+
list: (params: { page?: number; search?: string }) =>
|
|
132
|
+
httpClient.get<Paginated<Entity>>('/entities', { params }),
|
|
133
|
+
detail: (id: string) => httpClient.get<Entity>(`/entities/${id}`),
|
|
134
|
+
create: (data: CreateEntityDTO) => httpClient.post<Entity>('/entities', data),
|
|
135
|
+
update: (id: string, data: Partial<CreateEntityDTO>) =>
|
|
136
|
+
httpClient.patch<Entity>(`/entities/${id}`, data),
|
|
137
|
+
remove: (id: string) => httpClient.delete<void>(`/entities/${id}`),
|
|
138
|
+
};
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Hooks (queries + mutations)
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
// modules/entity/hooks/use-entity-list.ts
|
|
145
|
+
import { useQuery } from '@tanstack/react-query';
|
|
146
|
+
import { entityService } from '../services/entity.service';
|
|
147
|
+
|
|
148
|
+
export const entityKeys = {
|
|
149
|
+
all: ['entities'] as const,
|
|
150
|
+
list: (params: unknown) => [...entityKeys.all, 'list', params] as const,
|
|
151
|
+
detail: (id: string) => [...entityKeys.all, 'detail', id] as const,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export function useEntityList(params: { page?: number; search?: string }) {
|
|
155
|
+
return useQuery({
|
|
156
|
+
queryKey: entityKeys.list(params),
|
|
157
|
+
queryFn: () => entityService.list(params),
|
|
158
|
+
placeholderData: (prev) => prev,
|
|
159
|
+
});
|
|
95
160
|
}
|
|
96
161
|
```
|
|
97
162
|
|
|
98
|
-
**ViewModel** - Business logic and state:
|
|
99
163
|
```typescript
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
164
|
+
// modules/entity/hooks/use-entity-mutations.ts
|
|
165
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
166
|
+
import { entityService } from '../services/entity.service';
|
|
167
|
+
import { entityKeys } from './use-entity-list';
|
|
168
|
+
import type { CreateEntityDTO } from '../types/entity.model';
|
|
169
|
+
|
|
170
|
+
export function useCreateEntity() {
|
|
171
|
+
const qc = useQueryClient();
|
|
172
|
+
return useMutation({
|
|
173
|
+
mutationFn: (data: CreateEntityDTO) => entityService.create(data),
|
|
174
|
+
onSuccess: () => qc.invalidateQueries({ queryKey: entityKeys.all }),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function useDeleteEntity() {
|
|
179
|
+
const qc = useQueryClient();
|
|
180
|
+
return useMutation({
|
|
181
|
+
mutationFn: (id: string) => entityService.remove(id),
|
|
182
|
+
onSuccess: () => qc.invalidateQueries({ queryKey: entityKeys.all }),
|
|
183
|
+
});
|
|
119
184
|
}
|
|
120
185
|
```
|
|
121
186
|
|
|
122
|
-
|
|
187
|
+
### Components (pure UI)
|
|
188
|
+
|
|
123
189
|
```typescript
|
|
124
|
-
// components/entity-table.tsx
|
|
190
|
+
// modules/entity/components/entity-table.tsx
|
|
125
191
|
interface EntityTableProps {
|
|
126
192
|
data: Entity[];
|
|
193
|
+
isLoading?: boolean;
|
|
127
194
|
onEdit: (entity: Entity) => void;
|
|
128
195
|
onDelete: (entity: Entity) => void;
|
|
129
196
|
}
|
|
130
197
|
|
|
131
|
-
export function EntityTable({ data, onEdit, onDelete }: EntityTableProps) {
|
|
198
|
+
export function EntityTable({ data, isLoading, onEdit, onDelete }: EntityTableProps) {
|
|
199
|
+
if (isLoading) return <TableSkeleton rows={5} />;
|
|
200
|
+
|
|
132
201
|
return (
|
|
133
202
|
<Table>
|
|
134
203
|
{data.map((entity) => (
|
|
@@ -136,6 +205,7 @@ export function EntityTable({ data, onEdit, onDelete }: EntityTableProps) {
|
|
|
136
205
|
<TableCell>{entity.name}</TableCell>
|
|
137
206
|
<TableCell>
|
|
138
207
|
<Button onClick={() => onEdit(entity)}>Edit</Button>
|
|
208
|
+
<Button variant="destructive" onClick={() => onDelete(entity)}>Delete</Button>
|
|
139
209
|
</TableCell>
|
|
140
210
|
</TableRow>
|
|
141
211
|
))}
|
|
@@ -144,125 +214,139 @@ export function EntityTable({ data, onEdit, onDelete }: EntityTableProps) {
|
|
|
144
214
|
}
|
|
145
215
|
```
|
|
146
216
|
|
|
147
|
-
###
|
|
217
|
+
### Page (composition)
|
|
148
218
|
|
|
149
219
|
```typescript
|
|
150
|
-
//
|
|
151
|
-
import {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
async post<T>(endpoint: string, data: unknown): Promise<T> {
|
|
167
|
-
// Similar to get with method: 'POST' and body
|
|
168
|
-
},
|
|
169
|
-
};
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
### Page Component Pattern
|
|
173
|
-
|
|
174
|
-
```typescript
|
|
175
|
-
// pages/entities-page.tsx
|
|
176
|
-
export default function EntitiesPage() {
|
|
177
|
-
const vm = useEntitiesPageViewModel();
|
|
178
|
-
|
|
179
|
-
useEffect(() => {
|
|
180
|
-
vm.loadData();
|
|
181
|
-
}, [vm.loadData]);
|
|
220
|
+
// modules/entity/pages/entity-list-page.tsx
|
|
221
|
+
import { useState } from 'react';
|
|
222
|
+
import { useEntityList } from '../hooks/use-entity-list';
|
|
223
|
+
import { useDeleteEntity } from '../hooks/use-entity-mutations';
|
|
224
|
+
import { EntityTable } from '../components/entity-table';
|
|
225
|
+
import { EntityFormDialog } from '../components/entity-form-dialog';
|
|
226
|
+
import type { Entity } from '../types/entity.model';
|
|
227
|
+
|
|
228
|
+
export default function EntityListPage() {
|
|
229
|
+
const [params, setParams] = useState({ page: 1 });
|
|
230
|
+
const [editing, setEditing] = useState<Entity | null>(null);
|
|
231
|
+
const [formOpen, setFormOpen] = useState(false);
|
|
232
|
+
|
|
233
|
+
const { data, isLoading } = useEntityList(params);
|
|
234
|
+
const deleteEntity = useDeleteEntity();
|
|
182
235
|
|
|
183
236
|
return (
|
|
184
237
|
<div className="container mx-auto p-6">
|
|
185
238
|
<div className="flex justify-between items-center mb-6">
|
|
186
239
|
<h1 className="text-2xl font-bold">Entities</h1>
|
|
187
|
-
<Button onClick={() =>
|
|
240
|
+
<Button onClick={() => { setEditing(null); setFormOpen(true); }}>Add New</Button>
|
|
188
241
|
</div>
|
|
189
242
|
|
|
190
243
|
<EntityTable
|
|
191
|
-
data={
|
|
192
|
-
isLoading={
|
|
193
|
-
onEdit={
|
|
194
|
-
onDelete={
|
|
244
|
+
data={data?.data ?? []}
|
|
245
|
+
isLoading={isLoading}
|
|
246
|
+
onEdit={(e) => { setEditing(e); setFormOpen(true); }}
|
|
247
|
+
onDelete={(e) => deleteEntity.mutate(e.id)}
|
|
195
248
|
/>
|
|
196
249
|
|
|
197
|
-
<EntityFormDialog
|
|
198
|
-
open={vm.isFormOpen}
|
|
199
|
-
onOpenChange={vm.setIsFormOpen}
|
|
200
|
-
entity={vm.editingEntity}
|
|
201
|
-
onSubmit={vm.handleSubmit}
|
|
202
|
-
/>
|
|
250
|
+
<EntityFormDialog open={formOpen} onOpenChange={setFormOpen} entity={editing} />
|
|
203
251
|
</div>
|
|
204
252
|
);
|
|
205
253
|
}
|
|
206
254
|
```
|
|
207
255
|
|
|
208
|
-
|
|
256
|
+
### HTTP Client
|
|
209
257
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
258
|
+
```typescript
|
|
259
|
+
// lib/http-client.ts
|
|
260
|
+
const BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
|
261
|
+
|
|
262
|
+
const buildUrl = (path: string, params?: Record<string, unknown>) => {
|
|
263
|
+
const url = new URL(`${BASE_URL}${path}`);
|
|
264
|
+
if (params) {
|
|
265
|
+
Object.entries(params).forEach(([k, v]) => {
|
|
266
|
+
if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
return url.toString();
|
|
270
|
+
};
|
|
214
271
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
272
|
+
const request = async <T>(path: string, init: RequestInit & { params?: Record<string, unknown> } = {}): Promise<T> => {
|
|
273
|
+
const { params, ...rest } = init;
|
|
274
|
+
const token = await getAuthToken();
|
|
275
|
+
const res = await fetch(buildUrl(path, params), {
|
|
276
|
+
...rest,
|
|
277
|
+
headers: {
|
|
278
|
+
'Content-Type': 'application/json',
|
|
279
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
280
|
+
...rest.headers,
|
|
281
|
+
},
|
|
282
|
+
});
|
|
221
283
|
|
|
222
|
-
|
|
284
|
+
if (!res.ok) {
|
|
285
|
+
const body = await res.json().catch(() => ({}));
|
|
286
|
+
throw new HttpError(res.status, body?.message ?? res.statusText, body);
|
|
287
|
+
}
|
|
223
288
|
|
|
224
|
-
|
|
289
|
+
return res.status === 204 ? (undefined as T) : res.json();
|
|
290
|
+
};
|
|
225
291
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
292
|
+
export const httpClient = {
|
|
293
|
+
get: <T>(path: string, opts?: { params?: Record<string, unknown> }) => request<T>(path, { method: 'GET', ...opts }),
|
|
294
|
+
post: <T>(path: string, body?: unknown) => request<T>(path, { method: 'POST', body: JSON.stringify(body) }),
|
|
295
|
+
patch: <T>(path: string, body?: unknown) => request<T>(path, { method: 'PATCH', body: JSON.stringify(body) }),
|
|
296
|
+
put: <T>(path: string, body?: unknown) => request<T>(path, { method: 'PUT', body: JSON.stringify(body) }),
|
|
297
|
+
delete: <T>(path: string) => request<T>(path, { method: 'DELETE' }),
|
|
298
|
+
};
|
|
299
|
+
```
|
|
229
300
|
|
|
230
|
-
|
|
231
|
-
name: z.string().min(1, 'Name is required'),
|
|
232
|
-
email: z.string().email('Invalid email'),
|
|
233
|
-
});
|
|
301
|
+
## Adding a New Module
|
|
234
302
|
|
|
235
|
-
|
|
303
|
+
1. Create module directory structure:
|
|
304
|
+
```bash
|
|
305
|
+
mkdir -p src/app/modules/{module}/{types,services,hooks,components,pages}
|
|
236
306
|
```
|
|
237
307
|
|
|
308
|
+
2. Create types (`types/{module}.model.ts`, `types/{module}.schema.ts`)
|
|
309
|
+
3. Create service (`services/{module}.service.ts`)
|
|
310
|
+
4. Create hooks (`hooks/use-{module}-list.ts`, `hooks/use-{module}-mutations.ts`)
|
|
311
|
+
5. Create components (`components/*.tsx`)
|
|
312
|
+
6. Create page (`pages/{module}-list-page.tsx`)
|
|
313
|
+
7. Add route in `src/app/router.tsx`
|
|
314
|
+
8. Add menu item in `src/app/config/menu.ts`
|
|
315
|
+
|
|
316
|
+
## Form with Zod + React Hook Form
|
|
317
|
+
|
|
238
318
|
```typescript
|
|
239
|
-
// components/entity-form.tsx
|
|
319
|
+
// modules/entity/components/entity-form.tsx
|
|
240
320
|
import { useForm } from 'react-hook-form';
|
|
241
321
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
322
|
+
import { createEntitySchema, type CreateEntityFormData } from '../types/entity.schema';
|
|
323
|
+
import { useCreateEntity } from '../hooks/use-entity-mutations';
|
|
242
324
|
|
|
243
|
-
export function EntityForm({
|
|
244
|
-
const form = useForm<
|
|
245
|
-
resolver: zodResolver(
|
|
325
|
+
export function EntityForm({ onSuccess }: { onSuccess: () => void }) {
|
|
326
|
+
const form = useForm<CreateEntityFormData>({
|
|
327
|
+
resolver: zodResolver(createEntitySchema),
|
|
246
328
|
});
|
|
329
|
+
const createEntity = useCreateEntity();
|
|
247
330
|
|
|
248
331
|
return (
|
|
249
332
|
<Form {...form}>
|
|
250
|
-
<form onSubmit={form.handleSubmit(
|
|
333
|
+
<form onSubmit={form.handleSubmit((data) => createEntity.mutate(data, { onSuccess }))}>
|
|
251
334
|
<FormField name="name" control={form.control} render={...} />
|
|
335
|
+
<Button type="submit" disabled={createEntity.isPending}>Save</Button>
|
|
252
336
|
</form>
|
|
253
337
|
</Form>
|
|
254
338
|
);
|
|
255
339
|
}
|
|
256
340
|
```
|
|
257
341
|
|
|
258
|
-
|
|
342
|
+
## Loading States
|
|
259
343
|
|
|
260
344
|
```typescript
|
|
261
|
-
//
|
|
345
|
+
// Initial load → skeleton
|
|
262
346
|
{isLoading && <TableSkeleton rows={5} />}
|
|
263
347
|
|
|
264
|
-
//
|
|
265
|
-
{
|
|
348
|
+
// Background refresh → overlay
|
|
349
|
+
{isFetching && !isLoading && (
|
|
266
350
|
<div className="absolute inset-0 bg-background/50 flex items-center justify-center">
|
|
267
351
|
<Spinner />
|
|
268
352
|
</div>
|
|
@@ -277,28 +361,20 @@ VITE_API_BASE_URL=http://localhost:8080
|
|
|
277
361
|
VITE_APP_NAME={project_name}
|
|
278
362
|
```
|
|
279
363
|
|
|
280
|
-
### Tailwind Config
|
|
281
|
-
```typescript
|
|
282
|
-
// tailwind.config.ts
|
|
283
|
-
export default {
|
|
284
|
-
content: ['./src/**/*.{ts,tsx}'],
|
|
285
|
-
theme: {
|
|
286
|
-
extend: {
|
|
287
|
-
// Custom theme extensions
|
|
288
|
-
},
|
|
289
|
-
},
|
|
290
|
-
plugins: [],
|
|
291
|
-
};
|
|
292
|
-
```
|
|
293
|
-
|
|
294
364
|
### Path Aliases (tsconfig.json)
|
|
295
365
|
```json
|
|
296
366
|
{
|
|
297
367
|
"compilerOptions": {
|
|
298
368
|
"baseUrl": ".",
|
|
299
|
-
"paths": {
|
|
300
|
-
"@/*": ["./src/*"]
|
|
301
|
-
}
|
|
369
|
+
"paths": { "@/*": ["./src/*"] }
|
|
302
370
|
}
|
|
303
371
|
}
|
|
304
372
|
```
|
|
373
|
+
|
|
374
|
+
## Rules
|
|
375
|
+
|
|
376
|
+
- Components NEVER call services directly — always via a hook
|
|
377
|
+
- Services are pure (no React imports)
|
|
378
|
+
- Query keys exported from the hook file, used for invalidation
|
|
379
|
+
- Validate external input with Zod at the boundary
|
|
380
|
+
- Server state lives in TanStack Query cache, not Zustand/Redux
|