// ThrillWiki API Client for NextJS Frontend // This file contains all API endpoint functions with full type safety import type { // Authentication Types LoginRequest, LoginResponse, SignupRequest, SignupResponse, EmailVerificationResponse, ResendVerificationRequest, ResendVerificationResponse, LogoutResponse, CurrentUserResponse, PasswordResetRequest, PasswordResetResponse, PasswordChangeRequest, PasswordChangeResponse, SocialProvidersResponse, AuthStatusResponse, // Django-CloudflareImages-Toolkit Types CloudflareImage, CloudflareImageUploadRequest, CloudflareImageUploadResponse, CloudflareDirectUploadRequest, CloudflareDirectUploadResponse, CloudflareImageVariant, CloudflareImageStats, CloudflareImageListResponse, CloudflareImageDeleteResponse, CloudflareWebhookPayload, EnhancedPhoto, EnhancedImageVariants, // Social Provider Management Types ConnectedProvider, AvailableProvider, SocialAuthStatus, // External API Types (merged from thrillwiki-real) Stats, TrendingItem, Park, Ride, Review, Company, PaginatedResponse, FilterOptions, User, RegisterRequest, // Content Moderation Types ModerationReport, ModerationReportsResponse, CreateModerationReport, ModerationQueueItem, ModerationQueueResponse, UpdateModerationReport, UpdateModerationReportData, ModerationAction, ModerationReportFilters, ModerationQueueFilters, ModerationActionFilters, ModerationStatsData, AssignQueueItemData, CompleteQueueItemData, CreateModerationActionData, // User Moderation Types UserModerationProfile, UserModerationAction, UserModerationActionResponse, UserModerationStats, // Bulk Operations Types BulkOperation, BulkOperationsResponse, CreateBulkOperation, BulkUpdateParks, BulkUpdateRides, BulkImportData, BulkExportData, BulkModerateContent, BulkUserActions, BulkOperationResult, // Park Reviews Types ParkReview, ParkReviewsResponse, CreateParkReview, UpdateParkReview, ParkReviewVote, ParkReviewVoteResponse, UploadParkReviewPhoto, ParkReviewFilters, // User Account Management Types CompleteUserProfile, AccountUpdate, ProfileUpdate, AvatarUpload, AvatarUploadResponse, AvatarDeleteResponse, UserPreferences, ThemeUpdate, NotificationSettings, PrivacySettings, SecuritySettings, SecuritySettingsUpdate, UserStatistics, TopList, TopListsResponse, CreateTopList, NotificationResponse, MarkNotificationsRead, MarkReadResponse, NotificationPreferences, DeletionRequest, VerifyDeletion, DeletionComplete, CancelDeletion, DeletionEligibility, // Parks API Types ParkListResponse, ParkSummary, CreateParkRequest, ParkDetail, ParkFilterOptions, ParkSearchFilters, ParkCompanySearchResponse, ParkSearchSuggestionsResponse, ParkImageSettings, ParkPhotosResponse, UploadParkPhoto, UpdateParkPhoto, // Rides API Types RideListResponse, RideSummary, CreateRideRequest, RideDetail, RideFilterOptions, RideImageSettings, RidePhotosResponse, UploadRidePhoto, UpdateRidePhoto, ManufacturerRideModels, // Search & Core API Types CompanySearchResponse, RideModelSearchResponse, SearchSuggestionsResponse, EntitySearchResponse, EntityNotFoundRequest, EntityNotFoundResponse, EntitySuggestionsResponse, // Maps API Types MapLocationsResponse, LocationDetail, MapSearchResponse, MapStatsResponse, MapCacheResponse, CacheInvalidateResponse, // Health & Statistics Types HealthCheckResponse, SimpleHealthResponse, PerformanceMetricsResponse, SystemStatsResponse, StatsRecalculateResponse, // Trending & Discovery Types TrendingResponse, NewContentResponse, TriggerTrendingResponse, // Reviews & Rankings Types LatestReviewsResponse, ReviewSummary, RankingsResponse, RideRankingDetail, RankingCalculationResponse, // Email Service Types ContactEmailRequest, ContactEmailResponse, NewsletterSubscribeRequest, NewsletterSubscribeResponse, NewsletterUnsubscribeRequest, NewsletterUnsubscribeResponse, // History API Types EntityHistoryResponse, RecentChangesResponse, // Utility Types SearchFilters, BoundingBox, // Hybrid Rides Filtering Types HybridRideData, HybridRideFilterMetadata, HybridRideResponse, HybridRideFilters, HybridRideProgressiveResponse, // Queue Routing Response Types QueueRoutingResponse, AutoApprovedResponse, QueuedResponse, FailedResponse, EditSubmission, PhotoSubmission, QueueConfiguration, } from '@/types/api'; // ============================================================================ // API Configuration // ============================================================================ const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || '/v1'; interface ApiConfig { baseURL: string; timeout: number; headers: Record; } const defaultConfig: ApiConfig = { baseURL: API_BASE_URL, timeout: 30000, headers: { 'Content-Type': 'application/json', }, }; // Helper function from external API (merged from thrillwiki-real) const createQuery = (params: object): string => { const filteredParams = Object.entries(params).reduce((acc, [key, value]) => { if (value !== null && value !== undefined && value !== '') { acc[key] = value; } return acc; }, {} as { [key: string]: any }); const query = new URLSearchParams(filteredParams).toString(); return query ? `?${query}` : ''; }; // ============================================================================ // HTTP Client Utilities // ============================================================================ class ApiError extends Error { constructor( message: string, public status: number, public response?: any ) { super(message); this.name = 'ApiError'; } } async function makeRequest( endpoint: string, options: RequestInit = {}, config: Partial = {} ): Promise { const finalConfig = { ...defaultConfig, ...config }; const url = `${finalConfig.baseURL}${endpoint}`; // Don't set Content-Type for FormData - let browser handle it const isFormData = options.body instanceof FormData; const baseHeaders = isFormData ? {} : finalConfig.headers; // Add auth token if available const token = getAuthToken(); const authHeaders: Record = token ? { 'Authorization': `Bearer ${token}` } : {}; const finalOptions: RequestInit = { ...options, headers: { ...baseHeaders, ...authHeaders, ...(options.headers as Record || {}), }, }; try { const response = await fetch(url, finalOptions); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new ApiError( errorData.detail || `HTTP ${response.status}: ${response.statusText}`, response.status, errorData ); } // Handle 204 No Content responses if (response.status === 204) { return {} as T; } return await response.json(); } catch (error) { if (error instanceof ApiError) { throw error; } throw new ApiError('Network error', 0, error); } } function getAuthToken(): string | null { if (typeof window === 'undefined') return null; return localStorage.getItem('authToken'); } function setAuthToken(token: string): void { if (typeof window !== 'undefined') { localStorage.setItem('authToken', token); } } function removeAuthToken(): void { if (typeof window !== 'undefined') { localStorage.removeItem('authToken'); } } function getRefreshToken(): string | null { if (typeof window === 'undefined') return null; return localStorage.getItem('refreshToken'); } function setRefreshToken(token: string): void { if (typeof window !== 'undefined') { localStorage.setItem('refreshToken', token); } } function removeRefreshToken(): void { if (typeof window !== 'undefined') { localStorage.removeItem('refreshToken'); } } // ============================================================================ // Authentication API // ============================================================================ export const authApi = { async login(data: LoginRequest): Promise { const response = await makeRequest('/auth/login/', { method: 'POST', body: JSON.stringify(data), }); // Store access token on successful login setAuthToken(response.access); // Store refresh token separately setRefreshToken(response.refresh); return response; }, async signup(data: SignupRequest): Promise { const response = await makeRequest('/auth/signup/', { method: 'POST', body: JSON.stringify(data), }); // Only store tokens if email verification is not required if (response.access && response.refresh) { setAuthToken(response.access); setRefreshToken(response.refresh); } return response; }, async verifyEmail(token: string): Promise { return makeRequest(`/auth/verify-email/${token}/`); }, async resendVerificationEmail(data: ResendVerificationRequest): Promise { return makeRequest('/auth/resend-verification/', { method: 'POST', body: JSON.stringify(data), }); }, async refreshToken(): Promise<{ access: string; refresh: string }> { const refreshToken = getRefreshToken(); if (!refreshToken) { throw new ApiError('No refresh token available', 401); } const response = await makeRequest<{ access: string; refresh: string }>('/auth/token/refresh/', { method: 'POST', body: JSON.stringify({ refresh: refreshToken }), }); // Update stored tokens setAuthToken(response.access); setRefreshToken(response.refresh); return response; }, async logout(): Promise { const refreshToken = getRefreshToken(); const requestBody = refreshToken ? { refresh: refreshToken } : {}; const response = await makeRequest('/auth/logout/', { method: 'POST', body: JSON.stringify(requestBody), }); // Remove tokens on successful logout removeAuthToken(); removeRefreshToken(); return response; }, async getCurrentUser(): Promise { return makeRequest('/auth/user/'); }, async resetPassword(data: PasswordResetRequest): Promise { return makeRequest('/auth/password/reset/', { method: 'POST', body: JSON.stringify(data), }); }, async changePassword(data: PasswordChangeRequest): Promise { return makeRequest('/auth/password/change/', { method: 'POST', body: JSON.stringify(data), }); }, async getSocialProviders(): Promise { return makeRequest('/auth/providers/'); }, async checkAuthStatus(): Promise { return makeRequest('/auth/status/', { method: 'POST', }); }, // Social Authentication Methods async googleLogin(accessToken: string): Promise { const response = await makeRequest('/auth/social/google/', { method: 'POST', body: JSON.stringify({ access_token: accessToken }), }); // Store JWT tokens on successful social login setAuthToken(response.access); setRefreshToken(response.refresh); return response; }, async discordLogin(accessToken: string): Promise { const response = await makeRequest('/auth/social/discord/', { method: 'POST', body: JSON.stringify({ access_token: accessToken }), }); // Store JWT tokens on successful social login setAuthToken(response.access); setRefreshToken(response.refresh); return response; }, async connectSocialAccount(provider: 'google' | 'discord', accessToken: string): Promise<{ success: boolean; message: string }> { return makeRequest(`/auth/social/${provider}/connect/`, { method: 'POST', body: JSON.stringify({ access_token: accessToken }), }); }, async disconnectSocialAccount(provider: 'google' | 'discord'): Promise<{ success: boolean; message: string }> { return makeRequest(`/auth/social/${provider}/disconnect/`, { method: 'POST', }); }, async getSocialConnections(): Promise<{ google: { connected: boolean; email?: string }; discord: { connected: boolean; username?: string }; }> { return makeRequest('/auth/social/connections/'); }, // Social Provider Management Methods async getAvailableProviders(): Promise { return makeRequest('/auth/social/providers/available/'); }, async getConnectedProviders(): Promise { return makeRequest('/auth/social/connected/'); }, async connectProvider(provider: string, data: { access_token: string }): Promise<{ success: boolean; message: string; provider: ConnectedProvider }> { return makeRequest(`/auth/social/connect/${provider}/`, { method: 'POST', body: JSON.stringify(data), }); }, async disconnectProvider(provider: string): Promise<{ success: boolean; message: string }> { return makeRequest(`/auth/social/disconnect/${provider}/`, { method: 'POST', }); }, async getSocialAuthStatus(): Promise { return makeRequest('/auth/social/status/'); }, }; // ============================================================================ // User Account Management API // ============================================================================ export const accountApi = { async getProfile(): Promise { return makeRequest('/accounts/profile/'); }, async updateAccount(data: AccountUpdate): Promise { return makeRequest('/accounts/profile/account/', { method: 'PATCH', body: JSON.stringify(data), }); }, async updateProfile(data: ProfileUpdate): Promise { return makeRequest('/accounts/profile/update/', { method: 'PATCH', body: JSON.stringify(data), }); }, async uploadAvatar(file: File): Promise { const formData = new FormData(); formData.append('avatar', file); // Get auth token to ensure it's available const token = getAuthToken(); if (!token) { throw new ApiError('Authentication required for avatar upload', 401); } return makeRequest('/auth/user/avatar/', { method: 'POST', body: formData, headers: {}, // Let browser set Content-Type for FormData }); }, // Backward compatibility alias async updateAvatar(formData: FormData): Promise { // Get auth token to ensure it's available const token = getAuthToken(); if (!token) { throw new ApiError('Authentication required for avatar upload', 401); } return makeRequest('/auth/user/avatar/', { method: 'POST', body: formData, headers: {}, // Let browser set Content-Type for FormData }); }, async deleteAvatar(): Promise { return makeRequest('/auth/user/avatar/', { method: 'DELETE', }); }, async saveAvatarImage(data: { cloudflare_image_id: string }): Promise { return makeRequest('/accounts/profile/avatar/save/', { method: 'POST', body: JSON.stringify(data), }); }, async getPreferences(): Promise { return makeRequest('/accounts/preferences/'); }, async updatePreferences(data: Partial): Promise { return makeRequest('/accounts/preferences/update/', { method: 'PATCH', body: JSON.stringify(data), }); }, async updateTheme(data: ThemeUpdate): Promise { return makeRequest('/accounts/preferences/theme/', { method: 'PATCH', body: JSON.stringify(data), }); }, async getNotificationSettings(): Promise { return makeRequest('/accounts/settings/notifications/'); }, async updateNotificationSettings(data: Partial): Promise { return makeRequest('/accounts/settings/notifications/update/', { method: 'PATCH', body: JSON.stringify(data), }); }, async getPrivacySettings(): Promise { return makeRequest('/accounts/settings/privacy/'); }, async updatePrivacySettings(data: Partial): Promise { return makeRequest('/accounts/settings/privacy/update/', { method: 'PATCH', body: JSON.stringify(data), }); }, async getSecuritySettings(): Promise { return makeRequest('/accounts/settings/security/'); }, async updateSecuritySettings(data: SecuritySettingsUpdate): Promise { return makeRequest('/accounts/settings/security/update/', { method: 'PATCH', body: JSON.stringify(data), }); }, async getStatistics(): Promise { return makeRequest('/accounts/statistics/'); }, async getTopLists(): Promise { return makeRequest('/accounts/top-lists/'); }, async createTopList(data: CreateTopList): Promise { return makeRequest('/accounts/top-lists/create/', { method: 'POST', body: JSON.stringify(data), }); }, async updateTopList(listId: number, data: Partial): Promise { return makeRequest(`/accounts/top-lists/${listId}/`, { method: 'PATCH', body: JSON.stringify(data), }); }, async deleteTopList(listId: number): Promise { return makeRequest(`/accounts/top-lists/${listId}/delete/`, { method: 'DELETE', }); }, async getNotifications(params?: { unread_only?: boolean; notification_type?: string; limit?: number; offset?: number; }): Promise { const searchParams = new URLSearchParams(); if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null) { searchParams.append(key, value.toString()); } }); } const query = searchParams.toString(); return makeRequest(`/accounts/notifications/${query ? `?${query}` : ''}`); }, async markNotificationsRead(data: MarkNotificationsRead): Promise { return makeRequest('/accounts/notifications/mark-read/', { method: 'PATCH', body: JSON.stringify(data), }); }, async getNotificationPreferences(): Promise { return makeRequest('/accounts/notification-preferences/'); }, async updateNotificationPreferences(data: Partial): Promise { return makeRequest('/accounts/notification-preferences/update/', { method: 'PATCH', body: JSON.stringify(data), }); }, // Account deletion with enhanced error handling for superuser/admin accounts async requestAccountDeletion(): Promise { return makeRequest('/accounts/delete-account/request/', { method: 'POST', }); }, async verifyAccountDeletion(data: VerifyDeletion): Promise { return makeRequest('/accounts/delete-account/verify/', { method: 'POST', body: JSON.stringify(data), }); }, async cancelAccountDeletion(): Promise { return makeRequest('/accounts/delete-account/cancel/', { method: 'POST', }); }, // Admin endpoints async deleteUser(userId: string): Promise { return makeRequest(`/accounts/users/${userId}/delete/`, { method: 'DELETE', }); }, async checkUserDeletionEligibility(userId: string): Promise { return makeRequest(`/accounts/users/${userId}/deletion-check/`); }, }; // ============================================================================ // Parks API // ============================================================================ export const parksApi = { async getParks(params?: ParkSearchFilters): Promise { const searchParams = new URLSearchParams(); if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== undefined) { searchParams.append(key, value.toString()); } }); } const query = searchParams.toString(); return makeRequest(`/parks/${query ? `?${query}` : ''}`); }, async createPark(data: CreateParkRequest): Promise { return makeRequest('/parks/', { method: 'POST', body: JSON.stringify(data), }); }, async getPark(parkId: number): Promise { return makeRequest(`/parks/${parkId}/`); }, async updatePark(parkId: number, data: Partial): Promise { return makeRequest(`/parks/${parkId}/`, { method: 'PATCH', body: JSON.stringify(data), }); }, async deletePark(parkId: number): Promise { return makeRequest(`/parks/${parkId}/`, { method: 'DELETE', }); }, async getFilterOptions(): Promise { return makeRequest('/parks/filter-options/'); }, async searchCompanies(query: string): Promise { return makeRequest(`/parks/search/companies/?q=${encodeURIComponent(query)}`); }, async getSearchSuggestions(query: string): Promise { return makeRequest(`/parks/search-suggestions/?q=${encodeURIComponent(query)}`); }, async setParkImages(parkId: number, data: ParkImageSettings): Promise { return makeRequest(`/parks/${parkId}/image-settings/`, { method: 'PATCH', body: JSON.stringify(data), }); }, async getParkPhotos(parkId: number, params?: { page?: number; page_size?: number; photo_type?: string; }): Promise { const searchParams = new URLSearchParams(); if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== undefined) { searchParams.append(key, value.toString()); } }); } const query = searchParams.toString(); return makeRequest(`/parks/${parkId}/photos/${query ? `?${query}` : ''}`); }, async uploadParkPhoto(parkId: number, data: UploadParkPhoto): Promise { const formData = new FormData(); formData.append('image', data.image); if (data.caption) formData.append('caption', data.caption); if (data.photo_type) formData.append('photo_type', data.photo_type); return makeRequest(`/parks/${parkId}/photos/`, { method: 'POST', body: formData, headers: {}, // Let browser set Content-Type for FormData }); }, async updateParkPhoto(parkId: number, photoId: number, data: UpdateParkPhoto): Promise { return makeRequest(`/parks/${parkId}/photos/${photoId}/`, { method: 'PATCH', body: JSON.stringify(data), }); }, async deleteParkPhoto(parkId: number, photoId: number): Promise { return makeRequest(`/parks/${parkId}/photos/${photoId}/`, { method: 'DELETE', }); }, async saveParkPhoto(parkId: number, data: { cloudflare_image_id: string; caption?: string; alt_text?: string; photo_type?: string; is_primary?: boolean }): Promise { return makeRequest(`/parks/${parkId}/photos/save_image/`, { method: 'POST', body: JSON.stringify(data), }); }, }; // ============================================================================ // Rides API // ============================================================================ export const ridesApi = { // Hybrid filtering (recommended) async getHybridRides(filters?: HybridRideFilters): Promise { const searchParams = new URLSearchParams(); if (filters) { Object.entries(filters).forEach(([key, value]) => { if (value !== undefined && value !== null && value !== '') { searchParams.append(key, value.toString()); } }); } const query = searchParams.toString(); return makeRequest(`/rides/hybrid/${query ? `?${query}` : ''}`); }, async getHybridRidesProgressive(filters?: HybridRideFilters & { offset: number }): Promise { const searchParams = new URLSearchParams(); if (filters) { Object.entries(filters).forEach(([key, value]) => { if (value !== undefined && value !== null && value !== '') { searchParams.append(key, value.toString()); } }); } const query = searchParams.toString(); return makeRequest(`/rides/hybrid/progressive/${query ? `?${query}` : ''}`); }, async getHybridRideFilterMetadata(): Promise { return makeRequest('/rides/hybrid/filter-metadata/'); }, // Legacy rides listing async getRides(filters?: SearchFilters): Promise { const searchParams = new URLSearchParams(); if (filters) { Object.entries(filters).forEach(([key, value]) => { if (value !== undefined && value !== null) { if (Array.isArray(value)) { value.forEach(v => searchParams.append(key, v.toString())); } else { searchParams.append(key, value.toString()); } } }); } const query = searchParams.toString(); return makeRequest(`/rides/${query ? `?${query}` : ''}`); }, async createRide(data: CreateRideRequest): Promise { return makeRequest('/rides/', { method: 'POST', body: JSON.stringify(data), }); }, async getRide(rideId: number): Promise { return makeRequest(`/rides/${rideId}/`); }, async updateRide(rideId: number, data: Partial): Promise { return makeRequest(`/rides/${rideId}/`, { method: 'PATCH', body: JSON.stringify(data), }); }, async deleteRide(rideId: number): Promise { return makeRequest(`/rides/${rideId}/`, { method: 'DELETE', }); }, async getFilterOptions(): Promise { return makeRequest('/rides/filter-options/'); }, async searchCompanies(query: string): Promise { return makeRequest(`/rides/search/companies/?q=${encodeURIComponent(query)}`); }, async searchRideModels(query: string): Promise { return makeRequest(`/rides/search/ride-models/?q=${encodeURIComponent(query)}`); }, async getSearchSuggestions(query: string): Promise { return makeRequest(`/rides/search-suggestions/?q=${encodeURIComponent(query)}`); }, async setRideImages(rideId: number, data: RideImageSettings): Promise { return makeRequest(`/rides/${rideId}/image-settings/`, { method: 'PATCH', body: JSON.stringify(data), }); }, async getRidePhotos(rideId: number, params?: { page?: number; page_size?: number; photo_type?: string; }): Promise { const searchParams = new URLSearchParams(); if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== undefined) { searchParams.append(key, value.toString()); } }); } const query = searchParams.toString(); return makeRequest(`/rides/${rideId}/photos/${query ? `?${query}` : ''}`); }, async uploadRidePhoto(rideId: number, data: UploadRidePhoto): Promise { const formData = new FormData(); formData.append('image', data.image); if (data.caption) formData.append('caption', data.caption); if (data.photo_type) formData.append('photo_type', data.photo_type); return makeRequest(`/rides/${rideId}/photos/`, { method: 'POST', body: formData, headers: {}, // Let browser set Content-Type for FormData }); }, async updateRidePhoto(rideId: number, photoId: number, data: UpdateRidePhoto): Promise { return makeRequest(`/rides/${rideId}/photos/${photoId}/`, { method: 'PATCH', body: JSON.stringify(data), }); }, async deleteRidePhoto(rideId: number, photoId: number): Promise { return makeRequest(`/rides/${rideId}/photos/${photoId}/`, { method: 'DELETE', }); }, async getManufacturerRideModels(manufacturerSlug: string): Promise { return makeRequest(`/rides/manufacturers/${manufacturerSlug}/`); }, async saveRidePhoto(rideId: number, data: { cloudflare_image_id: string; caption?: string; alt_text?: string; photo_type?: string; is_primary?: boolean }): Promise { return makeRequest(`/rides/${rideId}/photos/save_image/`, { method: 'POST', body: JSON.stringify(data), }); }, }; // ============================================================================ // Core/Search API // ============================================================================ export const coreApi = { async searchEntities(params: { q: string; entity_types?: string[]; limit?: number; }): Promise { const searchParams = new URLSearchParams(); searchParams.append('q', params.q); if (params.entity_types) { params.entity_types.forEach(type => searchParams.append('entity_types', type)); } if (params.limit) { searchParams.append('limit', params.limit.toString()); } return makeRequest(`/core/entities/search/?${searchParams.toString()}`); }, async reportEntityNotFound(data: EntityNotFoundRequest): Promise { return makeRequest('/core/entities/not-found/', { method: 'POST', body: JSON.stringify(data), }); }, async getEntitySuggestions(params: { q: string; limit?: number; }): Promise { const searchParams = new URLSearchParams(); searchParams.append('q', params.q); if (params.limit) { searchParams.append('limit', params.limit.toString()); } return makeRequest(`/core/entities/suggestions/?${searchParams.toString()}`); }, }; // ============================================================================ // Maps API // ============================================================================ export const mapsApi = { async getMapLocations(params?: { bounds?: string; zoom?: number; entity_types?: string[]; categories?: string[]; }): Promise { const searchParams = new URLSearchParams(); if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== undefined) { if (Array.isArray(value)) { value.forEach(v => searchParams.append(key, v)); } else { searchParams.append(key, value.toString()); } } }); } const query = searchParams.toString(); return makeRequest(`/maps/locations/${query ? `?${query}` : ''}`); }, async getLocationDetail(locationType: 'park' | 'ride', locationId: number): Promise { return makeRequest(`/maps/locations/${locationType}/${locationId}/`); }, async searchMap(params: { q: string; bounds?: string; entity_types?: string[]; }): Promise { const searchParams = new URLSearchParams(); searchParams.append('q', params.q); if (params.bounds) { searchParams.append('bounds', params.bounds); } if (params.entity_types) { params.entity_types.forEach(type => searchParams.append('entity_types', type)); } return makeRequest(`/maps/search/?${searchParams.toString()}`); }, async getMapBounds(bounds: BoundingBox & { zoom?: number }): Promise { const searchParams = new URLSearchParams(); searchParams.append('sw_lat', bounds.sw_lat.toString()); searchParams.append('sw_lng', bounds.sw_lng.toString()); searchParams.append('ne_lat', bounds.ne_lat.toString()); searchParams.append('ne_lng', bounds.ne_lng.toString()); if (bounds.zoom) { searchParams.append('zoom', bounds.zoom.toString()); } return makeRequest(`/maps/bounds/?${searchParams.toString()}`); }, async getMapStats(): Promise { return makeRequest('/maps/stats/'); }, async getMapCache(): Promise { return makeRequest('/maps/cache/'); }, async invalidateMapCache(): Promise { return makeRequest('/maps/cache/invalidate/', { method: 'POST', }); }, }; // ============================================================================ // Health & Statistics API // ============================================================================ export const healthApi = { async getHealthCheck(): Promise { return makeRequest('/health/'); }, async getSimpleHealth(): Promise { return makeRequest('/health/simple/'); }, async getPerformanceMetrics(): Promise { return makeRequest('/health/performance/'); }, async getSystemStats(): Promise { return makeRequest('/stats/'); }, async recalculateStats(): Promise { return makeRequest('/stats/recalculate/', { method: 'POST', }); }, }; // ============================================================================ // Trending & Discovery API // ============================================================================ export const trendingApi = { async getTrending(params?: { time_period?: string; entity_types?: string[]; limit?: number; }): Promise { const searchParams = new URLSearchParams(); if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== undefined) { if (Array.isArray(value)) { value.forEach(v => searchParams.append(key, v)); } else { searchParams.append(key, value.toString()); } } }); } const query = searchParams.toString(); return makeRequest(`/trending/${query ? `?${query}` : ''}`); }, async getNewContent(params?: { days?: number; entity_types?: string[]; limit?: number; }): Promise { const searchParams = new URLSearchParams(); if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== undefined) { if (Array.isArray(value)) { value.forEach(v => searchParams.append(key, v)); } else { searchParams.append(key, value.toString()); } } }); } const query = searchParams.toString(); return makeRequest(`/new-content/${query ? `?${query}` : ''}`); }, async triggerTrendingCalculation(): Promise { return makeRequest('/trending/calculate/', { method: 'POST', }); }, }; // ============================================================================ // Reviews & Rankings API // ============================================================================ export const reviewsApi = { async getLatestReviews(params?: { limit?: number; entity_types?: string[]; min_rating?: number; }): Promise { const searchParams = new URLSearchParams(); if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== undefined) { if (Array.isArray(value)) { value.forEach(v => searchParams.append(key, v)); } else { searchParams.append(key, value.toString()); } } }); } const query = searchParams.toString(); return makeRequest(`/reviews/latest/${query ? `?${query}` : ''}`); }, }; export const rankingsApi = { async getRankings(params?: { category?: string; park_id?: number; manufacturer_id?: number; roller_coaster_type?: string; track_material?: string; min_rating?: number; max_rating?: number; min_reviews?: number; ordering?: string; limit?: number; offset?: number; }): Promise { const searchParams = new URLSearchParams(); if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== undefined) { searchParams.append(key, value.toString()); } }); } const query = searchParams.toString(); return makeRequest(`/rankings/${query ? `?${query}` : ''}`); }, async getRideRanking(rideId: number): Promise { return makeRequest(`/rankings/${rideId}/`); }, async triggerRankingCalculation(): Promise { return makeRequest('/rankings/calculate/', { method: 'POST', }); }, }; // ============================================================================ // Email Service API // ============================================================================ export const emailApi = { async sendContactEmail(data: ContactEmailRequest): Promise { return makeRequest('/email/contact/', { method: 'POST', body: JSON.stringify(data), }); }, async subscribeNewsletter(data: NewsletterSubscribeRequest): Promise { return makeRequest('/email/newsletter/subscribe/', { method: 'POST', body: JSON.stringify(data), }); }, async unsubscribeNewsletter(data: NewsletterUnsubscribeRequest): Promise { return makeRequest('/email/newsletter/unsubscribe/', { method: 'POST', body: JSON.stringify(data), }); }, }; // ============================================================================ // Content Moderation API // ============================================================================ export const moderationApi = { // Moderation Reports async getReports(params?: ModerationReportFilters): Promise { const searchParams = new URLSearchParams(); if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== undefined) { searchParams.append(key, value.toString()); } }); } const query = searchParams.toString(); return makeRequest(`/moderation/reports/${query ? `?${query}` : ''}`); }, async createReport(data: CreateModerationReport): Promise { return makeRequest('/moderation/reports/', { method: 'POST', body: JSON.stringify(data), }); }, async getReport(reportId: number): Promise { return makeRequest(`/moderation/reports/${reportId}/`); }, async updateReport(reportId: number, data: UpdateModerationReportData): Promise { return makeRequest(`/moderation/reports/${reportId}/`, { method: 'PATCH', body: JSON.stringify(data), }); }, async deleteReport(reportId: number): Promise { return makeRequest(`/moderation/reports/${reportId}/`, { method: 'DELETE', }); }, async assignReport(reportId: number, moderatorId: number): Promise { return makeRequest(`/moderation/reports/${reportId}/assign/`, { method: 'POST', body: JSON.stringify({ moderator_id: moderatorId }), }); }, async resolveReport(reportId: number, data: { resolution_action: string; resolution_notes?: string; }): Promise { return makeRequest(`/moderation/reports/${reportId}/resolve/`, { method: 'POST', body: JSON.stringify(data), }); }, async getReportStats(): Promise { return makeRequest('/moderation/reports/stats/'); }, // Moderation Queue async getQueue(params?: ModerationQueueFilters): Promise { const searchParams = new URLSearchParams(); if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== undefined) { searchParams.append(key, value.toString()); } }); } const query = searchParams.toString(); return makeRequest(`/moderation/queue/${query ? `?${query}` : ''}`); }, async getMyQueue(): Promise { return makeRequest('/moderation/queue/my_queue/'); }, async assignQueueItem(itemId: number, data: AssignQueueItemData): Promise { return makeRequest(`/moderation/queue/${itemId}/assign/`, { method: 'POST', body: JSON.stringify(data), }); }, async unassignQueueItem(itemId: number): Promise { return makeRequest(`/moderation/queue/${itemId}/unassign/`, { method: 'POST', }); }, async completeQueueItem(itemId: number, data: CompleteQueueItemData): Promise { return makeRequest(`/moderation/queue/${itemId}/complete/`, { method: 'POST', body: JSON.stringify(data), }); }, // Moderation Actions async getActions(params?: ModerationActionFilters): Promise> { const searchParams = new URLSearchParams(); if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== undefined) { searchParams.append(key, value.toString()); } }); } const query = searchParams.toString(); return makeRequest>(`/moderation/actions/${query ? `?${query}` : ''}`); }, async createAction(data: CreateModerationActionData): Promise { return makeRequest('/moderation/actions/', { method: 'POST', body: JSON.stringify(data), }); }, async getAction(actionId: number): Promise { return makeRequest(`/moderation/actions/${actionId}/`); }, async deactivateAction(actionId: number): Promise { return makeRequest(`/moderation/actions/${actionId}/deactivate/`, { method: 'POST', }); }, async getActiveActions(): Promise> { return makeRequest>('/moderation/actions/active/'); }, async getExpiredActions(): Promise> { return makeRequest>('/moderation/actions/expired/'); }, }; // ============================================================================ // User Moderation API // ============================================================================ export const userModerationApi = { async getUserModerationProfile(userId: number): Promise { return makeRequest(`/admin/users/${userId}/moderate/`); }, async moderateUser(userId: number, action: UserModerationAction): Promise { return makeRequest(`/admin/users/${userId}/moderate/`, { method: 'POST', body: JSON.stringify(action), }); }, async getUserModerationHistory(userId: number, params?: { limit?: number; offset?: number; action_type?: "WARNING" | "CONTENT_REMOVAL" | "CONTENT_EDIT" | "USER_SUSPENSION" | "USER_BAN" | "ACCOUNT_RESTRICTION"; }): Promise<{ results: ModerationAction[]; count: number }> { const searchParams = new URLSearchParams(); if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== undefined) { searchParams.append(key, value.toString()); } }); } const query = searchParams.toString(); return makeRequest(`/admin/users/${userId}/moderate/history/${query ? `?${query}` : ''}`); }, async removeUserRestriction(userId: number, actionId: number): Promise<{ success: boolean; message: string }> { return makeRequest(`/admin/users/${userId}/moderate/restrictions/${actionId}/remove/`, { method: 'POST', }); }, async getModerationStats(): Promise { return makeRequest('/admin/users/moderation-stats/'); }, async searchUsersForModeration(params: { query?: string; role?: "USER" | "MODERATOR" | "ADMIN" | "SUPERUSER"; has_restrictions?: boolean; registration_after?: string; registration_before?: string; last_activity_before?: string; risk_level?: "LOW" | "MEDIUM" | "HIGH" | "CRITICAL"; page?: number; page_size?: number; }): Promise<{ count: number; results: Array<{ id: number; username: string; display_name: string; email: string; role: string; date_joined: string; last_login: string | null; is_active: boolean; restriction_count: number; risk_level: string; }>; }> { const searchParams = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value !== undefined) { searchParams.append(key, value.toString()); } }); const query = searchParams.toString(); return makeRequest(`/admin/users/search/${query ? `?${query}` : ''}`); }, }; // ============================================================================ // Bulk Operations API // ============================================================================ export const bulkOperationsApi = { async getOperations(params?: { status?: "PENDING" | "RUNNING" | "COMPLETED" | "FAILED" | "CANCELLED"; operation_type?: "UPDATE_PARKS" | "UPDATE_RIDES" | "IMPORT_DATA" | "EXPORT_DATA" | "RECALCULATE_STATS" | "MODERATE_CONTENT" | "USER_ACTIONS"; created_by?: number; created_after?: string; created_before?: string; page?: number; page_size?: number; ordering?: string; }): Promise { const searchParams = new URLSearchParams(); if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== undefined) { searchParams.append(key, value.toString()); } }); } const query = searchParams.toString(); return makeRequest(`/admin/bulk-operations/${query ? `?${query}` : ''}`); }, async createOperation(data: CreateBulkOperation): Promise { return makeRequest('/admin/bulk-operations/', { method: 'POST', body: JSON.stringify(data), }); }, async getOperation(operationId: string): Promise { return makeRequest(`/admin/bulk-operations/${operationId}/`); }, async cancelOperation(operationId: string): Promise<{ success: boolean; message: string }> { return makeRequest(`/admin/bulk-operations/${operationId}/cancel/`, { method: 'POST', }); }, async retryOperation(operationId: string): Promise { return makeRequest(`/admin/bulk-operations/${operationId}/retry/`, { method: 'POST', }); }, // Specific bulk operation helpers async bulkUpdateParks(data: BulkUpdateParks): Promise { return this.createOperation(data); }, async bulkUpdateRides(data: BulkUpdateRides): Promise { return this.createOperation(data); }, async bulkImportData(data: BulkImportData): Promise { return this.createOperation(data); }, async bulkExportData(data: BulkExportData): Promise { return this.createOperation(data); }, async bulkModerateContent(data: BulkModerateContent): Promise { return this.createOperation(data); }, async bulkUserActions(data: BulkUserActions): Promise { return this.createOperation(data); }, // Operation monitoring async getOperationLogs(operationId: string, params?: { level?: "DEBUG" | "INFO" | "WARNING" | "ERROR"; limit?: number; offset?: number; }): Promise<{ logs: Array<{ timestamp: string; level: string; message: string; details?: any; }>; count: number; }> { const searchParams = new URLSearchParams(); if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== undefined) { searchParams.append(key, value.toString()); } }); } const query = searchParams.toString(); return makeRequest(`/admin/bulk-operations/${operationId}/logs/${query ? `?${query}` : ''}`); }, }; // ============================================================================ // Park Reviews API // ============================================================================ export const parkReviewsApi = { async getParkReviews(parkId: number, filters?: ParkReviewFilters): Promise { const searchParams = new URLSearchParams(); if (filters) { Object.entries(filters).forEach(([key, value]) => { if (value !== undefined && value !== null) { if (Array.isArray(value)) { value.forEach(v => searchParams.append(key, v.toString())); } else { searchParams.append(key, value.toString()); } } }); } const query = searchParams.toString(); return makeRequest(`/parks/${parkId}/reviews/${query ? `?${query}` : ''}`); }, async createParkReview(parkId: number, data: CreateParkReview): Promise { return makeRequest(`/parks/${parkId}/reviews/`, { method: 'POST', body: JSON.stringify(data), }); }, async getParkReview(parkId: number, reviewId: number): Promise { return makeRequest(`/parks/${parkId}/reviews/${reviewId}/`); }, async updateParkReview(parkId: number, reviewId: number, data: UpdateParkReview): Promise { return makeRequest(`/parks/${parkId}/reviews/${reviewId}/`, { method: 'PATCH', body: JSON.stringify(data), }); }, async deleteParkReview(parkId: number, reviewId: number): Promise { return makeRequest(`/parks/${parkId}/reviews/${reviewId}/`, { method: 'DELETE', }); }, async voteParkReview(parkId: number, reviewId: number, vote: ParkReviewVote): Promise { return makeRequest(`/parks/${parkId}/reviews/${reviewId}/vote/`, { method: 'POST', body: JSON.stringify(vote), }); }, async removeParkReviewVote(parkId: number, reviewId: number): Promise { return makeRequest(`/parks/${parkId}/reviews/${reviewId}/vote/`, { method: 'DELETE', }); }, async uploadParkReviewPhoto(parkId: number, reviewId: number, data: UploadParkReviewPhoto): Promise { const formData = new FormData(); formData.append('image', data.image); if (data.caption) formData.append('caption', data.caption); if (data.photo_type) formData.append('photo_type', data.photo_type); return makeRequest(`/parks/${parkId}/reviews/${reviewId}/photos/`, { method: 'POST', body: formData, headers: {}, // Let browser set Content-Type for FormData }); }, async deleteParkReviewPhoto(parkId: number, reviewId: number, photoId: number): Promise { return makeRequest(`/parks/${parkId}/reviews/${reviewId}/photos/${photoId}/`, { method: 'DELETE', }); }, async reportParkReview(parkId: number, reviewId: number, data: { reason: string; description: string; }): Promise<{ success: boolean; message: string }> { return makeRequest(`/parks/${parkId}/reviews/${reviewId}/report/`, { method: 'POST', body: JSON.stringify(data), }); }, // Park review statistics and analytics async getParkReviewStats(parkId: number): Promise<{ total_reviews: number; average_rating: number; rating_distribution: { "1": number; "2": number; "3": number; "4": number; "5": number; "6": number; "7": number; "8": number; "9": number; "10": number; }; category_averages: { atmosphere: number; cleanliness: number; staff_friendliness: number; value_for_money: number; food_quality: number; ride_variety: number; }; recent_trends: { last_30_days_average: number; trend_direction: "UP" | "DOWN" | "STABLE"; trend_percentage: number; }; verified_visits_percentage: number; photos_count: number; }> { return makeRequest(`/parks/${parkId}/reviews/stats/`); }, }; // ============================================================================ // Django-CloudflareImages-Toolkit API (Updated to match actual endpoints) // ============================================================================ export const cloudflareImagesApi = { // Direct Upload Flow - Get temporary upload URL async createDirectUploadUrl(data?: CloudflareDirectUploadRequest): Promise { return makeRequest('/cloudflare-images/api/upload-url/', { method: 'POST', body: JSON.stringify(data || {}), }); }, // Upload image using temporary URL (client-side to Cloudflare) async uploadToCloudflare(uploadUrl: string, file: File, metadata?: Record): Promise { const formData = new FormData(); formData.append('file', file); if (metadata) { Object.entries(metadata).forEach(([key, value]) => { formData.append(`metadata[${key}]`, value.toString()); }); } // Upload directly to Cloudflare (bypasses our API) const response = await fetch(uploadUrl, { method: 'POST', body: formData, }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new ApiError( errorData.errors?.[0]?.message || `Upload failed: ${response.status}`, response.status, errorData ); } return await response.json(); }, // Complete upload flow - get URL then upload async uploadImage(file: File, options?: { expiry_minutes?: number; // Minutes until upload URL expires metadata?: Record; require_signed_urls?: boolean; filename?: string; }): Promise<{ uploadResponse: CloudflareImageUploadResponse; directUploadResponse: CloudflareDirectUploadResponse; }> { // Step 1: Get temporary upload URL const directUploadResponse = await this.createDirectUploadUrl({ expiry_minutes: options?.expiry_minutes, metadata: options?.metadata, require_signed_urls: options?.require_signed_urls, filename: options?.filename, }); // Step 2: Upload to Cloudflare const uploadResponse = await this.uploadToCloudflare( directUploadResponse.upload_url, file, options?.metadata ); return { uploadResponse, directUploadResponse }; }, // List images with pagination async listImages(params?: { status?: "pending" | "uploaded" | "failed" | "expired"; limit?: number; offset?: number; }): Promise { const searchParams = new URLSearchParams(); if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== undefined) { searchParams.append(key, value.toString()); } }); } const query = searchParams.toString(); return makeRequest(`/cloudflare-images/api/images/${query ? `?${query}` : ''}`); }, // Get image details async getImage(imageId: string): Promise { return makeRequest(`/cloudflare-images/api/images/${imageId}/`); }, // Check image status async checkImageStatus(imageId: string): Promise { return makeRequest(`/cloudflare-images/api/images/${imageId}/check_status/`, { method: 'POST', }); }, // Update image metadata async updateImage(imageId: string, data: { metadata?: Record; require_signed_urls?: boolean; }): Promise { return makeRequest(`/cloudflare-images/api/images/${imageId}/`, { method: 'PATCH', body: JSON.stringify(data), }); }, // Delete image async deleteImage(imageId: string): Promise { return makeRequest(`/cloudflare-images/api/images/${imageId}/`, { method: 'DELETE', }); }, // Get account statistics async getStats(): Promise { return makeRequest('/cloudflare-images/api/stats/'); }, // Get available variants async getVariants(): Promise { return makeRequest('/cloudflare-images/api/variants/'); }, // Create new variant async createVariant(data: Omit): Promise { return makeRequest('/cloudflare-images/api/variants/', { method: 'POST', body: JSON.stringify(data), }); }, // Update variant async updateVariant(variantId: string, data: Partial>): Promise { return makeRequest(`/cloudflare-images/api/variants/${variantId}/`, { method: 'PATCH', body: JSON.stringify(data), }); }, // Delete variant async deleteVariant(variantId: string): Promise { return makeRequest(`/cloudflare-images/api/variants/${variantId}/`, { method: 'DELETE', }); }, // Webhook handling (for server-side processing) async processWebhook(payload: CloudflareWebhookPayload): Promise<{ success: boolean; message: string }> { return makeRequest('/cloudflare-images/api/webhook/', { method: 'POST', body: JSON.stringify(payload), }); }, // Utility functions for URL generation generateImageUrl(imageId: string, variant: string = 'public'): string { // This would typically use your Cloudflare account hash // The actual implementation would get this from your backend configuration return `/cloudflare-images/serve/${imageId}/${variant}`; }, generateSignedUrl(imageId: string, variant: string = 'public', expiryMinutes: number = 60): Promise<{ url: string; expires_at: string }> { return makeRequest(`/cloudflare-images/api/signed-url/`, { method: 'POST', body: JSON.stringify({ image_id: imageId, variant, expiry_minutes: expiryMinutes, }), }); }, // Batch operations async batchDelete(imageIds: string[]): Promise<{ success: string[]; failed: Array<{ id: string; error: string }>; }> { return makeRequest('/cloudflare-images/api/batch/delete/', { method: 'POST', body: JSON.stringify({ image_ids: imageIds }), }); }, async batchUpdateMetadata(updates: Array<{ image_id: string; metadata: Record; }>): Promise<{ success: string[]; failed: Array<{ id: string; error: string }>; }> { return makeRequest('/cloudflare-images/api/batch/update-metadata/', { method: 'POST', body: JSON.stringify({ updates }), }); }, // Search and filter images async searchImages(params: { metadata_key?: string; metadata_value?: string; uploaded_after?: string; // ISO date uploaded_before?: string; // ISO date filename_contains?: string; page?: number; per_page?: number; }): Promise { const searchParams = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value !== undefined) { searchParams.append(key, value.toString()); } }); const query = searchParams.toString(); return makeRequest(`/cloudflare-images/api/search/${query ? `?${query}` : ''}`); }, // Cleanup expired upload URLs async cleanupExpiredUploads(): Promise<{ cleaned_count: number; message: string; }> { return makeRequest('/cloudflare-images/api/cleanup/', { method: 'POST', }); }, // Backward compatibility aliases getDirectUploadUrl: function(data?: CloudflareDirectUploadRequest): Promise { return this.createDirectUploadUrl(data); }, }; // ============================================================================ // History API // ============================================================================ export const historyApi = { async getEntityHistory( entityType: 'park' | 'ride', entityId: number, params?: { limit?: number; offset?: number; field?: string; } ): Promise { const searchParams = new URLSearchParams(); if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== undefined) { searchParams.append(key, value.toString()); } }); } const query = searchParams.toString(); return makeRequest(`/history/${entityType}/${entityId}/${query ? `?${query}` : ''}`); }, async getRecentChanges(params?: { entity_types?: string[]; actions?: string[]; limit?: number; days?: number; }): Promise { const searchParams = new URLSearchParams(); if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== undefined) { if (Array.isArray(value)) { value.forEach(v => searchParams.append(key, v)); } else { searchParams.append(key, value.toString()); } } }); } const query = searchParams.toString(); return makeRequest(`/history/recent/${query ? `?${query}` : ''}`); }, }; // ============================================================================ // Utility Functions // ============================================================================ export const apiUtils = { // Token management getAuthToken, setAuthToken, removeAuthToken, // Error handling isApiError: (error: any): error is ApiError => error instanceof ApiError, // URL building helpers buildQueryString: (params: Record): string => { const searchParams = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null) { if (Array.isArray(value)) { value.forEach(v => searchParams.append(key, v.toString())); } else { searchParams.append(key, value.toString()); } } }); return searchParams.toString(); }, // File upload helpers createFormData: (data: Record): FormData => { const formData = new FormData(); Object.entries(data).forEach(([key, value]) => { if (value !== undefined && value !== null) { if (value instanceof File) { formData.append(key, value); } else { formData.append(key, value.toString()); } } }); return formData; }, // Response helpers handlePaginatedResponse: (response: any): { data: T[]; pagination: { count: number; next: string | null; previous: string | null; hasNext: boolean; hasPrevious: boolean; }; } => ({ data: response.results || [], pagination: { count: response.count || 0, next: response.next || null, previous: response.previous || null, hasNext: !!response.next, hasPrevious: !!response.previous, }, }), }; // ============================================================================ // External API Client Class (merged from thrillwiki-real) // ============================================================================ class ApiClient { private baseUrl: string constructor(baseUrl: string) { this.baseUrl = baseUrl } private async fetchJson(endpoint: string, options?: RequestInit): Promise { const response = await fetch(`${this.baseUrl}${endpoint}`, options) if (!response.ok) { const errorData = await response.json().catch(() => ({ message: response.statusText })) throw new Error(errorData.message || `API error: ${response.status}`) } // Handle cases where the response body might be empty const text = await response.text(); return text ? JSON.parse(text) : ({} as T); } async healthCheck(): Promise<{ status: string }> { return this.fetchJson("/health/simple/") } async getHealthDetail(): Promise { return this.fetchJson('/health/detail/'); } async getStats(): Promise { return this.fetchJson("/stats/") } async getTrendingContent(params: { timeframe: string; limit: number }): Promise<{ trending_parks: TrendingItem[]; trending_rides: TrendingItem[]; }> { return this.fetchJson(`/trending/${createQuery(params)}`) } async getNewContent(params: { days: number; limit: number }): Promise<{ new_parks: Park[]; new_rides: Ride[]; }> { return this.fetchJson(`/new-content/${createQuery(params)}`) } async getLatestReviews(params: { limit: number }): Promise { return this.fetchJson(`/reviews/latest/${createQuery(params)}`) } async searchEntities(params: { query: string; entity_types?: string[]; include_suggestions?: boolean; }): Promise<{ matches: any[]; suggestion?: string; }> { return this.fetchJson(`/search/${createQuery(params)}`) } async getParks(params: object): Promise> { return this.fetchJson(`/parks/${createQuery(params)}`); } async getParkFilterOptions(): Promise { return makeRequest('/parks/filters/'); } // Rides methods async getRides(params: object): Promise> { const query = apiUtils.buildQueryString(params); return makeRequest(`/rides/${query ? `?${query}` : ''}`); } async getRideFilterOptions(): Promise { return makeRequest('/rides/filters/'); } // Companies methods async getCompanies(params: object): Promise> { const query = apiUtils.buildQueryString(params); return makeRequest(`/companies/${query ? `?${query}` : ''}`); } async getCompanyFilterOptions(): Promise { return makeRequest('/companies/filters/'); } async getMapLocations(params: object): Promise { return this.fetchJson(`/map/locations/${createQuery(params)}`); } async getRankings(params: object): Promise { return this.fetchJson(`/rankings/${createQuery(params)}`); } async getReviews(params: object): Promise> { return this.fetchJson(`/reviews/${createQuery(params)}`); } async searchCompanies(params: object): Promise { return this.fetchJson(`/companies/search/${createQuery(params)}`); } async getPerformanceMetrics(): Promise { return this.fetchJson('/performance/'); } async getTimeline(params: object): Promise { return this.fetchJson(`/timeline/${createQuery(params)}`); } async getEmailTemplates(): Promise { return this.fetchJson('/admin/emails/'); } // Auth methods async checkAuthStatus(): Promise { return this.fetchJson('/auth/status/'); } async getCurrentUser(): Promise { return this.fetchJson('/auth/user/'); } async updateCurrentUser(userData: Partial): Promise { return this.fetchJson('/auth/user/', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(userData), }); } // Backward compatibility alias - uses correct accounts endpoint async updateAvatar(formData: FormData): Promise { return makeRequest('/accounts/profile/avatar/upload/', { method: 'POST', body: formData, headers: {}, // Let browser set Content-Type for FormData }); } async login(credentials: LoginRequest): Promise<{ user: User }> { return this.fetchJson('/auth/login/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentials), }); } async register(userData: RegisterRequest): Promise<{ user: User }> { return this.fetchJson('/auth/register/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(userData), }); } async logout(): Promise { await this.fetchJson('/auth/logout/', { method: 'POST' }); } async changePassword(passwordData: object): Promise { await this.fetchJson('/auth/password/change/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(passwordData), }); } async deleteAccount(): Promise { await this.fetchJson('/auth/user/', { method: 'DELETE' }); } async refreshToken(): Promise { await this.fetchJson('/auth/token/refresh/', { method: 'POST' }); } } // Create instance matching external API pattern export const apiClient = new ApiClient(API_BASE_URL); // ============================================================================ // External API Client Methods (merged from thrillwiki-real) // ============================================================================ export const externalApi = { // Health and stats methods async healthCheck(): Promise<{ status: string }> { return makeRequest("/health/simple/"); }, async getHealthDetail(): Promise { return makeRequest('/health/detail/'); }, async getStats(): Promise { return makeRequest("/stats/"); }, // Trending and discovery methods async getTrendingContent(params: { timeframe: string; limit: number }): Promise<{ trending_parks: TrendingItem[]; trending_rides: TrendingItem[]; }> { const query = apiUtils.buildQueryString(params); return makeRequest(`/trending/${query ? `?${query}` : ''}`); }, async getNewContent(params: { days: number; limit: number }): Promise<{ new_parks: Park[]; new_rides: Ride[]; }> { const query = apiUtils.buildQueryString(params); return makeRequest(`/new-content/${query ? `?${query}` : ''}`); }, async getLatestReviews(params: { limit: number }): Promise { const query = apiUtils.buildQueryString(params); return makeRequest(`/reviews/latest/${query ? `?${query}` : ''}`); }, // Search methods async searchEntities(params: { query: string; entity_types?: string[]; include_suggestions?: boolean; }): Promise<{ matches: any[]; suggestion?: string; }> { const query = apiUtils.buildQueryString(params); return makeRequest(`/search/${query ? `?${query}` : ''}`); }, // Parks methods async getParks(params: object): Promise> { const query = apiUtils.buildQueryString(params); return makeRequest(`/parks/${query ? `?${query}` : ''}`); }, async getParkFilterOptions(): Promise { return makeRequest('/parks/filters/'); }, // Rides methods async getRides(params: object): Promise> { const query = apiUtils.buildQueryString(params); return makeRequest(`/rides/${query ? `?${query}` : ''}`); }, async getRideFilterOptions(): Promise { return makeRequest('/rides/filters/'); }, // Companies methods async getCompanies(params: object): Promise> { const query = apiUtils.buildQueryString(params); return makeRequest(`/companies/${query ? `?${query}` : ''}`); }, async getCompanyFilterOptions(): Promise { return makeRequest('/companies/filters/'); }, async searchCompanies(params: object): Promise { const query = apiUtils.buildQueryString(params); return makeRequest(`/companies/search/${query ? `?${query}` : ''}`); }, // Additional utility methods async getEntitySuggestions(params: object): Promise { const query = apiUtils.buildQueryString(params); return makeRequest(`/search/suggest/${query ? `?${query}` : ''}`); }, async getMapLocations(params: object): Promise { const query = apiUtils.buildQueryString(params); return makeRequest(`/map/locations/${query ? `?${query}` : ''}`); }, async getRankings(params: object): Promise { const query = apiUtils.buildQueryString(params); return makeRequest(`/rankings/${query ? `?${query}` : ''}`); }, async getReviews(params: object): Promise> { const query = apiUtils.buildQueryString(params); return makeRequest(`/reviews/${query ? `?${query}` : ''}`); }, async getPerformanceMetrics(): Promise { return makeRequest('/performance/'); }, async getTimeline(params: object): Promise { const query = apiUtils.buildQueryString(params); return makeRequest(`/timeline/${query ? `?${query}` : ''}`); }, async getEmailTemplates(): Promise { return makeRequest('/admin/emails/'); }, // Auth methods from external API async updateCurrentUser(userData: Partial): Promise { return makeRequest('/auth/user/', { method: 'PATCH', body: JSON.stringify(userData), }); }, async login(credentials: LoginRequest): Promise<{ user: User }> { return makeRequest('/auth/login/', { method: 'POST', body: JSON.stringify(credentials), }); }, async register(userData: RegisterRequest): Promise<{ user: User }> { return makeRequest('/auth/register/', { method: 'POST', body: JSON.stringify(userData), }); }, async deleteAccount(): Promise { await makeRequest('/auth/user/', { method: 'DELETE' }); }, async refreshToken(): Promise { await makeRequest('/auth/token/refresh/', { method: 'POST' }); }, }; // Default export with all APIs export default { auth: authApi, account: accountApi, parks: parksApi, rides: ridesApi, core: coreApi, maps: mapsApi, health: healthApi, trending: trendingApi, reviews: reviewsApi, rankings: rankingsApi, email: emailApi, history: historyApi, moderation: moderationApi, userModeration: userModerationApi, bulkOperations: bulkOperationsApi, parkReviews: parkReviewsApi, cloudflareImages: cloudflareImagesApi, external: externalApi, utils: apiUtils, }; // ============================================================================ // Usage Examples // ============================================================================ /* // Authentication import { authApi } from './api'; const login = async () => { try { const response = await authApi.login({ username: 'user@example.com', password: 'password123' }); console.log('Logged in:', response.user); } catch (error) { if (apiUtils.isApiError(error)) { console.error('Login failed:', error.message); } } }; // Signup with display_name const signup = async () => { try { const response = await authApi.signup({ username: 'newuser', email: 'user@example.com', password: 'password123', password_confirm: 'password123', display_name: 'New User' }); console.log('Signed up:', response.user); } catch (error) { if (apiUtils.isApiError(error)) { console.error('Signup failed:', error.message); } } }; // Parks with filtering import { parksApi } from './api'; const getParksInUSA = async () => { const parks = await parksApi.getParks({ country: 'United States', page_size: 50, ordering: 'name' }); return parks.results; }; // Rides with complex filtering import { ridesApi } from './api'; const getRollerCoasters = async () => { const rides = await ridesApi.getRides({ categories: ['RC'], min_height_ft: 100, track_material: 'STEEL', has_inversions: true, ordering: '-average_rating' }); return rides.results; }; // File upload import { accountApi } from './api'; const uploadAvatar = async (file: File) => { try { const response = await accountApi.uploadAvatar(file); console.log('Avatar uploaded:', response.avatar_url); } catch (error) { console.error('Upload failed:', error); } }; // Using default export import api from './api'; const getUserProfile = async () => { const profile = await api.account.getProfile(); const stats = await api.account.getStatistics(); return { profile, stats }; }; */