mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 09:11:08 -05:00
- Added comprehensive documentation for hybrid filtering implementation, including architecture, API endpoints, performance characteristics, and usage examples. - Developed a hybrid pagination and client-side filtering recommendation, detailing server-side responsibilities and client-side logic. - Created a test script for hybrid filtering endpoints, covering various test cases including basic filtering, search functionality, pagination, and edge cases.
2593 lines
78 KiB
TypeScript
2593 lines
78 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,
|
|
|
|
// 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<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) {
|
|
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 = {
|
|
// Hybrid filtering (recommended)
|
|
async getHybridRides(filters?: HybridRideFilters): Promise<HybridRideResponse> {
|
|
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<HybridRideResponse>(`/rides/hybrid/${query ? `?${query}` : ''}`);
|
|
},
|
|
|
|
async getHybridRidesProgressive(filters?: HybridRideFilters & { offset: number }): Promise<HybridRideProgressiveResponse> {
|
|
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<HybridRideProgressiveResponse>(`/rides/hybrid/progressive/${query ? `?${query}` : ''}`);
|
|
},
|
|
|
|
async getHybridRideFilterMetadata(): Promise<HybridRideFilterMetadata> {
|
|
return makeRequest<HybridRideFilterMetadata>('/rides/hybrid/filter-metadata/');
|
|
},
|
|
|
|
// Legacy rides listing
|
|
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 };
|
|
};
|
|
*/
|