Files
SkyArtShop/website/assets/js/utils.js
Local Server e4b3de4a46 Updatweb
2025-12-19 20:44:46 -06:00

331 lines
8.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 = `
<span class="toast-icon" aria-hidden="true">${
icons[type] || icons.info
}</span>
<span class="toast-message">${escapeHtml(message)}</span>
<button class="toast-close" onclick="this.parentElement.remove()" aria-label="Close notification">×</button>
`;
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,
};
}