React & Next.js
React & Next.js Integration
Section titled “React & Next.js Integration”Comprehensive guide for integrating Splinterpic into React and Next.js applications with TypeScript support, hooks, and best practices.
Installation
Section titled “Installation”npm install axios swr# oryarn add axios swr# orpnpm add axios swrTypeScript Types
Section titled “TypeScript Types”Create a types file for Splinterpic:
export interface GenerateImageRequest { prompt: string; model?: string; template_id?: string; collection_id?: string;}
export interface Image { id: string; prompt: string; model: string; r2_key: string; r2_url: string; created_at: string; user_id: string; cost: number; template_id?: string; collection_id?: string; is_deleted: boolean;}
export interface ImagesResponse { images: Image[]; total: number;}
export interface Model { id: string; name: string; description: string; cost_per_image: number; is_recommended: boolean;}
export interface ModelsResponse { models: Model[];}
export interface Collection { id: string; name: string; description?: string; created_at: string; user_id: string;}
export interface CollectionsResponse { collections: Collection[];}
export interface ApiError { error: string;}API Client
Section titled “API Client”Create a centralized API client:
import axios, { AxiosInstance } from 'axios';import type { GenerateImageRequest, Image, ImagesResponse, Model, ModelsResponse, Collection, CollectionsResponse,} from '@/types/splinterpic';
export class SplinterpicClient { private client: AxiosInstance;
constructor(baseURL: string) { this.client = axios.create({ baseURL, headers: { 'Content-Type': 'application/json', }, timeout: 30000, }); }
async generate(request: GenerateImageRequest): Promise<Image> { const { data } = await this.client.post<Image>('/api/generate', request); return data; }
async getImages(filters?: { limit?: number; offset?: number; collection_id?: string; }): Promise<ImagesResponse> { const { data } = await this.client.get<ImagesResponse>('/api/images', { params: filters, }); return data; }
async getImage(imageId: string): Promise<Image> { const { data } = await this.client.get<Image>(`/api/images/${imageId}`); return data; }
async deleteImage(imageId: string): Promise<void> { await this.client.delete(`/api/images/${imageId}`); }
async getModels(): Promise<ModelsResponse> { const { data } = await this.client.get<ModelsResponse>('/api/models'); return data; }
async createCollection(name: string, description?: string): Promise<Collection> { const { data } = await this.client.post<Collection>('/api/collections', { name, description, }); return data; }
async getCollections(): Promise<CollectionsResponse> { const { data } = await this.client.get<CollectionsResponse>('/api/collections'); return data; }}
// Export singleton instanceexport const splinterpic = new SplinterpicClient( process.env.NEXT_PUBLIC_SPLINTERPIC_API_URL || '');React Hooks
Section titled “React Hooks”useImageGeneration Hook
Section titled “useImageGeneration Hook”import { useState } from 'react';import { splinterpic } from '@/lib/splinterpic';import type { GenerateImageRequest, Image } from '@/types/splinterpic';
export function useImageGeneration() { const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null); const [image, setImage] = useState<Image | null>(null);
const generate = async (request: GenerateImageRequest) => { setLoading(true); setError(null); setImage(null);
try { const result = await splinterpic.generate(request); setImage(result); return result; } catch (err: any) { const errorMessage = err.response?.data?.error || err.message || 'Failed to generate image'; setError(errorMessage); throw new Error(errorMessage); } finally { setLoading(false); } };
const reset = () => { setImage(null); setError(null); setLoading(false); };
return { generate, loading, error, image, reset, };}useImages Hook with SWR
Section titled “useImages Hook with SWR”import useSWR from 'swr';import { splinterpic } from '@/lib/splinterpic';import type { ImagesResponse } from '@/types/splinterpic';
interface UseImagesOptions { limit?: number; offset?: number; collection_id?: string;}
export function useImages(options: UseImagesOptions = {}) { const key = ['images', options];
const { data, error, isLoading, mutate } = useSWR<ImagesResponse>( key, () => splinterpic.getImages(options), { revalidateOnFocus: false, dedupingInterval: 5000, } );
return { images: data?.images || [], total: data?.total || 0, isLoading, error, refresh: mutate, };}useModels Hook
Section titled “useModels Hook”import useSWR from 'swr';import { splinterpic } from '@/lib/splinterpic';import type { Model } from '@/types/splinterpic';
export function useModels() { const { data, error, isLoading } = useSWR<Model[]>( 'models', async () => { const response = await splinterpic.getModels(); return response.models; }, { revalidateOnMount: true, revalidateOnFocus: false, dedupingInterval: 60000, // Cache for 1 minute } );
return { models: data || [], isLoading, error, recommendedModel: data?.find(m => m.is_recommended), };}React Components
Section titled “React Components”Image Generation Form
Section titled “Image Generation Form”'use client';
import { useState } from 'react';import { useImageGeneration } from '@/hooks/useImageGeneration';import { useModels } from '@/hooks/useModels';
export function ImageGenerationForm() { const [prompt, setPrompt] = useState(''); const [selectedModel, setSelectedModel] = useState('');
const { generate, loading, error, image } = useImageGeneration(); const { models, recommendedModel } = useModels();
const handleSubmit = async (e: React.FormEvent) => { e.preventDefault();
if (!prompt.trim()) { return; }
try { await generate({ prompt: prompt.trim(), model: selectedModel || recommendedModel?.id, }); } catch (err) { // Error is already handled in the hook console.error('Generation failed:', err); } };
return ( <div className="max-w-2xl mx-auto p-6"> <form onSubmit={handleSubmit} className="space-y-4"> <div> <label htmlFor="prompt" className="block text-sm font-medium mb-2"> Image Description </label> <textarea id="prompt" value={prompt} onChange={(e) => setPrompt(e.target.value)} placeholder="A serene mountain landscape at sunset..." className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-500" rows={4} disabled={loading} /> </div>
<div> <label htmlFor="model" className="block text-sm font-medium mb-2"> AI Model </label> <select id="model" value={selectedModel} onChange={(e) => setSelectedModel(e.target.value)} className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-500" disabled={loading} > <option value="">Use recommended model</option> {models.map((model) => ( <option key={model.id} value={model.id}> {model.name} - ${model.cost_per_image.toFixed(4)} {model.is_recommended && ' (Recommended)'} </option> ))} </select> </div>
<button type="submit" disabled={loading || !prompt.trim()} className="w-full bg-blue-600 text-white py-3 rounded-lg font-semibold hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors" > {loading ? 'Generating...' : 'Generate Image'} </button>
{error && ( <div className="p-4 bg-red-50 border border-red-200 rounded-lg text-red-700"> {error} </div> )}
{image && ( <div className="mt-6 space-y-4"> <div className="relative aspect-square rounded-lg overflow-hidden"> <img src={image.r2_url} alt={image.prompt} className="w-full h-full object-cover" /> </div> <div className="text-sm text-gray-600 space-y-1"> <p> <strong>ID:</strong> {image.id} </p> <p> <strong>Model:</strong> {image.model} </p> <p> <strong>Cost:</strong> ${image.cost.toFixed(4)} </p> </div> </div> )} </form> </div> );}Image Gallery Component
Section titled “Image Gallery Component”'use client';
import { useState } from 'react';import { useImages } from '@/hooks/useImages';import type { Image } from '@/types/splinterpic';
export function ImageGallery() { const [page, setPage] = useState(0); const limit = 12;
const { images, total, isLoading, error, refresh } = useImages({ limit, offset: page * limit, });
const totalPages = Math.ceil(total / limit);
if (error) { return ( <div className="p-4 bg-red-50 border border-red-200 rounded-lg text-red-700"> Failed to load images. Please try again. </div> ); }
return ( <div className="space-y-6"> <div className="flex items-center justify-between"> <h2 className="text-2xl font-bold">Your Images</h2> <button onClick={() => refresh()} className="px-4 py-2 text-sm font-medium text-blue-600 hover:text-blue-700" > Refresh </button> </div>
{isLoading ? ( <div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4"> {Array.from({ length: limit }).map((_, i) => ( <div key={i} className="aspect-square bg-gray-200 rounded-lg animate-pulse" /> ))} </div> ) : ( <> <div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4"> {images.map((image) => ( <ImageCard key={image.id} image={image} onDelete={refresh} /> ))} </div>
{totalPages > 1 && ( <div className="flex items-center justify-center gap-2"> <button onClick={() => setPage(p => Math.max(0, p - 1))} disabled={page === 0} className="px-4 py-2 border rounded-lg hover:bg-gray-50 disabled:opacity-50" > Previous </button> <span className="text-sm text-gray-600"> Page {page + 1} of {totalPages} </span> <button onClick={() => setPage(p => Math.min(totalPages - 1, p + 1))} disabled={page >= totalPages - 1} className="px-4 py-2 border rounded-lg hover:bg-gray-50 disabled:opacity-50" > Next </button> </div> )} </> )} </div> );}
function ImageCard({ image, onDelete }: { image: Image; onDelete: () => void }) { const [deleting, setDeleting] = useState(false);
const handleDelete = async () => { if (!confirm('Are you sure you want to delete this image?')) { return; }
setDeleting(true); try { await splinterpic.deleteImage(image.id); onDelete(); } catch (err) { alert('Failed to delete image'); setDeleting(false); } };
return ( <div className="group relative aspect-square rounded-lg overflow-hidden bg-gray-100"> <img src={image.r2_url} alt={image.prompt} className="w-full h-full object-cover" /> <div className="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-60 transition-all flex items-end p-4"> <div className="opacity-0 group-hover:opacity-100 transition-opacity text-white text-sm space-y-2 w-full"> <p className="font-medium truncate">{image.prompt}</p> <div className="flex items-center justify-between"> <span className="text-xs">${image.cost.toFixed(4)}</span> <button onClick={handleDelete} disabled={deleting} className="px-3 py-1 bg-red-600 hover:bg-red-700 rounded text-xs font-medium disabled:opacity-50" > {deleting ? 'Deleting...' : 'Delete'} </button> </div> </div> </div> </div> );}Next.js App Router Integration
Section titled “Next.js App Router Integration”Server Component (app/page.tsx)
Section titled “Server Component (app/page.tsx)”import { ImageGenerationForm } from '@/components/ImageGenerationForm';import { ImageGallery } from '@/components/ImageGallery';
export default function HomePage() { return ( <main className="container mx-auto py-12 space-y-12"> <section> <h1 className="text-4xl font-bold text-center mb-8"> AI Image Generation </h1> <ImageGenerationForm /> </section>
<section> <ImageGallery /> </section> </main> );}API Route (app/api/generate/route.ts)
Section titled “API Route (app/api/generate/route.ts)”import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) { try { const body = await request.json();
const response = await fetch( `${process.env.SPLINTERPIC_API_URL}/api/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(body), } );
const data = await response.json();
if (!response.ok) { return NextResponse.json( { error: data.error || 'Generation failed' }, { status: response.status } ); }
return NextResponse.json(data); } catch (error) { console.error('Generation error:', error); return NextResponse.json( { error: 'Internal server error' }, { status: 500 } ); }}Environment Variables (.env.local)
Section titled “Environment Variables (.env.local)”# Public (client-side)NEXT_PUBLIC_SPLINTERPIC_API_URL=https://splinterpic-worker.your-name.workers.dev
# Private (server-side only)SPLINTERPIC_API_URL=https://splinterpic-worker.your-name.workers.devAdvanced Patterns
Section titled “Advanced Patterns”Image Upload with Progress
Section titled “Image Upload with Progress”'use client';
import { useState } from 'react';import { useImageGeneration } from '@/hooks/useImageGeneration';
export function ImageGenerationWithProgress() { const [prompt, setPrompt] = useState(''); const [progress, setProgress] = useState(0);
const { generate, loading, error, image } = useImageGeneration();
const handleSubmit = async (e: React.FormEvent) => { e.preventDefault();
// Simulate progress const progressInterval = setInterval(() => { setProgress(prev => { if (prev >= 90) return prev; return prev + 10; }); }, 500);
try { await generate({ prompt }); setProgress(100); } catch (err) { console.error(err); } finally { clearInterval(progressInterval); setTimeout(() => setProgress(0), 1000); } };
return ( <form onSubmit={handleSubmit} className="space-y-4"> <textarea value={prompt} onChange={(e) => setPrompt(e.target.value)} placeholder="Describe your image..." className="w-full p-3 border rounded-lg" rows={4} />
<button type="submit" disabled={loading} className="w-full bg-blue-600 text-white py-3 rounded-lg" > Generate </button>
{loading && ( <div className="space-y-2"> <div className="w-full bg-gray-200 rounded-full h-2"> <div className="bg-blue-600 h-2 rounded-full transition-all duration-300" style={{ width: `${progress}%` }} /> </div> <p className="text-sm text-gray-600 text-center"> Generating... {progress}% </p> </div> )}
{error && ( <div className="p-4 bg-red-50 border border-red-200 rounded-lg text-red-700"> {error} </div> )}
{image && ( <img src={image.r2_url} alt={image.prompt} className="w-full rounded-lg" /> )} </form> );}Batch Generation Component
Section titled “Batch Generation Component”'use client';
import { useState } from 'react';import { splinterpic } from '@/lib/splinterpic';import type { Image } from '@/types/splinterpic';
interface BatchResult { prompt: string; image?: Image; error?: string; status: 'pending' | 'generating' | 'completed' | 'failed';}
export function BatchGeneration() { const [prompts, setPrompts] = useState(''); const [results, setResults] = useState<BatchResult[]>([]); const [generating, setGenerating] = useState(false);
const handleBatchGenerate = async () => { const promptList = prompts .split('\n') .map(p => p.trim()) .filter(Boolean);
if (promptList.length === 0) return;
setGenerating(true); setResults(promptList.map(prompt => ({ prompt, status: 'pending' })));
for (let i = 0; i < promptList.length; i++) { const prompt = promptList[i];
setResults(prev => { const updated = [...prev]; updated[i] = { ...updated[i], status: 'generating' }; return updated; });
try { const image = await splinterpic.generate({ prompt });
setResults(prev => { const updated = [...prev]; updated[i] = { prompt, image, status: 'completed' }; return updated; });
// Rate limiting - wait 2 seconds between requests if (i < promptList.length - 1) { await new Promise(resolve => setTimeout(resolve, 2000)); } } catch (err: any) { setResults(prev => { const updated = [...prev]; updated[i] = { prompt, error: err.message || 'Failed', status: 'failed', }; return updated; }); } }
setGenerating(false); };
return ( <div className="space-y-6"> <div> <label className="block text-sm font-medium mb-2"> Enter prompts (one per line) </label> <textarea value={prompts} onChange={(e) => setPrompts(e.target.value)} placeholder="Mountain landscape Ocean sunset City at night" className="w-full p-3 border rounded-lg font-mono text-sm" rows={8} disabled={generating} /> </div>
<button onClick={handleBatchGenerate} disabled={generating || !prompts.trim()} className="w-full bg-blue-600 text-white py-3 rounded-lg font-semibold hover:bg-blue-700 disabled:bg-gray-400" > {generating ? 'Generating...' : 'Generate All'} </button>
{results.length > 0 && ( <div className="space-y-4"> <h3 className="font-semibold">Results ({results.length})</h3> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> {results.map((result, index) => ( <div key={index} className="border rounded-lg p-4 space-y-2" > <p className="text-sm font-medium truncate">{result.prompt}</p> {result.status === 'pending' && ( <p className="text-sm text-gray-500">Waiting...</p> )} {result.status === 'generating' && ( <div className="flex items-center gap-2"> <div className="w-4 h-4 border-2 border-blue-600 border-t-transparent rounded-full animate-spin" /> <p className="text-sm text-blue-600">Generating...</p> </div> )} {result.status === 'completed' && result.image && ( <img src={result.image.r2_url} alt={result.prompt} className="w-full aspect-square object-cover rounded" /> )} {result.status === 'failed' && ( <p className="text-sm text-red-600">{result.error}</p> )} </div> ))} </div> </div> )} </div> );}Best Practices
Section titled “Best Practices”1. Environment Variables
Section titled “1. Environment Variables”Always use environment variables for API URLs:
// Goodconst apiUrl = process.env.NEXT_PUBLIC_SPLINTERPIC_API_URL;
// Bad - Never hardcodeconst apiUrl = 'https://splinterpic-worker.workers.dev';2. Error Boundary
Section titled “2. Error Boundary”Wrap components with error boundaries:
'use client';
import { Component, ReactNode } from 'react';
interface Props { children: ReactNode; fallback?: ReactNode;}
interface State { hasError: boolean;}
export class ErrorBoundary extends Component<Props, State> { constructor(props: Props) { super(props); this.state = { hasError: false }; }
static getDerivedStateFromError() { return { hasError: true }; }
render() { if (this.state.hasError) { return this.props.fallback || <div>Something went wrong</div>; }
return this.props.children; }}3. Loading States
Section titled “3. Loading States”Always show loading indicators:
{loading ? ( <div className="flex items-center justify-center p-8"> <div className="w-8 h-8 border-4 border-blue-600 border-t-transparent rounded-full animate-spin" /> </div>) : ( <Content />)}4. Type Safety
Section titled “4. Type Safety”Use TypeScript for all API interactions:
// Goodconst image: Image = await splinterpic.generate(request);
// Badconst image = await splinterpic.generate(request);Support
Section titled “Support”- Documentation: API Reference
- Email: support@splinterpic.com