// ============================================================================ // 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.startsWith("/auth/complete")) { // Extract token from query params const token = params.token || (endpoint.includes("?") ? new URLSearchParams(endpoint.split("?")[1]).get("token") : null); return mockApiHandlers.authComplete({ token }); } 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(); } if (endpoint.startsWith("/target_channels/") && method === "DELETE") { const id = endpoint.split("/")[2]; return mockApiHandlers.deleteTargetChannel(id); } // External Channels if (endpoint.startsWith("/external_channels/target/") && method === "GET") { const targetChannelId = endpoint.split("/")[3]; return mockApiHandlers.getExternalChannels(targetChannelId); } if (endpoint === "/external_channels" && method === "POST") { return mockApiHandlers.createExternalChannel(body); } if ( endpoint.startsWith("/external_channels/") && !endpoint.includes("/target/") && !endpoint.includes("/links") && method === "GET" ) { const id = endpoint.split("/")[2]; return mockApiHandlers.getExternalChannel(id); } if ( endpoint.startsWith("/external_channels/") && endpoint.includes("/links") && method === "PATCH" ) { const id = endpoint.split("/")[2]; return mockApiHandlers.updateExternalChannelLinks(id, body); } if ( endpoint.startsWith("/external_channels/") && !endpoint.includes("/links") && 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); } // 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); } // Placements if (endpoint === "/placements" && method === "GET") { return mockApiHandlers.getPlacements(params); } if (endpoint === "/placements" && method === "POST") { return mockApiHandlers.createPlacement(body); } if (endpoint.match(/^\/placements\/[^\/]+$/) && method === "GET") { const id = endpoint.split("/")[2]; return mockApiHandlers.getPlacement(id); } if (endpoint.match(/^\/placements\/[^\/]+$/) && method === "PATCH") { const id = endpoint.split("/")[2]; return mockApiHandlers.updatePlacement(id, body); } if (endpoint.match(/^\/placements\/[^\/]+$/) && method === "DELETE") { const id = endpoint.split("/")[2]; return mockApiHandlers.deletePlacement(id); } if (endpoint.includes("/views/fetch") && method === "POST") { const id = endpoint.split("/")[2]; return mockApiHandlers.fetchPlacementViews(id); } if (endpoint.includes("/views/history") && method === "GET") { const id = endpoint.split("/")[2]; return mockApiHandlers.getPlacementViewsHistory(id, params); } if (endpoint.includes("/views/manual") && method === "POST") { const id = endpoint.split("/")[2]; return mockApiHandlers.setPlacementViewsManually(id, params?.views_count); } // Analytics if (endpoint === "/analytics/spending") { return mockApiHandlers.getSpendingAnalytics(params); } if (endpoint === "/analytics/placements") { return mockApiHandlers.getPlacementsAnalytics(params); } if (endpoint === "/analytics/creatives") { return mockApiHandlers.getCreativesAnalytics(params); } if (endpoint === "/analytics/external-channels") { return mockApiHandlers.getExternalChannelsAnalytics(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) { // FastAPI returns errors in format { "detail": "..." } // Transform to our ApiError format if (data.detail) { throw { error: { code: response.status === 401 ? "UNAUTHORIZED" : response.status === 403 ? "FORBIDDEN" : response.status === 404 ? "NOT_FOUND" : response.status === 422 ? "VALIDATION_ERROR" : "API_ERROR", message: data.detail, }, detail: data.detail, }; } // If it's already in our format, throw as is 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 => { // File import is now done on client side, no need for mock handling 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; }, };