1125 lines
27 KiB
Markdown
1125 lines
27 KiB
Markdown
# SiliconPin Code Patterns and Examples
|
|
|
|
This document provides common patterns and examples for extending the SiliconPin platform.
|
|
|
|
## Table of Contents
|
|
|
|
- [Authentication Patterns](#authentication-patterns)
|
|
- [API Route Patterns](#api-route-patterns)
|
|
- [Database Patterns](#database-patterns)
|
|
- [Form Handling](#form-handling)
|
|
- [Component Patterns](#component-patterns)
|
|
- [State Management](#state-management)
|
|
- [Error Handling](#error-handling)
|
|
|
|
## Authentication Patterns
|
|
|
|
### Using Authentication in Components
|
|
|
|
```typescript
|
|
// components/ProfileCard.tsx
|
|
'use client';
|
|
|
|
import { useAuth } from '@/contexts/AuthContext';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
|
|
export function ProfileCard() {
|
|
const { user, isLoading, logout } = useAuth();
|
|
|
|
if (isLoading) {
|
|
return <div>Loading...</div>;
|
|
}
|
|
|
|
if (!user) {
|
|
return <div>Please log in to view your profile.</div>;
|
|
}
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Welcome, {user.name}!</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p>Email: {user.email}</p>
|
|
<p>Member since: {new Date(user.createdAt).toLocaleDateString()}</p>
|
|
<Button onClick={logout} variant="outline">
|
|
Sign Out
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Protected Route Component
|
|
|
|
```typescript
|
|
// components/ProtectedRoute.tsx
|
|
'use client';
|
|
|
|
import { useAuth } from '@/contexts/AuthContext';
|
|
import { useRouter } from 'next/navigation';
|
|
import { useEffect } from 'react';
|
|
|
|
interface ProtectedRouteProps {
|
|
children: React.ReactNode;
|
|
redirectTo?: string;
|
|
}
|
|
|
|
export function ProtectedRoute({
|
|
children,
|
|
redirectTo = '/auth'
|
|
}: ProtectedRouteProps) {
|
|
const { user, isLoading } = useAuth();
|
|
const router = useRouter();
|
|
|
|
useEffect(() => {
|
|
if (!isLoading && !user) {
|
|
router.push(redirectTo);
|
|
}
|
|
}, [user, isLoading, router, redirectTo]);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
<div>Loading...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!user) {
|
|
return null; // Will redirect
|
|
}
|
|
|
|
return <>{children}</>;
|
|
}
|
|
```
|
|
|
|
## API Route Patterns
|
|
|
|
### Basic CRUD API Route
|
|
|
|
```typescript
|
|
// app/api/posts/route.ts
|
|
import { authMiddleware } from '@/lib/auth-middleware'
|
|
import { connectDB } from '@/lib/mongodb'
|
|
import { Post } from '@/models/post'
|
|
import { z } from 'zod'
|
|
|
|
const CreatePostSchema = z.object({
|
|
title: z.string().min(1, 'Title is required'),
|
|
content: z.string().min(10, 'Content must be at least 10 characters'),
|
|
tags: z.array(z.string()).optional(),
|
|
})
|
|
|
|
// GET /api/posts - Get all posts
|
|
export async function GET(request: Request) {
|
|
try {
|
|
await connectDB()
|
|
|
|
const url = new URL(request.url)
|
|
const page = parseInt(url.searchParams.get('page') || '1')
|
|
const limit = parseInt(url.searchParams.get('limit') || '10')
|
|
const skip = (page - 1) * limit
|
|
|
|
const posts = await Post.find()
|
|
.sort({ createdAt: -1 })
|
|
.skip(skip)
|
|
.limit(limit)
|
|
.populate('author', 'name email')
|
|
|
|
const total = await Post.countDocuments()
|
|
|
|
return Response.json({
|
|
success: true,
|
|
data: {
|
|
posts,
|
|
pagination: {
|
|
page,
|
|
limit,
|
|
total,
|
|
pages: Math.ceil(total / limit),
|
|
},
|
|
},
|
|
})
|
|
} catch (error) {
|
|
return Response.json({ success: false, error: 'Failed to fetch posts' }, { status: 500 })
|
|
}
|
|
}
|
|
|
|
// POST /api/posts - Create new post
|
|
export async function POST(request: Request) {
|
|
try {
|
|
const user = await authMiddleware(request)
|
|
if (!user) {
|
|
return Response.json({ success: false, error: 'Authentication required' }, { status: 401 })
|
|
}
|
|
|
|
const body = await request.json()
|
|
const validatedData = CreatePostSchema.parse(body)
|
|
|
|
await connectDB()
|
|
|
|
const post = new Post({
|
|
...validatedData,
|
|
author: user.id,
|
|
})
|
|
|
|
await post.save()
|
|
await post.populate('author', 'name email')
|
|
|
|
return Response.json(
|
|
{
|
|
success: true,
|
|
data: { post },
|
|
message: 'Post created successfully',
|
|
},
|
|
{ status: 201 }
|
|
)
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
return Response.json(
|
|
{
|
|
success: false,
|
|
error: 'Validation failed',
|
|
details: error.errors.map((err) => ({
|
|
field: err.path.join('.'),
|
|
message: err.message,
|
|
})),
|
|
},
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
return Response.json({ success: false, error: 'Failed to create post' }, { status: 500 })
|
|
}
|
|
}
|
|
```
|
|
|
|
### Dynamic API Route with ID
|
|
|
|
```typescript
|
|
// app/api/posts/[id]/route.ts
|
|
import { authMiddleware } from '@/lib/auth-middleware'
|
|
import { connectDB } from '@/lib/mongodb'
|
|
import { Post } from '@/models/post'
|
|
import mongoose from 'mongoose'
|
|
|
|
interface RouteParams {
|
|
params: {
|
|
id: string
|
|
}
|
|
}
|
|
|
|
// GET /api/posts/[id] - Get single post
|
|
export async function GET(request: Request, { params }: RouteParams) {
|
|
try {
|
|
if (!mongoose.Types.ObjectId.isValid(params.id)) {
|
|
return Response.json({ success: false, error: 'Invalid post ID' }, { status: 400 })
|
|
}
|
|
|
|
await connectDB()
|
|
|
|
const post = await Post.findById(params.id).populate('author', 'name email')
|
|
|
|
if (!post) {
|
|
return Response.json({ success: false, error: 'Post not found' }, { status: 404 })
|
|
}
|
|
|
|
return Response.json({
|
|
success: true,
|
|
data: { post },
|
|
})
|
|
} catch (error) {
|
|
return Response.json({ success: false, error: 'Failed to fetch post' }, { status: 500 })
|
|
}
|
|
}
|
|
|
|
// DELETE /api/posts/[id] - Delete post
|
|
export async function DELETE(request: Request, { params }: RouteParams) {
|
|
try {
|
|
const user = await authMiddleware(request)
|
|
if (!user) {
|
|
return Response.json({ success: false, error: 'Authentication required' }, { status: 401 })
|
|
}
|
|
|
|
if (!mongoose.Types.ObjectId.isValid(params.id)) {
|
|
return Response.json({ success: false, error: 'Invalid post ID' }, { status: 400 })
|
|
}
|
|
|
|
await connectDB()
|
|
|
|
const post = await Post.findById(params.id)
|
|
if (!post) {
|
|
return Response.json({ success: false, error: 'Post not found' }, { status: 404 })
|
|
}
|
|
|
|
// Check if user owns the post
|
|
if (post.author.toString() !== user.id) {
|
|
return Response.json(
|
|
{ success: false, error: 'Not authorized to delete this post' },
|
|
{ status: 403 }
|
|
)
|
|
}
|
|
|
|
await Post.findByIdAndDelete(params.id)
|
|
|
|
return Response.json({
|
|
success: true,
|
|
message: 'Post deleted successfully',
|
|
})
|
|
} catch (error) {
|
|
return Response.json({ success: false, error: 'Failed to delete post' }, { status: 500 })
|
|
}
|
|
}
|
|
```
|
|
|
|
## Database Patterns
|
|
|
|
### Mongoose Model with Validation
|
|
|
|
```typescript
|
|
// models/post.ts
|
|
import mongoose, { Document, Schema } from 'mongoose'
|
|
import { z } from 'zod'
|
|
|
|
// Zod schema for validation
|
|
export const PostValidationSchema = z.object({
|
|
title: z.string().min(1, 'Title is required').max(200, 'Title too long'),
|
|
content: z.string().min(10, 'Content must be at least 10 characters'),
|
|
tags: z.array(z.string()).max(10, 'Maximum 10 tags allowed').optional(),
|
|
published: z.boolean().default(false),
|
|
})
|
|
|
|
// TypeScript interface
|
|
export interface IPost extends Document {
|
|
title: string
|
|
content: string
|
|
tags?: string[]
|
|
published: boolean
|
|
author: mongoose.Types.ObjectId
|
|
createdAt: Date
|
|
updatedAt: Date
|
|
}
|
|
|
|
// Mongoose schema
|
|
const PostSchema = new Schema<IPost>(
|
|
{
|
|
title: {
|
|
type: String,
|
|
required: [true, 'Title is required'],
|
|
trim: true,
|
|
maxlength: [200, 'Title cannot exceed 200 characters'],
|
|
},
|
|
content: {
|
|
type: String,
|
|
required: [true, 'Content is required'],
|
|
minlength: [10, 'Content must be at least 10 characters'],
|
|
},
|
|
tags: [
|
|
{
|
|
type: String,
|
|
trim: true,
|
|
lowercase: true,
|
|
},
|
|
],
|
|
published: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
author: {
|
|
type: Schema.Types.ObjectId,
|
|
ref: 'User',
|
|
required: true,
|
|
},
|
|
},
|
|
{
|
|
timestamps: true,
|
|
toJSON: { virtuals: true },
|
|
toObject: { virtuals: true },
|
|
}
|
|
)
|
|
|
|
// Indexes for performance
|
|
PostSchema.index({ author: 1, createdAt: -1 })
|
|
PostSchema.index({ published: 1, createdAt: -1 })
|
|
PostSchema.index({ tags: 1 })
|
|
|
|
// Virtual for excerpt
|
|
PostSchema.virtual('excerpt').get(function () {
|
|
return this.content.length > 150 ? this.content.substring(0, 150) + '...' : this.content
|
|
})
|
|
|
|
export const Post = mongoose.models.Post || mongoose.model<IPost>('Post', PostSchema)
|
|
```
|
|
|
|
### Database Connection with Error Handling
|
|
|
|
```typescript
|
|
// lib/mongodb.ts (enhanced)
|
|
import mongoose from 'mongoose'
|
|
|
|
const MONGODB_URI = process.env.MONGODB_URI
|
|
|
|
if (!MONGODB_URI) {
|
|
throw new Error('Please define the MONGODB_URI environment variable')
|
|
}
|
|
|
|
interface CachedConnection {
|
|
conn: typeof mongoose | null
|
|
promise: Promise<typeof mongoose> | null
|
|
}
|
|
|
|
// Cache connection in development to avoid connection limit
|
|
declare global {
|
|
var mongoose: CachedConnection | undefined
|
|
}
|
|
|
|
let cached: CachedConnection = global.mongoose || { conn: null, promise: null }
|
|
|
|
if (!global.mongoose) {
|
|
global.mongoose = cached
|
|
}
|
|
|
|
export async function connectDB(): Promise<typeof mongoose> {
|
|
if (cached.conn) {
|
|
return cached.conn
|
|
}
|
|
|
|
if (!cached.promise) {
|
|
const opts = {
|
|
bufferCommands: false,
|
|
maxPoolSize: 10,
|
|
serverSelectionTimeoutMS: 5000,
|
|
socketTimeoutMS: 45000,
|
|
family: 4, // Use IPv4, skip trying IPv6
|
|
}
|
|
|
|
cached.promise = mongoose.connect(MONGODB_URI!, opts)
|
|
}
|
|
|
|
try {
|
|
cached.conn = await cached.promise
|
|
console.log('MongoDB connected successfully')
|
|
return cached.conn
|
|
} catch (error) {
|
|
cached.promise = null
|
|
console.error('MongoDB connection error:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
// Graceful shutdown
|
|
process.on('SIGINT', async () => {
|
|
if (cached.conn) {
|
|
await cached.conn.disconnect()
|
|
console.log('MongoDB disconnected through app termination')
|
|
process.exit(0)
|
|
}
|
|
})
|
|
```
|
|
|
|
## Form Handling
|
|
|
|
### Form with Validation and Error Handling
|
|
|
|
```typescript
|
|
// components/CreatePostForm.tsx
|
|
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useForm } from 'react-hook-form';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { z } from 'zod';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
|
|
const CreatePostSchema = z.object({
|
|
title: z.string().min(1, 'Title is required').max(200, 'Title too long'),
|
|
content: z.string().min(10, 'Content must be at least 10 characters'),
|
|
tags: z.string().optional(),
|
|
});
|
|
|
|
type CreatePostForm = z.infer<typeof CreatePostSchema>;
|
|
|
|
export function CreatePostForm() {
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const queryClient = useQueryClient();
|
|
|
|
const form = useForm<CreatePostForm>({
|
|
resolver: zodResolver(CreatePostSchema),
|
|
defaultValues: {
|
|
title: '',
|
|
content: '',
|
|
tags: '',
|
|
},
|
|
});
|
|
|
|
const createPostMutation = useMutation({
|
|
mutationFn: async (data: CreatePostForm) => {
|
|
const response = await fetch('/api/posts', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify({
|
|
...data,
|
|
tags: data.tags?.split(',').map(tag => tag.trim()).filter(Boolean),
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.error || 'Failed to create post');
|
|
}
|
|
|
|
return response.json();
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['posts'] });
|
|
form.reset();
|
|
},
|
|
});
|
|
|
|
const onSubmit = async (data: CreatePostForm) => {
|
|
try {
|
|
setIsSubmitting(true);
|
|
await createPostMutation.mutateAsync(data);
|
|
} catch (error) {
|
|
console.error('Error creating post:', error);
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Create New Post</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
<div>
|
|
<Label htmlFor="title">Title</Label>
|
|
<Input
|
|
id="title"
|
|
{...form.register('title')}
|
|
className={form.formState.errors.title ? 'border-red-500' : ''}
|
|
/>
|
|
{form.formState.errors.title && (
|
|
<p className="text-red-500 text-sm mt-1">
|
|
{form.formState.errors.title.message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="content">Content</Label>
|
|
<Textarea
|
|
id="content"
|
|
rows={8}
|
|
{...form.register('content')}
|
|
className={form.formState.errors.content ? 'border-red-500' : ''}
|
|
/>
|
|
{form.formState.errors.content && (
|
|
<p className="text-red-500 text-sm mt-1">
|
|
{form.formState.errors.content.message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="tags">Tags (comma-separated)</Label>
|
|
<Input
|
|
id="tags"
|
|
placeholder="react, nextjs, typescript"
|
|
{...form.register('tags')}
|
|
/>
|
|
</div>
|
|
|
|
{createPostMutation.error && (
|
|
<div className="text-red-500 text-sm">
|
|
Error: {createPostMutation.error.message}
|
|
</div>
|
|
)}
|
|
|
|
<Button
|
|
type="submit"
|
|
disabled={isSubmitting || createPostMutation.isPending}
|
|
className="w-full"
|
|
>
|
|
{isSubmitting || createPostMutation.isPending ? 'Creating...' : 'Create Post'}
|
|
</Button>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
```
|
|
|
|
## Component Patterns
|
|
|
|
### Data Fetching Component
|
|
|
|
```typescript
|
|
// components/PostsList.tsx
|
|
'use client';
|
|
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
|
|
interface Post {
|
|
id: string;
|
|
title: string;
|
|
excerpt: string;
|
|
author: {
|
|
name: string;
|
|
};
|
|
createdAt: string;
|
|
tags: string[];
|
|
}
|
|
|
|
interface PostsResponse {
|
|
success: boolean;
|
|
data: {
|
|
posts: Post[];
|
|
pagination: {
|
|
page: number;
|
|
total: number;
|
|
pages: number;
|
|
};
|
|
};
|
|
}
|
|
|
|
interface PostsListProps {
|
|
page?: number;
|
|
limit?: number;
|
|
}
|
|
|
|
export function PostsList({ page = 1, limit = 10 }: PostsListProps) {
|
|
const {
|
|
data,
|
|
isLoading,
|
|
error,
|
|
refetch,
|
|
} = useQuery<PostsResponse>({
|
|
queryKey: ['posts', page, limit],
|
|
queryFn: async () => {
|
|
const response = await fetch(
|
|
`/api/posts?page=${page}&limit=${limit}`,
|
|
{ credentials: 'include' }
|
|
);
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to fetch posts');
|
|
}
|
|
|
|
return response.json();
|
|
},
|
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
});
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="space-y-4">
|
|
{Array.from({ length: 3 }).map((_, i) => (
|
|
<Card key={i}>
|
|
<CardHeader>
|
|
<Skeleton className="h-6 w-3/4" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Skeleton className="h-4 w-full mb-2" />
|
|
<Skeleton className="h-4 w-2/3" />
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<Card>
|
|
<CardContent className="p-6 text-center">
|
|
<p className="text-red-500 mb-4">
|
|
Error loading posts: {error.message}
|
|
</p>
|
|
<Button onClick={() => refetch()}>
|
|
Try Again
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
if (!data?.data.posts.length) {
|
|
return (
|
|
<Card>
|
|
<CardContent className="p-6 text-center">
|
|
<p>No posts found.</p>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{data.data.posts.map((post) => (
|
|
<Card key={post.id}>
|
|
<CardHeader>
|
|
<CardTitle className="line-clamp-2">{post.title}</CardTitle>
|
|
<div className="text-sm text-gray-500">
|
|
By {post.author.name} • {new Date(post.createdAt).toLocaleDateString()}
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-gray-700 mb-3 line-clamp-3">{post.excerpt}</p>
|
|
{post.tags.length > 0 && (
|
|
<div className="flex flex-wrap gap-2">
|
|
{post.tags.map((tag) => (
|
|
<span
|
|
key={tag}
|
|
className="px-2 py-1 text-xs bg-gray-100 rounded-full"
|
|
>
|
|
{tag}
|
|
</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
## State Management
|
|
|
|
### Custom Hook for API Data
|
|
|
|
```typescript
|
|
// hooks/usePosts.ts
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
|
|
interface Post {
|
|
id: string
|
|
title: string
|
|
content: string
|
|
author: {
|
|
id: string
|
|
name: string
|
|
}
|
|
createdAt: string
|
|
updatedAt: string
|
|
}
|
|
|
|
interface CreatePostData {
|
|
title: string
|
|
content: string
|
|
tags?: string[]
|
|
}
|
|
|
|
export function usePosts(page = 1, limit = 10) {
|
|
return useQuery({
|
|
queryKey: ['posts', page, limit],
|
|
queryFn: async () => {
|
|
const response = await fetch(`/api/posts?page=${page}&limit=${limit}`, {
|
|
credentials: 'include',
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to fetch posts')
|
|
}
|
|
|
|
return response.json()
|
|
},
|
|
})
|
|
}
|
|
|
|
export function usePost(id: string) {
|
|
return useQuery({
|
|
queryKey: ['posts', id],
|
|
queryFn: async () => {
|
|
const response = await fetch(`/api/posts/${id}`, {
|
|
credentials: 'include',
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to fetch post')
|
|
}
|
|
|
|
return response.json()
|
|
},
|
|
enabled: !!id,
|
|
})
|
|
}
|
|
|
|
export function useCreatePost() {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation({
|
|
mutationFn: async (data: CreatePostData) => {
|
|
const response = await fetch('/api/posts', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify(data),
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json()
|
|
throw new Error(error.error || 'Failed to create post')
|
|
}
|
|
|
|
return response.json()
|
|
},
|
|
onSuccess: () => {
|
|
// Invalidate and refetch posts list
|
|
queryClient.invalidateQueries({ queryKey: ['posts'] })
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useDeletePost() {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation({
|
|
mutationFn: async (id: string) => {
|
|
const response = await fetch(`/api/posts/${id}`, {
|
|
method: 'DELETE',
|
|
credentials: 'include',
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json()
|
|
throw new Error(error.error || 'Failed to delete post')
|
|
}
|
|
|
|
return response.json()
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['posts'] })
|
|
},
|
|
})
|
|
}
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
### Global Error Boundary
|
|
|
|
```typescript
|
|
// components/ErrorBoundary.tsx
|
|
'use client';
|
|
|
|
import React from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
|
|
interface ErrorBoundaryState {
|
|
hasError: boolean;
|
|
error?: Error;
|
|
}
|
|
|
|
interface ErrorBoundaryProps {
|
|
children: React.ReactNode;
|
|
fallback?: React.ComponentType<{ error: Error; reset: () => void }>;
|
|
}
|
|
|
|
export class ErrorBoundary extends React.Component<
|
|
ErrorBoundaryProps,
|
|
ErrorBoundaryState
|
|
> {
|
|
constructor(props: ErrorBoundaryProps) {
|
|
super(props);
|
|
this.state = { hasError: false };
|
|
}
|
|
|
|
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
return { hasError: true, error };
|
|
}
|
|
|
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
console.error('Error caught by boundary:', error, errorInfo);
|
|
}
|
|
|
|
render() {
|
|
if (this.state.hasError) {
|
|
const reset = () => this.setState({ hasError: false, error: undefined });
|
|
|
|
if (this.props.fallback) {
|
|
const FallbackComponent = this.props.fallback;
|
|
return <FallbackComponent error={this.state.error!} reset={reset} />;
|
|
}
|
|
|
|
return (
|
|
<Card className="max-w-md mx-auto mt-8">
|
|
<CardHeader>
|
|
<CardTitle className="text-red-600">Something went wrong</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-gray-600 mb-4">
|
|
We apologize for the inconvenience. Please try refreshing the page.
|
|
</p>
|
|
<div className="space-x-2">
|
|
<Button onClick={reset}>Try Again</Button>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => window.location.reload()}
|
|
>
|
|
Refresh Page
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return this.props.children;
|
|
}
|
|
}
|
|
|
|
// Usage in layout
|
|
export function Layout({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<ErrorBoundary>
|
|
{children}
|
|
</ErrorBoundary>
|
|
);
|
|
}
|
|
```
|
|
|
|
### API Error Utility
|
|
|
|
```typescript
|
|
// lib/api-error.ts
|
|
export class APIError extends Error {
|
|
constructor(
|
|
message: string,
|
|
public status: number,
|
|
public code?: string
|
|
) {
|
|
super(message)
|
|
this.name = 'APIError'
|
|
}
|
|
}
|
|
|
|
export function handleAPIError(error: unknown): APIError {
|
|
if (error instanceof APIError) {
|
|
return error
|
|
}
|
|
|
|
if (error instanceof Error) {
|
|
return new APIError(error.message, 500)
|
|
}
|
|
|
|
return new APIError('An unknown error occurred', 500)
|
|
}
|
|
|
|
// Usage in API routes
|
|
export async function POST(request: Request) {
|
|
try {
|
|
// Your API logic
|
|
} catch (error) {
|
|
const apiError = handleAPIError(error)
|
|
|
|
return Response.json(
|
|
{
|
|
success: false,
|
|
error: apiError.message,
|
|
code: apiError.code,
|
|
},
|
|
{ status: apiError.status }
|
|
)
|
|
}
|
|
}
|
|
```
|
|
|
|
## SiliconPin-Specific Patterns
|
|
|
|
### Topic Management Pattern
|
|
|
|
```typescript
|
|
// hooks/useTopics.ts
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
|
|
export function useTopics(searchTerm?: string, tags?: string[]) {
|
|
return useQuery({
|
|
queryKey: ['topics', searchTerm, tags],
|
|
queryFn: async () => {
|
|
const params = new URLSearchParams()
|
|
if (searchTerm) params.set('q', searchTerm)
|
|
if (tags?.length) params.set('tags', tags.join(','))
|
|
|
|
const response = await fetch(`/api/topics?${params}`, {
|
|
credentials: 'include',
|
|
})
|
|
|
|
if (!response.ok) throw new Error('Failed to fetch topics')
|
|
return response.json()
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useCreateTopic() {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation({
|
|
mutationFn: async (data: CreateTopicData) => {
|
|
const response = await fetch('/api/topics', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify(data),
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json()
|
|
throw new Error(error.error || 'Failed to create topic')
|
|
}
|
|
|
|
return response.json()
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['topics'] })
|
|
},
|
|
})
|
|
}
|
|
```
|
|
|
|
### Admin Dashboard Pattern
|
|
|
|
```typescript
|
|
// components/admin/AdminLayout.tsx
|
|
'use client'
|
|
|
|
import { useAuth } from '@/contexts/AuthContext'
|
|
import { useRouter } from 'next/navigation'
|
|
import { useEffect } from 'react'
|
|
|
|
interface AdminLayoutProps {
|
|
children: React.ReactNode
|
|
}
|
|
|
|
export function AdminLayout({ children }: AdminLayoutProps) {
|
|
const { user, isLoading } = useAuth()
|
|
const router = useRouter()
|
|
|
|
useEffect(() => {
|
|
if (!isLoading && (!user || user.role !== 'admin')) {
|
|
router.push('/auth')
|
|
}
|
|
}, [user, isLoading, router])
|
|
|
|
if (isLoading) {
|
|
return <div>Loading admin panel...</div>
|
|
}
|
|
|
|
if (!user || user.role !== 'admin') {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<div className="admin-layout">
|
|
<aside className="admin-sidebar">
|
|
{/* Admin Navigation */}
|
|
</aside>
|
|
<main className="admin-content">
|
|
{children}
|
|
</main>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
### Payment Integration Pattern
|
|
|
|
```typescript
|
|
// hooks/usePayments.ts
|
|
export function useInitiatePayment() {
|
|
return useMutation({
|
|
mutationFn: async (data: PaymentData) => {
|
|
const response = await fetch('/api/payments/initiate', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify(data),
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json()
|
|
throw new Error(error.error || 'Payment initiation failed')
|
|
}
|
|
|
|
const result = await response.json()
|
|
|
|
// Redirect to payment gateway
|
|
if (result.data.paymentUrl) {
|
|
window.location.href = result.data.paymentUrl
|
|
}
|
|
|
|
return result
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useUserBalance() {
|
|
return useQuery({
|
|
queryKey: ['user', 'balance'],
|
|
queryFn: async () => {
|
|
const response = await fetch('/api/user/balance', {
|
|
credentials: 'include',
|
|
})
|
|
|
|
if (!response.ok) throw new Error('Failed to fetch balance')
|
|
return response.json()
|
|
},
|
|
refetchInterval: 30000, // Refetch every 30 seconds
|
|
})
|
|
}
|
|
```
|
|
|
|
### Service Deployment Pattern
|
|
|
|
```typescript
|
|
// hooks/useServices.ts
|
|
export function useDeployService() {
|
|
return useMutation({
|
|
mutationFn: async (data: ServiceDeploymentData) => {
|
|
const endpoint = {
|
|
kubernetes: '/api/services/deploy-kubernetes',
|
|
vpn: '/api/services/deploy-vpn',
|
|
cloud: '/api/services/deploy-cloude',
|
|
}[data.type]
|
|
|
|
const response = await fetch(endpoint, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify(data),
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json()
|
|
throw new Error(error.error || 'Service deployment failed')
|
|
}
|
|
|
|
return response.json()
|
|
},
|
|
onSuccess: (data) => {
|
|
// Handle successful deployment
|
|
console.log('Service deployed:', data)
|
|
},
|
|
})
|
|
}
|
|
```
|
|
|
|
These patterns provide a solid foundation for extending the SiliconPin platform with additional features while maintaining consistency and best practices specific to the comprehensive web services platform.
|