@malamute/ai-rules 1.0.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 +174 -0
- package/bin/cli.js +5 -0
- package/configs/_shared/.claude/commands/fix-issue.md +38 -0
- package/configs/_shared/.claude/commands/generate-tests.md +49 -0
- package/configs/_shared/.claude/commands/review-pr.md +77 -0
- package/configs/_shared/.claude/rules/accessibility.md +270 -0
- package/configs/_shared/.claude/rules/performance.md +226 -0
- package/configs/_shared/.claude/rules/security.md +188 -0
- package/configs/_shared/.claude/skills/debug/SKILL.md +118 -0
- package/configs/_shared/.claude/skills/learning/SKILL.md +224 -0
- package/configs/_shared/.claude/skills/review/SKILL.md +86 -0
- package/configs/_shared/.claude/skills/spec/SKILL.md +112 -0
- package/configs/_shared/CLAUDE.md +174 -0
- package/configs/angular/.claude/rules/components.md +257 -0
- package/configs/angular/.claude/rules/state.md +250 -0
- package/configs/angular/.claude/rules/testing.md +422 -0
- package/configs/angular/.claude/settings.json +31 -0
- package/configs/angular/CLAUDE.md +251 -0
- package/configs/dotnet/.claude/rules/api.md +370 -0
- package/configs/dotnet/.claude/rules/architecture.md +199 -0
- package/configs/dotnet/.claude/rules/database/efcore.md +408 -0
- package/configs/dotnet/.claude/rules/testing.md +389 -0
- package/configs/dotnet/.claude/settings.json +9 -0
- package/configs/dotnet/CLAUDE.md +319 -0
- package/configs/nestjs/.claude/rules/auth.md +321 -0
- package/configs/nestjs/.claude/rules/database/prisma.md +305 -0
- package/configs/nestjs/.claude/rules/database/typeorm.md +379 -0
- package/configs/nestjs/.claude/rules/modules.md +215 -0
- package/configs/nestjs/.claude/rules/testing.md +315 -0
- package/configs/nestjs/.claude/rules/validation.md +279 -0
- package/configs/nestjs/.claude/settings.json +15 -0
- package/configs/nestjs/CLAUDE.md +263 -0
- package/configs/nextjs/.claude/rules/components.md +211 -0
- package/configs/nextjs/.claude/rules/state/redux-toolkit.md +429 -0
- package/configs/nextjs/.claude/rules/state/zustand.md +299 -0
- package/configs/nextjs/.claude/rules/testing.md +315 -0
- package/configs/nextjs/.claude/settings.json +29 -0
- package/configs/nextjs/CLAUDE.md +376 -0
- package/configs/python/.claude/rules/database/sqlalchemy.md +355 -0
- package/configs/python/.claude/rules/fastapi.md +272 -0
- package/configs/python/.claude/rules/flask.md +332 -0
- package/configs/python/.claude/rules/testing.md +374 -0
- package/configs/python/.claude/settings.json +18 -0
- package/configs/python/CLAUDE.md +273 -0
- package/package.json +41 -0
- package/src/install.js +315 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.test.tsx"
|
|
4
|
+
- "**/*.test.ts"
|
|
5
|
+
- "**/*.spec.tsx"
|
|
6
|
+
- "**/*.spec.ts"
|
|
7
|
+
- "**/*.e2e.ts"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Testing Guidelines (Next.js)
|
|
11
|
+
|
|
12
|
+
## Framework
|
|
13
|
+
|
|
14
|
+
- **Vitest** or **Jest** for unit/integration tests
|
|
15
|
+
- **React Testing Library** for component tests
|
|
16
|
+
- **Playwright** for E2E tests
|
|
17
|
+
|
|
18
|
+
## Test File Structure
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
component.tsx
|
|
22
|
+
component.test.tsx # Co-located tests
|
|
23
|
+
|
|
24
|
+
# or
|
|
25
|
+
__tests__/
|
|
26
|
+
component.test.tsx
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Component Testing
|
|
30
|
+
|
|
31
|
+
### Basic Component Test
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { render, screen } from '@testing-library/react';
|
|
35
|
+
import userEvent from '@testing-library/user-event';
|
|
36
|
+
import { UserCard } from './user-card';
|
|
37
|
+
|
|
38
|
+
describe('UserCard', () => {
|
|
39
|
+
const mockUser = { id: '1', name: 'John Doe', email: 'john@example.com' };
|
|
40
|
+
|
|
41
|
+
it('should render user information', () => {
|
|
42
|
+
render(<UserCard user={mockUser} onSelect={vi.fn()} />);
|
|
43
|
+
|
|
44
|
+
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
45
|
+
expect(screen.getByText('john@example.com')).toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should call onSelect when clicked', async () => {
|
|
49
|
+
const handleSelect = vi.fn();
|
|
50
|
+
const user = userEvent.setup();
|
|
51
|
+
|
|
52
|
+
render(<UserCard user={mockUser} onSelect={handleSelect} />);
|
|
53
|
+
|
|
54
|
+
await user.click(screen.getByRole('button'));
|
|
55
|
+
|
|
56
|
+
expect(handleSelect).toHaveBeenCalledWith(mockUser);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Testing Async Components
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import { render, screen } from '@testing-library/react';
|
|
65
|
+
|
|
66
|
+
// Mock the fetch/data function
|
|
67
|
+
vi.mock('@/lib/api', () => ({
|
|
68
|
+
getUsers: vi.fn().mockResolvedValue([
|
|
69
|
+
{ id: '1', name: 'John' },
|
|
70
|
+
{ id: '2', name: 'Jane' },
|
|
71
|
+
]),
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
describe('UsersPage', () => {
|
|
75
|
+
it('should render users', async () => {
|
|
76
|
+
// For async Server Components, render and await
|
|
77
|
+
const Page = await import('./page');
|
|
78
|
+
render(await Page.default());
|
|
79
|
+
|
|
80
|
+
expect(screen.getByText('John')).toBeInTheDocument();
|
|
81
|
+
expect(screen.getByText('Jane')).toBeInTheDocument();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Testing with Providers
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import { render } from '@testing-library/react';
|
|
90
|
+
|
|
91
|
+
function renderWithProviders(ui: React.ReactElement) {
|
|
92
|
+
return render(
|
|
93
|
+
<QueryClientProvider client={queryClient}>
|
|
94
|
+
<ThemeProvider>
|
|
95
|
+
{ui}
|
|
96
|
+
</ThemeProvider>
|
|
97
|
+
</QueryClientProvider>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
it('should render with providers', () => {
|
|
102
|
+
renderWithProviders(<MyComponent />);
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Server Actions Testing
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
import { createUser } from './actions';
|
|
110
|
+
|
|
111
|
+
describe('createUser', () => {
|
|
112
|
+
it('should create a user', async () => {
|
|
113
|
+
const formData = new FormData();
|
|
114
|
+
formData.set('name', 'John Doe');
|
|
115
|
+
formData.set('email', 'john@example.com');
|
|
116
|
+
|
|
117
|
+
const result = await createUser(formData);
|
|
118
|
+
|
|
119
|
+
expect(result.success).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should validate input', async () => {
|
|
123
|
+
const formData = new FormData();
|
|
124
|
+
formData.set('name', ''); // Invalid
|
|
125
|
+
|
|
126
|
+
const result = await createUser(formData);
|
|
127
|
+
|
|
128
|
+
expect(result.error).toBeDefined();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Hook Testing
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
import { renderHook, act } from '@testing-library/react';
|
|
137
|
+
import { useCounter } from './use-counter';
|
|
138
|
+
|
|
139
|
+
describe('useCounter', () => {
|
|
140
|
+
it('should increment counter', () => {
|
|
141
|
+
const { result } = renderHook(() => useCounter());
|
|
142
|
+
|
|
143
|
+
expect(result.current.count).toBe(0);
|
|
144
|
+
|
|
145
|
+
act(() => {
|
|
146
|
+
result.current.increment();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(result.current.count).toBe(1);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## E2E Testing with Playwright
|
|
155
|
+
|
|
156
|
+
### Setup
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
// playwright.config.ts
|
|
160
|
+
import { defineConfig } from '@playwright/test';
|
|
161
|
+
|
|
162
|
+
export default defineConfig({
|
|
163
|
+
testDir: './e2e',
|
|
164
|
+
use: {
|
|
165
|
+
baseURL: 'http://localhost:3000',
|
|
166
|
+
trace: 'on-first-retry',
|
|
167
|
+
},
|
|
168
|
+
webServer: {
|
|
169
|
+
command: 'nx serve app-name',
|
|
170
|
+
url: 'http://localhost:3000',
|
|
171
|
+
reuseExistingServer: !process.env.CI,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### E2E Test Example
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
// e2e/users.e2e.ts
|
|
180
|
+
import { test, expect } from '@playwright/test';
|
|
181
|
+
|
|
182
|
+
test.describe('Users Page', () => {
|
|
183
|
+
test.beforeEach(async ({ page }) => {
|
|
184
|
+
await page.goto('/users');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('should display user list', async ({ page }) => {
|
|
188
|
+
await expect(page.getByRole('heading', { name: 'Users' })).toBeVisible();
|
|
189
|
+
await expect(page.getByTestId('user-list')).toBeVisible();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('should create new user', async ({ page }) => {
|
|
193
|
+
await page.getByRole('button', { name: 'Add User' }).click();
|
|
194
|
+
|
|
195
|
+
await page.getByLabel('Name').fill('John Doe');
|
|
196
|
+
await page.getByLabel('Email').fill('john@example.com');
|
|
197
|
+
await page.getByRole('button', { name: 'Submit' }).click();
|
|
198
|
+
|
|
199
|
+
await expect(page.getByText('John Doe')).toBeVisible();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('should filter users', async ({ page }) => {
|
|
203
|
+
await page.getByPlaceholder('Search...').fill('John');
|
|
204
|
+
|
|
205
|
+
await expect(page.getByTestId('user-card')).toHaveCount(1);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Page Object Pattern
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
// e2e/pages/users.page.ts
|
|
214
|
+
import { Page, Locator } from '@playwright/test';
|
|
215
|
+
|
|
216
|
+
export class UsersPage {
|
|
217
|
+
readonly page: Page;
|
|
218
|
+
readonly heading: Locator;
|
|
219
|
+
readonly userList: Locator;
|
|
220
|
+
readonly searchInput: Locator;
|
|
221
|
+
readonly addButton: Locator;
|
|
222
|
+
|
|
223
|
+
constructor(page: Page) {
|
|
224
|
+
this.page = page;
|
|
225
|
+
this.heading = page.getByRole('heading', { name: 'Users' });
|
|
226
|
+
this.userList = page.getByTestId('user-list');
|
|
227
|
+
this.searchInput = page.getByPlaceholder('Search...');
|
|
228
|
+
this.addButton = page.getByRole('button', { name: 'Add User' });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async goto(): Promise<void> {
|
|
232
|
+
await this.page.goto('/users');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async search(query: string): Promise<void> {
|
|
236
|
+
await this.searchInput.fill(query);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async addUser(name: string, email: string): Promise<void> {
|
|
240
|
+
await this.addButton.click();
|
|
241
|
+
await this.page.getByLabel('Name').fill(name);
|
|
242
|
+
await this.page.getByLabel('Email').fill(email);
|
|
243
|
+
await this.page.getByRole('button', { name: 'Submit' }).click();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Mocking
|
|
249
|
+
|
|
250
|
+
### Mocking Modules
|
|
251
|
+
|
|
252
|
+
```tsx
|
|
253
|
+
// Mock entire module
|
|
254
|
+
vi.mock('@/lib/api', () => ({
|
|
255
|
+
getUsers: vi.fn(),
|
|
256
|
+
createUser: vi.fn(),
|
|
257
|
+
}));
|
|
258
|
+
|
|
259
|
+
// Mock with implementation
|
|
260
|
+
import { getUsers } from '@/lib/api';
|
|
261
|
+
|
|
262
|
+
vi.mocked(getUsers).mockResolvedValue([
|
|
263
|
+
{ id: '1', name: 'John' },
|
|
264
|
+
]);
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Mocking Next.js
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
// Mock next/navigation
|
|
271
|
+
vi.mock('next/navigation', () => ({
|
|
272
|
+
useRouter: () => ({
|
|
273
|
+
push: vi.fn(),
|
|
274
|
+
replace: vi.fn(),
|
|
275
|
+
back: vi.fn(),
|
|
276
|
+
}),
|
|
277
|
+
usePathname: () => '/users',
|
|
278
|
+
useSearchParams: () => new URLSearchParams(),
|
|
279
|
+
}));
|
|
280
|
+
|
|
281
|
+
// Mock next/headers
|
|
282
|
+
vi.mock('next/headers', () => ({
|
|
283
|
+
cookies: () => ({
|
|
284
|
+
get: vi.fn(),
|
|
285
|
+
set: vi.fn(),
|
|
286
|
+
}),
|
|
287
|
+
headers: () => new Headers(),
|
|
288
|
+
}));
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Commands
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
# Run tests
|
|
295
|
+
nx test [project]
|
|
296
|
+
|
|
297
|
+
# Run with coverage
|
|
298
|
+
nx test [project] --coverage
|
|
299
|
+
|
|
300
|
+
# Run E2E
|
|
301
|
+
nx e2e [project]-e2e
|
|
302
|
+
|
|
303
|
+
# Run E2E with UI
|
|
304
|
+
nx e2e [project]-e2e --ui
|
|
305
|
+
|
|
306
|
+
# Run specific test
|
|
307
|
+
nx test [project] --testFile=user-card.test.tsx
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Coverage Expectations
|
|
311
|
+
|
|
312
|
+
- >80% coverage on business logic
|
|
313
|
+
- Test all Server Actions
|
|
314
|
+
- Test user interactions
|
|
315
|
+
- E2E for critical user flows
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(nx serve *)",
|
|
5
|
+
"Bash(nx dev *)",
|
|
6
|
+
"Bash(nx build *)",
|
|
7
|
+
"Bash(nx test *)",
|
|
8
|
+
"Bash(nx lint *)",
|
|
9
|
+
"Bash(nx run-many *)",
|
|
10
|
+
"Bash(nx affected *)",
|
|
11
|
+
"Bash(nx g *)",
|
|
12
|
+
"Bash(nx generate *)",
|
|
13
|
+
"Bash(npm run *)",
|
|
14
|
+
"Bash(npm install *)",
|
|
15
|
+
"Bash(npm ci)",
|
|
16
|
+
"Bash(npx nx *)",
|
|
17
|
+
"Read",
|
|
18
|
+
"Edit",
|
|
19
|
+
"Write"
|
|
20
|
+
],
|
|
21
|
+
"deny": [
|
|
22
|
+
"Bash(rm -rf *)",
|
|
23
|
+
"Bash(nx reset)",
|
|
24
|
+
"Read(.env)",
|
|
25
|
+
"Read(.env.*)",
|
|
26
|
+
"Read(**/secrets/**)"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# Next.js Project Guidelines
|
|
2
|
+
|
|
3
|
+
@../_shared/CLAUDE.md
|
|
4
|
+
|
|
5
|
+
## Stack
|
|
6
|
+
|
|
7
|
+
- Next.js 15+ (App Router)
|
|
8
|
+
- React 19+
|
|
9
|
+
- TypeScript strict mode
|
|
10
|
+
- Nx monorepo
|
|
11
|
+
|
|
12
|
+
## Architecture - Nx Structure
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
apps/
|
|
16
|
+
[app-name]/
|
|
17
|
+
app/ # App Router
|
|
18
|
+
(routes)/ # Route groups
|
|
19
|
+
users/
|
|
20
|
+
page.tsx
|
|
21
|
+
_components/ # Private (co-located)
|
|
22
|
+
user-list.tsx
|
|
23
|
+
products/
|
|
24
|
+
page.tsx
|
|
25
|
+
_components/
|
|
26
|
+
layout.tsx
|
|
27
|
+
error.tsx
|
|
28
|
+
loading.tsx
|
|
29
|
+
|
|
30
|
+
libs/
|
|
31
|
+
[domain]/
|
|
32
|
+
feature/ # Feature-specific logic
|
|
33
|
+
ui/ # Presentational components
|
|
34
|
+
data-access/ # API calls, server actions
|
|
35
|
+
util/ # Helpers
|
|
36
|
+
|
|
37
|
+
shared/
|
|
38
|
+
ui/ # Design system components
|
|
39
|
+
util/ # Shared utilities
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Folder Conventions
|
|
43
|
+
|
|
44
|
+
| Pattern | Meaning |
|
|
45
|
+
|---------|---------|
|
|
46
|
+
| `_folder/` | Private - not a route, co-located components |
|
|
47
|
+
| `(folder)/` | Route group - organizational, not in URL |
|
|
48
|
+
| `[param]/` | Dynamic segment |
|
|
49
|
+
| `[...param]/` | Catch-all segment |
|
|
50
|
+
| `[[...param]]/` | Optional catch-all |
|
|
51
|
+
|
|
52
|
+
## React 19 / Next.js 15 - Core Principles
|
|
53
|
+
|
|
54
|
+
### Server Components by Default
|
|
55
|
+
|
|
56
|
+
Components are Server Components unless marked with `'use client'`.
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
// Server Component (default) - runs on server
|
|
60
|
+
// Can: fetch data, access DB, use secrets
|
|
61
|
+
// Cannot: use hooks, browser APIs, event handlers
|
|
62
|
+
export default async function UsersPage() {
|
|
63
|
+
const users = await fetchUsers();
|
|
64
|
+
return <UserList users={users} />;
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Client Components
|
|
69
|
+
|
|
70
|
+
Add `'use client'` directive for interactivity:
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
'use client';
|
|
74
|
+
|
|
75
|
+
// Client Component - runs in browser
|
|
76
|
+
// Can: use hooks, event handlers, browser APIs
|
|
77
|
+
// Cannot: directly access DB, use secrets
|
|
78
|
+
import { useState } from 'react';
|
|
79
|
+
|
|
80
|
+
export function SearchInput({ onSearch }: Props) {
|
|
81
|
+
const [query, setQuery] = useState('');
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<input
|
|
85
|
+
value={query}
|
|
86
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
87
|
+
onKeyDown={(event) => event.key === 'Enter' && onSearch(query)}
|
|
88
|
+
/>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### When to Use Client Components
|
|
94
|
+
|
|
95
|
+
| Use Client Component | Use Server Component |
|
|
96
|
+
|---------------------|---------------------|
|
|
97
|
+
| `useState`, `useEffect`, hooks | Data fetching |
|
|
98
|
+
| Event handlers (`onClick`, etc.) | Database access |
|
|
99
|
+
| Browser APIs | Sensitive operations |
|
|
100
|
+
| Interactive UI (forms, modals) | Static content |
|
|
101
|
+
|
|
102
|
+
## Data Fetching
|
|
103
|
+
|
|
104
|
+
### Server Components (Preferred)
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
// app/users/page.tsx
|
|
108
|
+
async function getUsers(): Promise<User[]> {
|
|
109
|
+
const response = await fetch('https://api.example.com/users', {
|
|
110
|
+
cache: 'no-store', // Dynamic data
|
|
111
|
+
// cache: 'force-cache', // Static data (default)
|
|
112
|
+
// next: { revalidate: 60 }, // ISR - revalidate every 60s
|
|
113
|
+
});
|
|
114
|
+
return response.json();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export default async function UsersPage() {
|
|
118
|
+
const users = await getUsers();
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<ul>
|
|
122
|
+
{users.map((user) => (
|
|
123
|
+
<li key={user.id}>{user.name}</li>
|
|
124
|
+
))}
|
|
125
|
+
</ul>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Server Actions (Mutations)
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
// app/users/actions.ts
|
|
134
|
+
'use server';
|
|
135
|
+
|
|
136
|
+
import { revalidatePath } from 'next/cache';
|
|
137
|
+
|
|
138
|
+
export async function createUser(formData: FormData): Promise<void> {
|
|
139
|
+
const name = formData.get('name') as string;
|
|
140
|
+
|
|
141
|
+
await db.user.create({ data: { name } });
|
|
142
|
+
|
|
143
|
+
revalidatePath('/users');
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
// app/users/_components/user-form.tsx
|
|
149
|
+
'use client';
|
|
150
|
+
|
|
151
|
+
import { createUser } from '../actions';
|
|
152
|
+
|
|
153
|
+
export function UserForm() {
|
|
154
|
+
return (
|
|
155
|
+
<form action={createUser}>
|
|
156
|
+
<input name="name" required />
|
|
157
|
+
<button type="submit">Create User</button>
|
|
158
|
+
</form>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### React 19 Hooks for Data
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
'use client';
|
|
167
|
+
|
|
168
|
+
import { useActionState, useOptimistic } from 'react';
|
|
169
|
+
|
|
170
|
+
// useActionState - form submission state
|
|
171
|
+
const [state, formAction, isPending] = useActionState(createUser, initialState);
|
|
172
|
+
|
|
173
|
+
// useOptimistic - optimistic UI updates
|
|
174
|
+
const [optimisticItems, addOptimistic] = useOptimistic(
|
|
175
|
+
items,
|
|
176
|
+
(state, newItem) => [...state, newItem]
|
|
177
|
+
);
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Component Patterns
|
|
181
|
+
|
|
182
|
+
### Page Components (Server)
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
// app/users/page.tsx
|
|
186
|
+
import { UserList } from './_components/user-list';
|
|
187
|
+
import { getUsers } from '@/lib/api';
|
|
188
|
+
|
|
189
|
+
export default async function UsersPage() {
|
|
190
|
+
const users = await getUsers();
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<main>
|
|
194
|
+
<h1>Users</h1>
|
|
195
|
+
<UserList users={users} />
|
|
196
|
+
</main>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Interactive Components (Client)
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
// app/users/_components/user-list.tsx
|
|
205
|
+
'use client';
|
|
206
|
+
|
|
207
|
+
import { useState } from 'react';
|
|
208
|
+
import { UserCard } from './user-card';
|
|
209
|
+
|
|
210
|
+
interface UserListProps {
|
|
211
|
+
users: User[];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function UserList({ users }: UserListProps) {
|
|
215
|
+
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<ul>
|
|
219
|
+
{users.map((user) => (
|
|
220
|
+
<UserCard
|
|
221
|
+
key={user.id}
|
|
222
|
+
user={user}
|
|
223
|
+
isSelected={user.id === selectedId}
|
|
224
|
+
onSelect={() => setSelectedId(user.id)}
|
|
225
|
+
/>
|
|
226
|
+
))}
|
|
227
|
+
</ul>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Shared UI Components
|
|
233
|
+
|
|
234
|
+
```tsx
|
|
235
|
+
// libs/shared/ui/button.tsx
|
|
236
|
+
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
237
|
+
variant?: 'primary' | 'secondary';
|
|
238
|
+
isLoading?: boolean;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function Button({
|
|
242
|
+
variant = 'primary',
|
|
243
|
+
isLoading,
|
|
244
|
+
children,
|
|
245
|
+
disabled,
|
|
246
|
+
...props
|
|
247
|
+
}: ButtonProps) {
|
|
248
|
+
return (
|
|
249
|
+
<button
|
|
250
|
+
className={`btn btn-${variant}`}
|
|
251
|
+
disabled={disabled || isLoading}
|
|
252
|
+
{...props}
|
|
253
|
+
>
|
|
254
|
+
{isLoading ? 'Loading...' : children}
|
|
255
|
+
</button>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Build & Commands
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
# Development
|
|
264
|
+
nx serve [app-name]
|
|
265
|
+
nx dev [app-name]
|
|
266
|
+
|
|
267
|
+
# Build
|
|
268
|
+
nx build [app-name]
|
|
269
|
+
nx build [app-name] --configuration=production
|
|
270
|
+
|
|
271
|
+
# Test
|
|
272
|
+
nx test [lib-name]
|
|
273
|
+
nx run-many -t test
|
|
274
|
+
nx affected -t test
|
|
275
|
+
|
|
276
|
+
# Lint
|
|
277
|
+
nx lint [project-name]
|
|
278
|
+
nx run-many -t lint
|
|
279
|
+
|
|
280
|
+
# Generate
|
|
281
|
+
nx g @nx/next:component [name] --project=[app]
|
|
282
|
+
nx g @nx/react:component [name] --project=[lib]
|
|
283
|
+
nx g @nx/next:page [name] --project=[app]
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Code Style
|
|
287
|
+
|
|
288
|
+
### Component Files
|
|
289
|
+
|
|
290
|
+
- One component per file
|
|
291
|
+
- Named exports (not default) for reusable components
|
|
292
|
+
- Default export only for pages (`page.tsx`)
|
|
293
|
+
- Props interface above component
|
|
294
|
+
|
|
295
|
+
```tsx
|
|
296
|
+
// Good
|
|
297
|
+
interface UserCardProps {
|
|
298
|
+
user: User;
|
|
299
|
+
onSelect: () => void;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function UserCard({ user, onSelect }: UserCardProps) {
|
|
303
|
+
return (/* ... */);
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Naming Conventions
|
|
308
|
+
|
|
309
|
+
| Element | Convention | Example |
|
|
310
|
+
|---------|------------|---------|
|
|
311
|
+
| Components | PascalCase | `UserCard` |
|
|
312
|
+
| Files | kebab-case | `user-card.tsx` |
|
|
313
|
+
| Hooks | camelCase with `use` | `useUserData` |
|
|
314
|
+
| Server Actions | camelCase | `createUser` |
|
|
315
|
+
| Route folders | kebab-case | `user-profile/` |
|
|
316
|
+
|
|
317
|
+
### Imports Order
|
|
318
|
+
|
|
319
|
+
```tsx
|
|
320
|
+
// 1. React/Next
|
|
321
|
+
import { useState } from 'react';
|
|
322
|
+
import { useRouter } from 'next/navigation';
|
|
323
|
+
|
|
324
|
+
// 2. External libraries
|
|
325
|
+
import { z } from 'zod';
|
|
326
|
+
|
|
327
|
+
// 3. Internal libs (@/)
|
|
328
|
+
import { Button } from '@/components/ui/button';
|
|
329
|
+
import { getUsers } from '@/lib/api';
|
|
330
|
+
|
|
331
|
+
// 4. Relative imports
|
|
332
|
+
import { UserCard } from './user-card';
|
|
333
|
+
|
|
334
|
+
// 5. Types
|
|
335
|
+
import type { User } from '@/types';
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Error Handling
|
|
339
|
+
|
|
340
|
+
### Error Boundaries
|
|
341
|
+
|
|
342
|
+
```tsx
|
|
343
|
+
// app/users/error.tsx
|
|
344
|
+
'use client';
|
|
345
|
+
|
|
346
|
+
interface ErrorProps {
|
|
347
|
+
error: Error & { digest?: string };
|
|
348
|
+
reset: () => void;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export default function Error({ error, reset }: ErrorProps) {
|
|
352
|
+
return (
|
|
353
|
+
<div>
|
|
354
|
+
<h2>Something went wrong!</h2>
|
|
355
|
+
<button onClick={reset}>Try again</button>
|
|
356
|
+
</div>
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Loading States
|
|
362
|
+
|
|
363
|
+
```tsx
|
|
364
|
+
// app/users/loading.tsx
|
|
365
|
+
export default function Loading() {
|
|
366
|
+
return <div>Loading...</div>;
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
## Performance
|
|
371
|
+
|
|
372
|
+
- Use Server Components for data-heavy UI
|
|
373
|
+
- Lazy load heavy Client Components with `next/dynamic`
|
|
374
|
+
- Use `<Image>` from `next/image` for optimized images
|
|
375
|
+
- Use `<Link>` from `next/link` for client-side navigation
|
|
376
|
+
- Avoid `useEffect` for data fetching - use Server Components or `use()`
|