|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ErrorHandler { |
|
|
constructor() { |
|
|
this.errors = []; |
|
|
this.maxErrors = 100; |
|
|
this.init(); |
|
|
} |
|
|
|
|
|
init() { |
|
|
|
|
|
window.addEventListener('error', (event) => { |
|
|
this.handleError(event.error || event.message, 'Global Error'); |
|
|
event.preventDefault(); |
|
|
}); |
|
|
|
|
|
|
|
|
window.addEventListener('unhandledrejection', (event) => { |
|
|
this.handleError(event.reason, 'Unhandled Promise'); |
|
|
event.preventDefault(); |
|
|
}); |
|
|
|
|
|
console.log('✅ Error Handler initialized'); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
handleError(error, context = 'Unknown') { |
|
|
const errorInfo = { |
|
|
message: this.getErrorMessage(error), |
|
|
context, |
|
|
timestamp: Date.now(), |
|
|
stack: error?.stack || null, |
|
|
url: window.location.href |
|
|
}; |
|
|
|
|
|
|
|
|
console.error(`[${context}]`, error); |
|
|
|
|
|
|
|
|
this.errors.push(errorInfo); |
|
|
if (this.errors.length > this.maxErrors) { |
|
|
this.errors.shift(); |
|
|
} |
|
|
|
|
|
|
|
|
this.showUserError(errorInfo); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getErrorMessage(error) { |
|
|
if (typeof error === 'string') return error; |
|
|
if (error?.message) return error.message; |
|
|
if (error?.toString) return error.toString(); |
|
|
return 'An unknown error occurred'; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
showUserError(errorInfo) { |
|
|
const message = this.getUserFriendlyMessage(errorInfo.message); |
|
|
|
|
|
if (window.uiManager) { |
|
|
window.uiManager.showToast(message, 'error', 5000); |
|
|
} else { |
|
|
|
|
|
console.error('Error:', message); |
|
|
alert(message); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getUserFriendlyMessage(technicalMessage) { |
|
|
const lowerMessage = technicalMessage.toLowerCase(); |
|
|
|
|
|
|
|
|
if (lowerMessage.includes('network') || lowerMessage.includes('fetch')) { |
|
|
return '🌐 Network error. Please check your connection.'; |
|
|
} |
|
|
|
|
|
|
|
|
if (lowerMessage.includes('timeout') || lowerMessage.includes('timed out')) { |
|
|
return '⏱️ Request timed out. Please try again.'; |
|
|
} |
|
|
|
|
|
|
|
|
if (lowerMessage.includes('404') || lowerMessage.includes('not found')) { |
|
|
return '🔍 Resource not found. It may have been moved or deleted.'; |
|
|
} |
|
|
|
|
|
|
|
|
if (lowerMessage.includes('401') || lowerMessage.includes('unauthorized')) { |
|
|
return '🔒 Authentication required. Please log in.'; |
|
|
} |
|
|
|
|
|
|
|
|
if (lowerMessage.includes('403') || lowerMessage.includes('forbidden')) { |
|
|
return '🚫 Access denied. You don\'t have permission.'; |
|
|
} |
|
|
|
|
|
|
|
|
if (lowerMessage.includes('500') || lowerMessage.includes('server error')) { |
|
|
return '⚠️ Server error. We\'re working on it!'; |
|
|
} |
|
|
|
|
|
|
|
|
if (lowerMessage.includes('database') || lowerMessage.includes('sql')) { |
|
|
return '💾 Database error. Please try again later.'; |
|
|
} |
|
|
|
|
|
|
|
|
if (lowerMessage.includes('api')) { |
|
|
return '🔌 API error. Using fallback data.'; |
|
|
} |
|
|
|
|
|
|
|
|
return `⚠️ ${technicalMessage}`; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getErrors() { |
|
|
return this.errors; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
clearErrors() { |
|
|
this.errors = []; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
exportErrors() { |
|
|
const data = JSON.stringify(this.errors, null, 2); |
|
|
const blob = new Blob([data], { type: 'application/json' }); |
|
|
const url = URL.createObjectURL(blob); |
|
|
|
|
|
const a = document.createElement('a'); |
|
|
a.href = url; |
|
|
a.download = `errors-${Date.now()}.json`; |
|
|
a.click(); |
|
|
|
|
|
URL.revokeObjectURL(url); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
class APIErrorHandler { |
|
|
static async handleAPIError(response, fallbackData = null) { |
|
|
let error = { |
|
|
status: response?.status || 500, |
|
|
statusText: response?.statusText || 'Unknown', |
|
|
url: response?.url || 'unknown' |
|
|
}; |
|
|
|
|
|
try { |
|
|
const data = await response.json(); |
|
|
error.message = data.message || data.error || 'API Error'; |
|
|
error.details = data.details || null; |
|
|
} catch (e) { |
|
|
error.message = `HTTP ${error.status}: ${error.statusText}`; |
|
|
} |
|
|
|
|
|
console.error('API Error:', error); |
|
|
|
|
|
|
|
|
if (window.errorHandler) { |
|
|
window.errorHandler.handleError(error, 'API Error'); |
|
|
} |
|
|
|
|
|
|
|
|
if (fallbackData) { |
|
|
console.warn('Using fallback data due to API error'); |
|
|
return { |
|
|
success: false, |
|
|
error: error.message, |
|
|
data: fallbackData, |
|
|
fallback: true |
|
|
}; |
|
|
} |
|
|
|
|
|
throw error; |
|
|
} |
|
|
|
|
|
static async fetchWithFallback(url, options = {}, fallbackData = null) { |
|
|
try { |
|
|
const response = await fetch(url, { |
|
|
...options, |
|
|
signal: options.signal || AbortSignal.timeout(options.timeout || 10000) |
|
|
}); |
|
|
|
|
|
if (!response.ok) { |
|
|
return await this.handleAPIError(response, fallbackData); |
|
|
} |
|
|
|
|
|
const data = await response.json(); |
|
|
return { |
|
|
success: true, |
|
|
data, |
|
|
fallback: false |
|
|
}; |
|
|
} catch (error) { |
|
|
console.error('Fetch error:', error); |
|
|
|
|
|
if (window.errorHandler) { |
|
|
window.errorHandler.handleError(error, 'Fetch Error'); |
|
|
} |
|
|
|
|
|
if (fallbackData) { |
|
|
return { |
|
|
success: false, |
|
|
error: error.message, |
|
|
data: fallbackData, |
|
|
fallback: true |
|
|
}; |
|
|
} |
|
|
|
|
|
throw error; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
class FormValidator { |
|
|
static validateRequired(value, fieldName) { |
|
|
if (!value || (typeof value === 'string' && value.trim() === '')) { |
|
|
return `${fieldName} is required`; |
|
|
} |
|
|
return null; |
|
|
} |
|
|
|
|
|
static validateEmail(email) { |
|
|
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; |
|
|
if (!re.test(email)) { |
|
|
return 'Invalid email address'; |
|
|
} |
|
|
return null; |
|
|
} |
|
|
|
|
|
static validateURL(url) { |
|
|
try { |
|
|
new URL(url); |
|
|
return null; |
|
|
} catch { |
|
|
return 'Invalid URL'; |
|
|
} |
|
|
} |
|
|
|
|
|
static validateNumber(value, min = null, max = null) { |
|
|
const num = Number(value); |
|
|
if (isNaN(num)) { |
|
|
return 'Must be a number'; |
|
|
} |
|
|
if (min !== null && num < min) { |
|
|
return `Must be at least ${min}`; |
|
|
} |
|
|
if (max !== null && num > max) { |
|
|
return `Must be at most ${max}`; |
|
|
} |
|
|
return null; |
|
|
} |
|
|
|
|
|
static validateForm(formElement) { |
|
|
const errors = {}; |
|
|
const inputs = formElement.querySelectorAll('[data-validate]'); |
|
|
|
|
|
inputs.forEach(input => { |
|
|
const rules = input.dataset.validate.split('|'); |
|
|
const fieldName = input.name || input.id; |
|
|
|
|
|
rules.forEach(rule => { |
|
|
let error = null; |
|
|
|
|
|
if (rule === 'required') { |
|
|
error = this.validateRequired(input.value, fieldName); |
|
|
} else if (rule === 'email') { |
|
|
error = this.validateEmail(input.value); |
|
|
} else if (rule === 'url') { |
|
|
error = this.validateURL(input.value); |
|
|
} else if (rule.startsWith('number')) { |
|
|
const params = rule.match(/number\((\d+),(\d+)\)/); |
|
|
error = this.validateNumber( |
|
|
input.value, |
|
|
params ? parseInt(params[1]) : null, |
|
|
params ? parseInt(params[2]) : null |
|
|
); |
|
|
} |
|
|
|
|
|
if (error) { |
|
|
errors[fieldName] = error; |
|
|
} |
|
|
}); |
|
|
}); |
|
|
|
|
|
return { |
|
|
valid: Object.keys(errors).length === 0, |
|
|
errors |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
class RetryHelper { |
|
|
static async retry(fn, options = {}) { |
|
|
const { |
|
|
maxAttempts = 3, |
|
|
delay = 1000, |
|
|
backoff = 2, |
|
|
onRetry = null |
|
|
} = options; |
|
|
|
|
|
let lastError; |
|
|
|
|
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) { |
|
|
try { |
|
|
return await fn(); |
|
|
} catch (error) { |
|
|
lastError = error; |
|
|
|
|
|
if (attempt < maxAttempts) { |
|
|
const waitTime = delay * Math.pow(backoff, attempt - 1); |
|
|
console.warn(`Attempt ${attempt} failed, retrying in ${waitTime}ms...`); |
|
|
|
|
|
if (onRetry) { |
|
|
onRetry(attempt, error); |
|
|
} |
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, waitTime)); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
throw lastError; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const errorHandler = new ErrorHandler(); |
|
|
|
|
|
|
|
|
if (typeof module !== 'undefined' && module.exports) { |
|
|
module.exports = { |
|
|
ErrorHandler, |
|
|
APIErrorHandler, |
|
|
FormValidator, |
|
|
RetryHelper, |
|
|
errorHandler |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
window.errorHandler = errorHandler; |
|
|
window.APIErrorHandler = APIErrorHandler; |
|
|
window.FormValidator = FormValidator; |
|
|
window.RetryHelper = RetryHelper; |
|
|
|
|
|
console.log('✅ Error Handler loaded and ready'); |
|
|
|