// Utility Functions for SkyArtShop // Centralized helper functions used across the application /** * API Request Handler with Error Management * @param {string} url - API endpoint * @param {object} options - Fetch options * @returns {Promise} - Response data */ async function apiRequest(url, options = {}) { const defaultOptions = { credentials: "include", headers: { "Content-Type": "application/json", ...options.headers, }, }; try { const response = await fetch(url, { ...defaultOptions, ...options }); if (!response.ok) { const error = await response.json().catch(() => ({ message: `HTTP ${response.status}: ${response.statusText}`, })); throw new Error(error.message || "Request failed"); } return await response.json(); } catch (error) { if (process.env.NODE_ENV !== "production") { console.error(`API Request Failed [${url}]:`, error); } throw error; } } /** * Debounce function to limit execution rate * @param {Function} func - Function to debounce * @param {number} wait - Wait time in milliseconds * @returns {Function} - Debounced function */ function debounce(func, wait = 300) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } /** * Throttle function to limit execution frequency * @param {Function} func - Function to throttle * @param {number} limit - Limit in milliseconds * @returns {Function} - Throttled function */ function throttle(func, limit = 300) { let inThrottle; return function executedFunction(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => (inThrottle = false), limit); } }; } /** * Escape HTML to prevent XSS * @param {string} text - Text to escape * @returns {string} - Escaped text */ function escapeHtml(text) { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } /** * Format date to readable string * @param {string} dateString - Date string to format * @returns {string} - Formatted date */ function formatDate(dateString) { if (!dateString) return "N/A"; const date = new Date(dateString); if (isNaN(date.getTime())) return "Invalid Date"; return new Intl.DateTimeFormat("en-US", { year: "numeric", month: "short", day: "numeric", }).format(date); } /** * Format currency * @param {number} amount - Amount to format * @param {string} currency - Currency code * @returns {string} - Formatted currency */ function formatCurrency(amount, currency = "USD") { return new Intl.NumberFormat("en-US", { style: "currency", currency: currency, }).format(amount); } /** * Show toast notification * @param {string} message - Message to display * @param {string} type - Type of notification (success, error, warning, info) * @param {number} duration - Duration in milliseconds */ function showToast(message, type = "info", duration = 3000) { // Remove existing toasts const existingToast = document.querySelector(".toast-notification"); if (existingToast) { existingToast.remove(); } const toast = document.createElement("div"); toast.className = `toast-notification toast-${type}`; toast.setAttribute("role", "alert"); toast.setAttribute("aria-live", "polite"); const icons = { success: "✓", error: "✗", warning: "⚠", info: "ℹ", }; toast.innerHTML = ` ${escapeHtml(message)} `; document.body.appendChild(toast); // Trigger animation setTimeout(() => toast.classList.add("show"), 10); // Auto remove setTimeout(() => { toast.classList.remove("show"); setTimeout(() => toast.remove(), 300); }, duration); } /** * LocalStorage with error handling */ const storage = { get(key, defaultValue = null) { try { const item = localStorage.getItem(key); return item ? JSON.parse(item) : defaultValue; } catch (error) { if (process.env.NODE_ENV !== "production") { console.error("Storage get error:", error); } return defaultValue; } }, set(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); return true; } catch (error) { if (process.env.NODE_ENV !== "production") { console.error("Storage set error:", error); } return false; } }, remove(key) { try { localStorage.removeItem(key); return true; } catch (error) { if (process.env.NODE_ENV !== "production") { console.error("Storage remove error:", error); } return false; } }, clear() { try { localStorage.clear(); return true; } catch (error) { if (process.env.NODE_ENV !== "production") { console.error("Storage clear error:", error); } return false; } }, }; /** * Validate email format * @param {string} email - Email to validate * @returns {boolean} - Valid or not */ function isValidEmail(email) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } /** * Get image URL with fallback * @param {string} imagePath - Image path from server * @param {string} fallback - Fallback image path * @returns {string} - Full image URL */ function getImageUrl(imagePath, fallback = "/assets/images/placeholder.png") { if (!imagePath) return fallback; // If already a full URL, return as is if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) { return imagePath; } // Handle relative paths return imagePath.startsWith("/") ? imagePath : `/${imagePath}`; } /** * Create accessible image element * @param {string} src - Image source * @param {string} alt - Alt text * @param {string} className - CSS classes * @returns {HTMLImageElement} - Image element */ function createImage(src, alt, className = "") { const img = document.createElement("img"); img.src = getImageUrl(src); img.alt = alt || "Image"; img.className = className; img.loading = "lazy"; // Add error handler with fallback img.onerror = function () { this.src = "/assets/images/placeholder.png"; this.onerror = null; // Prevent infinite loop }; return img; } /** * Trap focus within modal for accessibility * @param {HTMLElement} element - Element to trap focus in */ function trapFocus(element) { const focusableElements = element.querySelectorAll( 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])' ); const firstFocusable = focusableElements[0]; const lastFocusable = focusableElements[focusableElements.length - 1]; element.addEventListener("keydown", function (e) { if (e.key !== "Tab") return; if (e.shiftKey) { if (document.activeElement === firstFocusable) { lastFocusable.focus(); e.preventDefault(); } } else { if (document.activeElement === lastFocusable) { firstFocusable.focus(); e.preventDefault(); } } }); // Focus first element firstFocusable?.focus(); } /** * Announce to screen readers * @param {string} message - Message to announce * @param {string} priority - Priority level (polite, assertive) */ function announceToScreenReader(message, priority = "polite") { const announcement = document.createElement("div"); announcement.setAttribute("role", "status"); announcement.setAttribute("aria-live", priority); announcement.className = "sr-only"; announcement.textContent = message; document.body.appendChild(announcement); setTimeout(() => announcement.remove(), 1000); } // Export for use in other scripts if (typeof module !== "undefined" && module.exports) { module.exports = { apiRequest, debounce, throttle, escapeHtml, formatDate, formatCurrency, showToast, storage, isValidEmail, getImageUrl, createImage, trapFocus, announceToScreenReader, }; }