// Enhanced API Client with Caching, Retry Logic, and Better Error Handling class EnhancedAPIClient { constructor() { this.cache = new Map(); this.cacheExpiry = new Map(); this.defaultCacheDuration = 30000; // 30 seconds this.maxRetries = 3; this.retryDelay = 1000; // 1 second } /** * Fetch with automatic retry and exponential backoff */ async fetchWithRetry(url, options = {}, retries = this.maxRetries) { try { const response = await fetch(url, options); // If response is ok, return it if (response.ok) { return response; } // If we get a 429 (rate limit) or 5xx error, retry if ((response.status === 429 || response.status >= 500) && retries > 0) { const delay = this.retryDelay * (this.maxRetries - retries + 1); console.warn(`Request failed with status ${response.status}, retrying in ${delay}ms... (${retries} retries left)`); await this.sleep(delay); return this.fetchWithRetry(url, options, retries - 1); } // Otherwise throw error throw new Error(`HTTP ${response.status}: ${response.statusText}`); } catch (error) { // Network error - retry if we have retries left if (retries > 0 && error.name === 'TypeError') { const delay = this.retryDelay * (this.maxRetries - retries + 1); console.warn(`Network error, retrying in ${delay}ms... (${retries} retries left)`); await this.sleep(delay); return this.fetchWithRetry(url, options, retries - 1); } throw error; } } /** * Get data with caching support */ async get(url, options = {}) { const cacheKey = url + JSON.stringify(options); const cacheDuration = options.cacheDuration || this.defaultCacheDuration; // Check cache if (options.cache !== false && this.isCacheValid(cacheKey)) { console.log(`📦 Cache hit for ${url}`); return this.cache.get(cacheKey); } try { const response = await this.fetchWithRetry(url, { ...options, method: 'GET', headers: { 'Content-Type': 'application/json', ...options.headers } }); const data = await response.json(); // Store in cache if (options.cache !== false) { this.cache.set(cacheKey, data); this.cacheExpiry.set(cacheKey, Date.now() + cacheDuration); } return data; } catch (error) { console.error(`❌ GET request failed for ${url}:`, error); throw error; } } /** * Post data without caching */ async post(url, body = {}, options = {}) { try { const response = await this.fetchWithRetry(url, { ...options, method: 'POST', headers: { 'Content-Type': 'application/json', ...options.headers }, body: JSON.stringify(body) }); return await response.json(); } catch (error) { console.error(`❌ POST request failed for ${url}:`, error); throw error; } } /** * Check if cache is valid */ isCacheValid(key) { if (!this.cache.has(key)) return false; const expiry = this.cacheExpiry.get(key); if (!expiry || Date.now() > expiry) { this.cache.delete(key); this.cacheExpiry.delete(key); return false; } return true; } /** * Clear all cache */ clearCache() { this.cache.clear(); this.cacheExpiry.clear(); console.log('🗑️ Cache cleared'); } /** * Clear specific cache entry */ clearCacheEntry(url) { const keysToDelete = []; for (const key of this.cache.keys()) { if (key.startsWith(url)) { keysToDelete.push(key); } } keysToDelete.forEach(key => { this.cache.delete(key); this.cacheExpiry.delete(key); }); } /** * Sleep utility */ sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Batch requests with rate limiting */ async batchRequest(urls, options = {}) { const batchSize = options.batchSize || 5; const delay = options.delay || 100; const results = []; for (let i = 0; i < urls.length; i += batchSize) { const batch = urls.slice(i, i + batchSize); const batchPromises = batch.map(url => this.get(url, options)); const batchResults = await Promise.allSettled(batchPromises); results.push(...batchResults); // Delay between batches if (i + batchSize < urls.length) { await this.sleep(delay); } } return results; } } // Create global instance window.apiClient = new EnhancedAPIClient(); // Enhanced notification system with toast-style notifications class NotificationManager { constructor() { this.container = null; this.createContainer(); } createContainer() { if (document.getElementById('notification-container')) return; const container = document.createElement('div'); container.id = 'notification-container'; container.style.cssText = ` position: fixed; top: 100px; right: 20px; z-index: 10000; display: flex; flex-direction: column; gap: 10px; pointer-events: none; `; document.body.appendChild(container); this.container = container; } show(message, type = 'info', duration = 5000) { const toast = document.createElement('div'); toast.className = `notification-toast notification-${type}`; const icons = { success: ``, error: ``, warning: ``, info: `` }; toast.innerHTML = `