// ============================================================================ // API Client with Mock Support // ============================================================================ import { API_URL, USE_MOCKS, STORAGE_KEYS } from "@/lib/utils/constants"; import { mockApiHandlers } from "@/lib/mocks/handlers"; import type { ApiError } from "@/lib/types/api"; // ============================================================================ // Types // ============================================================================ interface RequestOptions extends RequestInit { params?: Record; requireAuth?: boolean; } // ============================================================================ // Helper Functions // ============================================================================ /** * Build URL with query parameters */ const buildUrl = (endpoint: string, params?: Record): string => { if (!params || Object.keys(params).length === 0) { return endpoint; } const searchParams = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null) { searchParams.append(key, String(value)); } }); const queryString = searchParams.toString(); return queryString ? `${endpoint}?${queryString}` : endpoint; }; /** * Get auth token from storage */ const getAuthToken = (): string | null => { if (typeof window === "undefined") return null; return localStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN); }; /** * Clear auth tokens from storage */ export const clearAuthTokens = (): void => { if (typeof window === "undefined") return; localStorage.removeItem(STORAGE_KEYS.ACCESS_TOKEN); localStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN); localStorage.removeItem(STORAGE_KEYS.USER); }; /** * Save auth tokens to storage */ export const saveAuthTokens = ( accessToken: string, refreshToken: string ): void => { if (typeof window === "undefined") return; localStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, accessToken); localStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, refreshToken); }; // ============================================================================ // Mock Router // ============================================================================ /** * Route request to appropriate mock handler */ const routeMockRequest = async ( endpoint: string, options: RequestOptions ): Promise => { const method = options.method || "GET"; const params = options.params || {}; const body = options.body ? JSON.parse(options.body as string) : undefined; console.log(`[MOCK API] ${method} ${endpoint}`, { params, body }); // Auth endpoints if (endpoint === "/auth/init") { return mockApiHandlers.authInit(); } if (endpoint === "/auth/complete") { return mockApiHandlers.authComplete(body); } if (endpoint === "/auth/refresh") { return mockApiHandlers.authRefresh(body); } if (endpoint === "/auth/me") { return mockApiHandlers.authMe(); } // Target Channels if (endpoint === "/target-channels" && method === "GET") { return mockApiHandlers.getTargetChannels(params); } if (endpoint.startsWith("/target-channels/") && method === "GET") { const id = endpoint.split("/")[2]; return mockApiHandlers.getTargetChannel(id); } if (endpoint.startsWith("/target-channels/") && method === "PATCH") { const id = endpoint.split("/")[2]; return mockApiHandlers.updateTargetChannel(id, body); } if (endpoint.startsWith("/target-channels/") && method === "DELETE") { const id = endpoint.split("/")[2]; return mockApiHandlers.deleteTargetChannel(id); } // External Channels if (endpoint === "/external-channels" && method === "GET") { return mockApiHandlers.getExternalChannels(params); } if (endpoint === "/external-channels" && method === "POST") { return mockApiHandlers.createExternalChannel(body); } if ( endpoint.startsWith("/external-channels/") && !endpoint.includes("/import") && method === "GET" ) { const id = endpoint.split("/")[2]; return mockApiHandlers.getExternalChannel(id); } if (endpoint.startsWith("/external-channels/") && method === "PATCH") { const id = endpoint.split("/")[2]; return mockApiHandlers.updateExternalChannel(id, body); } if (endpoint.startsWith("/external-channels/") && method === "DELETE") { const id = endpoint.split("/")[2]; return mockApiHandlers.deleteExternalChannel(id); } if (endpoint === "/external-channels/import" && method === "POST") { // FormData для импорта const formData = body as any; return mockApiHandlers.importExternalChannels( formData.file, formData.target_channel_id ); } // Creatives if (endpoint === "/creatives" && method === "GET") { return mockApiHandlers.getCreatives(params); } if (endpoint === "/creatives" && method === "POST") { return mockApiHandlers.createCreative(body); } if (endpoint.startsWith("/creatives/") && method === "GET") { const id = endpoint.split("/")[2]; return mockApiHandlers.getCreative(id); } if (endpoint.startsWith("/creatives/") && method === "PATCH") { const id = endpoint.split("/")[2]; return mockApiHandlers.updateCreative(id, body); } if (endpoint.startsWith("/creatives/") && method === "DELETE") { const id = endpoint.split("/")[2]; return mockApiHandlers.deleteCreative(id); } // Purchases if (endpoint === "/purchases" && method === "GET") { return mockApiHandlers.getPurchases(params); } if (endpoint === "/purchases" && method === "POST") { return mockApiHandlers.createPurchase(body); } if (endpoint.match(/^\/purchases\/[^\/]+$/) && method === "GET") { const id = endpoint.split("/")[2]; return mockApiHandlers.getPurchase(id); } if (endpoint.match(/^\/purchases\/[^\/]+$/) && method === "PATCH") { const id = endpoint.split("/")[2]; return mockApiHandlers.updatePurchase(id, body); } if (endpoint.match(/^\/purchases\/[^\/]+$/) && method === "DELETE") { const id = endpoint.split("/")[2]; return mockApiHandlers.deletePurchase(id); } if (endpoint.includes("/refresh-views") && method === "POST") { const id = endpoint.split("/")[2]; return mockApiHandlers.refreshPurchaseViews(id); } // Analytics if (endpoint === "/analytics/overview") { return mockApiHandlers.getAnalyticsOverview(params); } if (endpoint === "/analytics/costs") { return mockApiHandlers.getAnalyticsCosts(params); } if (endpoint === "/analytics/channels") { return mockApiHandlers.getAnalyticsChannels(params); } if (endpoint === "/analytics/creatives") { return mockApiHandlers.getAnalyticsCreatives(params); } throw { error: { code: "NOT_IMPLEMENTED", message: `Mock handler not implemented for ${method} ${endpoint}`, }, }; }; // ============================================================================ // API Client // ============================================================================ /** * Main API request function with mock support */ export const apiRequest = async ( endpoint: string, options: RequestOptions = {} ): Promise => { const { params, requireAuth = true, ...fetchOptions } = options; // Use mocks if enabled if (USE_MOCKS) { try { const result = await routeMockRequest(endpoint, options); return result as T; } catch (error) { throw error; } } // Real API request const url = buildUrl(`${API_URL}${endpoint}`, params); const headers: HeadersInit = { "Content-Type": "application/json", ...fetchOptions.headers, }; // Add auth token if required if (requireAuth) { const token = getAuthToken(); if (token) { (headers as Record)["Authorization"] = `Bearer ${token}`; } } try { const response = await fetch(url, { ...fetchOptions, headers, }); // Parse response const data = await response.json(); // Handle errors if (!response.ok) { const error: ApiError = data; throw error; } return data as T; } catch (error: any) { // Network error or parsing error if (!error.error) { throw { error: { code: "NETWORK_ERROR", message: error.message || "Network error occurred", }, } as ApiError; } throw error; } }; // ============================================================================ // Convenience Methods // ============================================================================ export const api = { get: (endpoint: string, options?: RequestOptions) => apiRequest(endpoint, { ...options, method: "GET" }), post: (endpoint: string, body?: any, options?: RequestOptions) => apiRequest(endpoint, { ...options, method: "POST", body: JSON.stringify(body), }), patch: (endpoint: string, body?: any, options?: RequestOptions) => apiRequest(endpoint, { ...options, method: "PATCH", body: JSON.stringify(body), }), delete: (endpoint: string, options?: RequestOptions) => apiRequest(endpoint, { ...options, method: "DELETE" }), // Special method for file upload upload: async ( endpoint: string, formData: FormData, options?: RequestOptions ): Promise => { if (USE_MOCKS) { // Handle mock file upload const file = formData.get("file") as File; const targetChannelId = formData.get("target_channel_id") as string; return mockApiHandlers.importExternalChannels(file, targetChannelId) as T; } const url = `${API_URL}${endpoint}`; const token = getAuthToken(); const headers: HeadersInit = {}; if (token) { headers["Authorization"] = `Bearer ${token}`; } const response = await fetch(url, { method: "POST", headers, body: formData, ...options, }); const data = await response.json(); if (!response.ok) { throw data as ApiError; } return data as T; }, };