Files
thrillwiki_django_no_react/docs/lib-api.ts
pacnpal 9bed782784 feat: Implement avatar upload system with Cloudflare integration
- Added migration to transition avatar data from CloudflareImageField to ForeignKey structure in UserProfile.
- Fixed UserProfileEvent avatar field to align with new avatar structure.
- Created serializers for social authentication, including connected and available providers.
- Developed request logging middleware for comprehensive request/response logging.
- Updated moderation and parks migrations to remove outdated triggers and adjust foreign key relationships.
- Enhanced rides migrations to ensure proper handling of image uploads and triggers.
- Introduced a test script for the 3-step avatar upload process, ensuring functionality with Cloudflare.
- Documented the fix for avatar upload issues, detailing root cause, implementation, and verification steps.
- Implemented automatic deletion of Cloudflare images upon avatar, park, and ride photo changes or removals.
2025-08-30 21:20:25 -04:00

2552 lines
76 KiB
TypeScript

// 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,
// 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<string, string>;
}
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<T>(
endpoint: string,
options: RequestInit = {},
config: Partial<ApiConfig> = {}
): Promise<T> {
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<string, string> = token ? { 'Authorization': `Bearer ${token}` } : {};
const finalOptions: RequestInit = {
...options,
headers: {
...baseHeaders,
...authHeaders,
...(options.headers as Record<string, string> || {}),
},
};
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<LoginResponse> {
const response = await makeRequest<LoginResponse>('/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<SignupResponse> {
const response = await makeRequest<SignupResponse>('/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<EmailVerificationResponse> {
return makeRequest<EmailVerificationResponse>(`/auth/verify-email/${token}/`);
},
async resendVerificationEmail(data: ResendVerificationRequest): Promise<ResendVerificationResponse> {
return makeRequest<ResendVerificationResponse>('/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<LogoutResponse> {
const refreshToken = getRefreshToken();
const requestBody = refreshToken ? { refresh: refreshToken } : {};
const response = await makeRequest<LogoutResponse>('/auth/logout/', {
method: 'POST',
body: JSON.stringify(requestBody),
});
// Remove tokens on successful logout
removeAuthToken();
removeRefreshToken();
return response;
},
async getCurrentUser(): Promise<CurrentUserResponse> {
return makeRequest<CurrentUserResponse>('/auth/user/');
},
async resetPassword(data: PasswordResetRequest): Promise<PasswordResetResponse> {
return makeRequest<PasswordResetResponse>('/auth/password/reset/', {
method: 'POST',
body: JSON.stringify(data),
});
},
async changePassword(data: PasswordChangeRequest): Promise<PasswordChangeResponse> {
return makeRequest<PasswordChangeResponse>('/auth/password/change/', {
method: 'POST',
body: JSON.stringify(data),
});
},
async getSocialProviders(): Promise<SocialProvidersResponse> {
return makeRequest<SocialProvidersResponse>('/auth/providers/');
},
async checkAuthStatus(): Promise<AuthStatusResponse> {
return makeRequest<AuthStatusResponse>('/auth/status/', {
method: 'POST',
});
},
// Social Authentication Methods
async googleLogin(accessToken: string): Promise<LoginResponse> {
const response = await makeRequest<LoginResponse>('/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<LoginResponse> {
const response = await makeRequest<LoginResponse>('/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<AvailableProvider[]> {
return makeRequest<AvailableProvider[]>('/auth/social/providers/available/');
},
async getConnectedProviders(): Promise<ConnectedProvider[]> {
return makeRequest<ConnectedProvider[]>('/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<SocialAuthStatus> {
return makeRequest<SocialAuthStatus>('/auth/social/status/');
},
};
// ============================================================================
// User Account Management API
// ============================================================================
export const accountApi = {
async getProfile(): Promise<CompleteUserProfile> {
return makeRequest<CompleteUserProfile>('/accounts/profile/');
},
async updateAccount(data: AccountUpdate): Promise<CompleteUserProfile> {
return makeRequest<CompleteUserProfile>('/accounts/profile/account/', {
method: 'PATCH',
body: JSON.stringify(data),
});
},
async updateProfile(data: ProfileUpdate): Promise<CompleteUserProfile> {
return makeRequest<CompleteUserProfile>('/accounts/profile/update/', {
method: 'PATCH',
body: JSON.stringify(data),
});
},
async uploadAvatar(file: File): Promise<AvatarUploadResponse> {
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<AvatarUploadResponse>('/auth/user/avatar/', {
method: 'POST',
body: formData,
headers: {}, // Let browser set Content-Type for FormData
});
},
// Backward compatibility alias
async updateAvatar(formData: FormData): Promise<AvatarUploadResponse> {
// Get auth token to ensure it's available
const token = getAuthToken();
if (!token) {
throw new ApiError('Authentication required for avatar upload', 401);
}
return makeRequest<AvatarUploadResponse>('/auth/user/avatar/', {
method: 'POST',
body: formData,
headers: {}, // Let browser set Content-Type for FormData
});
},
async deleteAvatar(): Promise<AvatarDeleteResponse> {
return makeRequest<AvatarDeleteResponse>('/auth/user/avatar/', {
method: 'DELETE',
});
},
async saveAvatarImage(data: { cloudflare_image_id: string }): Promise<AvatarUploadResponse> {
return makeRequest<AvatarUploadResponse>('/accounts/profile/avatar/save/', {
method: 'POST',
body: JSON.stringify(data),
});
},
async getPreferences(): Promise<UserPreferences> {
return makeRequest<UserPreferences>('/accounts/preferences/');
},
async updatePreferences(data: Partial<UserPreferences>): Promise<UserPreferences> {
return makeRequest<UserPreferences>('/accounts/preferences/update/', {
method: 'PATCH',
body: JSON.stringify(data),
});
},
async updateTheme(data: ThemeUpdate): Promise<ThemeUpdate> {
return makeRequest<ThemeUpdate>('/accounts/preferences/theme/', {
method: 'PATCH',
body: JSON.stringify(data),
});
},
async getNotificationSettings(): Promise<NotificationSettings> {
return makeRequest<NotificationSettings>('/accounts/settings/notifications/');
},
async updateNotificationSettings(data: Partial<NotificationSettings>): Promise<NotificationSettings> {
return makeRequest<NotificationSettings>('/accounts/settings/notifications/update/', {
method: 'PATCH',
body: JSON.stringify(data),
});
},
async getPrivacySettings(): Promise<PrivacySettings> {
return makeRequest<PrivacySettings>('/accounts/settings/privacy/');
},
async updatePrivacySettings(data: Partial<PrivacySettings>): Promise<PrivacySettings> {
return makeRequest<PrivacySettings>('/accounts/settings/privacy/update/', {
method: 'PATCH',
body: JSON.stringify(data),
});
},
async getSecuritySettings(): Promise<SecuritySettings> {
return makeRequest<SecuritySettings>('/accounts/settings/security/');
},
async updateSecuritySettings(data: SecuritySettingsUpdate): Promise<SecuritySettings> {
return makeRequest<SecuritySettings>('/accounts/settings/security/update/', {
method: 'PATCH',
body: JSON.stringify(data),
});
},
async getStatistics(): Promise<UserStatistics> {
return makeRequest<UserStatistics>('/accounts/statistics/');
},
async getTopLists(): Promise<TopListsResponse> {
return makeRequest<TopListsResponse>('/accounts/top-lists/');
},
async createTopList(data: CreateTopList): Promise<TopList> {
return makeRequest<TopList>('/accounts/top-lists/create/', {
method: 'POST',
body: JSON.stringify(data),
});
},
async updateTopList(listId: number, data: Partial<CreateTopList>): Promise<TopList> {
return makeRequest<TopList>(`/accounts/top-lists/${listId}/`, {
method: 'PATCH',
body: JSON.stringify(data),
});
},
async deleteTopList(listId: number): Promise<void> {
return makeRequest<void>(`/accounts/top-lists/${listId}/delete/`, {
method: 'DELETE',
});
},
async getNotifications(params?: {
unread_only?: boolean;
notification_type?: string;
limit?: number;
offset?: number;
}): Promise<NotificationResponse> {
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<NotificationResponse>(`/accounts/notifications/${query ? `?${query}` : ''}`);
},
async markNotificationsRead(data: MarkNotificationsRead): Promise<MarkReadResponse> {
return makeRequest<MarkReadResponse>('/accounts/notifications/mark-read/', {
method: 'PATCH',
body: JSON.stringify(data),
});
},
async getNotificationPreferences(): Promise<NotificationPreferences> {
return makeRequest<NotificationPreferences>('/accounts/notification-preferences/');
},
async updateNotificationPreferences(data: Partial<NotificationPreferences>): Promise<NotificationPreferences> {
return makeRequest<NotificationPreferences>('/accounts/notification-preferences/update/', {
method: 'PATCH',
body: JSON.stringify(data),
});
},
// Account deletion with enhanced error handling for superuser/admin accounts
async requestAccountDeletion(): Promise<DeletionRequest> {
return makeRequest<DeletionRequest>('/accounts/delete-account/request/', {
method: 'POST',
});
},
async verifyAccountDeletion(data: VerifyDeletion): Promise<DeletionComplete> {
return makeRequest<DeletionComplete>('/accounts/delete-account/verify/', {
method: 'POST',
body: JSON.stringify(data),
});
},
async cancelAccountDeletion(): Promise<CancelDeletion> {
return makeRequest<CancelDeletion>('/accounts/delete-account/cancel/', {
method: 'POST',
});
},
// Admin endpoints
async deleteUser(userId: string): Promise<DeletionComplete> {
return makeRequest<DeletionComplete>(`/accounts/users/${userId}/delete/`, {
method: 'DELETE',
});
},
async checkUserDeletionEligibility(userId: string): Promise<DeletionEligibility> {
return makeRequest<DeletionEligibility>(`/accounts/users/${userId}/deletion-check/`);
},
};
// ============================================================================
// Parks API
// ============================================================================
export const parksApi = {
async getParks(params?: ParkSearchFilters): Promise<ParkListResponse> {
const searchParams = new URLSearchParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
// Note: continent and park_type parameters are accepted but ignored by backend
// due to missing model fields (ParkLocation has no continent, Park has no park_type)
searchParams.append(key, value.toString());
}
});
}
const query = searchParams.toString();
return makeRequest<ParkListResponse>(`/parks/${query ? `?${query}` : ''}`);
},
async createPark(data: CreateParkRequest): Promise<QueueRoutingResponse> {
return makeRequest<QueueRoutingResponse>('/parks/', {
method: 'POST',
body: JSON.stringify(data),
});
},
async getPark(parkId: number): Promise<ParkDetail> {
return makeRequest<ParkDetail>(`/parks/${parkId}/`);
},
async updatePark(parkId: number, data: Partial<CreateParkRequest>): Promise<QueueRoutingResponse> {
return makeRequest<QueueRoutingResponse>(`/parks/${parkId}/`, {
method: 'PATCH',
body: JSON.stringify(data),
});
},
async deletePark(parkId: number): Promise<void> {
return makeRequest<void>(`/parks/${parkId}/`, {
method: 'DELETE',
});
},
async getFilterOptions(): Promise<ParkFilterOptions> {
return makeRequest<ParkFilterOptions>('/parks/filter-options/');
},
async searchCompanies(query: string): Promise<ParkCompanySearchResponse> {
return makeRequest<ParkCompanySearchResponse>(`/parks/search/companies/?q=${encodeURIComponent(query)}`);
},
async getSearchSuggestions(query: string): Promise<ParkSearchSuggestionsResponse> {
return makeRequest<ParkSearchSuggestionsResponse>(`/parks/search-suggestions/?q=${encodeURIComponent(query)}`);
},
async setParkImages(parkId: number, data: ParkImageSettings): Promise<ParkDetail> {
return makeRequest<ParkDetail>(`/parks/${parkId}/image-settings/`, {
method: 'PATCH',
body: JSON.stringify(data),
});
},
async getParkPhotos(parkId: number, params?: {
page?: number;
page_size?: number;
photo_type?: string;
}): Promise<ParkPhotosResponse> {
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<ParkPhotosResponse>(`/parks/${parkId}/photos/${query ? `?${query}` : ''}`);
},
async uploadParkPhoto(parkId: number, data: UploadParkPhoto): Promise<QueueRoutingResponse> {
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<QueueRoutingResponse>(`/parks/${parkId}/photos/`, {
method: 'POST',
body: formData,
headers: {}, // Let browser set Content-Type for FormData
});
},
async updateParkPhoto(parkId: number, photoId: number, data: UpdateParkPhoto): Promise<any> {
return makeRequest(`/parks/${parkId}/photos/${photoId}/`, {
method: 'PATCH',
body: JSON.stringify(data),
});
},
async deleteParkPhoto(parkId: number, photoId: number): Promise<void> {
return makeRequest<void>(`/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<any> {
return makeRequest(`/parks/${parkId}/photos/save_image/`, {
method: 'POST',
body: JSON.stringify(data),
});
},
};
// ============================================================================
// Rides API
// ============================================================================
export const ridesApi = {
async getRides(filters?: SearchFilters): Promise<RideListResponse> {
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<RideListResponse>(`/rides/${query ? `?${query}` : ''}`);
},
async createRide(data: CreateRideRequest): Promise<QueueRoutingResponse> {
return makeRequest<QueueRoutingResponse>('/rides/', {
method: 'POST',
body: JSON.stringify(data),
});
},
async getRide(rideId: number): Promise<RideDetail> {
return makeRequest<RideDetail>(`/rides/${rideId}/`);
},
async updateRide(rideId: number, data: Partial<CreateRideRequest>): Promise<QueueRoutingResponse> {
return makeRequest<QueueRoutingResponse>(`/rides/${rideId}/`, {
method: 'PATCH',
body: JSON.stringify(data),
});
},
async deleteRide(rideId: number): Promise<void> {
return makeRequest<void>(`/rides/${rideId}/`, {
method: 'DELETE',
});
},
async getFilterOptions(): Promise<RideFilterOptions> {
return makeRequest<RideFilterOptions>('/rides/filter-options/');
},
async searchCompanies(query: string): Promise<CompanySearchResponse> {
return makeRequest<CompanySearchResponse>(`/rides/search/companies/?q=${encodeURIComponent(query)}`);
},
async searchRideModels(query: string): Promise<RideModelSearchResponse> {
return makeRequest<RideModelSearchResponse>(`/rides/search/ride-models/?q=${encodeURIComponent(query)}`);
},
async getSearchSuggestions(query: string): Promise<SearchSuggestionsResponse> {
return makeRequest<SearchSuggestionsResponse>(`/rides/search-suggestions/?q=${encodeURIComponent(query)}`);
},
async setRideImages(rideId: number, data: RideImageSettings): Promise<RideDetail> {
return makeRequest<RideDetail>(`/rides/${rideId}/image-settings/`, {
method: 'PATCH',
body: JSON.stringify(data),
});
},
async getRidePhotos(rideId: number, params?: {
page?: number;
page_size?: number;
photo_type?: string;
}): Promise<RidePhotosResponse> {
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<RidePhotosResponse>(`/rides/${rideId}/photos/${query ? `?${query}` : ''}`);
},
async uploadRidePhoto(rideId: number, data: UploadRidePhoto): Promise<QueueRoutingResponse> {
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<QueueRoutingResponse>(`/rides/${rideId}/photos/`, {
method: 'POST',
body: formData,
headers: {}, // Let browser set Content-Type for FormData
});
},
async updateRidePhoto(rideId: number, photoId: number, data: UpdateRidePhoto): Promise<any> {
return makeRequest(`/rides/${rideId}/photos/${photoId}/`, {
method: 'PATCH',
body: JSON.stringify(data),
});
},
async deleteRidePhoto(rideId: number, photoId: number): Promise<void> {
return makeRequest<void>(`/rides/${rideId}/photos/${photoId}/`, {
method: 'DELETE',
});
},
async getManufacturerRideModels(manufacturerSlug: string): Promise<ManufacturerRideModels> {
return makeRequest<ManufacturerRideModels>(`/rides/manufacturers/${manufacturerSlug}/`);
},
async saveRidePhoto(rideId: number, data: { cloudflare_image_id: string; caption?: string; alt_text?: string; photo_type?: string; is_primary?: boolean }): Promise<any> {
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<EntitySearchResponse> {
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<EntitySearchResponse>(`/core/entities/search/?${searchParams.toString()}`);
},
async reportEntityNotFound(data: EntityNotFoundRequest): Promise<EntityNotFoundResponse> {
return makeRequest<EntityNotFoundResponse>('/core/entities/not-found/', {
method: 'POST',
body: JSON.stringify(data),
});
},
async getEntitySuggestions(params: {
q: string;
limit?: number;
}): Promise<EntitySuggestionsResponse> {
const searchParams = new URLSearchParams();
searchParams.append('q', params.q);
if (params.limit) {
searchParams.append('limit', params.limit.toString());
}
return makeRequest<EntitySuggestionsResponse>(`/core/entities/suggestions/?${searchParams.toString()}`);
},
};
// ============================================================================
// Maps API
// ============================================================================
export const mapsApi = {
async getMapLocations(params?: {
bounds?: string;
zoom?: number;
entity_types?: string[];
categories?: string[];
}): Promise<MapLocationsResponse> {
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<MapLocationsResponse>(`/maps/locations/${query ? `?${query}` : ''}`);
},
async getLocationDetail(locationType: 'park' | 'ride', locationId: number): Promise<LocationDetail> {
return makeRequest<LocationDetail>(`/maps/locations/${locationType}/${locationId}/`);
},
async searchMap(params: {
q: string;
bounds?: string;
entity_types?: string[];
}): Promise<MapSearchResponse> {
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<MapSearchResponse>(`/maps/search/?${searchParams.toString()}`);
},
async getMapBounds(bounds: BoundingBox & { zoom?: number }): Promise<MapLocationsResponse> {
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<MapLocationsResponse>(`/maps/bounds/?${searchParams.toString()}`);
},
async getMapStats(): Promise<MapStatsResponse> {
return makeRequest<MapStatsResponse>('/maps/stats/');
},
async getMapCache(): Promise<MapCacheResponse> {
return makeRequest<MapCacheResponse>('/maps/cache/');
},
async invalidateMapCache(): Promise<CacheInvalidateResponse> {
return makeRequest<CacheInvalidateResponse>('/maps/cache/invalidate/', {
method: 'POST',
});
},
};
// ============================================================================
// Health & Statistics API
// ============================================================================
export const healthApi = {
async getHealthCheck(): Promise<HealthCheckResponse> {
return makeRequest<HealthCheckResponse>('/health/');
},
async getSimpleHealth(): Promise<SimpleHealthResponse> {
return makeRequest<SimpleHealthResponse>('/health/simple/');
},
async getPerformanceMetrics(): Promise<PerformanceMetricsResponse> {
return makeRequest<PerformanceMetricsResponse>('/health/performance/');
},
async getSystemStats(): Promise<SystemStatsResponse> {
return makeRequest<SystemStatsResponse>('/stats/');
},
async recalculateStats(): Promise<StatsRecalculateResponse> {
return makeRequest<StatsRecalculateResponse>('/stats/recalculate/', {
method: 'POST',
});
},
};
// ============================================================================
// Trending & Discovery API
// ============================================================================
export const trendingApi = {
async getTrending(params?: {
time_period?: string;
entity_types?: string[];
limit?: number;
}): Promise<TrendingResponse> {
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<TrendingResponse>(`/trending/${query ? `?${query}` : ''}`);
},
async getNewContent(params?: {
days?: number;
entity_types?: string[];
limit?: number;
}): Promise<NewContentResponse> {
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<NewContentResponse>(`/new-content/${query ? `?${query}` : ''}`);
},
async triggerTrendingCalculation(): Promise<TriggerTrendingResponse> {
return makeRequest<TriggerTrendingResponse>('/trending/calculate/', {
method: 'POST',
});
},
};
// ============================================================================
// Reviews & Rankings API
// ============================================================================
export const reviewsApi = {
async getLatestReviews(params?: {
limit?: number;
entity_types?: string[];
min_rating?: number;
}): Promise<LatestReviewsResponse> {
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<LatestReviewsResponse>(`/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<RankingsResponse> {
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<RankingsResponse>(`/rankings/${query ? `?${query}` : ''}`);
},
async getRideRanking(rideId: number): Promise<RideRankingDetail> {
return makeRequest<RideRankingDetail>(`/rankings/${rideId}/`);
},
async triggerRankingCalculation(): Promise<RankingCalculationResponse> {
return makeRequest<RankingCalculationResponse>('/rankings/calculate/', {
method: 'POST',
});
},
};
// ============================================================================
// Email Service API
// ============================================================================
export const emailApi = {
async sendContactEmail(data: ContactEmailRequest): Promise<ContactEmailResponse> {
return makeRequest<ContactEmailResponse>('/email/contact/', {
method: 'POST',
body: JSON.stringify(data),
});
},
async subscribeNewsletter(data: NewsletterSubscribeRequest): Promise<NewsletterSubscribeResponse> {
return makeRequest<NewsletterSubscribeResponse>('/email/newsletter/subscribe/', {
method: 'POST',
body: JSON.stringify(data),
});
},
async unsubscribeNewsletter(data: NewsletterUnsubscribeRequest): Promise<NewsletterUnsubscribeResponse> {
return makeRequest<NewsletterUnsubscribeResponse>('/email/newsletter/unsubscribe/', {
method: 'POST',
body: JSON.stringify(data),
});
},
};
// ============================================================================
// Content Moderation API
// ============================================================================
export const moderationApi = {
// Moderation Reports
async getReports(params?: ModerationReportFilters): Promise<ModerationReportsResponse> {
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<ModerationReportsResponse>(`/moderation/reports/${query ? `?${query}` : ''}`);
},
async createReport(data: CreateModerationReport): Promise<ModerationReport> {
return makeRequest<ModerationReport>('/moderation/reports/', {
method: 'POST',
body: JSON.stringify(data),
});
},
async getReport(reportId: number): Promise<ModerationReport> {
return makeRequest<ModerationReport>(`/moderation/reports/${reportId}/`);
},
async updateReport(reportId: number, data: UpdateModerationReportData): Promise<ModerationReport> {
return makeRequest<ModerationReport>(`/moderation/reports/${reportId}/`, {
method: 'PATCH',
body: JSON.stringify(data),
});
},
async deleteReport(reportId: number): Promise<void> {
return makeRequest<void>(`/moderation/reports/${reportId}/`, {
method: 'DELETE',
});
},
async assignReport(reportId: number, moderatorId: number): Promise<ModerationReport> {
return makeRequest<ModerationReport>(`/moderation/reports/${reportId}/assign/`, {
method: 'POST',
body: JSON.stringify({ moderator_id: moderatorId }),
});
},
async resolveReport(reportId: number, data: {
resolution_action: string;
resolution_notes?: string;
}): Promise<ModerationReport> {
return makeRequest<ModerationReport>(`/moderation/reports/${reportId}/resolve/`, {
method: 'POST',
body: JSON.stringify(data),
});
},
async getReportStats(): Promise<ModerationStatsData> {
return makeRequest<ModerationStatsData>('/moderation/reports/stats/');
},
// Moderation Queue
async getQueue(params?: ModerationQueueFilters): Promise<ModerationQueueResponse> {
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<ModerationQueueResponse>(`/moderation/queue/${query ? `?${query}` : ''}`);
},
async getMyQueue(): Promise<ModerationQueueResponse> {
return makeRequest<ModerationQueueResponse>('/moderation/queue/my_queue/');
},
async assignQueueItem(itemId: number, data: AssignQueueItemData): Promise<ModerationQueueItem> {
return makeRequest<ModerationQueueItem>(`/moderation/queue/${itemId}/assign/`, {
method: 'POST',
body: JSON.stringify(data),
});
},
async unassignQueueItem(itemId: number): Promise<ModerationQueueItem> {
return makeRequest<ModerationQueueItem>(`/moderation/queue/${itemId}/unassign/`, {
method: 'POST',
});
},
async completeQueueItem(itemId: number, data: CompleteQueueItemData): Promise<ModerationQueueItem> {
return makeRequest<ModerationQueueItem>(`/moderation/queue/${itemId}/complete/`, {
method: 'POST',
body: JSON.stringify(data),
});
},
// Moderation Actions
async getActions(params?: ModerationActionFilters): Promise<PaginatedResponse<ModerationAction>> {
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<PaginatedResponse<ModerationAction>>(`/moderation/actions/${query ? `?${query}` : ''}`);
},
async createAction(data: CreateModerationActionData): Promise<ModerationAction> {
return makeRequest<ModerationAction>('/moderation/actions/', {
method: 'POST',
body: JSON.stringify(data),
});
},
async getAction(actionId: number): Promise<ModerationAction> {
return makeRequest<ModerationAction>(`/moderation/actions/${actionId}/`);
},
async deactivateAction(actionId: number): Promise<ModerationAction> {
return makeRequest<ModerationAction>(`/moderation/actions/${actionId}/deactivate/`, {
method: 'POST',
});
},
async getActiveActions(): Promise<PaginatedResponse<ModerationAction>> {
return makeRequest<PaginatedResponse<ModerationAction>>('/moderation/actions/active/');
},
async getExpiredActions(): Promise<PaginatedResponse<ModerationAction>> {
return makeRequest<PaginatedResponse<ModerationAction>>('/moderation/actions/expired/');
},
};
// ============================================================================
// User Moderation API
// ============================================================================
export const userModerationApi = {
async getUserModerationProfile(userId: number): Promise<UserModerationProfile> {
return makeRequest<UserModerationProfile>(`/admin/users/${userId}/moderate/`);
},
async moderateUser(userId: number, action: UserModerationAction): Promise<UserModerationActionResponse> {
return makeRequest<UserModerationActionResponse>(`/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<UserModerationStats> {
return makeRequest<UserModerationStats>('/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<BulkOperationsResponse> {
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<BulkOperationsResponse>(`/admin/bulk-operations/${query ? `?${query}` : ''}`);
},
async createOperation(data: CreateBulkOperation): Promise<BulkOperationResult> {
return makeRequest<BulkOperationResult>('/admin/bulk-operations/', {
method: 'POST',
body: JSON.stringify(data),
});
},
async getOperation(operationId: string): Promise<BulkOperation> {
return makeRequest<BulkOperation>(`/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<BulkOperationResult> {
return makeRequest<BulkOperationResult>(`/admin/bulk-operations/${operationId}/retry/`, {
method: 'POST',
});
},
// Specific bulk operation helpers
async bulkUpdateParks(data: BulkUpdateParks): Promise<BulkOperationResult> {
return this.createOperation(data);
},
async bulkUpdateRides(data: BulkUpdateRides): Promise<BulkOperationResult> {
return this.createOperation(data);
},
async bulkImportData(data: BulkImportData): Promise<BulkOperationResult> {
return this.createOperation(data);
},
async bulkExportData(data: BulkExportData): Promise<BulkOperationResult> {
return this.createOperation(data);
},
async bulkModerateContent(data: BulkModerateContent): Promise<BulkOperationResult> {
return this.createOperation(data);
},
async bulkUserActions(data: BulkUserActions): Promise<BulkOperationResult> {
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<ParkReviewsResponse> {
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<ParkReviewsResponse>(`/parks/${parkId}/reviews/${query ? `?${query}` : ''}`);
},
async createParkReview(parkId: number, data: CreateParkReview): Promise<ParkReview> {
return makeRequest<ParkReview>(`/parks/${parkId}/reviews/`, {
method: 'POST',
body: JSON.stringify(data),
});
},
async getParkReview(parkId: number, reviewId: number): Promise<ParkReview> {
return makeRequest<ParkReview>(`/parks/${parkId}/reviews/${reviewId}/`);
},
async updateParkReview(parkId: number, reviewId: number, data: UpdateParkReview): Promise<ParkReview> {
return makeRequest<ParkReview>(`/parks/${parkId}/reviews/${reviewId}/`, {
method: 'PATCH',
body: JSON.stringify(data),
});
},
async deleteParkReview(parkId: number, reviewId: number): Promise<void> {
return makeRequest<void>(`/parks/${parkId}/reviews/${reviewId}/`, {
method: 'DELETE',
});
},
async voteParkReview(parkId: number, reviewId: number, vote: ParkReviewVote): Promise<ParkReviewVoteResponse> {
return makeRequest<ParkReviewVoteResponse>(`/parks/${parkId}/reviews/${reviewId}/vote/`, {
method: 'POST',
body: JSON.stringify(vote),
});
},
async removeParkReviewVote(parkId: number, reviewId: number): Promise<ParkReviewVoteResponse> {
return makeRequest<ParkReviewVoteResponse>(`/parks/${parkId}/reviews/${reviewId}/vote/`, {
method: 'DELETE',
});
},
async uploadParkReviewPhoto(parkId: number, reviewId: number, data: UploadParkReviewPhoto): Promise<any> {
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<void> {
return makeRequest<void>(`/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<CloudflareDirectUploadResponse> {
return makeRequest<CloudflareDirectUploadResponse>('/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<string, any>): Promise<CloudflareImageUploadResponse> {
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<string, any>;
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<CloudflareImageListResponse> {
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<CloudflareImageListResponse>(`/cloudflare-images/api/images/${query ? `?${query}` : ''}`);
},
// Get image details
async getImage(imageId: string): Promise<CloudflareImage> {
return makeRequest<CloudflareImage>(`/cloudflare-images/api/images/${imageId}/`);
},
// Check image status
async checkImageStatus(imageId: string): Promise<CloudflareImage> {
return makeRequest<CloudflareImage>(`/cloudflare-images/api/images/${imageId}/check_status/`, {
method: 'POST',
});
},
// Update image metadata
async updateImage(imageId: string, data: {
metadata?: Record<string, any>;
require_signed_urls?: boolean;
}): Promise<CloudflareImage> {
return makeRequest<CloudflareImage>(`/cloudflare-images/api/images/${imageId}/`, {
method: 'PATCH',
body: JSON.stringify(data),
});
},
// Delete image
async deleteImage(imageId: string): Promise<CloudflareImageDeleteResponse> {
return makeRequest<CloudflareImageDeleteResponse>(`/cloudflare-images/api/images/${imageId}/`, {
method: 'DELETE',
});
},
// Get account statistics
async getStats(): Promise<CloudflareImageStats> {
return makeRequest<CloudflareImageStats>('/cloudflare-images/api/stats/');
},
// Get available variants
async getVariants(): Promise<CloudflareImageVariant[]> {
return makeRequest<CloudflareImageVariant[]>('/cloudflare-images/api/variants/');
},
// Create new variant
async createVariant(data: Omit<CloudflareImageVariant, 'id'>): Promise<CloudflareImageVariant> {
return makeRequest<CloudflareImageVariant>('/cloudflare-images/api/variants/', {
method: 'POST',
body: JSON.stringify(data),
});
},
// Update variant
async updateVariant(variantId: string, data: Partial<Omit<CloudflareImageVariant, 'id'>>): Promise<CloudflareImageVariant> {
return makeRequest<CloudflareImageVariant>(`/cloudflare-images/api/variants/${variantId}/`, {
method: 'PATCH',
body: JSON.stringify(data),
});
},
// Delete variant
async deleteVariant(variantId: string): Promise<void> {
return makeRequest<void>(`/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<string, any>;
}>): 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<CloudflareImageListResponse> {
const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
searchParams.append(key, value.toString());
}
});
const query = searchParams.toString();
return makeRequest<CloudflareImageListResponse>(`/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<CloudflareDirectUploadResponse> {
return this.createDirectUploadUrl(data);
},
};
// ============================================================================
// History API
// ============================================================================
export const historyApi = {
async getEntityHistory(
entityType: 'park' | 'ride',
entityId: number,
params?: {
limit?: number;
offset?: number;
field?: string;
}
): Promise<EntityHistoryResponse> {
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<EntityHistoryResponse>(`/history/${entityType}/${entityId}/${query ? `?${query}` : ''}`);
},
async getRecentChanges(params?: {
entity_types?: string[];
actions?: string[];
limit?: number;
days?: number;
}): Promise<RecentChangesResponse> {
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<RecentChangesResponse>(`/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, any>): 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<string, any>): 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: <T>(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<T>(endpoint: string, options?: RequestInit): Promise<T> {
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<any> {
return this.fetchJson('/health/detail/');
}
async getStats(): Promise<Stats> {
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<Review[]> {
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<PaginatedResponse<Park>> {
return this.fetchJson(`/parks/${createQuery(params)}`);
}
async getParkFilterOptions(): Promise<FilterOptions> {
return makeRequest('/parks/filters/');
}
// Rides methods
async getRides(params: object): Promise<PaginatedResponse<Ride>> {
const query = apiUtils.buildQueryString(params);
return makeRequest(`/rides/${query ? `?${query}` : ''}`);
}
async getRideFilterOptions(): Promise<FilterOptions> {
return makeRequest('/rides/filters/');
}
// Companies methods
async getCompanies(params: object): Promise<PaginatedResponse<Company>> {
const query = apiUtils.buildQueryString(params);
return makeRequest(`/companies/${query ? `?${query}` : ''}`);
}
async getCompanyFilterOptions(): Promise<FilterOptions> {
return makeRequest('/companies/filters/');
}
async getMapLocations(params: object): Promise<any> {
return this.fetchJson(`/map/locations/${createQuery(params)}`);
}
async getRankings(params: object): Promise<any> {
return this.fetchJson(`/rankings/${createQuery(params)}`);
}
async getReviews(params: object): Promise<PaginatedResponse<Review>> {
return this.fetchJson(`/reviews/${createQuery(params)}`);
}
async searchCompanies(params: object): Promise<any> {
return this.fetchJson(`/companies/search/${createQuery(params)}`);
}
async getPerformanceMetrics(): Promise<any> {
return this.fetchJson('/performance/');
}
async getTimeline(params: object): Promise<any> {
return this.fetchJson(`/timeline/${createQuery(params)}`);
}
async getEmailTemplates(): Promise<any> {
return this.fetchJson('/admin/emails/');
}
// Auth methods
async checkAuthStatus(): Promise<any> {
return this.fetchJson('/auth/status/');
}
async getCurrentUser(): Promise<User> {
return this.fetchJson('/auth/user/');
}
async updateCurrentUser(userData: Partial<User>): Promise<User> {
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<User> {
return makeRequest<User>('/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<void> {
await this.fetchJson('/auth/logout/', { method: 'POST' });
}
async changePassword(passwordData: object): Promise<void> {
await this.fetchJson('/auth/password/change/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(passwordData),
});
}
async deleteAccount(): Promise<void> {
await this.fetchJson('/auth/user/', { method: 'DELETE' });
}
async refreshToken(): Promise<void> {
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<any> {
return makeRequest('/health/detail/');
},
async getStats(): Promise<Stats> {
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<Review[]> {
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<PaginatedResponse<Park>> {
const query = apiUtils.buildQueryString(params);
return makeRequest(`/parks/${query ? `?${query}` : ''}`);
},
async getParkFilterOptions(): Promise<FilterOptions> {
return makeRequest('/parks/filters/');
},
// Rides methods
async getRides(params: object): Promise<PaginatedResponse<Ride>> {
const query = apiUtils.buildQueryString(params);
return makeRequest(`/rides/${query ? `?${query}` : ''}`);
},
async getRideFilterOptions(): Promise<FilterOptions> {
return makeRequest('/rides/filters/');
},
// Companies methods
async getCompanies(params: object): Promise<PaginatedResponse<Company>> {
const query = apiUtils.buildQueryString(params);
return makeRequest(`/companies/${query ? `?${query}` : ''}`);
},
async getCompanyFilterOptions(): Promise<FilterOptions> {
return makeRequest('/companies/filters/');
},
async searchCompanies(params: object): Promise<any> {
const query = apiUtils.buildQueryString(params);
return makeRequest(`/companies/search/${query ? `?${query}` : ''}`);
},
// Additional utility methods
async getEntitySuggestions(params: object): Promise<any> {
const query = apiUtils.buildQueryString(params);
return makeRequest(`/search/suggest/${query ? `?${query}` : ''}`);
},
async getMapLocations(params: object): Promise<any> {
const query = apiUtils.buildQueryString(params);
return makeRequest(`/map/locations/${query ? `?${query}` : ''}`);
},
async getRankings(params: object): Promise<any> {
const query = apiUtils.buildQueryString(params);
return makeRequest(`/rankings/${query ? `?${query}` : ''}`);
},
async getReviews(params: object): Promise<PaginatedResponse<Review>> {
const query = apiUtils.buildQueryString(params);
return makeRequest(`/reviews/${query ? `?${query}` : ''}`);
},
async getPerformanceMetrics(): Promise<any> {
return makeRequest('/performance/');
},
async getTimeline(params: object): Promise<any> {
const query = apiUtils.buildQueryString(params);
return makeRequest(`/timeline/${query ? `?${query}` : ''}`);
},
async getEmailTemplates(): Promise<any> {
return makeRequest('/admin/emails/');
},
// Auth methods from external API
async updateCurrentUser(userData: Partial<User>): Promise<User> {
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<void> {
await makeRequest('/auth/user/', { method: 'DELETE' });
},
async refreshToken(): Promise<void> {
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 };
};
*/