@malamute/ai-rules 1.0.0 → 1.2.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 +270 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
- package/configs/_shared/.claude/rules/conventions/git.md +265 -0
- package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
- package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
- package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/.claude/rules/devops/docker.md +275 -0
- package/configs/_shared/.claude/rules/devops/nx.md +194 -0
- package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
- package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
- package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
- package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
- package/configs/_shared/.claude/rules/quality/logging.md +45 -0
- package/configs/_shared/.claude/rules/quality/observability.md +240 -0
- package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
- package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/.claude/skills/dev/api-endpoint/SKILL.md +126 -0
- package/configs/_shared/.claude/{commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
- package/configs/_shared/.claude/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
- package/configs/angular/.claude/rules/core/resource.md +285 -0
- package/configs/angular/.claude/rules/core/signals.md +323 -0
- package/configs/angular/.claude/rules/http.md +338 -0
- package/configs/angular/.claude/rules/routing.md +291 -0
- package/configs/angular/.claude/rules/ssr.md +312 -0
- package/configs/angular/.claude/rules/state/signal-store.md +408 -0
- package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
- package/configs/angular/.claude/rules/testing.md +7 -7
- package/configs/angular/.claude/rules/ui/aria.md +422 -0
- package/configs/angular/.claude/rules/ui/forms.md +424 -0
- package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/.claude/settings.json +1 -0
- package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
- package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/dotnet/.claude/rules/background-services.md +552 -0
- package/configs/dotnet/.claude/rules/configuration.md +426 -0
- package/configs/dotnet/.claude/rules/ddd.md +447 -0
- package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
- package/configs/dotnet/.claude/rules/mediatr.md +320 -0
- package/configs/dotnet/.claude/rules/middleware.md +489 -0
- package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
- package/configs/dotnet/.claude/rules/validation.md +388 -0
- package/configs/dotnet/.claude/settings.json +21 -3
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
- package/configs/fastapi/.claude/rules/dependencies.md +170 -0
- package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
- package/configs/fastapi/.claude/rules/lifespan.md +274 -0
- package/configs/fastapi/.claude/rules/middleware.md +229 -0
- package/configs/fastapi/.claude/rules/pydantic.md +433 -0
- package/configs/fastapi/.claude/rules/responses.md +251 -0
- package/configs/fastapi/.claude/rules/routers.md +202 -0
- package/configs/fastapi/.claude/rules/security.md +222 -0
- package/configs/fastapi/.claude/rules/testing.md +251 -0
- package/configs/fastapi/.claude/rules/websockets.md +298 -0
- package/configs/fastapi/.claude/settings.json +33 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/flask/.claude/rules/blueprints.md +208 -0
- package/configs/flask/.claude/rules/cli.md +285 -0
- package/configs/flask/.claude/rules/configuration.md +281 -0
- package/configs/flask/.claude/rules/context.md +238 -0
- package/configs/flask/.claude/rules/error-handlers.md +278 -0
- package/configs/flask/.claude/rules/extensions.md +278 -0
- package/configs/flask/.claude/rules/flask.md +171 -0
- package/configs/flask/.claude/rules/marshmallow.md +206 -0
- package/configs/flask/.claude/rules/security.md +267 -0
- package/configs/flask/.claude/rules/testing.md +284 -0
- package/configs/flask/.claude/settings.json +33 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
- package/configs/nestjs/.claude/rules/filters.md +376 -0
- package/configs/nestjs/.claude/rules/interceptors.md +317 -0
- package/configs/nestjs/.claude/rules/middleware.md +321 -0
- package/configs/nestjs/.claude/rules/modules.md +26 -0
- package/configs/nestjs/.claude/rules/pipes.md +351 -0
- package/configs/nestjs/.claude/rules/websockets.md +451 -0
- package/configs/nestjs/.claude/settings.json +16 -2
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nextjs/.claude/rules/api-routes.md +358 -0
- package/configs/nextjs/.claude/rules/authentication.md +355 -0
- package/configs/nextjs/.claude/rules/components.md +52 -0
- package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
- package/configs/nextjs/.claude/rules/database.md +400 -0
- package/configs/nextjs/.claude/rules/middleware.md +303 -0
- package/configs/nextjs/.claude/rules/routing.md +324 -0
- package/configs/nextjs/.claude/rules/seo.md +350 -0
- package/configs/nextjs/.claude/rules/server-actions.md +353 -0
- package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
- package/configs/nextjs/.claude/settings.json +5 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/package.json +23 -9
- package/src/cli.js +220 -0
- package/src/config.js +29 -0
- package/src/index.js +13 -0
- package/src/installer.js +361 -0
- package/src/merge.js +116 -0
- package/src/tech-config.json +29 -0
- package/src/utils.js +96 -0
- package/configs/python/.claude/rules/flask.md +0 -332
- package/configs/python/.claude/settings.json +0 -18
- package/configs/python/CLAUDE.md +0 -273
- package/src/install.js +0 -315
- /package/configs/_shared/.claude/rules/{accessibility.md → domain/frontend/accessibility.md} +0 -0
- /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
- /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{review → git/review}/SKILL.md +0 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "apps/**/app/**/*.tsx"
|
|
4
|
+
- "apps/**/app/**/*.ts"
|
|
5
|
+
- "app/**/*.tsx"
|
|
6
|
+
- "app/**/*.ts"
|
|
7
|
+
- "**/actions.ts"
|
|
8
|
+
- "**/actions/*.ts"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Next.js Server Actions
|
|
12
|
+
|
|
13
|
+
## Basic Server Action
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// app/actions.ts
|
|
17
|
+
'use server';
|
|
18
|
+
|
|
19
|
+
import { revalidatePath } from 'next/cache';
|
|
20
|
+
|
|
21
|
+
export async function createPost(formData: FormData) {
|
|
22
|
+
const title = formData.get('title') as string;
|
|
23
|
+
const content = formData.get('content') as string;
|
|
24
|
+
|
|
25
|
+
await db.post.create({ data: { title, content } });
|
|
26
|
+
|
|
27
|
+
revalidatePath('/posts');
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## With Validation (Zod)
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// app/actions.ts
|
|
35
|
+
'use server';
|
|
36
|
+
|
|
37
|
+
import { z } from 'zod';
|
|
38
|
+
import { revalidatePath } from 'next/cache';
|
|
39
|
+
|
|
40
|
+
const CreatePostSchema = z.object({
|
|
41
|
+
title: z.string().min(1).max(100),
|
|
42
|
+
content: z.string().min(1),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export async function createPost(formData: FormData) {
|
|
46
|
+
const parsed = CreatePostSchema.safeParse({
|
|
47
|
+
title: formData.get('title'),
|
|
48
|
+
content: formData.get('content'),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (!parsed.success) {
|
|
52
|
+
return { error: parsed.error.flatten().fieldErrors };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await db.post.create({ data: parsed.data });
|
|
56
|
+
revalidatePath('/posts');
|
|
57
|
+
|
|
58
|
+
return { success: true };
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Return Type Pattern
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
// Type-safe action results
|
|
66
|
+
type ActionResult<T> =
|
|
67
|
+
| { success: true; data: T }
|
|
68
|
+
| { success: false; error: string; fieldErrors?: Record<string, string[]> };
|
|
69
|
+
|
|
70
|
+
export async function createUser(formData: FormData): Promise<ActionResult<User>> {
|
|
71
|
+
const parsed = UserSchema.safeParse(Object.fromEntries(formData));
|
|
72
|
+
|
|
73
|
+
if (!parsed.success) {
|
|
74
|
+
return {
|
|
75
|
+
success: false,
|
|
76
|
+
error: 'Validation failed',
|
|
77
|
+
fieldErrors: parsed.error.flatten().fieldErrors,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const user = await db.user.create({ data: parsed.data });
|
|
83
|
+
revalidatePath('/users');
|
|
84
|
+
return { success: true, data: user };
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return { success: false, error: 'Failed to create user' };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## useActionState (React 19)
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
'use client';
|
|
95
|
+
|
|
96
|
+
import { useActionState } from 'react';
|
|
97
|
+
import { createPost } from './actions';
|
|
98
|
+
|
|
99
|
+
export function PostForm() {
|
|
100
|
+
const [state, formAction, isPending] = useActionState(createPost, null);
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<form action={formAction}>
|
|
104
|
+
<input name="title" disabled={isPending} />
|
|
105
|
+
{state?.fieldErrors?.title && (
|
|
106
|
+
<span className="error">{state.fieldErrors.title}</span>
|
|
107
|
+
)}
|
|
108
|
+
|
|
109
|
+
<textarea name="content" disabled={isPending} />
|
|
110
|
+
{state?.fieldErrors?.content && (
|
|
111
|
+
<span className="error">{state.fieldErrors.content}</span>
|
|
112
|
+
)}
|
|
113
|
+
|
|
114
|
+
<button type="submit" disabled={isPending}>
|
|
115
|
+
{isPending ? 'Creating...' : 'Create Post'}
|
|
116
|
+
</button>
|
|
117
|
+
|
|
118
|
+
{state?.error && <div className="error">{state.error}</div>}
|
|
119
|
+
</form>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## useOptimistic
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
'use client';
|
|
128
|
+
|
|
129
|
+
import { useOptimistic } from 'react';
|
|
130
|
+
import { toggleLike } from './actions';
|
|
131
|
+
|
|
132
|
+
interface Post {
|
|
133
|
+
id: string;
|
|
134
|
+
likes: number;
|
|
135
|
+
isLiked: boolean;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function LikeButton({ post }: { post: Post }) {
|
|
139
|
+
const [optimisticPost, addOptimistic] = useOptimistic(
|
|
140
|
+
post,
|
|
141
|
+
(state, newLiked: boolean) => ({
|
|
142
|
+
...state,
|
|
143
|
+
isLiked: newLiked,
|
|
144
|
+
likes: newLiked ? state.likes + 1 : state.likes - 1,
|
|
145
|
+
})
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
async function handleClick() {
|
|
149
|
+
addOptimistic(!optimisticPost.isLiked);
|
|
150
|
+
await toggleLike(post.id);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<button onClick={handleClick}>
|
|
155
|
+
{optimisticPost.isLiked ? '❤️' : '🤍'} {optimisticPost.likes}
|
|
156
|
+
</button>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Revalidation Strategies
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
'use server';
|
|
165
|
+
|
|
166
|
+
import { revalidatePath, revalidateTag } from 'next/cache';
|
|
167
|
+
|
|
168
|
+
export async function createPost(data: PostData) {
|
|
169
|
+
await db.post.create({ data });
|
|
170
|
+
|
|
171
|
+
// Revalidate specific path
|
|
172
|
+
revalidatePath('/posts');
|
|
173
|
+
|
|
174
|
+
// Revalidate dynamic path
|
|
175
|
+
revalidatePath(`/posts/${data.slug}`);
|
|
176
|
+
|
|
177
|
+
// Revalidate layout (all child pages)
|
|
178
|
+
revalidatePath('/posts', 'layout');
|
|
179
|
+
|
|
180
|
+
// Revalidate by cache tag
|
|
181
|
+
revalidateTag('posts');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// In data fetching, use tags
|
|
185
|
+
async function getPosts() {
|
|
186
|
+
return fetch('/api/posts', {
|
|
187
|
+
next: { tags: ['posts'] },
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Redirect After Action
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
'use server';
|
|
196
|
+
|
|
197
|
+
import { redirect } from 'next/navigation';
|
|
198
|
+
|
|
199
|
+
export async function createPost(formData: FormData) {
|
|
200
|
+
const post = await db.post.create({
|
|
201
|
+
data: {
|
|
202
|
+
title: formData.get('title') as string,
|
|
203
|
+
content: formData.get('content') as string,
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
redirect(`/posts/${post.slug}`);
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## With Authentication
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
'use server';
|
|
215
|
+
|
|
216
|
+
import { auth } from '@/lib/auth';
|
|
217
|
+
import { redirect } from 'next/navigation';
|
|
218
|
+
|
|
219
|
+
export async function createPost(formData: FormData) {
|
|
220
|
+
const session = await auth();
|
|
221
|
+
|
|
222
|
+
if (!session?.user) {
|
|
223
|
+
redirect('/login');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
await db.post.create({
|
|
227
|
+
data: {
|
|
228
|
+
title: formData.get('title') as string,
|
|
229
|
+
content: formData.get('content') as string,
|
|
230
|
+
authorId: session.user.id,
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
revalidatePath('/posts');
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## File Upload
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
'use server';
|
|
242
|
+
|
|
243
|
+
export async function uploadFile(formData: FormData) {
|
|
244
|
+
const file = formData.get('file') as File;
|
|
245
|
+
|
|
246
|
+
if (!file || file.size === 0) {
|
|
247
|
+
return { error: 'No file provided' };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Validate file type
|
|
251
|
+
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
|
|
252
|
+
if (!allowedTypes.includes(file.type)) {
|
|
253
|
+
return { error: 'Invalid file type' };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Validate file size (5MB)
|
|
257
|
+
if (file.size > 5 * 1024 * 1024) {
|
|
258
|
+
return { error: 'File too large' };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const bytes = await file.arrayBuffer();
|
|
262
|
+
const buffer = Buffer.from(bytes);
|
|
263
|
+
|
|
264
|
+
// Save to storage (S3, local, etc.)
|
|
265
|
+
const url = await uploadToStorage(buffer, file.name);
|
|
266
|
+
|
|
267
|
+
return { success: true, url };
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Progressive Enhancement
|
|
272
|
+
|
|
273
|
+
```tsx
|
|
274
|
+
// Works without JavaScript (form submits normally)
|
|
275
|
+
// Enhanced with JavaScript (no page reload)
|
|
276
|
+
|
|
277
|
+
export function ContactForm() {
|
|
278
|
+
return (
|
|
279
|
+
<form action={sendMessage}>
|
|
280
|
+
<input name="email" type="email" required />
|
|
281
|
+
<textarea name="message" required />
|
|
282
|
+
<button type="submit">Send</button>
|
|
283
|
+
</form>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Error Handling
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
'use server';
|
|
292
|
+
|
|
293
|
+
export async function riskyAction(formData: FormData) {
|
|
294
|
+
try {
|
|
295
|
+
await someRiskyOperation();
|
|
296
|
+
return { success: true };
|
|
297
|
+
} catch (error) {
|
|
298
|
+
// Log server-side
|
|
299
|
+
console.error('Action failed:', error);
|
|
300
|
+
|
|
301
|
+
// Return safe error to client
|
|
302
|
+
if (error instanceof KnownError) {
|
|
303
|
+
return { success: false, error: error.message };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return { success: false, error: 'An unexpected error occurred' };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## Binding Arguments
|
|
312
|
+
|
|
313
|
+
```tsx
|
|
314
|
+
// Pass additional data to action
|
|
315
|
+
import { updatePost } from './actions';
|
|
316
|
+
|
|
317
|
+
export function EditButton({ postId }: { postId: string }) {
|
|
318
|
+
const updateWithId = updatePost.bind(null, postId);
|
|
319
|
+
|
|
320
|
+
return (
|
|
321
|
+
<form action={updateWithId}>
|
|
322
|
+
<input name="title" />
|
|
323
|
+
<button type="submit">Update</button>
|
|
324
|
+
</form>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// actions.ts
|
|
329
|
+
'use server';
|
|
330
|
+
|
|
331
|
+
export async function updatePost(postId: string, formData: FormData) {
|
|
332
|
+
const title = formData.get('title') as string;
|
|
333
|
+
await db.post.update({ where: { id: postId }, data: { title } });
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## Non-Form Usage
|
|
338
|
+
|
|
339
|
+
```tsx
|
|
340
|
+
'use client';
|
|
341
|
+
|
|
342
|
+
import { deletePost } from './actions';
|
|
343
|
+
|
|
344
|
+
export function DeleteButton({ postId }: { postId: string }) {
|
|
345
|
+
async function handleDelete() {
|
|
346
|
+
if (confirm('Are you sure?')) {
|
|
347
|
+
await deletePost(postId);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return <button onClick={handleDelete}>Delete</button>;
|
|
352
|
+
}
|
|
353
|
+
```
|
|
@@ -82,7 +82,7 @@ export const useUserStore = create<UserState>((set) => ({
|
|
|
82
82
|
|
|
83
83
|
```typescript
|
|
84
84
|
// stores/app-store.ts
|
|
85
|
-
import { create } from 'zustand';
|
|
85
|
+
import { create, StateCreator } from 'zustand';
|
|
86
86
|
|
|
87
87
|
// User slice
|
|
88
88
|
interface UserSlice {
|
|
@@ -90,7 +90,7 @@ interface UserSlice {
|
|
|
90
90
|
setUser: (user: User | null) => void;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
const createUserSlice = (set
|
|
93
|
+
const createUserSlice: StateCreator<AppStore, [], [], UserSlice> = (set) => ({
|
|
94
94
|
user: null,
|
|
95
95
|
setUser: (user) => set({ user }),
|
|
96
96
|
});
|
|
@@ -103,13 +103,13 @@ interface CartSlice {
|
|
|
103
103
|
clearCart: () => void;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
const createCartSlice = (set
|
|
106
|
+
const createCartSlice: StateCreator<AppStore, [], [], CartSlice> = (set) => ({
|
|
107
107
|
items: [],
|
|
108
|
-
addItem: (item) => set((state
|
|
108
|
+
addItem: (item) => set((state) => ({
|
|
109
109
|
items: [...state.items, item]
|
|
110
110
|
})),
|
|
111
|
-
removeItem: (id) => set((state
|
|
112
|
-
items: state.items.filter((item
|
|
111
|
+
removeItem: (id) => set((state) => ({
|
|
112
|
+
items: state.items.filter((item) => item.id !== id)
|
|
113
113
|
})),
|
|
114
114
|
clearCart: () => set({ items: [] }),
|
|
115
115
|
});
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"Bash(npm install *)",
|
|
15
15
|
"Bash(npm ci)",
|
|
16
16
|
"Bash(npx nx *)",
|
|
17
|
+
"Bash(npx prisma *)",
|
|
17
18
|
"Read",
|
|
18
19
|
"Edit",
|
|
19
20
|
"Write"
|
|
@@ -25,5 +26,9 @@
|
|
|
25
26
|
"Read(.env.*)",
|
|
26
27
|
"Read(**/secrets/**)"
|
|
27
28
|
]
|
|
29
|
+
},
|
|
30
|
+
"env": {
|
|
31
|
+
"NODE_ENV": "development",
|
|
32
|
+
"NX_DAEMON": "true"
|
|
28
33
|
}
|
|
29
34
|
}
|