// 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 = `
${
icons[type] || icons.info
}
${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,
};
}