@relipa/ai-flow-kit 0.2.0 → 0.2.1
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/custom/rules/java/spring-boot-rules.md +209 -0
- package/custom/rules/javascript/nestjs-examples.md +41 -0
- package/custom/rules/javascript/nestjs-rules.md +42 -0
- package/custom/rules/javascript/nodejs-express-examples.md +35 -0
- package/custom/rules/javascript/nodejs-express-rules.md +49 -0
- package/custom/rules/javascript/reactjs-examples.md +380 -0
- package/custom/rules/javascript/reactjs-rules.md +173 -0
- package/custom/rules/php/php-examples.md +161 -0
- package/custom/rules/php/php-rules.md +127 -0
- package/custom/rules/python/python-django-examples.md +34 -0
- package/custom/rules/python/python-django-rules.md +48 -0
- package/custom/rules/python/python-examples.md +32 -0
- package/custom/rules/python/python-fastapi-examples.md +30 -0
- package/custom/rules/python/python-fastapi-rules.md +35 -0
- package/custom/rules/python/python-ml-examples.md +187 -0
- package/custom/rules/python/python-ml-rules.md +121 -0
- package/custom/rules/python/python-rules.md +58 -0
- package/custom/skills/ba-skills/skill-ba-qna-template-v1.md +4 -4
- package/custom/skills/ba-skills/skill-ba-qna-v1.md +6 -0
- package/custom/skills/create-system-requirement/SKILL.md +52 -16
- package/custom/skills/create-system-requirement/system-requirement-template-v1.md +128 -0
- package/custom/skills/impact-analysis/SKILL.md +106 -106
- package/custom/skills/report-customer/SKILL.md +99 -99
- package/custom/templates/nestjs.md +5 -72
- package/custom/templates/nodejs-express.md +5 -73
- package/custom/templates/php-plain.md +5 -261
- package/custom/templates/php.md +5 -261
- package/custom/templates/python-django.md +5 -71
- package/custom/templates/python-fastapi.md +5 -54
- package/custom/templates/python-ml.md +1 -269
- package/custom/templates/python.md +5 -79
- package/custom/templates/reactjs.md +5 -492
- package/custom/templates/shared/gate-workflow.md +1 -0
- package/custom/templates/shared/ml-gate-workflow.md +1 -0
- package/custom/templates/spring-boot.md +5 -224
- package/docs/common/CHANGELOG.md +20 -10
- package/package.json +1 -1
- package/scripts/init.js +143 -40
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
# React.js Code Examples
|
|
2
|
+
|
|
3
|
+
Reference examples for each rule area. Read the relevant section when generating code for that area.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Component Rules
|
|
8
|
+
|
|
9
|
+
### Always use Functional Components
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
// ✅ Good
|
|
13
|
+
const UserCard = ({ user }: UserCardProps) => {
|
|
14
|
+
return <div>{user.name}</div>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// ❌ Bad — class component
|
|
18
|
+
class UserCard extends React.Component {}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Define Props with TypeScript interface
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
// ✅ Good
|
|
25
|
+
interface UserCardProps {
|
|
26
|
+
user: User;
|
|
27
|
+
onDelete?: (id: number) => void;
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const UserCard = ({ user, onDelete, className }: UserCardProps) => {
|
|
32
|
+
// ...
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ❌ Bad — any, or no type
|
|
36
|
+
const UserCard = ({ user }: any) => {};
|
|
37
|
+
const UserCard = (props) => {};
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Keep components focused and small
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
// ✅ Good — logic extracted to hook
|
|
44
|
+
const UserList = () => {
|
|
45
|
+
const { users, isLoading, error, deleteUser } = useUserList();
|
|
46
|
+
|
|
47
|
+
if (isLoading) return <Spinner />;
|
|
48
|
+
if (error) return <ErrorMessage message={error.message} />;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<ul>
|
|
52
|
+
{users.map(user => (
|
|
53
|
+
<UserCard key={user.id} user={user} onDelete={deleteUser} />
|
|
54
|
+
))}
|
|
55
|
+
</ul>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// ❌ Bad — logic mixed into component
|
|
60
|
+
const UserList = () => {
|
|
61
|
+
const [users, setUsers] = useState([]);
|
|
62
|
+
const [loading, setLoading] = useState(true);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
fetch('/api/users')
|
|
66
|
+
.then(r => r.json())
|
|
67
|
+
.then(data => { setUsers(data); setLoading(false); });
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const handleDelete = async (id) => {
|
|
71
|
+
await fetch(`/api/users/${id}`, { method: 'DELETE' });
|
|
72
|
+
setUsers(prev => prev.filter(u => u.id !== id));
|
|
73
|
+
};
|
|
74
|
+
// ... long JSX
|
|
75
|
+
};
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Export convention
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
// Named export for regular components
|
|
82
|
+
export const UserCard = ({ user }: UserCardProps) => { ... };
|
|
83
|
+
|
|
84
|
+
// Default export only for page-level components
|
|
85
|
+
export default function UserPage() { ... }
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Custom Hook Rules
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
// ✅ Good
|
|
94
|
+
const useUserList = () => {
|
|
95
|
+
const { data: users = [], isLoading, error } = useQuery({
|
|
96
|
+
queryKey: ['users'],
|
|
97
|
+
queryFn: userApi.getAll,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const { mutate: deleteUser } = useMutation({
|
|
101
|
+
mutationFn: userApi.delete,
|
|
102
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return { users, isLoading, error, deleteUser };
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Usage
|
|
109
|
+
const { users, isLoading, deleteUser } = useUserList();
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## State Management Rules
|
|
115
|
+
|
|
116
|
+
### Server state → TanStack Query
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
// Fetching
|
|
120
|
+
const { data, isLoading, error } = useQuery({
|
|
121
|
+
queryKey: ['users', filters], // include deps in key
|
|
122
|
+
queryFn: () => userApi.getAll(filters),
|
|
123
|
+
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Mutation
|
|
127
|
+
const { mutate, isPending } = useMutation({
|
|
128
|
+
mutationFn: userApi.create,
|
|
129
|
+
onSuccess: () => {
|
|
130
|
+
queryClient.invalidateQueries({ queryKey: ['users'] });
|
|
131
|
+
toast.success('User created');
|
|
132
|
+
},
|
|
133
|
+
onError: (error) => toast.error(error.message),
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Client/UI state → Zustand (or useState for local)
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
// Local state — use useState
|
|
141
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
142
|
+
|
|
143
|
+
// Shared UI state — use Zustand
|
|
144
|
+
const useAuthStore = create<AuthStore>((set) => ({
|
|
145
|
+
user: null,
|
|
146
|
+
token: null,
|
|
147
|
+
login: (user, token) => set({ user, token }),
|
|
148
|
+
logout: () => set({ user: null, token: null }),
|
|
149
|
+
}));
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Form Rules
|
|
155
|
+
|
|
156
|
+
Use **React Hook Form + Zod** for all forms:
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
// 1. Define schema with Zod
|
|
160
|
+
const createUserSchema = z.object({
|
|
161
|
+
email: z.string().email('Invalid email'),
|
|
162
|
+
fullName: z.string().min(2, 'Min 2 characters').max(100),
|
|
163
|
+
password: z.string().min(8, 'Min 8 characters'),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
type CreateUserForm = z.infer<typeof createUserSchema>;
|
|
167
|
+
|
|
168
|
+
// 2. Use in component
|
|
169
|
+
const CreateUserForm = () => {
|
|
170
|
+
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<CreateUserForm>({
|
|
171
|
+
resolver: zodResolver(createUserSchema),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const onSubmit = async (data: CreateUserForm) => {
|
|
175
|
+
await createUser(data);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
180
|
+
<input {...register('email')} />
|
|
181
|
+
{errors.email && <span>{errors.email.message}</span>}
|
|
182
|
+
|
|
183
|
+
<button type="submit" disabled={isSubmitting}>
|
|
184
|
+
{isSubmitting ? 'Saving...' : 'Create'}
|
|
185
|
+
</button>
|
|
186
|
+
</form>
|
|
187
|
+
);
|
|
188
|
+
};
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## API Layer Rules
|
|
194
|
+
|
|
195
|
+
One `axios` instance shared across the app with interceptors:
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
// src/services/api.ts
|
|
199
|
+
const api = axios.create({
|
|
200
|
+
baseURL: import.meta.env.VITE_API_URL,
|
|
201
|
+
timeout: 10000,
|
|
202
|
+
headers: { 'Content-Type': 'application/json' },
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Attach token automatically
|
|
206
|
+
api.interceptors.request.use((config) => {
|
|
207
|
+
const token = useAuthStore.getState().token;
|
|
208
|
+
if (token) config.headers.Authorization = `Bearer ${token}`;
|
|
209
|
+
return config;
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Handle 401 globally
|
|
213
|
+
api.interceptors.response.use(
|
|
214
|
+
(response) => response,
|
|
215
|
+
(error) => {
|
|
216
|
+
if (error.response?.status === 401) {
|
|
217
|
+
useAuthStore.getState().logout();
|
|
218
|
+
window.location.href = '/login';
|
|
219
|
+
}
|
|
220
|
+
return Promise.reject(error);
|
|
221
|
+
}
|
|
222
|
+
);
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Feature API module:
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
// src/features/users/api.ts
|
|
229
|
+
export const userApi = {
|
|
230
|
+
getAll: (params?: UserListParams): Promise<User[]> =>
|
|
231
|
+
api.get('/users', { params }).then(r => r.data),
|
|
232
|
+
|
|
233
|
+
getById: (id: number): Promise<User> =>
|
|
234
|
+
api.get(`/users/${id}`).then(r => r.data),
|
|
235
|
+
|
|
236
|
+
create: (data: CreateUserRequest): Promise<User> =>
|
|
237
|
+
api.post('/users', data).then(r => r.data),
|
|
238
|
+
|
|
239
|
+
update: (id: number, data: UpdateUserRequest): Promise<User> =>
|
|
240
|
+
api.put(`/users/${id}`, data).then(r => r.data),
|
|
241
|
+
|
|
242
|
+
delete: (id: number): Promise<void> =>
|
|
243
|
+
api.delete(`/users/${id}`).then(r => r.data),
|
|
244
|
+
};
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## TypeScript Rules
|
|
250
|
+
|
|
251
|
+
```tsx
|
|
252
|
+
// ✅ Good
|
|
253
|
+
interface User {
|
|
254
|
+
id: number;
|
|
255
|
+
email: string;
|
|
256
|
+
fullName: string;
|
|
257
|
+
createdAt: string;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
type UserStatus = 'active' | 'inactive' | 'banned';
|
|
261
|
+
|
|
262
|
+
const getUser = async (id: number): Promise<User> => {
|
|
263
|
+
return api.get(`/users/${id}`).then(r => r.data);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// ❌ Bad
|
|
267
|
+
const getUser = async (id: any) => {
|
|
268
|
+
return api.get(`/users/${id}`).then((r: any) => r.data);
|
|
269
|
+
};
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Styling Rules (Tailwind CSS)
|
|
275
|
+
|
|
276
|
+
```tsx
|
|
277
|
+
// ✅ Good — cn() for conditional classes
|
|
278
|
+
import { cn } from '\@/utils/cn';
|
|
279
|
+
|
|
280
|
+
const Button = ({ variant = 'primary', disabled, className, children }: ButtonProps) => (
|
|
281
|
+
<button
|
|
282
|
+
className={cn(
|
|
283
|
+
'px-4 py-2 rounded font-medium transition-colors',
|
|
284
|
+
variant === 'primary' && 'bg-blue-600 text-white hover:bg-blue-700',
|
|
285
|
+
variant === 'outline' && 'border border-gray-300 hover:bg-gray-50',
|
|
286
|
+
disabled && 'opacity-50 cursor-not-allowed',
|
|
287
|
+
className
|
|
288
|
+
)}
|
|
289
|
+
disabled={disabled}
|
|
290
|
+
>
|
|
291
|
+
{children}
|
|
292
|
+
</button>
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
// ❌ Bad — inline style
|
|
296
|
+
<button style={{ backgroundColor: 'blue', padding: '8px 16px' }}>
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Performance Rules
|
|
302
|
+
|
|
303
|
+
```tsx
|
|
304
|
+
// Lazy routes
|
|
305
|
+
const UserPage = React.lazy(() => import('./pages/UserPage'));
|
|
306
|
+
|
|
307
|
+
<Suspense fallback={<PageSkeleton />}>
|
|
308
|
+
<UserPage />
|
|
309
|
+
</Suspense>
|
|
310
|
+
|
|
311
|
+
// useMemo for expensive computation
|
|
312
|
+
const sortedUsers = useMemo(
|
|
313
|
+
() => [...users].sort((a, b) => a.fullName.localeCompare(b.fullName)),
|
|
314
|
+
[users]
|
|
315
|
+
);
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Error Handling Rules
|
|
321
|
+
|
|
322
|
+
```tsx
|
|
323
|
+
// Error boundary at route level
|
|
324
|
+
<ErrorBoundary fallback={<ErrorPage />}>
|
|
325
|
+
<Routes>
|
|
326
|
+
<Route path="/users" element={<UserPage />} />
|
|
327
|
+
</Routes>
|
|
328
|
+
</ErrorBoundary>
|
|
329
|
+
|
|
330
|
+
// Async error handling
|
|
331
|
+
const { mutate } = useMutation({
|
|
332
|
+
mutationFn: userApi.create,
|
|
333
|
+
onSuccess: () => toast.success('User created successfully'),
|
|
334
|
+
onError: (error: AxiosError<ApiError>) =>
|
|
335
|
+
toast.error(error.response?.data?.message ?? 'Something went wrong'),
|
|
336
|
+
});
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Testing Rules
|
|
342
|
+
|
|
343
|
+
### Unit/Component tests with React Testing Library
|
|
344
|
+
|
|
345
|
+
```tsx
|
|
346
|
+
// Test behavior, not implementation
|
|
347
|
+
describe('UserCard', () => {
|
|
348
|
+
it('should display user name and email', () => {
|
|
349
|
+
const user: User = { id: 1, email: 'test\@example.com', fullName: 'Test User' };
|
|
350
|
+
render(<UserCard user={user} />);
|
|
351
|
+
|
|
352
|
+
expect(screen.getByText('Test User')).toBeInTheDocument();
|
|
353
|
+
expect(screen.getByText('test\@example.com')).toBeInTheDocument();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('should call onDelete with user id when delete button clicked', async () => {
|
|
357
|
+
const onDelete = vi.fn();
|
|
358
|
+
const user: User = { id: 1, email: 'test\@example.com', fullName: 'Test User' };
|
|
359
|
+
|
|
360
|
+
render(<UserCard user={user} onDelete={onDelete} />);
|
|
361
|
+
await userEvent.click(screen.getByRole('button', { name: /delete/i }));
|
|
362
|
+
|
|
363
|
+
expect(onDelete).toHaveBeenCalledWith(1);
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### Hook tests
|
|
369
|
+
|
|
370
|
+
```tsx
|
|
371
|
+
import { renderHook, waitFor } from '\@testing-library/react';
|
|
372
|
+
|
|
373
|
+
it('should fetch users', async () => {
|
|
374
|
+
const { result } = renderHook(() => useUserList(), { wrapper: QueryWrapper });
|
|
375
|
+
|
|
376
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
377
|
+
|
|
378
|
+
expect(result.current.users).toHaveLength(2);
|
|
379
|
+
});
|
|
380
|
+
```
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# React.js Rules
|
|
2
|
+
|
|
3
|
+
Full coding rules for this stack. Read this in full before writing or modifying any React code in this project — not just once, keep applying it to every edit in the session, not only the first.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Project Structure
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
src/
|
|
11
|
+
├── components/
|
|
12
|
+
│ ├── ui/ # Generic, reusable UI (Button, Input, Modal...)
|
|
13
|
+
│ └── [feature]/ # Feature-specific components
|
|
14
|
+
├── pages/ # Route-level components (thin, delegate to features)
|
|
15
|
+
├── features/ # Self-contained feature modules
|
|
16
|
+
│ └── [feature]/
|
|
17
|
+
│ ├── components/ # UI specific to this feature
|
|
18
|
+
│ ├── hooks/ # Custom hooks for this feature
|
|
19
|
+
│ ├── api.ts # API calls for this feature
|
|
20
|
+
│ ├── store.ts # Zustand slice (if needed)
|
|
21
|
+
│ ├── types.ts # TypeScript types/interfaces
|
|
22
|
+
│ └── index.ts # Public exports
|
|
23
|
+
├── hooks/ # Shared custom hooks
|
|
24
|
+
├── services/
|
|
25
|
+
│ └── api.ts # Axios instance + interceptors
|
|
26
|
+
├── store/ # Global Zustand store
|
|
27
|
+
├── types/ # Shared TypeScript types
|
|
28
|
+
├── utils/ # Pure utility functions
|
|
29
|
+
├── constants/ # App-wide constants
|
|
30
|
+
└── App.tsx
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Component Rules
|
|
36
|
+
|
|
37
|
+
### Always use Functional Components
|
|
38
|
+
|
|
39
|
+
- Never use class components.
|
|
40
|
+
|
|
41
|
+
### Define Props with TypeScript interface
|
|
42
|
+
|
|
43
|
+
- No `any`, no untyped `props` param.
|
|
44
|
+
|
|
45
|
+
### Keep components focused and small
|
|
46
|
+
|
|
47
|
+
- One component = one responsibility
|
|
48
|
+
- If a component exceeds ~150 lines, split it
|
|
49
|
+
- Extract logic to custom hooks, keep JSX clean
|
|
50
|
+
|
|
51
|
+
### Export convention
|
|
52
|
+
|
|
53
|
+
- Named export for regular components.
|
|
54
|
+
- Default export only for page-level components.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Custom Hook Rules
|
|
59
|
+
|
|
60
|
+
- Prefix with `use`: `useUserList`, `useAuth`, `useModal`
|
|
61
|
+
- Extract all side effects, API calls, and complex state from components
|
|
62
|
+
- Return objects (not arrays) for multiple values — easier to destructure
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## State Management Rules
|
|
67
|
+
|
|
68
|
+
### Server state → TanStack Query
|
|
69
|
+
|
|
70
|
+
Use for all API data (fetching, caching, mutations).
|
|
71
|
+
|
|
72
|
+
### Client/UI state → Zustand (or useState for local)
|
|
73
|
+
|
|
74
|
+
### Rules:
|
|
75
|
+
- **Never** use global state for server/API data — that's TanStack Query's job
|
|
76
|
+
- **Never** fetch in `useEffect` manually — use TanStack Query
|
|
77
|
+
- **Never** put loading/error state in Zustand — TanStack Query handles that
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Form Rules
|
|
82
|
+
|
|
83
|
+
Use **React Hook Form + Zod** for all forms — define the schema with Zod, infer the form type from it, wire it into `useForm` via `zodResolver`.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## API Layer Rules
|
|
88
|
+
|
|
89
|
+
One `axios` instance shared across the app with interceptors (attach auth token on request, handle 401 globally on response). Each feature gets its own thin API module built on that instance.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## TypeScript Rules
|
|
94
|
+
|
|
95
|
+
- **No `any`** — use `unknown`, proper types, or generics
|
|
96
|
+
- Define shared types in `types.ts` files
|
|
97
|
+
- Use `interface` for object shapes, `type` for unions/intersections
|
|
98
|
+
- Always type function return values for public hooks and API functions
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Styling Rules (Tailwind CSS)
|
|
103
|
+
|
|
104
|
+
- Use utility classes directly — avoid custom CSS unless necessary
|
|
105
|
+
- Extract repeated class combinations into components or a `cn()` helper
|
|
106
|
+
- Use `className` prop for external style overrides
|
|
107
|
+
- Never use inline `style={{ }}` for anything Tailwind can express
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Performance Rules
|
|
112
|
+
|
|
113
|
+
- Use `React.memo` only when profiling shows a real problem — do not optimize prematurely
|
|
114
|
+
- Use `useMemo` / `useCallback` for expensive computations or stable references passed to children
|
|
115
|
+
- Lazy-load routes with `React.lazy` + `Suspense`
|
|
116
|
+
- Avoid anonymous functions in JSX props when passing to memoized children
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Error Handling Rules
|
|
121
|
+
|
|
122
|
+
- Use React Error Boundary at the route level
|
|
123
|
+
- Handle API errors in TanStack Query `onError` callbacks
|
|
124
|
+
- Show user-friendly messages — never raw error objects
|
|
125
|
+
- Use toast notifications for async operation feedback
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Testing Rules
|
|
130
|
+
|
|
131
|
+
### Unit/Component tests with React Testing Library
|
|
132
|
+
|
|
133
|
+
- Test behavior, not implementation.
|
|
134
|
+
|
|
135
|
+
### Hook tests
|
|
136
|
+
|
|
137
|
+
- Use `renderHook` + `waitFor` from `@testing-library/react`.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Naming Conventions
|
|
142
|
+
|
|
143
|
+
| Element | Convention | Example |
|
|
144
|
+
|---------|-----------|---------|
|
|
145
|
+
| Component file | PascalCase | `UserCard.tsx` |
|
|
146
|
+
| Hook file | camelCase | `useUserList.ts` |
|
|
147
|
+
| Utility file | camelCase | `formatDate.ts` |
|
|
148
|
+
| Type/Interface | PascalCase | `User`, `CreateUserRequest` |
|
|
149
|
+
| Component | PascalCase | `UserCard`, `OrderList` |
|
|
150
|
+
| Hook | `use` + PascalCase | `useUserList`, `useAuth` |
|
|
151
|
+
| Event handler | `handle` + Action | `handleDelete`, `handleSubmit` |
|
|
152
|
+
| Boolean variable | `is/has/can` prefix | `isLoading`, `hasError`, `canEdit` |
|
|
153
|
+
| Constant | UPPER_SNAKE_CASE | `MAX_RETRY`, `API_TIMEOUT` |
|
|
154
|
+
| CSS class (custom) | kebab-case | `user-card`, `nav-item` |
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Common Anti-Patterns to Avoid
|
|
159
|
+
|
|
160
|
+
- ❌ Fetching in `useEffect` → use TanStack Query
|
|
161
|
+
- ❌ Using `any` type → define proper TypeScript types
|
|
162
|
+
- ❌ Storing server data in Zustand → that's TanStack Query's responsibility
|
|
163
|
+
- ❌ Class components → use functional components
|
|
164
|
+
- ❌ `index.js` everywhere → use named files for traceability
|
|
165
|
+
- ❌ Direct DOM manipulation → use React state/refs
|
|
166
|
+
- ❌ Mutating state directly → always use setter functions
|
|
167
|
+
- ❌ Large components doing everything → split into smaller focused components
|
|
168
|
+
- ❌ Prop drilling more than 2 levels → use context or Zustand
|
|
169
|
+
- ❌ Hardcoding API URLs → use `import.meta.env.VITE_API_URL`
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
When explaining changes, refer to the [React Official Documentation](https://react.dev), [TanStack Query](https://tanstack.com/query), and [React Hook Form](https://react-hook-form.com) conventions.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# PHP Code Examples
|
|
2
|
+
|
|
3
|
+
Reference examples for each rule area. Read the relevant section when generating code for that area.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## General
|
|
8
|
+
|
|
9
|
+
```php
|
|
10
|
+
// ✅ Good
|
|
11
|
+
declare(strict_types=1);
|
|
12
|
+
|
|
13
|
+
class UserService
|
|
14
|
+
{
|
|
15
|
+
public function __construct(
|
|
16
|
+
private readonly UserRepository $userRepository,
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
public function findById(int $id): UserDto
|
|
20
|
+
{
|
|
21
|
+
$user = $this->userRepository->findById($id);
|
|
22
|
+
if ($user === null) {
|
|
23
|
+
throw new NotFoundException("User $id not found");
|
|
24
|
+
}
|
|
25
|
+
return UserDto::fromArray($user);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ❌ Bad — no strict types, logic in global scope
|
|
30
|
+
$pdo = new PDO(...);
|
|
31
|
+
$user = $pdo->query("SELECT * FROM users WHERE id = $_GET[id]")->fetch();
|
|
32
|
+
echo $user['name'];
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Security
|
|
38
|
+
|
|
39
|
+
```php
|
|
40
|
+
// ✅ Good — prepared statement
|
|
41
|
+
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
|
|
42
|
+
$stmt->execute([':email' => $email]);
|
|
43
|
+
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
44
|
+
|
|
45
|
+
// ✅ Good — safe HTML output
|
|
46
|
+
echo htmlspecialchars($user['name'], ENT_QUOTES, 'UTF-8');
|
|
47
|
+
|
|
48
|
+
// ❌ Bad — SQL injection
|
|
49
|
+
$result = $pdo->query("SELECT * FROM users WHERE email = '$email'");
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Database / Repository
|
|
55
|
+
|
|
56
|
+
```php
|
|
57
|
+
// ✅ Good
|
|
58
|
+
class UserRepository
|
|
59
|
+
{
|
|
60
|
+
public function __construct(private readonly \PDO $pdo) {}
|
|
61
|
+
|
|
62
|
+
public function findByEmail(string $email): ?array
|
|
63
|
+
{
|
|
64
|
+
$stmt = $this->pdo->prepare(
|
|
65
|
+
'SELECT id, email, full_name FROM users WHERE email = :email AND deleted = 0'
|
|
66
|
+
);
|
|
67
|
+
$stmt->execute([':email' => $email]);
|
|
68
|
+
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
|
|
69
|
+
return $row ?: null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public function create(string $email, string $fullName, string $passwordHash): int
|
|
73
|
+
{
|
|
74
|
+
$stmt = $this->pdo->prepare(
|
|
75
|
+
'INSERT INTO users (email, full_name, password_hash) VALUES (:email, :full_name, :password_hash)'
|
|
76
|
+
);
|
|
77
|
+
$stmt->execute([
|
|
78
|
+
':email' => $email,
|
|
79
|
+
':full_name' => $fullName,
|
|
80
|
+
':password_hash' => $passwordHash,
|
|
81
|
+
]);
|
|
82
|
+
return (int) $this->pdo->lastInsertId();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Controller
|
|
90
|
+
|
|
91
|
+
```php
|
|
92
|
+
// ✅ Good
|
|
93
|
+
declare(strict_types=1);
|
|
94
|
+
|
|
95
|
+
class UserController
|
|
96
|
+
{
|
|
97
|
+
public function __construct(private readonly UserService $userService) {}
|
|
98
|
+
|
|
99
|
+
public function create(): void
|
|
100
|
+
{
|
|
101
|
+
$body = json_decode(file_get_contents('php://input'), true) ?? [];
|
|
102
|
+
$email = trim($body['email'] ?? '');
|
|
103
|
+
$fullName = trim($body['full_name'] ?? '');
|
|
104
|
+
$password = $body['password'] ?? '';
|
|
105
|
+
|
|
106
|
+
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
107
|
+
http_response_code(400);
|
|
108
|
+
echo json_encode(['error' => 'Invalid email']);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
$user = $this->userService->create($email, $fullName, $password);
|
|
113
|
+
http_response_code(201);
|
|
114
|
+
echo json_encode($user);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Error Handling
|
|
122
|
+
|
|
123
|
+
```php
|
|
124
|
+
// ✅ Good — centralised handler
|
|
125
|
+
set_exception_handler(function (\Throwable $e): void {
|
|
126
|
+
$status = match (true) {
|
|
127
|
+
$e instanceof NotFoundException => 404,
|
|
128
|
+
$e instanceof ValidationException => 422,
|
|
129
|
+
$e instanceof UnauthorizedException => 401,
|
|
130
|
+
default => 500,
|
|
131
|
+
};
|
|
132
|
+
http_response_code($status);
|
|
133
|
+
header('Content-Type: application/json');
|
|
134
|
+
if ($status === 500) {
|
|
135
|
+
error_log($e->getMessage() . ' ' . $e->getTraceAsString());
|
|
136
|
+
echo json_encode(['error' => 'Internal server error']);
|
|
137
|
+
} else {
|
|
138
|
+
echo json_encode(['error' => $e->getMessage()]);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Testing
|
|
146
|
+
|
|
147
|
+
```php
|
|
148
|
+
class UserServiceTest extends TestCase
|
|
149
|
+
{
|
|
150
|
+
public function testCreateThrowsOnDuplicateEmail(): void
|
|
151
|
+
{
|
|
152
|
+
$repo = $this->createMock(UserRepository::class);
|
|
153
|
+
$repo->method('findByEmail')->willReturn(['id' => 1]);
|
|
154
|
+
|
|
155
|
+
$service = new UserService($repo);
|
|
156
|
+
|
|
157
|
+
$this->expectException(ValidationException::class);
|
|
158
|
+
$service->create('dup@example.com', 'Test', 'password');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|