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:
- Database Layer - Define schema and migrations
- Type Layer - Create TypeScript interfaces
- Model Layer - Implement data operations (CRUD)
- API Layer - Create REST endpoints
- Component Layer - Build reusable UI components
- Page Layer - Integrate components into pages
- Configuration - Update constants and settings
- 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:studio4. Verify in Supabase
- Login to your Supabase dashboard
- Navigate to Table Editor
- Verify the new table was created
- Check column types and constraints
- 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_atandupdated_attimestamps - 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
- Database: Use
pnpm db:studioto verify schema - API: Test endpoints with tools like Postman
- Components: Test in Storybook if available
- Integration: Test the complete user flow
- 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.