|
|
|
|
|
class EnhancedAPIClient { |
|
|
constructor() { |
|
|
this.cache = new Map(); |
|
|
this.cacheExpiry = new Map(); |
|
|
this.defaultCacheDuration = 30000; |
|
|
this.maxRetries = 3; |
|
|
this.retryDelay = 1000; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async fetchWithRetry(url, options = {}, retries = this.maxRetries) { |
|
|
try { |
|
|
const response = await fetch(url, options); |
|
|
|
|
|
|
|
|
if (response.ok) { |
|
|
return response; |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
|
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`); |
|
|
} catch (error) { |
|
|
|
|
|
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; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async get(url, options = {}) { |
|
|
const cacheKey = url + JSON.stringify(options); |
|
|
const cacheDuration = options.cacheDuration || this.defaultCacheDuration; |
|
|
|
|
|
|
|
|
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(); |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
clearCache() { |
|
|
this.cache.clear(); |
|
|
this.cacheExpiry.clear(); |
|
|
console.log('🗑️ Cache cleared'); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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(ms) { |
|
|
return new Promise(resolve => setTimeout(resolve, ms)); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
if (i + batchSize < urls.length) { |
|
|
await this.sleep(delay); |
|
|
} |
|
|
} |
|
|
|
|
|
return results; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
window.apiClient = new EnhancedAPIClient(); |
|
|
|
|
|
|
|
|
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: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>`, |
|
|
error: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>`, |
|
|
warning: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>`, |
|
|
info: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>` |
|
|
}; |
|
|
|
|
|
toast.innerHTML = ` |
|
|
<div style="display: flex; align-items: center; gap: 12px;"> |
|
|
<div class="notification-icon">${icons[type] || icons.info}</div> |
|
|
<div class="notification-message">${message}</div> |
|
|
<button class="notification-close" onclick="this.parentElement.parentElement.remove()"> |
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
|
|
<line x1="18" y1="6" x2="6" y2="18"></line> |
|
|
<line x1="6" y1="6" x2="18" y2="18"></line> |
|
|
</svg> |
|
|
</button> |
|
|
</div> |
|
|
`; |
|
|
|
|
|
toast.style.cssText = ` |
|
|
min-width: 300px; |
|
|
max-width: 500px; |
|
|
padding: 16px 20px; |
|
|
background: rgba(17, 24, 39, 0.95); |
|
|
backdrop-filter: blur(20px) saturate(180%); |
|
|
border: 1px solid ${this.getBorderColor(type)}; |
|
|
border-left: 4px solid ${this.getBorderColor(type)}; |
|
|
border-radius: 12px; |
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); |
|
|
color: var(--text-primary); |
|
|
animation: slideInRight 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
|
|
pointer-events: all; |
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
|
|
`; |
|
|
|
|
|
this.container.appendChild(toast); |
|
|
|
|
|
|
|
|
if (duration > 0) { |
|
|
setTimeout(() => { |
|
|
toast.style.animation = 'slideOutRight 0.3s cubic-bezier(0.4, 0, 0.2, 1)'; |
|
|
setTimeout(() => toast.remove(), 300); |
|
|
}, duration); |
|
|
} |
|
|
} |
|
|
|
|
|
getBorderColor(type) { |
|
|
const colors = { |
|
|
success: '#10b981', |
|
|
error: '#ef4444', |
|
|
warning: '#f59e0b', |
|
|
info: '#3b82f6' |
|
|
}; |
|
|
return colors[type] || colors.info; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
window.notificationManager = new NotificationManager(); |
|
|
|
|
|
|
|
|
window.showSuccess = (message) => window.notificationManager.show(message, 'success'); |
|
|
window.showError = (message) => window.notificationManager.show(message, 'error'); |
|
|
window.showWarning = (message) => window.notificationManager.show(message, 'warning'); |
|
|
window.showInfo = (message) => window.notificationManager.show(message, 'info'); |
|
|
|
|
|
|
|
|
const style = document.createElement('style'); |
|
|
style.textContent = ` |
|
|
@keyframes slideInRight { |
|
|
from { |
|
|
opacity: 0; |
|
|
transform: translateX(100px); |
|
|
} |
|
|
to { |
|
|
opacity: 1; |
|
|
transform: translateX(0); |
|
|
} |
|
|
} |
|
|
|
|
|
@keyframes slideOutRight { |
|
|
from { |
|
|
opacity: 1; |
|
|
transform: translateX(0); |
|
|
} |
|
|
to { |
|
|
opacity: 0; |
|
|
transform: translateX(100px); |
|
|
} |
|
|
} |
|
|
|
|
|
.notification-toast:hover { |
|
|
transform: translateX(-4px); |
|
|
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.5); |
|
|
} |
|
|
|
|
|
.notification-close { |
|
|
background: none; |
|
|
border: none; |
|
|
color: var(--text-secondary); |
|
|
cursor: pointer; |
|
|
padding: 4px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
border-radius: 4px; |
|
|
transition: all 0.2s; |
|
|
} |
|
|
|
|
|
.notification-close:hover { |
|
|
background: rgba(255, 255, 255, 0.1); |
|
|
color: var(--text-primary); |
|
|
} |
|
|
|
|
|
.notification-icon { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
} |
|
|
|
|
|
.notification-message { |
|
|
flex: 1; |
|
|
font-size: 14px; |
|
|
line-height: 1.5; |
|
|
} |
|
|
|
|
|
.notification-success .notification-icon { |
|
|
color: #10b981; |
|
|
} |
|
|
|
|
|
.notification-error .notification-icon { |
|
|
color: #ef4444; |
|
|
} |
|
|
|
|
|
.notification-warning .notification-icon { |
|
|
color: #f59e0b; |
|
|
} |
|
|
|
|
|
.notification-info .notification-icon { |
|
|
color: #3b82f6; |
|
|
} |
|
|
`; |
|
|
document.head.appendChild(style); |
|
|
|
|
|
console.log('✅ Enhanced API Client and Notification Manager loaded'); |
|
|
|