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