Adding New Features

Complete guide for adding new features like wallpaper functionality to the application

Adding New Features

This guide demonstrates how to add new features to the application using a wallpaper functionality as a complete example. Follow these steps as a template for any new feature development.

Overview

When adding a new feature, you need to work through these layers:

  1. Database Layer - Define schema and migrations
  2. Type Layer - Create TypeScript interfaces
  3. Model Layer - Implement data operations (CRUD)
  4. API Layer - Create REST endpoints
  5. Component Layer - Build reusable UI components
  6. Page Layer - Integrate components into pages
  7. Configuration - Update constants and settings
  8. Storage - Configure file upload/download

Step-by-Step Implementation

1. Database Schema Design

File: src/db/schema.ts

Add your new table definition:

export const wallpapers = pgTable("wallpapers", {
  id: integer().primaryKey().generatedAlwaysAsIdentity(),
  uuid: varchar({ length: 255 }).notNull().unique(),
  created_at: timestamp({ withTimezone: true }),
  updated_at: timestamp({ withTimezone: true }),
  title: varchar({ length: 255 }).notNull(),
  description: text(),
  image_url: varchar({ length: 500 }).notNull(),
  thumbnail_url: varchar({ length: 500 }),
  category_uuid: varchar({ length: 255 }),
  tags: text(), // JSON string for tags array
  resolution: varchar({ length: 50 }), // e.g., "1920x1080"
  file_size: integer(), // file size in bytes
  download_count: integer().notNull().default(0),
  view_count: integer().notNull().default(0),
  status: varchar({ length: 50 }).notNull().default("active"),
  user_uuid: varchar({ length: 255 }), // uploader
  is_featured: boolean().notNull().default(false),
  credits_required: integer().notNull().default(0),
});

2. TypeScript Interface Definition

File: src/types/wallpaper.d.ts

Create comprehensive type definitions:

export interface Wallpaper {
  id: number;
  uuid: string;
  created_at?: Date | string;
  updated_at?: Date | string;
  title: string;
  description?: string;
  image_url: string;
  thumbnail_url?: string;
  category_uuid?: string;
  tags?: string[];
  resolution?: string;
  file_size?: number;
  download_count: number;
  view_count: number;
  status: string;
  user_uuid?: string;
  is_featured: boolean;
  credits_required: number;
}

export interface WallpaperFilter {
  category?: string;
  tags?: string[];
  resolution?: string;
  featured?: boolean;
  search?: string;
}

export interface WallpaperUpload {
  title: string;
  description?: string;
  category_uuid?: string;
  tags?: string[];
  credits_required?: number;
}

export interface WallpaperGridProps {
  wallpapers: Wallpaper[];
  onSelect?: (wallpaper: Wallpaper) => void;
  isLoading?: boolean;
}

export interface WallpaperCardProps {
  wallpaper: Wallpaper;
  onDownload?: (wallpaper: Wallpaper) => void;
  onView?: (wallpaper: Wallpaper) => void;
}

3. Database Migration

Push your schema changes to the database:

# Push changes directly (development)
pnpm db:push

# Or generate migration files (production)
pnpm db:generate
pnpm db:migrate

# Verify in Drizzle Studio
pnpm db:studio

4. Verify in Supabase

  1. Login to your Supabase dashboard
  2. Navigate to Table Editor
  3. Verify the new table was created
  4. Check column types and constraints
  5. Ensure indexes are properly set

5. Data Operations Model

File: src/models/wallpaper.ts

Implement CRUD operations:

import { db } from "@/db";
import { wallpapers } from "@/db/schema";
import { desc, eq, like, and, sql } from "drizzle-orm";

export type WallpaperRow = typeof wallpapers.$inferSelect;
export type NewWallpaper = typeof wallpapers.$inferInsert;

// CREATE - Add new wallpaper
export async function createWallpaper(input: NewWallpaper): Promise<WallpaperRow> {
  const database = db();
  const [row] = await database
    .insert(wallpapers)
    .values(input)
    .returning();
  return row;
}

// READ - Get wallpapers with pagination and filters
export async function listWallpapers(
  page: number = 1,
  limit: number = 20,
  filter?: WallpaperFilter
): Promise<WallpaperRow[]> {
  const database = db();
  const offset = (page - 1) * limit;
  
  let query = database
    .select()
    .from(wallpapers)
    .where(eq(wallpapers.status, "active"));

  if (filter?.category) {
    query = query.where(eq(wallpapers.category_uuid, filter.category));
  }
  
  if (filter?.search) {
    query = query.where(like(wallpapers.title, `%${filter.search}%`));
  }

  return query
    .orderBy(desc(wallpapers.created_at))
    .limit(limit)
    .offset(offset);
}

// READ - Get single wallpaper by UUID
export async function getWallpaperByUuid(uuid: string): Promise<WallpaperRow | null> {
  const database = db();
  const rows = await database
    .select()
    .from(wallpapers)
    .where(eq(wallpapers.uuid, uuid))
    .limit(1);
  return rows[0] ?? null;
}

// UPDATE - Update wallpaper
export async function updateWallpaper(
  uuid: string,
  updates: Partial<NewWallpaper>
): Promise<WallpaperRow | null> {
  const database = db();
  const [row] = await database
    .update(wallpapers)
    .set(updates)
    .where(eq(wallpapers.uuid, uuid))
    .returning();
  return row ?? null;
}

// DELETE - Remove wallpaper
export async function deleteWallpaper(uuid: string): Promise<number> {
  const database = db();
  const result = await database
    .delete(wallpapers)
    .where(eq(wallpapers.uuid, uuid));
  return (result as unknown as { rowCount?: number }).rowCount ?? 0;
}

// UTILITY - Increment view count
export async function incrementViewCount(uuid: string) {
  const database = db();
  await database
    .update(wallpapers)
    .set({ view_count: sql`${wallpapers.view_count} + 1` })
    .where(eq(wallpapers.uuid, uuid));
}

// UTILITY - Increment download count
export async function incrementDownloadCount(uuid: string) {
  const database = db();
  await database
    .update(wallpapers)
    .set({ download_count: sql`${wallpapers.download_count} + 1` })
    .where(eq(wallpapers.uuid, uuid));
}

6. OSS Storage Configuration

The storage system is already configured in src/lib/storage.ts. Use it for file uploads:

Environment Variables (.env.local):

STORAGE_ENDPOINT="https://your-endpoint.r2.cloudflarestorage.com"
STORAGE_ACCESS_KEY="your-access-key"
STORAGE_SECRET_KEY="your-secret-key"
STORAGE_BUCKET="your-bucket"
STORAGE_DOMAIN="your-domain.com"

Usage Example:

import { newStorage } from "@/lib/storage";

const storage = newStorage();
const result = await storage.uploadFile({
  body: buffer,
  key: `wallpapers/${filename}`,
  contentType: "image/jpeg",
  disposition: "inline"
});

7. API Routes with AI Integration

Create RESTful API endpoints in src/app/api/wallpapers/:

Basic CRUD Operations

File: src/app/api/wallpapers/route.ts

import { NextRequest } from "next/server";
import { respData, respErr } from "@/lib/resp";
import { listWallpapers, createWallpaper } from "@/models/wallpaper";
import { getUuid } from "@/lib/hash";

// GET /api/wallpapers - List wallpapers
export async function GET(request: NextRequest) {
  try {
    const searchParams = request.nextUrl.searchParams;
    const page = parseInt(searchParams.get("page") || "1");
    const limit = parseInt(searchParams.get("limit") || "20");
    const category = searchParams.get("category") || undefined;
    const search = searchParams.get("search") || undefined;

    const wallpapers = await listWallpapers(page, limit, {
      category,
      search
    });

    return respData(wallpapers);
  } catch (error) {
    console.error("Get wallpapers failed:", error);
    return respErr("Failed to get wallpapers");
  }
}

// POST /api/wallpapers - Create wallpaper
export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const { title, description, image_url, category_uuid, tags, credits_required } = body;

    if (!title || !image_url) {
      return respErr("Title and image_url are required");
    }

    const wallpaper = await createWallpaper({
      uuid: getUuid(),
      title,
      description,
      image_url,
      category_uuid,
      tags: tags ? JSON.stringify(tags) : null,
      credits_required: credits_required || 0,
      created_at: new Date(),
    });

    return respData(wallpaper);
  } catch (error) {
    console.error("Create wallpaper failed:", error);
    return respErr("Failed to create wallpaper");
  }
}

AI-Powered Wallpaper Generation

File: src/app/api/wallpapers/generate/route.ts

import { NextRequest } from "next/server";
import { respData, respErr } from "@/lib/resp";
import { generateText2Image } from "@/aisdk/generate-text2image";
import { kling } from "@/aisdk/kling";
import { openai } from "@ai-sdk/openai";
import { replicate } from "@ai-sdk/replicate";
import { newStorage } from "@/lib/storage";
import { getUuid } from "@/lib/hash";
import { createWallpaper } from "@/models/wallpaper";
import { getUserCredits, deductCredits } from "@/models/credit";
import { getSession } from "@/auth/session";

export async function POST(request: NextRequest) {
  try {
    const session = await getSession();
    if (!session?.user?.uuid) {
      return respErr("Authentication required");
    }

    const body = await request.json();
    const { 
      prompt, 
      provider = "kling",
      model,
      size = { width: 1024, height: 1024 },
      aspectRatio = "1:1",
      style,
      negativePrompt,
      steps = 20,
      guidanceScale = 7.5,
      category_uuid,
      title,
      description 
    } = body;

    if (!prompt) {
      return respErr("Prompt is required");
    }

    // Check user credits
    const userCredits = await getUserCredits(session.user.uuid);
    const requiredCredits = 10; // Cost per generation
    
    if (userCredits < requiredCredits) {
      return respErr("Insufficient credits");
    }

    // Select AI model based on provider
    let imageModel;
    let providerOptions = {};

    switch (provider) {
      case "kling":
        imageModel = kling.image(model || "kling-v1");
        providerOptions = {
          kling: {
            style: style || "realistic",
          }
        };
        break;
        
      case "openai":
        imageModel = openai.image(model || "dall-e-3");
        providerOptions = {
          openai: {
            quality: "hd",
            style: style || "natural",
          }
        };
        break;
        
      case "replicate":
        imageModel = replicate.image(model || "stability-ai/sdxl");
        providerOptions = {
          replicate: {
            output_quality: 90,
          }
        };
        break;
        
      default:
        return respErr("Unsupported provider");
    }

    // Generate wallpaper using AI SDK
    const result = await generateText2Image({
      model: imageModel,
      prompt,
      negativePrompt,
      n: 1,
      steps,
      guidanceScale,
      size,
      aspectRatio,
      providerOptions,
    });

    if (result.warnings.length > 0) {
      console.log("Generation warnings:", result.warnings);
    }

    if (!result.image) {
      return respErr("Failed to generate image");
    }

    // Upload to storage
    const storage = newStorage();
    const batch = getUuid();
    const filename = `wallpaper_${provider}_${batch}.png`;
    const key = `wallpapers/generated/${filename}`;
    
    let imageBuffer: Buffer;
    
    // Handle different image formats from AI SDK
    if (typeof result.image === 'string' && result.image.startsWith('http')) {
      // URL format - download the image
      const response = await fetch(result.image);
      imageBuffer = Buffer.from(await response.arrayBuffer());
    } else {
      // Base64 format
      const base64Data = typeof result.image === 'string' 
        ? result.image 
        : result.image.base64;
      imageBuffer = Buffer.from(base64Data, "base64");
    }

    // Upload to OSS
    const uploadResult = await storage.uploadFile({
      body: imageBuffer,
      key,
      contentType: "image/png",
      disposition: "inline",
    });

    // Generate thumbnail (optional)
    const thumbnailKey = `wallpapers/thumbnails/${filename}`;
    // You can add thumbnail generation logic here

    // Save to database
    const wallpaper = await createWallpaper({
      uuid: getUuid(),
      title: title || `AI Generated - ${prompt.substring(0, 50)}...`,
      description: description || `Generated with ${provider}: ${prompt}`,
      image_url: uploadResult.url,
      thumbnail_url: uploadResult.url, // Use same URL or generate thumbnail
      category_uuid,
      tags: JSON.stringify([provider, "ai-generated", style].filter(Boolean)),
      resolution: `${size.width}x${size.height}`,
      file_size: imageBuffer.length,
      user_uuid: session.user.uuid,
      credits_required: 5, // Credits required to download
      status: "active",
      created_at: new Date(),
    });

    // Deduct user credits
    await deductCredits(session.user.uuid, requiredCredits, {
      type: "wallpaper_generation",
      description: `Generated wallpaper: ${wallpaper.title}`,
      related_uuid: wallpaper.uuid,
    });

    return respData({
      wallpaper,
      credits_used: requiredCredits,
      generation_info: {
        provider,
        model,
        prompt,
        size,
        aspectRatio,
      }
    });

  } catch (error) {
    console.error("Wallpaper generation failed:", error);
    return respErr("Failed to generate wallpaper");
  }
}

AI Enhancement Routes

File: src/app/api/wallpapers/enhance/route.ts

import { NextRequest } from "next/server";
import { respData, respErr } from "@/lib/resp";
import { generateImage2Image } from "@/aisdk/generate-image2image";
import { stableDiffusion } from "@/aisdk/stable-diffusion";

export async function POST(request: NextRequest) {
  try {
    const session = await getSession();
    if (!session?.user?.uuid) {
      return respErr("Authentication required");
    }

    const body = await request.json();
    const { 
      wallpaper_uuid,
      enhancement_type, // "upscale", "style_transfer", "colorize"
      prompt,
      strength = 0.7,
    } = body;

    const originalWallpaper = await getWallpaperByUuid(wallpaper_uuid);
    if (!originalWallpaper) {
      return respErr("Wallpaper not found");
    }

    // Download original image
    const imageResponse = await fetch(originalWallpaper.image_url);
    const imageBuffer = Buffer.from(await imageResponse.arrayBuffer());

    // Use image-to-image generation for enhancement
    const model = stableDiffusion.image2image("stable-diffusion-xl");
    
    const result = await generateImage2Image({
      model,
      image: imageBuffer,
      prompt: prompt || `enhance this wallpaper, make it more ${enhancement_type}`,
      strength,
      n: 1,
      providerOptions: {
        "stable-diffusion": {
          scheduler: "DPMSolverMultistep",
        }
      }
    });

    // Upload enhanced version
    const storage = newStorage();
    const enhancedFilename = `enhanced_${getUuid()}.png`;
    const enhancedKey = `wallpapers/enhanced/${enhancedFilename}`;
    
    const uploadResult = await storage.uploadFile({
      body: Buffer.from(result.image.base64, "base64"),
      key: enhancedKey,
      contentType: "image/png",
    });

    // Create new wallpaper record for enhanced version
    const enhancedWallpaper = await createWallpaper({
      uuid: getUuid(),
      title: `Enhanced - ${originalWallpaper.title}`,
      description: `AI enhanced version: ${enhancement_type}`,
      image_url: uploadResult.url,
      thumbnail_url: uploadResult.url,
      category_uuid: originalWallpaper.category_uuid,
      tags: JSON.stringify([...JSON.parse(originalWallpaper.tags || "[]"), "ai-enhanced", enhancement_type]),
      resolution: originalWallpaper.resolution,
      file_size: Buffer.from(result.image.base64, "base64").length,
      user_uuid: session.user.uuid,
      credits_required: 8,
      created_at: new Date(),
    });

    return respData({
      original: originalWallpaper,
      enhanced: enhancedWallpaper,
      enhancement_type,
    });

  } catch (error) {
    console.error("Enhancement failed:", error);
    return respErr("Failed to enhance wallpaper");
  }
}

AI Provider Management

Environment Variables for AI Providers:

# Kling AI
KLING_ACCESS_KEY="your-kling-access-key"
KLING_SECRET_KEY="your-kling-secret-key"

# OpenAI
OPENAI_API_KEY="your-openai-api-key"

# Replicate
REPLICATE_API_TOKEN="your-replicate-token"

# Stable Diffusion
STABILITY_API_KEY="your-stability-key"

Provider Configuration:

// src/services/constant.ts
export const AIProviderConfig = {
  Providers: {
    kling: {
      name: "Kling AI",
      models: ["kling-v1", "kling-v1.5"],
      features: ["text2image", "image2image", "video"],
      cost_per_generation: 10,
    },
    openai: {
      name: "OpenAI DALL-E",
      models: ["dall-e-3", "dall-e-2"],
      features: ["text2image"],
      cost_per_generation: 15,
    },
    replicate: {
      name: "Replicate",
      models: ["stability-ai/sdxl", "midjourney/v4"],
      features: ["text2image", "image2image"],
      cost_per_generation: 8,
    },
  },
  
  Styles: [
    "realistic", "artistic", "anime", "digital-art", 
    "photography", "cinematic", "fantasy", "minimalist"
  ],
  
  AspectRatios: [
    { label: "Square", value: "1:1", size: { width: 1024, height: 1024 } },
    { label: "Portrait", value: "9:16", size: { width: 768, height: 1344 } },
    { label: "Landscape", value: "16:9", size: { width: 1344, height: 768 } },
    { label: "Ultrawide", value: "21:9", size: { width: 1536, height: 640 } },
  ]
};

8. Configuration Constants

File: src/services/constant.ts

Add feature-specific constants:

export const WallpaperConfig = {
  Categories: [
    { uuid: "nature", name: "Nature", name_zh: "自然风景", sort: 1 },
    { uuid: "abstract", name: "Abstract", name_zh: "抽象艺术", sort: 2 },
    { uuid: "minimal", name: "Minimal", name_zh: "极简风格", sort: 3 },
    { uuid: "technology", name: "Technology", name_zh: "科技", sort: 4 },
  ],
  
  Resolutions: [
    "1920x1080",
    "2560x1440", 
    "3840x2160",
    "1080x1920"
  ],
  
  Credits: {
    FreeDownloads: 3,
    PremiumCost: 10,
    UploadReward: 50,
  },

  Status: {
    Active: "active",
    Inactive: "inactive",
    Pending: "pending",
  }
};

9. Frontend Component Design

Create reusable components in src/components/blocks/wallpaper/:

File: src/components/blocks/wallpaper/WallpaperCard.tsx

import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Download, Eye } from "lucide-react";
import type { Wallpaper } from "@/types/wallpaper";

interface WallpaperCardProps {
  wallpaper: Wallpaper;
  onDownload?: (wallpaper: Wallpaper) => void;
  onView?: (wallpaper: Wallpaper) => void;
}

export function WallpaperCard({ wallpaper, onDownload, onView }: WallpaperCardProps) {
  return (
    <Card className="group overflow-hidden hover:shadow-lg transition-shadow">
      <div className="relative aspect-video overflow-hidden">
        <img
          src={wallpaper.thumbnail_url || wallpaper.image_url}
          alt={wallpaper.title}
          className="object-cover w-full h-full group-hover:scale-105 transition-transform duration-300"
        />
        {wallpaper.is_featured && (
          <Badge className="absolute top-2 left-2" variant="secondary">
            Featured
          </Badge>
        )}
        <div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors flex items-center justify-center opacity-0 group-hover:opacity-100">
          <div className="flex gap-2">
            <Button
              size="sm"
              variant="secondary"
              onClick={() => onView?.(wallpaper)}
            >
              <Eye className="w-4 h-4" />
            </Button>
            <Button
              size="sm"
              onClick={() => onDownload?.(wallpaper)}
            >
              <Download className="w-4 h-4" />
            </Button>
          </div>
        </div>
      </div>
      <CardContent className="p-4">
        <h3 className="font-semibold truncate">{wallpaper.title}</h3>
        {wallpaper.description && (
          <p className="text-sm text-muted-foreground line-clamp-2 mt-1">
            {wallpaper.description}
          </p>
        )}
        <div className="flex items-center justify-between mt-3 text-xs text-muted-foreground">
          <span>{wallpaper.resolution}</span>
          <div className="flex items-center gap-3">
            <span>{wallpaper.view_count} views</span>
            <span>{wallpaper.download_count} downloads</span>
          </div>
        </div>
      </CardContent>
    </Card>
  );
}

10. Page Integration

File: src/app/[locale]/(default)/wallpapers/page.tsx

import { WallpaperGrid } from "@/components/blocks/wallpaper/WallpaperGrid";
import { WallpaperFilter } from "@/components/blocks/wallpaper/WallpaperFilter";

export default function WallpapersPage() {
  return (
    <div className="container mx-auto py-8">
      <div className="flex flex-col lg:flex-row gap-8">
        <aside className="lg:w-64">
          <WallpaperFilter />
        </aside>
        <main className="flex-1">
          <WallpaperGrid />
        </main>
      </div>
    </div>
  );
}

Best Practices

Database Design

  • Always use UUIDs for public-facing IDs
  • Include created_at and updated_at timestamps
  • Use appropriate data types and constraints
  • Consider indexing for frequently queried fields

TypeScript

  • Define comprehensive interfaces
  • Use proper typing for all functions
  • Separate database types from UI component types

API Design

  • Follow RESTful conventions
  • Use proper HTTP status codes
  • Implement error handling
  • Add input validation

Component Architecture

  • Keep components small and focused
  • Use composition over inheritance
  • Make components reusable
  • Follow the existing UI patterns

File Organization

  • Group related files together
  • Use consistent naming conventions
  • Follow the established folder structure
  • Keep business logic separate from UI logic

Testing Your Implementation

  1. Database: Use pnpm db:studio to verify schema
  2. API: Test endpoints with tools like Postman
  3. Components: Test in Storybook if available
  4. Integration: Test the complete user flow
  5. Performance: Monitor query performance and bundle size

This template can be adapted for any new feature by following the same layered approach and maintaining consistency with the existing codebase architecture.