// ThrillWiki API Client for NextJS Frontend // Last updated: 2025-08-29 // This file contains the complete API client implementation for ThrillWiki import { ApiResponse, PaginatedResponse, // Moderation types ModerationReport, CreateModerationReportData, UpdateModerationReportData, ModerationQueue, CompleteQueueItemData, ModerationAction, CreateModerationActionData, BulkOperation, CreateBulkOperationData, UserModerationProfile, ModerationStatsData, // Filter types ModerationReportFilters, ModerationQueueFilters, ModerationActionFilters, BulkOperationFilters, ParkFilters, RideFilters, SearchFilters, // Entity types Park, Ride, Manufacturer, RideModel, ParkPhoto, RidePhoto, RideReview, CreateRideReviewData, // Auth types LoginData, SignupData, AuthResponse, UserProfile, PasswordResetData, PasswordChangeData, // Stats types GlobalStats, TrendingContent, // Utility types ApiClientConfig, RequestConfig, } from '@/types/api'; // ============================================================================ // API Client Configuration // ============================================================================ const DEFAULT_CONFIG: ApiClientConfig = { baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api/v1', timeout: 30000, retries: 3, retryDelay: 1000, headers: { 'Content-Type': 'application/json', }, }; // ============================================================================ // HTTP Client Class // ============================================================================ class HttpClient { private config: ApiClientConfig; private authToken: string | null = null; constructor(config: Partial = {}) { this.config = { ...DEFAULT_CONFIG, ...config }; } setAuthToken(token: string | null) { this.authToken = token; } private getHeaders(customHeaders: Record = {}): Record { const headers = { ...this.config.headers, ...customHeaders }; if (this.authToken) { headers.Authorization = `Bearer ${this.authToken}`; } return headers; } private async makeRequest(config: RequestConfig): Promise> { const url = `${this.config.baseURL}${config.url}`; const headers = this.getHeaders(config.headers); const requestConfig: RequestInit = { method: config.method, headers, signal: AbortSignal.timeout(config.timeout || this.config.timeout), }; if (config.data && ['POST', 'PUT', 'PATCH'].includes(config.method)) { requestConfig.body = JSON.stringify(config.data); } // Add query parameters for GET requests const finalUrl = config.params && config.method === 'GET' ? `${url}?${new URLSearchParams(config.params).toString()}` : url; try { const response = await fetch(finalUrl, requestConfig); const data = await response.json(); if (!response.ok) { return { status: 'error', data: null, error: data.error || { code: 'HTTP_ERROR', message: `HTTP ${response.status}: ${response.statusText}`, }, }; } return data; } catch (error) { return { status: 'error', data: null, error: { code: 'NETWORK_ERROR', message: error instanceof Error ? error.message : 'Network request failed', }, }; } } async get(url: string, params?: Record, headers?: Record): Promise> { return this.makeRequest({ method: 'GET', url, params, headers }); } async post(url: string, data?: any, headers?: Record): Promise> { return this.makeRequest({ method: 'POST', url, data, headers }); } async put(url: string, data?: any, headers?: Record): Promise> { return this.makeRequest({ method: 'PUT', url, data, headers }); } async patch(url: string, data?: any, headers?: Record): Promise> { return this.makeRequest({ method: 'PATCH', url, data, headers }); } async delete(url: string, headers?: Record): Promise> { return this.makeRequest({ method: 'DELETE', url, headers }); } } // ============================================================================ // API Client Class // ============================================================================ export class ThrillWikiApiClient { private http: HttpClient; constructor(config?: Partial) { this.http = new HttpClient(config); } setAuthToken(token: string | null) { this.http.setAuthToken(token); } // ============================================================================ // Authentication API // ============================================================================ auth = { login: async (data: LoginData): Promise> => { return this.http.post('/auth/login/', data); }, signup: async (data: SignupData): Promise> => { return this.http.post('/auth/signup/', data); }, logout: async (): Promise> => { return this.http.post('/auth/logout/'); }, getCurrentUser: async (): Promise> => { return this.http.get('/auth/user/'); }, resetPassword: async (data: PasswordResetData): Promise> => { return this.http.post('/auth/password/reset/', data); }, changePassword: async (data: PasswordChangeData): Promise> => { return this.http.post('/auth/password/change/', data); }, getAuthStatus: async (): Promise> => { return this.http.get('/auth/status/'); }, getSocialProviders: async (): Promise> => { return this.http.get('/auth/providers/'); }, }; // ============================================================================ // Moderation API // ============================================================================ moderation = { // Reports reports: { list: async (filters?: ModerationReportFilters): Promise>> => { return this.http.get>('/moderation/reports/', filters); }, create: async (data: CreateModerationReportData): Promise> => { return this.http.post('/moderation/reports/', data); }, get: async (id: number): Promise> => { return this.http.get(`/moderation/reports/${id}/`); }, update: async (id: number, data: Partial): Promise> => { return this.http.patch(`/moderation/reports/${id}/`, data); }, delete: async (id: number): Promise> => { return this.http.delete(`/moderation/reports/${id}/`); }, assign: async (id: number, moderatorId: number): Promise> => { return this.http.post(`/moderation/reports/${id}/assign/`, { moderator_id: moderatorId }); }, resolve: async (id: number, resolutionAction: string, resolutionNotes?: string): Promise> => { return this.http.post(`/moderation/reports/${id}/resolve/`, { resolution_action: resolutionAction, resolution_notes: resolutionNotes || '', }); }, getStats: async (): Promise> => { return this.http.get('/moderation/reports/stats/'); }, }, // Queue queue: { list: async (filters?: ModerationQueueFilters): Promise>> => { return this.http.get>('/moderation/queue/', filters); }, create: async (data: Partial): Promise> => { return this.http.post('/moderation/queue/', data); }, get: async (id: number): Promise> => { return this.http.get(`/moderation/queue/${id}/`); }, update: async (id: number, data: Partial): Promise> => { return this.http.patch(`/moderation/queue/${id}/`, data); }, delete: async (id: number): Promise> => { return this.http.delete(`/moderation/queue/${id}/`); }, assign: async (id: number, moderatorId: number): Promise> => { return this.http.post(`/moderation/queue/${id}/assign/`, { moderator_id: moderatorId }); }, unassign: async (id: number): Promise> => { return this.http.post(`/moderation/queue/${id}/unassign/`); }, complete: async (id: number, data: CompleteQueueItemData): Promise> => { return this.http.post(`/moderation/queue/${id}/complete/`, data); }, getMyQueue: async (): Promise>> => { return this.http.get>('/moderation/queue/my_queue/'); }, }, // Actions actions: { list: async (filters?: ModerationActionFilters): Promise>> => { return this.http.get>('/moderation/actions/', filters); }, create: async (data: CreateModerationActionData): Promise> => { return this.http.post('/moderation/actions/', data); }, get: async (id: number): Promise> => { return this.http.get(`/moderation/actions/${id}/`); }, update: async (id: number, data: Partial): Promise> => { return this.http.patch(`/moderation/actions/${id}/`, data); }, delete: async (id: number): Promise> => { return this.http.delete(`/moderation/actions/${id}/`); }, deactivate: async (id: number): Promise> => { return this.http.post(`/moderation/actions/${id}/deactivate/`); }, getActive: async (): Promise>> => { return this.http.get>('/moderation/actions/active/'); }, getExpired: async (): Promise>> => { return this.http.get>('/moderation/actions/expired/'); }, }, // Bulk Operations bulkOperations: { list: async (filters?: BulkOperationFilters): Promise>> => { return this.http.get>('/moderation/bulk-operations/', filters); }, create: async (data: CreateBulkOperationData): Promise> => { return this.http.post('/moderation/bulk-operations/', data); }, get: async (id: string): Promise> => { return this.http.get(`/moderation/bulk-operations/${id}/`); }, update: async (id: string, data: Partial): Promise> => { return this.http.patch(`/moderation/bulk-operations/${id}/`, data); }, delete: async (id: string): Promise> => { return this.http.delete(`/moderation/bulk-operations/${id}/`); }, cancel: async (id: string): Promise> => { return this.http.post(`/moderation/bulk-operations/${id}/cancel/`); }, retry: async (id: string): Promise> => { return this.http.post(`/moderation/bulk-operations/${id}/retry/`); }, getLogs: async (id: string): Promise> => { return this.http.get(`/moderation/bulk-operations/${id}/logs/`); }, getRunning: async (): Promise>> => { return this.http.get>('/moderation/bulk-operations/running/'); }, }, // User Moderation users: { get: async (id: number): Promise> => { return this.http.get(`/moderation/users/${id}/`); }, moderate: async (id: number, data: CreateModerationActionData): Promise> => { return this.http.post(`/moderation/users/${id}/moderate/`, data); }, search: async (params: { query?: string; role?: string; has_restrictions?: boolean }): Promise>> => { return this.http.get>('/moderation/users/search/', params); }, getStats: async (): Promise> => { return this.http.get('/moderation/users/stats/'); }, }, }; // ============================================================================ // Parks API // ============================================================================ parks = { list: async (filters?: ParkFilters): Promise>> => { return this.http.get>('/parks/', filters); }, get: async (slug: string): Promise> => { return this.http.get(`/parks/${slug}/`); }, getRides: async (parkSlug: string, filters?: RideFilters): Promise>> => { return this.http.get>(`/parks/${parkSlug}/rides/`, filters); }, getPhotos: async (parkSlug: string, filters?: SearchFilters): Promise>> => { return this.http.get>(`/parks/${parkSlug}/photos/`, filters); }, // Park operators and owners operators: { list: async (filters?: SearchFilters): Promise>> => { return this.http.get>('/parks/operators/', filters); }, get: async (slug: string): Promise> => { return this.http.get(`/parks/operators/${slug}/`); }, }, owners: { list: async (filters?: SearchFilters): Promise>> => { return this.http.get>('/parks/owners/', filters); }, get: async (slug: string): Promise> => { return this.http.get(`/parks/owners/${slug}/`); }, }, }; // ============================================================================ // Rides API // ============================================================================ rides = { list: async (filters?: RideFilters): Promise>> => { return this.http.get>('/rides/', filters); }, get: async (parkSlug: string, rideSlug: string): Promise> => { return this.http.get(`/rides/${parkSlug}/${rideSlug}/`); }, getPhotos: async (parkSlug: string, rideSlug: string, filters?: SearchFilters): Promise>> => { return this.http.get>(`/rides/${parkSlug}/${rideSlug}/photos/`, filters); }, getReviews: async (parkSlug: string, rideSlug: string, filters?: SearchFilters): Promise>> => { return this.http.get>(`/rides/${parkSlug}/${rideSlug}/reviews/`, filters); }, createReview: async (parkSlug: string, rideSlug: string, data: CreateRideReviewData): Promise> => { return this.http.post(`/rides/${parkSlug}/${rideSlug}/reviews/`, data); }, // Manufacturers manufacturers: { list: async (filters?: SearchFilters): Promise>> => { return this.http.get>('/rides/manufacturers/', filters); }, get: async (slug: string): Promise> => { return this.http.get(`/rides/manufacturers/${slug}/`); }, getRides: async (slug: string, filters?: RideFilters): Promise>> => { return this.http.get>(`/rides/manufacturers/${slug}/rides/`, filters); }, getModels: async (slug: string, filters?: SearchFilters): Promise>> => { return this.http.get>(`/rides/manufacturers/${slug}/models/`, filters); }, }, // Designers designers: { list: async (filters?: SearchFilters): Promise>> => { return this.http.get>('/rides/designers/', filters); }, get: async (slug: string): Promise> => { return this.http.get(`/rides/designers/${slug}/`); }, }, // Ride Models models: { list: async (filters?: SearchFilters): Promise>> => { return this.http.get>('/rides/models/', filters); }, get: async (manufacturerSlug: string, modelSlug: string): Promise> => { return this.http.get(`/rides/models/${manufacturerSlug}/${modelSlug}/`); }, }, }; // ============================================================================ // Statistics API // ============================================================================ stats = { getGlobal: async (): Promise> => { return this.http.get('/stats/'); }, recalculate: async (): Promise> => { return this.http.post('/stats/recalculate/'); }, getTrending: async (params?: { content_type?: string; time_period?: string }): Promise> => { return this.http.get('/trending/', params); }, getNewContent: async (): Promise> => { return this.http.get('/new-content/'); }, triggerTrendingCalculation: async (): Promise> => { return this.http.post('/trending/calculate/'); }, getLatestReviews: async (params?: { limit?: number; park?: string; ride?: string }): Promise>> => { return this.http.get>('/reviews/latest/', params); }, }; // ============================================================================ // Rankings API // ============================================================================ rankings = { list: async (filters?: SearchFilters): Promise>> => { return this.http.get>('/rankings/', filters); }, calculate: async (): Promise> => { return this.http.post('/rankings/calculate/'); }, }; // ============================================================================ // Health Check API // ============================================================================ health = { check: async (): Promise> => { return this.http.get('/health/'); }, simple: async (): Promise> => { return this.http.get('/health/simple/'); }, performance: async (): Promise> => { return this.http.get('/health/performance/'); }, }; // ============================================================================ // Accounts API // ============================================================================ accounts = { getProfile: async (username: string): Promise> => { return this.http.get(`/accounts/users/${username}/`); }, updateProfile: async (data: Partial): Promise> => { return this.http.patch('/accounts/profile/', data); }, getSettings: async (): Promise> => { return this.http.get('/accounts/settings/'); }, updateSettings: async (data: any): Promise> => { return this.http.patch('/accounts/settings/', data); }, }; // ============================================================================ // Maps API // ============================================================================ maps = { getParkLocations: async (params?: { bounds?: string; zoom?: number }): Promise> => { return this.http.get('/maps/park-locations/', params); }, getRideLocations: async (parkSlug: string, params?: { bounds?: string; zoom?: number }): Promise> => { return this.http.get(`/maps/parks/${parkSlug}/ride-locations/`, params); }, getUnifiedMap: async (params?: { bounds?: string; zoom?: number; include_parks?: boolean; include_rides?: boolean }): Promise> => { return this.http.get('/maps/unified/', params); }, }; // ============================================================================ // Email API // ============================================================================ email = { sendTestEmail: async (data: { to: string; subject: string; message: string }): Promise> => { return this.http.post('/email/send-test/', data); }, getTemplates: async (): Promise> => { return this.http.get('/email/templates/'); }, }; } // ============================================================================ // Default Export and Utilities // ============================================================================ // Create default client instance export const apiClient = new ThrillWikiApiClient(); // Utility functions for common operations export const apiUtils = { // Set authentication token for all requests setAuthToken: (token: string | null) => { apiClient.setAuthToken(token); }, // Check if response is successful isSuccess: (response: ApiResponse): response is ApiResponse & { status: 'success'; data: T } => { return response.status === 'success' && response.data !== null; }, // Check if response is an error isError: (response: ApiResponse): response is ApiResponse & { status: 'error'; error: NonNullable['error']> } => { return response.status === 'error' && response.error !== null; }, // Extract data from successful response or throw error unwrap: (response: ApiResponse): T => { if (apiUtils.isSuccess(response)) { return response.data; } throw new Error(response.error?.message || 'API request failed'); }, // Handle paginated responses extractPaginatedData: (response: ApiResponse>): T[] => { if (apiUtils.isSuccess(response)) { return response.data.results; } return []; }, // Build query string from filters buildQueryString: (filters: Record): string => { const params = new URLSearchParams(); Object.entries(filters).forEach(([key, value]) => { if (value !== undefined && value !== null && value !== '') { if (Array.isArray(value)) { value.forEach(v => params.append(key, String(v))); } else { params.append(key, String(value)); } } }); return params.toString(); }, // Format error message for display formatError: (error: ApiResponse['error']): string => { if (!error) return 'Unknown error occurred'; if (error.details && typeof error.details === 'object') { // Handle validation errors const fieldErrors = Object.entries(error.details) .map(([field, messages]) => `${field}: ${Array.isArray(messages) ? messages.join(', ') : messages}`) .join('; '); return fieldErrors || error.message; } return error.message; }, // Check if user has required role hasRole: (user: UserProfile | null, requiredRole: UserProfile['role']): boolean => { if (!user) return false; const roleHierarchy = ['USER', 'MODERATOR', 'ADMIN', 'SUPERUSER']; const userRoleIndex = roleHierarchy.indexOf(user.role); const requiredRoleIndex = roleHierarchy.indexOf(requiredRole); return userRoleIndex >= requiredRoleIndex; }, // Check if user can moderate canModerate: (user: UserProfile | null): boolean => { return apiUtils.hasRole(user, 'MODERATOR'); }, // Check if user is admin isAdmin: (user: UserProfile | null): boolean => { return apiUtils.hasRole(user, 'ADMIN'); }, }; // Export types for convenience export type { ApiResponse, PaginatedResponse, ModerationReport, ModerationQueue, ModerationAction, BulkOperation, UserModerationProfile, Park, Ride, Manufacturer, RideModel, UserProfile, GlobalStats, TrendingContent, } from './types-api'; export default apiClient;