@codihaus/claude-skills 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/knowledge/domains/_index.md +105 -0
- package/knowledge/domains/ecommerce/_index.md +499 -0
- package/knowledge/domains/saas/_index.md +371 -0
- package/knowledge/stacks/_index.md +101 -0
- package/knowledge/stacks/directus/_index.md +349 -0
- package/knowledge/stacks/nextjs/_index.md +654 -0
- package/knowledge/stacks/nuxt/_index.md +469 -0
- package/package.json +3 -1
- package/project-scripts/graph.py +330 -0
- package/skills/_registry.md +61 -0
- package/skills/dev-coding/SKILL.md +16 -5
- package/skills/dev-coding-backend/SKILL.md +116 -251
- package/skills/dev-coding-frontend/SKILL.md +134 -388
- package/skills/dev-review/SKILL.md +13 -2
- package/skills/dev-scout/SKILL.md +180 -2
- package/skills/dev-scout/references/stack-patterns.md +371 -0
- package/skills/dev-specs/SKILL.md +74 -2
- package/src/commands/init.js +89 -12
- package/src/utils/project-setup.js +444 -0
- package/src/utils/skills.js +87 -1
- /package/{skills/dev-coding-frontend/references/nextjs.md → knowledge/stacks/nextjs/references/performance.md} +0 -0
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
# Next.js
|
|
2
|
+
|
|
3
|
+
> React meta-framework with SSR, App Router, and Server Components
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
**What it is:**
|
|
8
|
+
- Full-stack React framework
|
|
9
|
+
- Server-side rendering (SSR) and static generation (SSG)
|
|
10
|
+
- App Router with file-based routing (app/)
|
|
11
|
+
- React Server Components by default
|
|
12
|
+
- API routes and Server Actions
|
|
13
|
+
- Built-in image optimization
|
|
14
|
+
|
|
15
|
+
**When to use:**
|
|
16
|
+
- React applications needing SEO (SSR)
|
|
17
|
+
- Full-stack apps (frontend + API)
|
|
18
|
+
- Projects wanting convention over configuration
|
|
19
|
+
- Teams familiar with React ecosystem
|
|
20
|
+
|
|
21
|
+
**Key concepts:**
|
|
22
|
+
- **App Router** = File-based routes in `app/`
|
|
23
|
+
- **Server Components** = Default, run on server
|
|
24
|
+
- **Client Components** = Use `"use client"` directive
|
|
25
|
+
- **Server Actions** = Form handling with `"use server"`
|
|
26
|
+
- **Route Handlers** = API endpoints in `app/api/`
|
|
27
|
+
- **Middleware** = Edge functions for request interception
|
|
28
|
+
- **Layouts** = Shared UI in `layout.tsx`
|
|
29
|
+
|
|
30
|
+
## Best Practices
|
|
31
|
+
|
|
32
|
+
### Project Structure
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
nextjs-app/
|
|
36
|
+
├── app/ # App Router (Next.js 13+)
|
|
37
|
+
│ ├── (auth)/ # Route groups (no URL impact)
|
|
38
|
+
│ │ ├── login/
|
|
39
|
+
│ │ └── register/
|
|
40
|
+
│ ├── api/ # API routes
|
|
41
|
+
│ │ └── [route]/route.ts
|
|
42
|
+
│ ├── dashboard/
|
|
43
|
+
│ │ ├── page.tsx
|
|
44
|
+
│ │ └── layout.tsx
|
|
45
|
+
│ ├── layout.tsx # Root layout
|
|
46
|
+
│ ├── page.tsx # Home page
|
|
47
|
+
│ └── globals.css
|
|
48
|
+
├── components/ # React components
|
|
49
|
+
│ ├── ui/ # UI primitives
|
|
50
|
+
│ ├── forms/ # Form components
|
|
51
|
+
│ └── common/ # Shared components
|
|
52
|
+
├── lib/ # Utilities and configs
|
|
53
|
+
│ ├── actions/ # Server Actions
|
|
54
|
+
│ ├── api.ts # API client
|
|
55
|
+
│ ├── auth.ts # Auth utilities
|
|
56
|
+
│ └── utils.ts # Helpers
|
|
57
|
+
├── hooks/ # Custom React hooks
|
|
58
|
+
├── types/ # TypeScript types
|
|
59
|
+
├── public/ # Static assets
|
|
60
|
+
└── middleware.ts # Edge middleware
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Data Fetching
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
DO:
|
|
67
|
+
✓ Fetch in Server Components by default (no useEffect)
|
|
68
|
+
✓ Use async/await directly in components
|
|
69
|
+
✓ Use Server Actions for mutations
|
|
70
|
+
✓ Use fetch with caching options for external APIs
|
|
71
|
+
✓ Use React cache() for request deduplication
|
|
72
|
+
|
|
73
|
+
DON'T:
|
|
74
|
+
✗ Use useEffect for initial data in Server Components
|
|
75
|
+
✗ Forget revalidation strategy (stale data)
|
|
76
|
+
✗ Mix Server and Client data fetching unnecessarily
|
|
77
|
+
✗ Ignore loading.tsx and error.tsx
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Server vs Client Components
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
Server Components (default):
|
|
84
|
+
✓ Data fetching
|
|
85
|
+
✓ Accessing backend resources
|
|
86
|
+
✓ Sensitive logic (API keys, tokens)
|
|
87
|
+
✓ Large dependencies
|
|
88
|
+
|
|
89
|
+
Client Components ("use client"):
|
|
90
|
+
✓ Interactivity (onClick, onChange)
|
|
91
|
+
✓ State (useState, useReducer)
|
|
92
|
+
✓ Effects (useEffect)
|
|
93
|
+
✓ Browser APIs (localStorage, etc.)
|
|
94
|
+
✓ Custom hooks with state
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Server Actions
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
DO:
|
|
101
|
+
✓ Define with "use server" directive
|
|
102
|
+
✓ Use for form submissions and mutations
|
|
103
|
+
✓ Validate input with Zod
|
|
104
|
+
✓ Return typed responses
|
|
105
|
+
✓ Use revalidatePath/revalidateTag after mutations
|
|
106
|
+
|
|
107
|
+
DON'T:
|
|
108
|
+
✗ Use for read operations (use Server Components)
|
|
109
|
+
✗ Expose sensitive data in return values
|
|
110
|
+
✗ Skip input validation
|
|
111
|
+
✗ Forget error handling
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### API Routes (Route Handlers)
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
DO:
|
|
118
|
+
✓ Use app/api/ for REST endpoints
|
|
119
|
+
✓ Export named functions (GET, POST, PUT, DELETE)
|
|
120
|
+
✓ Use NextRequest/NextResponse
|
|
121
|
+
✓ Validate request body
|
|
122
|
+
✓ Return proper status codes
|
|
123
|
+
|
|
124
|
+
DON'T:
|
|
125
|
+
✗ Use for data consumed by own app (use Server Components)
|
|
126
|
+
✗ Skip authentication checks
|
|
127
|
+
✗ Return sensitive data without authorization
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### State Management
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
DO:
|
|
134
|
+
✓ Lift state up for shared client state
|
|
135
|
+
✓ Use React Context for global client state
|
|
136
|
+
✓ Use Server Components to avoid client state when possible
|
|
137
|
+
✓ Use URL state (searchParams) for shareable state
|
|
138
|
+
✓ Consider Zustand for complex client state
|
|
139
|
+
|
|
140
|
+
DON'T:
|
|
141
|
+
✗ Use Redux by default (often overkill with Server Components)
|
|
142
|
+
✗ Store server data in client state (refetch instead)
|
|
143
|
+
✗ Forget URL state for filters/pagination
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Anti-Patterns
|
|
147
|
+
|
|
148
|
+
| Anti-Pattern | Problem | Better Approach |
|
|
149
|
+
|--------------|---------|-----------------|
|
|
150
|
+
| useEffect for data | Waterfalls, spinners | Fetch in Server Component |
|
|
151
|
+
| "use client" everywhere | Loses RSC benefits | Only where needed |
|
|
152
|
+
| Huge Client Components | Large bundle | Split server/client |
|
|
153
|
+
| Fetching in layout.tsx | Blocks nested routes | Fetch in page.tsx or parallel |
|
|
154
|
+
| No loading.tsx | No loading UI | Add loading.tsx per route |
|
|
155
|
+
| Ignoring cache | Redundant fetches | Use fetch cache options |
|
|
156
|
+
| API routes for own data | Extra roundtrip | Server Components/Actions |
|
|
157
|
+
|
|
158
|
+
## For /dev-specs
|
|
159
|
+
|
|
160
|
+
### Page Specification
|
|
161
|
+
|
|
162
|
+
```markdown
|
|
163
|
+
## Page: /products/[id]
|
|
164
|
+
|
|
165
|
+
### Route
|
|
166
|
+
- Path: `/products/:id`
|
|
167
|
+
- File: `app/products/[id]/page.tsx`
|
|
168
|
+
- Layout: Uses `app/products/layout.tsx`
|
|
169
|
+
|
|
170
|
+
### Data Requirements
|
|
171
|
+
- Product details (Server Component fetch)
|
|
172
|
+
- Related products (parallel fetch)
|
|
173
|
+
|
|
174
|
+
### Components
|
|
175
|
+
| Component | Type | Purpose |
|
|
176
|
+
|-----------|------|---------|
|
|
177
|
+
| ProductGallery | Client | Image carousel with interactions |
|
|
178
|
+
| ProductInfo | Server | Static product details |
|
|
179
|
+
| AddToCart | Client | Cart interaction with state |
|
|
180
|
+
|
|
181
|
+
### Loading State
|
|
182
|
+
- `loading.tsx` with product skeleton
|
|
183
|
+
|
|
184
|
+
### SEO
|
|
185
|
+
- generateMetadata for dynamic title/description
|
|
186
|
+
- Open Graph images
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Server Action Specification
|
|
190
|
+
|
|
191
|
+
```markdown
|
|
192
|
+
## Action: addToCart
|
|
193
|
+
|
|
194
|
+
### Purpose
|
|
195
|
+
Add product to user's cart
|
|
196
|
+
|
|
197
|
+
### File
|
|
198
|
+
`lib/actions/cart.ts`
|
|
199
|
+
|
|
200
|
+
### Input
|
|
201
|
+
```typescript
|
|
202
|
+
{
|
|
203
|
+
productId: string,
|
|
204
|
+
quantity: number,
|
|
205
|
+
variant?: string,
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Validation
|
|
210
|
+
- Zod schema for input
|
|
211
|
+
- Check product exists
|
|
212
|
+
- Check stock availability
|
|
213
|
+
|
|
214
|
+
### Side Effects
|
|
215
|
+
- Update cart in database
|
|
216
|
+
- revalidatePath('/cart')
|
|
217
|
+
|
|
218
|
+
### Returns
|
|
219
|
+
```typescript
|
|
220
|
+
{ success: true, cartId: string }
|
|
221
|
+
| { success: false, error: string }
|
|
222
|
+
```
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### API Route Specification
|
|
226
|
+
|
|
227
|
+
```markdown
|
|
228
|
+
## API: POST /api/webhooks/stripe
|
|
229
|
+
|
|
230
|
+
### Purpose
|
|
231
|
+
Handle Stripe webhook events
|
|
232
|
+
|
|
233
|
+
### File
|
|
234
|
+
`app/api/webhooks/stripe/route.ts`
|
|
235
|
+
|
|
236
|
+
### Authentication
|
|
237
|
+
- Verify Stripe signature
|
|
238
|
+
- No user auth (webhook)
|
|
239
|
+
|
|
240
|
+
### Events Handled
|
|
241
|
+
| Event | Action |
|
|
242
|
+
|-------|--------|
|
|
243
|
+
| checkout.session.completed | Create order |
|
|
244
|
+
| payment_intent.failed | Update order status |
|
|
245
|
+
|
|
246
|
+
### Response
|
|
247
|
+
- 200: Event processed
|
|
248
|
+
- 400: Invalid payload
|
|
249
|
+
- 401: Invalid signature
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## For /dev-coding
|
|
253
|
+
|
|
254
|
+
### Server Component with Data Fetching
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
// app/products/[id]/page.tsx
|
|
258
|
+
import { notFound } from 'next/navigation';
|
|
259
|
+
import { getProduct, getRelatedProducts } from '@/lib/api';
|
|
260
|
+
|
|
261
|
+
interface Props {
|
|
262
|
+
params: Promise<{ id: string }>;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export async function generateMetadata({ params }: Props) {
|
|
266
|
+
const { id } = await params;
|
|
267
|
+
const product = await getProduct(id);
|
|
268
|
+
|
|
269
|
+
if (!product) return { title: 'Not Found' };
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
title: product.name,
|
|
273
|
+
description: product.description,
|
|
274
|
+
openGraph: {
|
|
275
|
+
images: [product.image],
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export default async function ProductPage({ params }: Props) {
|
|
281
|
+
const { id } = await params;
|
|
282
|
+
|
|
283
|
+
// Parallel fetching
|
|
284
|
+
const [product, related] = await Promise.all([
|
|
285
|
+
getProduct(id),
|
|
286
|
+
getRelatedProducts(id),
|
|
287
|
+
]);
|
|
288
|
+
|
|
289
|
+
if (!product) notFound();
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<div>
|
|
293
|
+
<h1>{product.name}</h1>
|
|
294
|
+
<p>{product.price}</p>
|
|
295
|
+
<AddToCartButton productId={id} />
|
|
296
|
+
<RelatedProducts products={related} />
|
|
297
|
+
</div>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Client Component
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
// components/AddToCartButton.tsx
|
|
306
|
+
"use client";
|
|
307
|
+
|
|
308
|
+
import { useState, useTransition } from 'react';
|
|
309
|
+
import { addToCart } from '@/lib/actions/cart';
|
|
310
|
+
|
|
311
|
+
interface Props {
|
|
312
|
+
productId: string;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function AddToCartButton({ productId }: Props) {
|
|
316
|
+
const [isPending, startTransition] = useTransition();
|
|
317
|
+
const [error, setError] = useState<string | null>(null);
|
|
318
|
+
|
|
319
|
+
const handleClick = () => {
|
|
320
|
+
setError(null);
|
|
321
|
+
startTransition(async () => {
|
|
322
|
+
const result = await addToCart({ productId, quantity: 1 });
|
|
323
|
+
if (!result.success) {
|
|
324
|
+
setError(result.error);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
return (
|
|
330
|
+
<>
|
|
331
|
+
<button onClick={handleClick} disabled={isPending}>
|
|
332
|
+
{isPending ? 'Adding...' : 'Add to Cart'}
|
|
333
|
+
</button>
|
|
334
|
+
{error && <p className="text-red-500">{error}</p>}
|
|
335
|
+
</>
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Server Action
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
// lib/actions/cart.ts
|
|
344
|
+
"use server";
|
|
345
|
+
|
|
346
|
+
import { z } from 'zod';
|
|
347
|
+
import { revalidatePath } from 'next/cache';
|
|
348
|
+
import { auth } from '@/lib/auth';
|
|
349
|
+
import { db } from '@/lib/db';
|
|
350
|
+
|
|
351
|
+
const addToCartSchema = z.object({
|
|
352
|
+
productId: z.string().min(1),
|
|
353
|
+
quantity: z.number().int().positive(),
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
type AddToCartInput = z.infer<typeof addToCartSchema>;
|
|
357
|
+
type AddToCartResult =
|
|
358
|
+
| { success: true; cartId: string }
|
|
359
|
+
| { success: false; error: string };
|
|
360
|
+
|
|
361
|
+
export async function addToCart(input: AddToCartInput): Promise<AddToCartResult> {
|
|
362
|
+
// Validate input
|
|
363
|
+
const parsed = addToCartSchema.safeParse(input);
|
|
364
|
+
if (!parsed.success) {
|
|
365
|
+
return { success: false, error: 'Invalid input' };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Check auth
|
|
369
|
+
const session = await auth();
|
|
370
|
+
if (!session?.user) {
|
|
371
|
+
return { success: false, error: 'Please sign in' };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
// Add to cart
|
|
376
|
+
const cart = await db.cart.upsert({
|
|
377
|
+
where: { userId: session.user.id },
|
|
378
|
+
create: {
|
|
379
|
+
userId: session.user.id,
|
|
380
|
+
items: { create: parsed.data },
|
|
381
|
+
},
|
|
382
|
+
update: {
|
|
383
|
+
items: { create: parsed.data },
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
revalidatePath('/cart');
|
|
388
|
+
return { success: true, cartId: cart.id };
|
|
389
|
+
} catch (error) {
|
|
390
|
+
return { success: false, error: 'Failed to add to cart' };
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### API Route Handler
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
// app/api/products/route.ts
|
|
399
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
400
|
+
import { z } from 'zod';
|
|
401
|
+
import { auth } from '@/lib/auth';
|
|
402
|
+
import { db } from '@/lib/db';
|
|
403
|
+
|
|
404
|
+
const createProductSchema = z.object({
|
|
405
|
+
name: z.string().min(1),
|
|
406
|
+
price: z.number().positive(),
|
|
407
|
+
description: z.string().optional(),
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
export async function GET(request: NextRequest) {
|
|
411
|
+
const searchParams = request.nextUrl.searchParams;
|
|
412
|
+
const limit = parseInt(searchParams.get('limit') || '10');
|
|
413
|
+
const offset = parseInt(searchParams.get('offset') || '0');
|
|
414
|
+
|
|
415
|
+
const products = await db.product.findMany({
|
|
416
|
+
take: limit,
|
|
417
|
+
skip: offset,
|
|
418
|
+
orderBy: { createdAt: 'desc' },
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
return NextResponse.json({ data: products });
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export async function POST(request: NextRequest) {
|
|
425
|
+
// Check auth
|
|
426
|
+
const session = await auth();
|
|
427
|
+
if (!session?.user?.isAdmin) {
|
|
428
|
+
return NextResponse.json(
|
|
429
|
+
{ error: 'Unauthorized' },
|
|
430
|
+
{ status: 401 }
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Validate body
|
|
435
|
+
const body = await request.json();
|
|
436
|
+
const parsed = createProductSchema.safeParse(body);
|
|
437
|
+
|
|
438
|
+
if (!parsed.success) {
|
|
439
|
+
return NextResponse.json(
|
|
440
|
+
{ error: 'Invalid input', details: parsed.error.flatten() },
|
|
441
|
+
{ status: 400 }
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Create product
|
|
446
|
+
const product = await db.product.create({
|
|
447
|
+
data: parsed.data,
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
return NextResponse.json({ data: product }, { status: 201 });
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Middleware (Auth)
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
// middleware.ts
|
|
458
|
+
import { NextResponse } from 'next/server';
|
|
459
|
+
import type { NextRequest } from 'next/server';
|
|
460
|
+
import { auth } from '@/lib/auth';
|
|
461
|
+
|
|
462
|
+
export async function middleware(request: NextRequest) {
|
|
463
|
+
const session = await auth();
|
|
464
|
+
|
|
465
|
+
// Protect dashboard routes
|
|
466
|
+
if (request.nextUrl.pathname.startsWith('/dashboard')) {
|
|
467
|
+
if (!session) {
|
|
468
|
+
return NextResponse.redirect(new URL('/login', request.url));
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Protect admin routes
|
|
473
|
+
if (request.nextUrl.pathname.startsWith('/admin')) {
|
|
474
|
+
if (!session?.user?.isAdmin) {
|
|
475
|
+
return NextResponse.redirect(new URL('/unauthorized', request.url));
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return NextResponse.next();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export const config = {
|
|
483
|
+
matcher: ['/dashboard/:path*', '/admin/:path*'],
|
|
484
|
+
};
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### Loading and Error States
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
// app/products/[id]/loading.tsx
|
|
491
|
+
export default function Loading() {
|
|
492
|
+
return (
|
|
493
|
+
<div className="animate-pulse">
|
|
494
|
+
<div className="h-8 bg-gray-200 rounded w-1/4 mb-4" />
|
|
495
|
+
<div className="h-4 bg-gray-200 rounded w-1/2 mb-2" />
|
|
496
|
+
<div className="h-64 bg-gray-200 rounded" />
|
|
497
|
+
</div>
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// app/products/[id]/error.tsx
|
|
502
|
+
"use client";
|
|
503
|
+
|
|
504
|
+
interface Props {
|
|
505
|
+
error: Error & { digest?: string };
|
|
506
|
+
reset: () => void;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export default function Error({ error, reset }: Props) {
|
|
510
|
+
return (
|
|
511
|
+
<div className="text-center py-10">
|
|
512
|
+
<h2>Something went wrong!</h2>
|
|
513
|
+
<p className="text-gray-500">{error.message}</p>
|
|
514
|
+
<button onClick={reset} className="mt-4 btn">
|
|
515
|
+
Try again
|
|
516
|
+
</button>
|
|
517
|
+
</div>
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
## For /dev-review
|
|
523
|
+
|
|
524
|
+
### Server Component Checklist
|
|
525
|
+
|
|
526
|
+
- [ ] Data fetching in Server Components (not useEffect)
|
|
527
|
+
- [ ] Parallel fetching with Promise.all
|
|
528
|
+
- [ ] Proper loading.tsx for each route segment
|
|
529
|
+
- [ ] error.tsx for error boundaries
|
|
530
|
+
- [ ] not-found.tsx for 404 handling
|
|
531
|
+
- [ ] generateMetadata for SEO
|
|
532
|
+
|
|
533
|
+
### Client Component Checklist
|
|
534
|
+
|
|
535
|
+
- [ ] "use client" only where needed
|
|
536
|
+
- [ ] Minimal bundle size (no large deps)
|
|
537
|
+
- [ ] useTransition for non-blocking updates
|
|
538
|
+
- [ ] Proper error handling in actions
|
|
539
|
+
- [ ] Loading states during transitions
|
|
540
|
+
|
|
541
|
+
### Security Checklist
|
|
542
|
+
|
|
543
|
+
- [ ] Server Actions validate input
|
|
544
|
+
- [ ] API routes check authorization
|
|
545
|
+
- [ ] Middleware protects routes
|
|
546
|
+
- [ ] No secrets exposed to client
|
|
547
|
+
- [ ] CSRF protection on mutations
|
|
548
|
+
- [ ] Rate limiting on API routes
|
|
549
|
+
|
|
550
|
+
### Performance Checklist
|
|
551
|
+
|
|
552
|
+
- [ ] Images use next/image
|
|
553
|
+
- [ ] Dynamic imports for heavy components
|
|
554
|
+
- [ ] Route segments have loading.tsx
|
|
555
|
+
- [ ] Proper cache headers on fetch
|
|
556
|
+
- [ ] Static generation where possible
|
|
557
|
+
|
|
558
|
+
### Common Mistakes
|
|
559
|
+
|
|
560
|
+
| Mistake | Impact | Fix |
|
|
561
|
+
|---------|--------|-----|
|
|
562
|
+
| useEffect for data | Waterfalls | Server Component fetch |
|
|
563
|
+
| "use client" on page | Loses SSR | Keep pages as Server Components |
|
|
564
|
+
| No loading.tsx | No loading UI | Add per route segment |
|
|
565
|
+
| Fetching in layout | Blocks children | Fetch in page or parallel |
|
|
566
|
+
| Large Client Components | Big bundle | Split and lazy load |
|
|
567
|
+
| No error boundary | Crashes page | Add error.tsx |
|
|
568
|
+
|
|
569
|
+
## Integration
|
|
570
|
+
|
|
571
|
+
### With Directus
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
// lib/directus.ts
|
|
575
|
+
import { createDirectus, rest, authentication } from '@directus/sdk';
|
|
576
|
+
|
|
577
|
+
const directus = createDirectus<Schema>(process.env.DIRECTUS_URL!)
|
|
578
|
+
.with(rest())
|
|
579
|
+
.with(authentication());
|
|
580
|
+
|
|
581
|
+
export default directus;
|
|
582
|
+
|
|
583
|
+
// lib/api/products.ts
|
|
584
|
+
import directus from '@/lib/directus';
|
|
585
|
+
import { readItems, readItem } from '@directus/sdk';
|
|
586
|
+
|
|
587
|
+
export async function getProducts() {
|
|
588
|
+
return directus.request(
|
|
589
|
+
readItems('products', {
|
|
590
|
+
filter: { status: { _eq: 'published' } },
|
|
591
|
+
sort: ['-date_created'],
|
|
592
|
+
})
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
export async function getProduct(id: string) {
|
|
597
|
+
return directus.request(readItem('products', id));
|
|
598
|
+
}
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
### With NextAuth.js
|
|
602
|
+
|
|
603
|
+
```typescript
|
|
604
|
+
// lib/auth.ts
|
|
605
|
+
import NextAuth from 'next-auth';
|
|
606
|
+
import GitHub from 'next-auth/providers/github';
|
|
607
|
+
|
|
608
|
+
export const { handlers, auth, signIn, signOut } = NextAuth({
|
|
609
|
+
providers: [GitHub],
|
|
610
|
+
callbacks: {
|
|
611
|
+
session({ session, token }) {
|
|
612
|
+
if (token.sub) session.user.id = token.sub;
|
|
613
|
+
return session;
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
// app/api/auth/[...nextauth]/route.ts
|
|
619
|
+
import { handlers } from '@/lib/auth';
|
|
620
|
+
export const { GET, POST } = handlers;
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
### With Prisma
|
|
624
|
+
|
|
625
|
+
```typescript
|
|
626
|
+
// lib/db.ts
|
|
627
|
+
import { PrismaClient } from '@prisma/client';
|
|
628
|
+
|
|
629
|
+
const globalForPrisma = globalThis as unknown as {
|
|
630
|
+
prisma: PrismaClient | undefined;
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
export const db = globalForPrisma.prisma ?? new PrismaClient();
|
|
634
|
+
|
|
635
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
636
|
+
globalForPrisma.prisma = db;
|
|
637
|
+
}
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
## Resources
|
|
641
|
+
|
|
642
|
+
### Official Docs
|
|
643
|
+
- Docs: https://nextjs.org/docs
|
|
644
|
+
- App Router: https://nextjs.org/docs/app
|
|
645
|
+
- Learn: https://nextjs.org/learn
|
|
646
|
+
|
|
647
|
+
### Context7 Queries
|
|
648
|
+
|
|
649
|
+
```
|
|
650
|
+
Query: "Next.js 15 App Router data fetching"
|
|
651
|
+
Query: "Next.js Server Actions best practices"
|
|
652
|
+
Query: "Next.js middleware authentication"
|
|
653
|
+
Query: "Next.js Server Components patterns"
|
|
654
|
+
```
|