| |
| |
| |
| |
|
|
| 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'); |
|
|