cursor-kit-cli 1.2.0-beta → 1.2.0-beta.2
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/cli.cjs +333 -56
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +334 -57
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +39 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +33 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/commands/docs.md +5 -3
- package/templates/commands/explain.md +5 -3
- package/templates/commands/fix.md +5 -3
- package/templates/commands/implement.md +5 -3
- package/templates/commands/refactor.md +5 -3
- package/templates/commands/review.md +5 -3
- package/templates/commands/test.md +5 -3
- package/templates/manifest.json +11 -8
- package/templates/rules/git.mdc +0 -2
- package/templates/rules/toc.mdc +17 -9
- package/templates/skills/aesthetic/SKILL.md +121 -0
- package/templates/skills/aesthetic/assets/design-guideline-template.md +163 -0
- package/templates/skills/aesthetic/assets/design-story-template.md +135 -0
- package/templates/skills/aesthetic/references/design-principles.md +62 -0
- package/templates/skills/aesthetic/references/design-resources.md +75 -0
- package/templates/skills/aesthetic/references/micro-interactions.md +53 -0
- package/templates/skills/aesthetic/references/storytelling-design.md +50 -0
- package/templates/skills/backend-development/SKILL.mdc +95 -0
- package/templates/skills/backend-development/references/backend-api-design.md +495 -0
- package/templates/skills/backend-development/references/backend-architecture.md +454 -0
- package/templates/skills/backend-development/references/backend-authentication.md +338 -0
- package/templates/skills/backend-development/references/backend-code-quality.md +659 -0
- package/templates/skills/backend-development/references/backend-debugging.md +904 -0
- package/templates/skills/backend-development/references/backend-devops.md +494 -0
- package/templates/skills/backend-development/references/backend-mindset.md +387 -0
- package/templates/skills/backend-development/references/backend-performance.md +397 -0
- package/templates/skills/backend-development/references/backend-security.md +290 -0
- package/templates/skills/backend-development/references/backend-technologies.md +256 -0
- package/templates/skills/backend-development/references/backend-testing.md +429 -0
- package/templates/skills/frontend-design/SKILL.mdc +41 -0
- package/templates/skills/frontend-design/references/animejs.md +396 -0
- package/templates/skills/frontend-development/SKILL.mdc +399 -0
- package/templates/skills/frontend-development/resources/common-patterns.md +331 -0
- package/templates/skills/frontend-development/resources/complete-examples.md +872 -0
- package/templates/skills/frontend-development/resources/component-patterns.md +502 -0
- package/templates/skills/frontend-development/resources/data-fetching.md +767 -0
- package/templates/skills/frontend-development/resources/file-organization.md +502 -0
- package/templates/skills/frontend-development/resources/loading-and-error-states.md +501 -0
- package/templates/skills/frontend-development/resources/performance.md +406 -0
- package/templates/skills/frontend-development/resources/routing-guide.md +364 -0
- package/templates/skills/frontend-development/resources/styling-guide.md +428 -0
- package/templates/skills/frontend-development/resources/typescript-standards.md +418 -0
- package/templates/skills/problem-solving/SKILL.mdc +96 -0
- package/templates/skills/problem-solving/references/attribution.md +69 -0
- package/templates/skills/problem-solving/references/collision-zone-thinking.md +79 -0
- package/templates/skills/problem-solving/references/inversion-exercise.md +91 -0
- package/templates/skills/problem-solving/references/meta-pattern-recognition.md +87 -0
- package/templates/skills/problem-solving/references/scale-game.md +95 -0
- package/templates/skills/problem-solving/references/simplification-cascades.md +80 -0
- package/templates/skills/problem-solving/references/when-stuck.md +72 -0
- package/templates/skills/research/SKILL.mdc +168 -0
- package/templates/skills/sequential-thinking/.env.example +8 -0
- package/templates/skills/sequential-thinking/README.md +183 -0
- package/templates/skills/sequential-thinking/SKILL.mdc +94 -0
- package/templates/skills/sequential-thinking/package.json +31 -0
- package/templates/skills/sequential-thinking/references/advanced-strategies.md +79 -0
- package/templates/skills/sequential-thinking/references/advanced-techniques.md +76 -0
- package/templates/skills/sequential-thinking/references/core-patterns.md +95 -0
- package/templates/skills/sequential-thinking/references/examples-api.md +88 -0
- package/templates/skills/sequential-thinking/references/examples-architecture.md +94 -0
- package/templates/skills/sequential-thinking/references/examples-debug.md +90 -0
- package/templates/skills/sequential-thinking/scripts/format-thought.js +159 -0
- package/templates/skills/sequential-thinking/scripts/process-thought.js +236 -0
- package/templates/skills/sequential-thinking/tests/format-thought.test.js +133 -0
- package/templates/skills/sequential-thinking/tests/process-thought.test.js +215 -0
- package/templates/skills/ui-styling/LICENSE.txt +202 -0
- package/templates/skills/ui-styling/SKILL.mdc +321 -0
- package/templates/skills/ui-styling/references/canvas-design-system.md +320 -0
- package/templates/skills/ui-styling/references/shadcn-accessibility.md +471 -0
- package/templates/skills/ui-styling/references/shadcn-components.md +424 -0
- package/templates/skills/ui-styling/references/shadcn-theming.md +373 -0
- package/templates/skills/ui-styling/references/tailwind-customization.md +483 -0
- package/templates/skills/ui-styling/references/tailwind-responsive.md +382 -0
- package/templates/skills/ui-styling/references/tailwind-utilities.md +455 -0
- package/templates/rules/frontend-design.mdc +0 -48
- package/templates/rules/performance.mdc +0 -54
- package/templates/rules/react.mdc +0 -58
- package/templates/rules/security.mdc +0 -50
- package/templates/rules/testing.mdc +0 -54
- package/templates/rules/typescript.mdc +0 -36
|
@@ -0,0 +1,872 @@
|
|
|
1
|
+
# Complete Examples
|
|
2
|
+
|
|
3
|
+
Full working examples combining all modern patterns: React.FC, lazy loading, Suspense, useSuspenseQuery, styling, routing, and error handling.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Example 1: Complete Modern Component
|
|
8
|
+
|
|
9
|
+
Combines: React.FC, useSuspenseQuery, cache-first, useCallback, styling, error handling
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
/**
|
|
13
|
+
* User profile display component
|
|
14
|
+
* Demonstrates modern patterns with Suspense and TanStack Query
|
|
15
|
+
*/
|
|
16
|
+
import React, { useState, useCallback, useMemo } from 'react';
|
|
17
|
+
import { Box, Paper, Typography, Button, Avatar } from '@mui/material';
|
|
18
|
+
import type { SxProps, Theme } from '@mui/material';
|
|
19
|
+
import { useSuspenseQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
20
|
+
import { userApi } from '../api/userApi';
|
|
21
|
+
import { useMuiSnackbar } from '@/hooks/useMuiSnackbar';
|
|
22
|
+
import type { User } from '~types/user';
|
|
23
|
+
|
|
24
|
+
// Styles object
|
|
25
|
+
const componentStyles: Record<string, SxProps<Theme>> = {
|
|
26
|
+
container: {
|
|
27
|
+
p: 3,
|
|
28
|
+
maxWidth: 600,
|
|
29
|
+
margin: '0 auto',
|
|
30
|
+
},
|
|
31
|
+
header: {
|
|
32
|
+
display: 'flex',
|
|
33
|
+
alignItems: 'center',
|
|
34
|
+
gap: 2,
|
|
35
|
+
mb: 3,
|
|
36
|
+
},
|
|
37
|
+
content: {
|
|
38
|
+
display: 'flex',
|
|
39
|
+
flexDirection: 'column',
|
|
40
|
+
gap: 2,
|
|
41
|
+
},
|
|
42
|
+
actions: {
|
|
43
|
+
display: 'flex',
|
|
44
|
+
gap: 1,
|
|
45
|
+
mt: 2,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
interface UserProfileProps {
|
|
50
|
+
userId: string;
|
|
51
|
+
onUpdate?: () => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const UserProfile: React.FC<UserProfileProps> = ({ userId, onUpdate }) => {
|
|
55
|
+
const queryClient = useQueryClient();
|
|
56
|
+
const { showSuccess, showError } = useMuiSnackbar();
|
|
57
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
58
|
+
|
|
59
|
+
// Suspense query - no isLoading needed!
|
|
60
|
+
const { data: user } = useSuspenseQuery({
|
|
61
|
+
queryKey: ['user', userId],
|
|
62
|
+
queryFn: () => userApi.getUser(userId),
|
|
63
|
+
staleTime: 5 * 60 * 1000,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Update mutation
|
|
67
|
+
const updateMutation = useMutation({
|
|
68
|
+
mutationFn: (updates: Partial<User>) =>
|
|
69
|
+
userApi.updateUser(userId, updates),
|
|
70
|
+
|
|
71
|
+
onSuccess: () => {
|
|
72
|
+
queryClient.invalidateQueries({ queryKey: ['user', userId] });
|
|
73
|
+
showSuccess('Profile updated');
|
|
74
|
+
setIsEditing(false);
|
|
75
|
+
onUpdate?.();
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
onError: () => {
|
|
79
|
+
showError('Failed to update profile');
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Memoized computed value
|
|
84
|
+
const fullName = useMemo(() => {
|
|
85
|
+
return `${user.firstName} ${user.lastName}`;
|
|
86
|
+
}, [user.firstName, user.lastName]);
|
|
87
|
+
|
|
88
|
+
// Event handlers with useCallback
|
|
89
|
+
const handleEdit = useCallback(() => {
|
|
90
|
+
setIsEditing(true);
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
const handleSave = useCallback(() => {
|
|
94
|
+
updateMutation.mutate({
|
|
95
|
+
firstName: user.firstName,
|
|
96
|
+
lastName: user.lastName,
|
|
97
|
+
});
|
|
98
|
+
}, [user, updateMutation]);
|
|
99
|
+
|
|
100
|
+
const handleCancel = useCallback(() => {
|
|
101
|
+
setIsEditing(false);
|
|
102
|
+
}, []);
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<Paper sx={componentStyles.container}>
|
|
106
|
+
<Box sx={componentStyles.header}>
|
|
107
|
+
<Avatar sx={{ width: 64, height: 64 }}>
|
|
108
|
+
{user.firstName[0]}{user.lastName[0]}
|
|
109
|
+
</Avatar>
|
|
110
|
+
<Box>
|
|
111
|
+
<Typography variant='h5'>{fullName}</Typography>
|
|
112
|
+
<Typography color='text.secondary'>{user.email}</Typography>
|
|
113
|
+
</Box>
|
|
114
|
+
</Box>
|
|
115
|
+
|
|
116
|
+
<Box sx={componentStyles.content}>
|
|
117
|
+
<Typography>Username: {user.username}</Typography>
|
|
118
|
+
<Typography>Roles: {user.roles.join(', ')}</Typography>
|
|
119
|
+
</Box>
|
|
120
|
+
|
|
121
|
+
<Box sx={componentStyles.actions}>
|
|
122
|
+
{!isEditing ? (
|
|
123
|
+
<Button variant='contained' onClick={handleEdit}>
|
|
124
|
+
Edit Profile
|
|
125
|
+
</Button>
|
|
126
|
+
) : (
|
|
127
|
+
<>
|
|
128
|
+
<Button
|
|
129
|
+
variant='contained'
|
|
130
|
+
onClick={handleSave}
|
|
131
|
+
disabled={updateMutation.isPending}
|
|
132
|
+
>
|
|
133
|
+
{updateMutation.isPending ? 'Saving...' : 'Save'}
|
|
134
|
+
</Button>
|
|
135
|
+
<Button onClick={handleCancel}>
|
|
136
|
+
Cancel
|
|
137
|
+
</Button>
|
|
138
|
+
</>
|
|
139
|
+
)}
|
|
140
|
+
</Box>
|
|
141
|
+
</Paper>
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export default UserProfile;
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Usage:**
|
|
149
|
+
```typescript
|
|
150
|
+
<SuspenseLoader>
|
|
151
|
+
<UserProfile userId='123' onUpdate={() => console.log('Updated')} />
|
|
152
|
+
</SuspenseLoader>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Example 2: Complete Feature Structure
|
|
158
|
+
|
|
159
|
+
Real example based on `features/posts/`:
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
features/
|
|
163
|
+
users/
|
|
164
|
+
api/
|
|
165
|
+
userApi.ts # API service layer
|
|
166
|
+
components/
|
|
167
|
+
UserProfile.tsx # Main component (from Example 1)
|
|
168
|
+
UserList.tsx # List component
|
|
169
|
+
UserBlog.tsx # Blog component
|
|
170
|
+
modals/
|
|
171
|
+
DeleteUserModal.tsx # Modal component
|
|
172
|
+
hooks/
|
|
173
|
+
useSuspenseUser.ts # Suspense query hook
|
|
174
|
+
useUserMutations.ts # Mutation hooks
|
|
175
|
+
useUserPermissions.ts # Feature-specific hook
|
|
176
|
+
helpers/
|
|
177
|
+
userHelpers.ts # Utility functions
|
|
178
|
+
validation.ts # Validation logic
|
|
179
|
+
types/
|
|
180
|
+
index.ts # TypeScript interfaces
|
|
181
|
+
index.ts # Public API exports
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### API Service (userApi.ts)
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
import apiClient from '@/lib/apiClient';
|
|
188
|
+
import type { User, CreateUserPayload, UpdateUserPayload } from '../types';
|
|
189
|
+
|
|
190
|
+
export const userApi = {
|
|
191
|
+
getUser: async (userId: string): Promise<User> => {
|
|
192
|
+
const { data } = await apiClient.get(`/users/${userId}`);
|
|
193
|
+
return data;
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
getUsers: async (): Promise<User[]> => {
|
|
197
|
+
const { data } = await apiClient.get('/users');
|
|
198
|
+
return data;
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
createUser: async (payload: CreateUserPayload): Promise<User> => {
|
|
202
|
+
const { data } = await apiClient.post('/users', payload);
|
|
203
|
+
return data;
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
updateUser: async (userId: string, payload: UpdateUserPayload): Promise<User> => {
|
|
207
|
+
const { data } = await apiClient.put(`/users/${userId}`, payload);
|
|
208
|
+
return data;
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
deleteUser: async (userId: string): Promise<void> => {
|
|
212
|
+
await apiClient.delete(`/users/${userId}`);
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Suspense Hook (useSuspenseUser.ts)
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
import { useSuspenseQuery } from '@tanstack/react-query';
|
|
221
|
+
import { userApi } from '../api/userApi';
|
|
222
|
+
import type { User } from '../types';
|
|
223
|
+
|
|
224
|
+
export function useSuspenseUser(userId: string) {
|
|
225
|
+
return useSuspenseQuery<User, Error>({
|
|
226
|
+
queryKey: ['user', userId],
|
|
227
|
+
queryFn: () => userApi.getUser(userId),
|
|
228
|
+
staleTime: 5 * 60 * 1000,
|
|
229
|
+
gcTime: 10 * 60 * 1000,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function useSuspenseUsers() {
|
|
234
|
+
return useSuspenseQuery<User[], Error>({
|
|
235
|
+
queryKey: ['users'],
|
|
236
|
+
queryFn: () => userApi.getUsers(),
|
|
237
|
+
staleTime: 1 * 60 * 1000, // Shorter for list
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Types (types/index.ts)
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
export interface User {
|
|
246
|
+
id: string;
|
|
247
|
+
username: string;
|
|
248
|
+
email: string;
|
|
249
|
+
firstName: string;
|
|
250
|
+
lastName: string;
|
|
251
|
+
roles: string[];
|
|
252
|
+
createdAt: string;
|
|
253
|
+
updatedAt: string;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export interface CreateUserPayload {
|
|
257
|
+
username: string;
|
|
258
|
+
email: string;
|
|
259
|
+
firstName: string;
|
|
260
|
+
lastName: string;
|
|
261
|
+
password: string;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export type UpdateUserPayload = Partial<Omit<User, 'id' | 'createdAt' | 'updatedAt'>>;
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Public Exports (index.ts)
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
// Export components
|
|
271
|
+
export { UserProfile } from './components/UserProfile';
|
|
272
|
+
export { UserList } from './components/UserList';
|
|
273
|
+
|
|
274
|
+
// Export hooks
|
|
275
|
+
export { useSuspenseUser, useSuspenseUsers } from './hooks/useSuspenseUser';
|
|
276
|
+
export { useUserMutations } from './hooks/useUserMutations';
|
|
277
|
+
|
|
278
|
+
// Export API
|
|
279
|
+
export { userApi } from './api/userApi';
|
|
280
|
+
|
|
281
|
+
// Export types
|
|
282
|
+
export type { User, CreateUserPayload, UpdateUserPayload } from './types';
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Example 3: Complete Route with Lazy Loading
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
/**
|
|
291
|
+
* User profile route
|
|
292
|
+
* Path: /users/:userId
|
|
293
|
+
*/
|
|
294
|
+
|
|
295
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
296
|
+
import { lazy } from 'react';
|
|
297
|
+
import { SuspenseLoader } from '~components/SuspenseLoader';
|
|
298
|
+
|
|
299
|
+
// Lazy load the UserProfile component
|
|
300
|
+
const UserProfile = lazy(() =>
|
|
301
|
+
import('@/features/users/components/UserProfile').then(
|
|
302
|
+
(module) => ({ default: module.UserProfile })
|
|
303
|
+
)
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
export const Route = createFileRoute('/users/$userId')({
|
|
307
|
+
component: UserProfilePage,
|
|
308
|
+
loader: ({ params }) => ({
|
|
309
|
+
crumb: `User ${params.userId}`,
|
|
310
|
+
}),
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
function UserProfilePage() {
|
|
314
|
+
const { userId } = Route.useParams();
|
|
315
|
+
|
|
316
|
+
return (
|
|
317
|
+
<SuspenseLoader>
|
|
318
|
+
<UserProfile
|
|
319
|
+
userId={userId}
|
|
320
|
+
onUpdate={() => console.log('Profile updated')}
|
|
321
|
+
/>
|
|
322
|
+
</SuspenseLoader>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export default UserProfilePage;
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Example 4: List with Search and Filtering
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
import React, { useState, useMemo } from 'react';
|
|
335
|
+
import { Box, TextField, List, ListItem } from '@mui/material';
|
|
336
|
+
import { useDebounce } from 'use-debounce';
|
|
337
|
+
import { useSuspenseQuery } from '@tanstack/react-query';
|
|
338
|
+
import { userApi } from '../api/userApi';
|
|
339
|
+
|
|
340
|
+
export const UserList: React.FC = () => {
|
|
341
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
342
|
+
const [debouncedSearch] = useDebounce(searchTerm, 300);
|
|
343
|
+
|
|
344
|
+
const { data: users } = useSuspenseQuery({
|
|
345
|
+
queryKey: ['users'],
|
|
346
|
+
queryFn: () => userApi.getUsers(),
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Memoized filtering
|
|
350
|
+
const filteredUsers = useMemo(() => {
|
|
351
|
+
if (!debouncedSearch) return users;
|
|
352
|
+
|
|
353
|
+
return users.filter(user =>
|
|
354
|
+
user.name.toLowerCase().includes(debouncedSearch.toLowerCase()) ||
|
|
355
|
+
user.email.toLowerCase().includes(debouncedSearch.toLowerCase())
|
|
356
|
+
);
|
|
357
|
+
}, [users, debouncedSearch]);
|
|
358
|
+
|
|
359
|
+
return (
|
|
360
|
+
<Box>
|
|
361
|
+
<TextField
|
|
362
|
+
value={searchTerm}
|
|
363
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
364
|
+
placeholder='Search users...'
|
|
365
|
+
fullWidth
|
|
366
|
+
sx={{ mb: 2 }}
|
|
367
|
+
/>
|
|
368
|
+
|
|
369
|
+
<List>
|
|
370
|
+
{filteredUsers.map(user => (
|
|
371
|
+
<ListItem key={user.id}>
|
|
372
|
+
{user.name} - {user.email}
|
|
373
|
+
</ListItem>
|
|
374
|
+
))}
|
|
375
|
+
</List>
|
|
376
|
+
</Box>
|
|
377
|
+
);
|
|
378
|
+
};
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## Example 5: Blog with Validation
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
import React from 'react';
|
|
387
|
+
import { Box, TextField, Button, Paper } from '@mui/material';
|
|
388
|
+
import { useBlog } from 'react-hook-blog';
|
|
389
|
+
import { zodResolver } from '@hookblog/resolvers/zod';
|
|
390
|
+
import { z } from 'zod';
|
|
391
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
392
|
+
import { userApi } from '../api/userApi';
|
|
393
|
+
import { useMuiSnackbar } from '@/hooks/useMuiSnackbar';
|
|
394
|
+
|
|
395
|
+
const userSchema = z.object({
|
|
396
|
+
username: z.string().min(3).max(50),
|
|
397
|
+
email: z.string().email(),
|
|
398
|
+
firstName: z.string().min(1),
|
|
399
|
+
lastName: z.string().min(1),
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
type UserBlogData = z.infer<typeof userSchema>;
|
|
403
|
+
|
|
404
|
+
interface CreateUserBlogProps {
|
|
405
|
+
onSuccess?: () => void;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export const CreateUserBlog: React.FC<CreateUserBlogProps> = ({ onSuccess }) => {
|
|
409
|
+
const queryClient = useQueryClient();
|
|
410
|
+
const { showSuccess, showError } = useMuiSnackbar();
|
|
411
|
+
|
|
412
|
+
const { register, handleSubmit, blogState: { errors }, reset } = useBlog<UserBlogData>({
|
|
413
|
+
resolver: zodResolver(userSchema),
|
|
414
|
+
defaultValues: {
|
|
415
|
+
username: '',
|
|
416
|
+
email: '',
|
|
417
|
+
firstName: '',
|
|
418
|
+
lastName: '',
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const createMutation = useMutation({
|
|
423
|
+
mutationFn: (data: UserBlogData) => userApi.createUser(data),
|
|
424
|
+
|
|
425
|
+
onSuccess: () => {
|
|
426
|
+
queryClient.invalidateQueries({ queryKey: ['users'] });
|
|
427
|
+
showSuccess('User created successfully');
|
|
428
|
+
reset();
|
|
429
|
+
onSuccess?.();
|
|
430
|
+
},
|
|
431
|
+
|
|
432
|
+
onError: () => {
|
|
433
|
+
showError('Failed to create user');
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const onSubmit = (data: UserBlogData) => {
|
|
438
|
+
createMutation.mutate(data);
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
return (
|
|
442
|
+
<Paper sx={{ p: 3, maxWidth: 500 }}>
|
|
443
|
+
<blog onSubmit={handleSubmit(onSubmit)}>
|
|
444
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
445
|
+
<TextField
|
|
446
|
+
{...register('username')}
|
|
447
|
+
label='Username'
|
|
448
|
+
error={!!errors.username}
|
|
449
|
+
helperText={errors.username?.message}
|
|
450
|
+
fullWidth
|
|
451
|
+
/>
|
|
452
|
+
|
|
453
|
+
<TextField
|
|
454
|
+
{...register('email')}
|
|
455
|
+
label='Email'
|
|
456
|
+
type='email'
|
|
457
|
+
error={!!errors.email}
|
|
458
|
+
helperText={errors.email?.message}
|
|
459
|
+
fullWidth
|
|
460
|
+
/>
|
|
461
|
+
|
|
462
|
+
<TextField
|
|
463
|
+
{...register('firstName')}
|
|
464
|
+
label='First Name'
|
|
465
|
+
error={!!errors.firstName}
|
|
466
|
+
helperText={errors.firstName?.message}
|
|
467
|
+
fullWidth
|
|
468
|
+
/>
|
|
469
|
+
|
|
470
|
+
<TextField
|
|
471
|
+
{...register('lastName')}
|
|
472
|
+
label='Last Name'
|
|
473
|
+
error={!!errors.lastName}
|
|
474
|
+
helperText={errors.lastName?.message}
|
|
475
|
+
fullWidth
|
|
476
|
+
/>
|
|
477
|
+
|
|
478
|
+
<Button
|
|
479
|
+
type='submit'
|
|
480
|
+
variant='contained'
|
|
481
|
+
disabled={createMutation.isPending}
|
|
482
|
+
>
|
|
483
|
+
{createMutation.isPending ? 'Creating...' : 'Create User'}
|
|
484
|
+
</Button>
|
|
485
|
+
</Box>
|
|
486
|
+
</blog>
|
|
487
|
+
</Paper>
|
|
488
|
+
);
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
export default CreateUserBlog;
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
---
|
|
495
|
+
|
|
496
|
+
## Example 2: Parent Container with Lazy Loading
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
import React from 'react';
|
|
500
|
+
import { Box } from '@mui/material';
|
|
501
|
+
import { SuspenseLoader } from '~components/SuspenseLoader';
|
|
502
|
+
|
|
503
|
+
// Lazy load heavy components
|
|
504
|
+
const UserList = React.lazy(() => import('./UserList'));
|
|
505
|
+
const UserStats = React.lazy(() => import('./UserStats'));
|
|
506
|
+
const ActivityFeed = React.lazy(() => import('./ActivityFeed'));
|
|
507
|
+
|
|
508
|
+
export const UserDashboard: React.FC = () => {
|
|
509
|
+
return (
|
|
510
|
+
<Box sx={{ p: 2 }}>
|
|
511
|
+
<SuspenseLoader>
|
|
512
|
+
<UserStats />
|
|
513
|
+
</SuspenseLoader>
|
|
514
|
+
|
|
515
|
+
<Box sx={{ display: 'flex', gap: 2, mt: 2 }}>
|
|
516
|
+
<Box sx={{ flex: 2 }}>
|
|
517
|
+
<SuspenseLoader>
|
|
518
|
+
<UserList />
|
|
519
|
+
</SuspenseLoader>
|
|
520
|
+
</Box>
|
|
521
|
+
|
|
522
|
+
<Box sx={{ flex: 1 }}>
|
|
523
|
+
<SuspenseLoader>
|
|
524
|
+
<ActivityFeed />
|
|
525
|
+
</SuspenseLoader>
|
|
526
|
+
</Box>
|
|
527
|
+
</Box>
|
|
528
|
+
</Box>
|
|
529
|
+
);
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
export default UserDashboard;
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
**Benefits:**
|
|
536
|
+
- Each section loads independently
|
|
537
|
+
- User sees partial content sooner
|
|
538
|
+
- Better perceived perblogance
|
|
539
|
+
|
|
540
|
+
---
|
|
541
|
+
|
|
542
|
+
## Example 3: Cache-First Strategy Implementation
|
|
543
|
+
|
|
544
|
+
Complete example based on useSuspensePost.ts:
|
|
545
|
+
|
|
546
|
+
```typescript
|
|
547
|
+
import { useSuspenseQuery, useQueryClient } from '@tanstack/react-query';
|
|
548
|
+
import { postApi } from '../api/postApi';
|
|
549
|
+
import type { Post } from '../types';
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Smart post hook with cache-first strategy
|
|
553
|
+
* Reuses data from grid cache when available
|
|
554
|
+
*/
|
|
555
|
+
export function useSuspensePost(blogId: number, postId: number) {
|
|
556
|
+
const queryClient = useQueryClient();
|
|
557
|
+
|
|
558
|
+
return useSuspenseQuery<Post, Error>({
|
|
559
|
+
queryKey: ['post', blogId, postId],
|
|
560
|
+
queryFn: async () => {
|
|
561
|
+
// Strategy 1: Check grid cache first (avoids API call)
|
|
562
|
+
const gridCache = queryClient.getQueryData<{ rows: Post[] }>([
|
|
563
|
+
'posts-v2',
|
|
564
|
+
blogId,
|
|
565
|
+
'summary'
|
|
566
|
+
]) || queryClient.getQueryData<{ rows: Post[] }>([
|
|
567
|
+
'posts-v2',
|
|
568
|
+
blogId,
|
|
569
|
+
'flat'
|
|
570
|
+
]);
|
|
571
|
+
|
|
572
|
+
if (gridCache?.rows) {
|
|
573
|
+
const cached = gridCache.rows.find(
|
|
574
|
+
(row) => row.S_ID === postId
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
if (cached) {
|
|
578
|
+
return cached; // Return from cache - no API call!
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Strategy 2: Not in cache, fetch from API
|
|
583
|
+
return postApi.getPost(blogId, postId);
|
|
584
|
+
},
|
|
585
|
+
staleTime: 5 * 60 * 1000, // Fresh for 5 minutes
|
|
586
|
+
gcTime: 10 * 60 * 1000, // Cache for 10 minutes
|
|
587
|
+
refetchOnWindowFocus: false, // Don't refetch on focus
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
**Why this pattern:**
|
|
593
|
+
- Checks grid cache before API
|
|
594
|
+
- Instant data if user came from grid
|
|
595
|
+
- Falls back to API if not cached
|
|
596
|
+
- Configurable cache times
|
|
597
|
+
|
|
598
|
+
---
|
|
599
|
+
|
|
600
|
+
## Example 4: Complete Route File
|
|
601
|
+
|
|
602
|
+
```typescript
|
|
603
|
+
/**
|
|
604
|
+
* Project catalog route
|
|
605
|
+
* Path: /project-catalog
|
|
606
|
+
*/
|
|
607
|
+
|
|
608
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
609
|
+
import { lazy } from 'react';
|
|
610
|
+
|
|
611
|
+
// Lazy load the PostTable component
|
|
612
|
+
const PostTable = lazy(() =>
|
|
613
|
+
import('@/features/posts/components/PostTable').then(
|
|
614
|
+
(module) => ({ default: module.PostTable })
|
|
615
|
+
)
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
// Route constants
|
|
619
|
+
const PROJECT_CATALOG_FORM_ID = 744;
|
|
620
|
+
const PROJECT_CATALOG_PROJECT_ID = 225;
|
|
621
|
+
|
|
622
|
+
export const Route = createFileRoute('/project-catalog/')({
|
|
623
|
+
component: ProjectCatalogPage,
|
|
624
|
+
loader: () => ({
|
|
625
|
+
crumb: 'Projects', // Breadcrumb title
|
|
626
|
+
}),
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
function ProjectCatalogPage() {
|
|
630
|
+
return (
|
|
631
|
+
<PostTable
|
|
632
|
+
blogId={PROJECT_CATALOG_FORM_ID}
|
|
633
|
+
projectId={PROJECT_CATALOG_PROJECT_ID}
|
|
634
|
+
tableType='active_projects'
|
|
635
|
+
title='Blog Dashboard'
|
|
636
|
+
/>
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
export default ProjectCatalogPage;
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
---
|
|
644
|
+
|
|
645
|
+
## Example 5: Dialog with Blog
|
|
646
|
+
|
|
647
|
+
```typescript
|
|
648
|
+
import React from 'react';
|
|
649
|
+
import {
|
|
650
|
+
Dialog,
|
|
651
|
+
DialogTitle,
|
|
652
|
+
DialogContent,
|
|
653
|
+
DialogActions,
|
|
654
|
+
Button,
|
|
655
|
+
TextField,
|
|
656
|
+
Box,
|
|
657
|
+
IconButton,
|
|
658
|
+
} from '@mui/material';
|
|
659
|
+
import { Close, PersonAdd } from '@mui/icons-material';
|
|
660
|
+
import { useBlog } from 'react-hook-blog';
|
|
661
|
+
import { zodResolver } from '@hookblog/resolvers/zod';
|
|
662
|
+
import { z } from 'zod';
|
|
663
|
+
|
|
664
|
+
const blogSchema = z.object({
|
|
665
|
+
name: z.string().min(1),
|
|
666
|
+
email: z.string().email(),
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
type BlogData = z.infer<typeof blogSchema>;
|
|
670
|
+
|
|
671
|
+
interface AddUserDialogProps {
|
|
672
|
+
open: boolean;
|
|
673
|
+
onClose: () => void;
|
|
674
|
+
onSubmit: (data: BlogData) => Promise<void>;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
export const AddUserDialog: React.FC<AddUserDialogProps> = ({
|
|
678
|
+
open,
|
|
679
|
+
onClose,
|
|
680
|
+
onSubmit,
|
|
681
|
+
}) => {
|
|
682
|
+
const { register, handleSubmit, blogState: { errors }, reset } = useBlog<BlogData>({
|
|
683
|
+
resolver: zodResolver(blogSchema),
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
const handleClose = () => {
|
|
687
|
+
reset();
|
|
688
|
+
onClose();
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
const handleBlogSubmit = async (data: BlogData) => {
|
|
692
|
+
await onSubmit(data);
|
|
693
|
+
handleClose();
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
return (
|
|
697
|
+
<Dialog open={open} onClose={handleClose} maxWidth='sm' fullWidth>
|
|
698
|
+
<DialogTitle>
|
|
699
|
+
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
700
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
701
|
+
<PersonAdd color='primary' />
|
|
702
|
+
Add User
|
|
703
|
+
</Box>
|
|
704
|
+
<IconButton onClick={handleClose} size='small'>
|
|
705
|
+
<Close />
|
|
706
|
+
</IconButton>
|
|
707
|
+
</Box>
|
|
708
|
+
</DialogTitle>
|
|
709
|
+
|
|
710
|
+
<blog onSubmit={handleSubmit(handleBlogSubmit)}>
|
|
711
|
+
<DialogContent>
|
|
712
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
713
|
+
<TextField
|
|
714
|
+
{...register('name')}
|
|
715
|
+
label='Name'
|
|
716
|
+
error={!!errors.name}
|
|
717
|
+
helperText={errors.name?.message}
|
|
718
|
+
fullWidth
|
|
719
|
+
autoFocus
|
|
720
|
+
/>
|
|
721
|
+
|
|
722
|
+
<TextField
|
|
723
|
+
{...register('email')}
|
|
724
|
+
label='Email'
|
|
725
|
+
type='email'
|
|
726
|
+
error={!!errors.email}
|
|
727
|
+
helperText={errors.email?.message}
|
|
728
|
+
fullWidth
|
|
729
|
+
/>
|
|
730
|
+
</Box>
|
|
731
|
+
</DialogContent>
|
|
732
|
+
|
|
733
|
+
<DialogActions>
|
|
734
|
+
<Button onClick={handleClose}>Cancel</Button>
|
|
735
|
+
<Button type='submit' variant='contained'>
|
|
736
|
+
Add User
|
|
737
|
+
</Button>
|
|
738
|
+
</DialogActions>
|
|
739
|
+
</blog>
|
|
740
|
+
</Dialog>
|
|
741
|
+
);
|
|
742
|
+
};
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
---
|
|
746
|
+
|
|
747
|
+
## Example 6: Parallel Data Fetching
|
|
748
|
+
|
|
749
|
+
```typescript
|
|
750
|
+
import React from 'react';
|
|
751
|
+
import { Box, Grid, Paper } from '@mui/material';
|
|
752
|
+
import { useSuspenseQueries } from '@tanstack/react-query';
|
|
753
|
+
import { userApi } from '../api/userApi';
|
|
754
|
+
import { statsApi } from '../api/statsApi';
|
|
755
|
+
import { activityApi } from '../api/activityApi';
|
|
756
|
+
|
|
757
|
+
export const Dashboard: React.FC = () => {
|
|
758
|
+
// Fetch all data in parallel with Suspense
|
|
759
|
+
const [statsQuery, usersQuery, activityQuery] = useSuspenseQueries({
|
|
760
|
+
queries: [
|
|
761
|
+
{
|
|
762
|
+
queryKey: ['stats'],
|
|
763
|
+
queryFn: () => statsApi.getStats(),
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
queryKey: ['users', 'active'],
|
|
767
|
+
queryFn: () => userApi.getActiveUsers(),
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
queryKey: ['activity', 'recent'],
|
|
771
|
+
queryFn: () => activityApi.getRecent(),
|
|
772
|
+
},
|
|
773
|
+
],
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
return (
|
|
777
|
+
<Box sx={{ p: 2 }}>
|
|
778
|
+
<Grid container spacing={2}>
|
|
779
|
+
<Grid size={{ xs: 12, md: 4 }}>
|
|
780
|
+
<Paper sx={{ p: 2 }}>
|
|
781
|
+
<h3>Stats</h3>
|
|
782
|
+
<p>Total: {statsQuery.data.total}</p>
|
|
783
|
+
</Paper>
|
|
784
|
+
</Grid>
|
|
785
|
+
|
|
786
|
+
<Grid size={{ xs: 12, md: 4 }}>
|
|
787
|
+
<Paper sx={{ p: 2 }}>
|
|
788
|
+
<h3>Active Users</h3>
|
|
789
|
+
<p>Count: {usersQuery.data.length}</p>
|
|
790
|
+
</Paper>
|
|
791
|
+
</Grid>
|
|
792
|
+
|
|
793
|
+
<Grid size={{ xs: 12, md: 4 }}>
|
|
794
|
+
<Paper sx={{ p: 2 }}>
|
|
795
|
+
<h3>Recent Activity</h3>
|
|
796
|
+
<p>Events: {activityQuery.data.length}</p>
|
|
797
|
+
</Paper>
|
|
798
|
+
</Grid>
|
|
799
|
+
</Grid>
|
|
800
|
+
</Box>
|
|
801
|
+
);
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
// Usage with Suspense
|
|
805
|
+
<SuspenseLoader>
|
|
806
|
+
<Dashboard />
|
|
807
|
+
</SuspenseLoader>
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
---
|
|
811
|
+
|
|
812
|
+
## Example 7: Optimistic Update
|
|
813
|
+
|
|
814
|
+
```typescript
|
|
815
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
816
|
+
import type { User } from '../types';
|
|
817
|
+
|
|
818
|
+
export const useToggleUserStatus = () => {
|
|
819
|
+
const queryClient = useQueryClient();
|
|
820
|
+
|
|
821
|
+
return useMutation({
|
|
822
|
+
mutationFn: (userId: string) => userApi.toggleStatus(userId),
|
|
823
|
+
|
|
824
|
+
// Optimistic update
|
|
825
|
+
onMutate: async (userId) => {
|
|
826
|
+
// Cancel outgoing refetches
|
|
827
|
+
await queryClient.cancelQueries({ queryKey: ['users'] });
|
|
828
|
+
|
|
829
|
+
// Snapshot previous value
|
|
830
|
+
const previousUsers = queryClient.getQueryData<User[]>(['users']);
|
|
831
|
+
|
|
832
|
+
// Optimistically update UI
|
|
833
|
+
queryClient.setQueryData<User[]>(['users'], (old) => {
|
|
834
|
+
return old?.map(user =>
|
|
835
|
+
user.id === userId
|
|
836
|
+
? { ...user, active: !user.active }
|
|
837
|
+
: user
|
|
838
|
+
) || [];
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
return { previousUsers };
|
|
842
|
+
},
|
|
843
|
+
|
|
844
|
+
// Rollback on error
|
|
845
|
+
onError: (err, userId, context) => {
|
|
846
|
+
queryClient.setQueryData(['users'], context?.previousUsers);
|
|
847
|
+
},
|
|
848
|
+
|
|
849
|
+
// Refetch after mutation
|
|
850
|
+
onSettled: () => {
|
|
851
|
+
queryClient.invalidateQueries({ queryKey: ['users'] });
|
|
852
|
+
},
|
|
853
|
+
});
|
|
854
|
+
};
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
---
|
|
858
|
+
|
|
859
|
+
## Summary
|
|
860
|
+
|
|
861
|
+
**Key Takeaways:**
|
|
862
|
+
|
|
863
|
+
1. **Component Pattern**: React.FC + lazy + Suspense + useSuspenseQuery
|
|
864
|
+
2. **Feature Structure**: Organized subdirectories (api/, components/, hooks/, etc.)
|
|
865
|
+
3. **Routing**: Folder-based with lazy loading
|
|
866
|
+
4. **Data Fetching**: useSuspenseQuery with cache-first strategy
|
|
867
|
+
5. **Blogs**: React Hook Blog + Zod validation
|
|
868
|
+
6. **Error Handling**: useMuiSnackbar + onError callbacks
|
|
869
|
+
7. **Perblogance**: useMemo, useCallback, React.memo, debouncing
|
|
870
|
+
8. **Styling**: Inline <100 lines, sx prop, MUI v7 syntax
|
|
871
|
+
|
|
872
|
+
**See other resources for detailed explanations of each pattern.**
|