ai-wpa/docs/PATTERNS.md

27 KiB

SiliconPin Code Patterns and Examples

This document provides common patterns and examples for extending the SiliconPin platform.

Table of Contents

Authentication Patterns

Using Authentication in Components

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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.