/** * Core Utilities Module for MYP Platform * Consolidated utilities to eliminate redundancy and improve performance */ (function(window) { 'use strict'; // Module namespace const MYP = window.MYP || {}; // ===== CONFIGURATION ===== const config = { apiTimeout: 30000, cacheExpiry: 5 * 60 * 1000, // 5 minutes notificationDuration: 5000, debounceDelay: 300, throttleDelay: 100 }; // ===== CACHE MANAGEMENT ===== const cache = new Map(); const requestCache = new Map(); // ===== CSRF TOKEN HANDLING ===== const csrf = { token: null, get() { if (!this.token) { const meta = document.querySelector('meta[name="csrf-token"]'); this.token = meta ? meta.getAttribute('content') : ''; } return this.token; }, headers() { return { 'X-CSRFToken': this.get(), 'Content-Type': 'application/json' }; } }; // ===== DOM UTILITIES ===== const dom = { // Cached selectors selectors: new Map(), // Get element with caching get(selector, parent = document) { const key = `${parent === document ? 'doc' : 'el'}_${selector}`; if (!this.selectors.has(key)) { this.selectors.set(key, parent.querySelector(selector)); } return this.selectors.get(key); }, // Get all elements getAll(selector, parent = document) { return parent.querySelectorAll(selector); }, // Clear cache clearCache() { this.selectors.clear(); }, // Safe element creation create(tag, attrs = {}, text = '') { const el = document.createElement(tag); Object.entries(attrs).forEach(([key, val]) => { if (key === 'class') { el.className = val; } else if (key === 'dataset') { Object.entries(val).forEach(([k, v]) => { el.dataset[k] = v; }); } else { el.setAttribute(key, val); } }); if (text) el.textContent = text; return el; } }; // ===== API REQUEST HANDLING ===== const api = { // Request deduplication pending: new Map(), async request(url, options = {}) { const key = `${options.method || 'GET'}_${url}`; // Check for pending request if (this.pending.has(key)) { return this.pending.get(key); } // Check cache for GET requests if (!options.method || options.method === 'GET') { const cached = requestCache.get(key); if (cached && Date.now() - cached.timestamp < config.cacheExpiry) { return Promise.resolve(cached.data); } } // Prepare request const requestOptions = { ...options, headers: { ...csrf.headers(), ...options.headers } }; // Create request promise const promise = fetch(url, requestOptions) .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then(data => { // Cache successful GET requests if (!options.method || options.method === 'GET') { requestCache.set(key, { data, timestamp: Date.now() }); } return data; }) .finally(() => { this.pending.delete(key); }); this.pending.set(key, promise); return promise; }, get(url, options = {}) { return this.request(url, { ...options, method: 'GET' }); }, post(url, data, options = {}) { return this.request(url, { ...options, method: 'POST', body: JSON.stringify(data) }); }, put(url, data, options = {}) { return this.request(url, { ...options, method: 'PUT', body: JSON.stringify(data) }); }, delete(url, options = {}) { return this.request(url, { ...options, method: 'DELETE' }); } }; // ===== UNIFIED NOTIFICATION SYSTEM ===== const notifications = { container: null, queue: [], init() { if (this.container) return; this.container = dom.create('div', { id: 'myp-notifications', class: 'fixed top-4 right-4 z-50 space-y-2' }); document.body.appendChild(this.container); }, show(message, type = 'info', duration = config.notificationDuration) { this.init(); const notification = dom.create('div', { class: `notification notification-${type} glass-navbar p-4 rounded-lg shadow-lg transform translate-x-full transition-transform duration-300`, role: 'alert' }); const content = dom.create('div', { class: 'flex items-center space-x-3' }); // Icon const icon = dom.create('i', { class: `fas ${this.getIcon(type)} text-lg` }); // Message const text = dom.create('span', { class: 'flex-1' }, message); // Close button const close = dom.create('button', { class: 'ml-4 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200', 'aria-label': 'Close' }); close.innerHTML = '×'; close.onclick = () => this.remove(notification); content.appendChild(icon); content.appendChild(text); content.appendChild(close); notification.appendChild(content); this.container.appendChild(notification); // Animate in requestAnimationFrame(() => { notification.classList.remove('translate-x-full'); }); // Auto remove if (duration > 0) { setTimeout(() => this.remove(notification), duration); } return notification; }, remove(notification) { notification.classList.add('translate-x-full'); setTimeout(() => { notification.remove(); }, 300); }, getIcon(type) { const icons = { success: 'fa-check-circle text-green-500', error: 'fa-exclamation-circle text-red-500', warning: 'fa-exclamation-triangle text-yellow-500', info: 'fa-info-circle text-blue-500' }; return icons[type] || icons.info; }, success(message, duration) { return this.show(message, 'success', duration); }, error(message, duration) { return this.show(message, 'error', duration); }, warning(message, duration) { return this.show(message, 'warning', duration); }, info(message, duration) { return this.show(message, 'info', duration); } }; // ===== PERFORMANCE UTILITIES ===== const performance = { debounce(func, delay = config.debounceDelay) { let timeoutId; return function(...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => func.apply(this, args), delay); }; }, throttle(func, delay = config.throttleDelay) { let lastCall = 0; return function(...args) { const now = Date.now(); if (now - lastCall >= delay) { lastCall = now; return func.apply(this, args); } }; }, memoize(func) { const cache = new Map(); return function(...args) { const key = JSON.stringify(args); if (cache.has(key)) { return cache.get(key); } const result = func.apply(this, args); cache.set(key, result); return result; }; }, lazy(selector, callback) { const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { callback(entry.target); observer.unobserve(entry.target); } }); }); dom.getAll(selector).forEach(el => observer.observe(el)); return observer; } }; // ===== STORAGE UTILITIES ===== const storage = { set(key, value, expiry = null) { const data = { value, timestamp: Date.now(), expiry }; try { localStorage.setItem(`myp_${key}`, JSON.stringify(data)); return true; } catch (e) { console.error('Storage error:', e); return false; } }, get(key) { try { const item = localStorage.getItem(`myp_${key}`); if (!item) return null; const data = JSON.parse(item); if (data.expiry && Date.now() - data.timestamp > data.expiry) { this.remove(key); return null; } return data.value; } catch (e) { console.error('Storage error:', e); return null; } }, remove(key) { localStorage.removeItem(`myp_${key}`); }, clear() { Object.keys(localStorage) .filter(key => key.startsWith('myp_')) .forEach(key => localStorage.removeItem(key)); } }; // ===== EVENT UTILITIES ===== const events = { listeners: new Map(), on(element, event, handler, options = {}) { const key = `${element}_${event}`; if (!this.listeners.has(key)) { this.listeners.set(key, new Set()); } this.listeners.get(key).add(handler); element.addEventListener(event, handler, options); }, off(element, event, handler) { const key = `${element}_${event}`; if (this.listeners.has(key)) { this.listeners.get(key).delete(handler); if (this.listeners.get(key).size === 0) { this.listeners.delete(key); } } element.removeEventListener(event, handler); }, once(element, event, handler, options = {}) { const onceHandler = (e) => { handler(e); this.off(element, event, onceHandler); }; this.on(element, event, onceHandler, options); }, emit(name, detail = {}) { const event = new CustomEvent(name, { detail, bubbles: true, cancelable: true }); document.dispatchEvent(event); }, cleanup() { this.listeners.forEach((handlers, key) => { const [element, event] = key.split('_'); handlers.forEach(handler => { element.removeEventListener(event, handler); }); }); this.listeners.clear(); } }; // ===== FORM UTILITIES ===== const forms = { serialize(form) { const data = new FormData(form); const obj = {}; for (const [key, value] of data.entries()) { if (obj[key]) { if (!Array.isArray(obj[key])) { obj[key] = [obj[key]]; } obj[key].push(value); } else { obj[key] = value; } } return obj; }, validate(form) { const inputs = form.querySelectorAll('[required]'); let valid = true; inputs.forEach(input => { if (!input.value.trim()) { input.classList.add('border-red-500'); valid = false; } else { input.classList.remove('border-red-500'); } }); return valid; }, reset(form) { form.reset(); form.querySelectorAll('.border-red-500').forEach(el => { el.classList.remove('border-red-500'); }); } }; // ===== INITIALIZATION ===== const init = () => { // Initialize notifications notifications.init(); // Clean up on page unload window.addEventListener('beforeunload', () => { events.cleanup(); dom.clearCache(); }); }; // ===== PUBLIC API ===== MYP.utils = { csrf, dom, api, notifications, performance, storage, events, forms, init, config }; // Auto-initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } // Export to global scope window.MYP = MYP; })(window);