// Relevant Reflex - Main JavaScript (function() { 'use strict'; // Global app object window.RR = window.RR || {}; // App configuration RR.config = { version: '1.0.0', apiUrl: '/api/', debounceDelay: 300, toastDuration: 3000, animationDuration: 300 }; // Initialize app when DOM is ready document.addEventListener('DOMContentLoaded', function() { RR.init(); }); // Main initialization RR.init = function() { console.log(`Relevant Reflex Panel Management System v${RR.config.version} initialized`); // Initialize core modules RR.navigation.init(); RR.forms.init(); RR.tables.init(); RR.modals.init(); RR.tooltips.init(); RR.lazyLoad.init(); RR.performance.init(); // Initialize page-specific modules if (typeof window.initDashboard === 'function') { window.initDashboard(); } }; // Navigation Module RR.navigation = { init: function() { this.setupMobileToggle(); this.setupSmoothScrolling(); this.setupActiveLinks(); }, setupMobileToggle: function() { const mobileToggle = document.querySelector('.mobile-toggle'); const navMenu = document.querySelector('.nav-menu'); if (!mobileToggle || !navMenu) return; // Toggle mobile menu mobileToggle.addEventListener('click', function(e) { e.preventDefault(); const isActive = mobileToggle.classList.contains('active'); if (isActive) { RR.navigation.closeMobileMenu(); } else { RR.navigation.openMobileMenu(); } }); // Close menu when clicking nav links navMenu.querySelectorAll('.nav-link').forEach(link => { link.addEventListener('click', function() { RR.navigation.closeMobileMenu(); }); }); // Close menu when clicking outside document.addEventListener('click', function(e) { const isClickInsideNav = navMenu.contains(e.target) || mobileToggle.contains(e.target); if (!isClickInsideNav && mobileToggle.classList.contains('active')) { RR.navigation.closeMobileMenu(); } }); // Close menu on escape key document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && mobileToggle.classList.contains('active')) { RR.navigation.closeMobileMenu(); } }); }, openMobileMenu: function() { const mobileToggle = document.querySelector('.mobile-toggle'); const navMenu = document.querySelector('.nav-menu'); mobileToggle.classList.add('active'); navMenu.classList.add('active'); mobileToggle.setAttribute('aria-expanded', 'true'); // Prevent body scroll document.body.style.overflow = 'hidden'; }, closeMobileMenu: function() { const mobileToggle = document.querySelector('.mobile-toggle'); const navMenu = document.querySelector('.nav-menu'); mobileToggle.classList.remove('active'); navMenu.classList.remove('active'); mobileToggle.setAttribute('aria-expanded', 'false'); // Restore body scroll document.body.style.overflow = ''; }, setupSmoothScrolling: function() { document.querySelectorAll('a[href^="#"]').forEach(anchor => { anchor.addEventListener('click', function(e) { const href = this.getAttribute('href'); if (href === '#') { e.preventDefault(); return; } const target = document.querySelector(href); if (target) { e.preventDefault(); const headerHeight = document.querySelector('.main-header')?.offsetHeight || 0; const targetPosition = target.offsetTop - headerHeight - 20; window.scrollTo({ top: targetPosition, behavior: 'smooth' }); } }); }); }, setupActiveLinks: function() { const currentPage = window.location.pathname; const navLinks = document.querySelectorAll('.nav-link'); navLinks.forEach(link => { const linkPath = new URL(link.href).pathname; if (linkPath === currentPage) { link.classList.add('active'); } else { link.classList.remove('active'); } }); } }; // Forms Module RR.forms = { init: function() { this.setupValidation(); this.setupFileUploads(); this.setupFormSubmissions(); this.setupRealTimeValidation(); }, setupValidation: function() { const forms = document.querySelectorAll('form[data-validate]'); forms.forEach(form => { form.addEventListener('submit', function(e) { if (!RR.forms.validateForm(this)) { e.preventDefault(); } }); }); }, validateForm: function(form) { const requiredFields = form.querySelectorAll('[required]'); let isValid = true; // Clear previous errors form.querySelectorAll('.error').forEach(el => el.classList.remove('error')); form.querySelectorAll('.error-message').forEach(el => el.remove()); requiredFields.forEach(field => { if (!this.validateField(field)) { isValid = false; } }); // Email validation const emailFields = form.querySelectorAll('input[type="email"]'); emailFields.forEach(field => { if (field.value && !this.isValidEmail(field.value)) { this.showFieldError(field, 'Please enter a valid email address'); isValid = false; } }); // Password confirmation const passwordField = form.querySelector('input[name="password"]'); const confirmField = form.querySelector('input[name="confirm_password"]'); if (passwordField && confirmField && passwordField.value !== confirmField.value) { this.showFieldError(confirmField, 'Passwords do not match'); isValid = false; } return isValid; }, validateField: function(field) { const value = field.value.trim(); if (!value) { this.showFieldError(field, 'This field is required'); return false; } // Minimum length validation const minLength = field.getAttribute('minlength'); if (minLength && value.length < parseInt(minLength)) { this.showFieldError(field, `Minimum ${minLength} characters required`); return false; } field.classList.remove('error'); return true; }, showFieldError: function(field, message) { field.classList.add('error'); // Remove existing error message const existingError = field.parentNode.querySelector('.error-message'); if (existingError) { existingError.remove(); } // Add new error message const errorEl = document.createElement('small'); errorEl.className = 'error-message'; errorEl.textContent = message; errorEl.style.color = 'var(--danger-color)'; errorEl.style.display = 'block'; errorEl.style.marginTop = 'var(--spacing-xs)'; field.parentNode.insertBefore(errorEl, field.nextSibling); }, isValidEmail: function(email) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); }, setupFileUploads: function() { const fileInputs = document.querySelectorAll('input[type="file"]'); fileInputs.forEach(input => { input.addEventListener('change', function() { const file = this.files[0]; if (file) { // Validate file size (5MB limit) if (file.size > 5 * 1024 * 1024) { RR.toast.show('File size must be less than 5MB', 'error'); this.value = ''; return; } // Show file name const label = this.nextElementSibling; if (label) { label.textContent = file.name; } } }); }); }, setupFormSubmissions: function() { const submitButtons = document.querySelectorAll('.btn[type="submit"], .btn-primary'); submitButtons.forEach(btn => { btn.addEventListener('click', function(e) { const form = this.closest('form'); if (form && this.type === 'submit') { RR.forms.addLoadingState(this); // Remove loading state after form submission setTimeout(() => { RR.forms.removeLoadingState(this); }, 2000); } }); }); }, setupRealTimeValidation: function() { const inputs = document.querySelectorAll('input[required], textarea[required]'); inputs.forEach(input => { input.addEventListener('blur', function() { if (this.value.trim()) { RR.forms.validateField(this); } }); input.addEventListener('input', function() { if (this.classList.contains('error') && this.value.trim()) { RR.forms.validateField(this); } }); }); }, addLoadingState: function(button) { if (button.classList.contains('loading')) return; const originalText = button.textContent; button.setAttribute('data-original-text', originalText); button.textContent = 'Loading...'; button.disabled = true; button.classList.add('loading'); }, removeLoadingState: function(button) { const originalText = button.getAttribute('data-original-text'); if (originalText) { button.textContent = originalText; button.removeAttribute('data-original-text'); } button.disabled = false; button.classList.remove('loading'); } }; // Tables Module RR.tables = { init: function() { this.setupSearch(); this.setupSorting(); this.setupActions(); this.setupFilters(); }, setupSearch: function() { const searchInputs = document.querySelectorAll('.search-input'); searchInputs.forEach(input => { let timeout; input.addEventListener('input', function() { clearTimeout(timeout); timeout = setTimeout(() => { const query = this.value.toLowerCase().trim(); RR.tables.filterTable(query); }, RR.config.debounceDelay); }); }); }, filterTable: function(query) { const table = document.querySelector('.data-table'); if (!table) return; const rows = table.querySelectorAll('tbody tr'); let visibleCount = 0; rows.forEach(row => { const text = row.textContent.toLowerCase(); const shouldShow = !query || text.includes(query); row.style.display = shouldShow ? '' : 'none'; if (shouldShow) visibleCount++; }); // Update pagination info if exists const paginationInfo = document.querySelector('.pagination-info'); if (paginationInfo && query) { paginationInfo.textContent = `Showing ${visibleCount} of ${rows.length} results`; } }, setupSorting: function() { const headers = document.querySelectorAll('.data-table th[data-sort]'); headers.forEach(header => { header.style.cursor = 'pointer'; header.addEventListener('click', function() { const sortKey = this.getAttribute('data-sort'); const isAscending = !this.classList.contains('sort-asc'); // Remove sort classes from all headers headers.forEach(h => h.classList.remove('sort-asc', 'sort-desc')); // Add sort class to current header this.classList.add(isAscending ? 'sort-asc' : 'sort-desc'); RR.tables.sortTable(sortKey, isAscending); }); }); }, sortTable: function(sortKey, ascending) { const table = document.querySelector('.data-table tbody'); if (!table) return; const rows = Array.from(table.querySelectorAll('tr')); const sortIndex = Array.from(table.parentNode.querySelectorAll('th')).findIndex(th => th.getAttribute('data-sort') === sortKey ); if (sortIndex === -1) return; rows.sort((a, b) => { const aText = a.cells[sortIndex]?.textContent.trim() || ''; const bText = b.cells[sortIndex]?.textContent.trim() || ''; // Try numeric sort first const aNum = parseFloat(aText.replace(/[^0-9.-]/g, '')); const bNum = parseFloat(bText.replace(/[^0-9.-]/g, '')); if (!isNaN(aNum) && !isNaN(bNum)) { return ascending ? aNum - bNum : bNum - aNum; } // Fall back to string sort return ascending ? aText.localeCompare(bText) : bText.localeCompare(aText); }); // Re-append sorted rows rows.forEach(row => table.appendChild(row)); }, setupActions: function() { // Edit buttons document.addEventListener('click', function(e) { if (e.target.closest('.btn-edit')) { e.preventDefault(); const id = e.target.closest('.btn-edit').getAttribute('data-id'); RR.tables.handleEdit(id); } if (e.target.closest('.btn-delete')) { e.preventDefault(); const id = e.target.closest('.btn-delete').getAttribute('data-id'); RR.tables.handleDelete(id); } if (e.target.closest('.btn-view')) { e.preventDefault(); const id = e.target.closest('.btn-view').getAttribute('data-id'); RR.tables.handleView(id); } }); }, handleEdit: function(id) { RR.toast.show(`Edit functionality for ID: ${id}`, 'info'); // Implement edit modal or redirect }, handleDelete: function(id) { if (confirm('Are you sure you want to delete this item? This action cannot be undone.')) { // Show loading state const button = document.querySelector(`[data-id="${id}"].btn-delete`); if (button) { button.disabled = true; button.innerHTML = ''; } // Simulate API call setTimeout(() => { const row = button?.closest('tr'); if (row) { row.style.transition = 'all 0.3s ease'; row.style.opacity = '0'; row.style.transform = 'translateX(-20px)'; setTimeout(() => { row.remove(); RR.toast.show('Item deleted successfully', 'success'); }, 300); } }, 1000); } }, handleView: function(id) { RR.toast.show(`View details for ID: ${id}`, 'info'); // Implement view modal or redirect }, setupFilters: function() { const filterSelects = document.querySelectorAll('.filter-select'); filterSelects.forEach(select => { select.addEventListener('change', function() { RR.tables.applyFilters(); }); }); }, applyFilters: function() { const table = document.querySelector('.data-table'); if (!table) return; const rows = table.querySelectorAll('tbody tr'); const filters = {}; // Get all filter values document.querySelectorAll('.filter-select').forEach(select => { if (select.value) { filters[select.id] = select.value.toLowerCase(); } }); // Apply filters rows.forEach(row => { let shouldShow = true; Object.keys(filters).forEach(filterId => { const filterValue = filters[filterId]; let cellText = ''; // Get cell text based on filter type if (filterId.includes('role')) { const roleElement = row.querySelector('.role-badge'); cellText = roleElement ? roleElement.textContent.toLowerCase() : ''; } else if (filterId.includes('status')) { const statusElement = row.querySelector('.status-badge'); cellText = statusElement ? statusElement.textContent.toLowerCase() : ''; } if (cellText && !cellText.includes(filterValue)) { shouldShow = false; } }); row.style.display = shouldShow ? '' : 'none'; }); } }; // Modals Module RR.modals = { init: function() { this.setupModalTriggers(); this.setupModalClosing(); this.setupKeyboardNavigation(); }, setupModalTriggers: function() { // Generic modal triggers document.addEventListener('click', function(e) { const trigger = e.target.closest('[data-modal]'); if (trigger) { e.preventDefault(); const modalId = trigger.getAttribute('data-modal'); RR.modals.open(modalId); } }); }, setupModalClosing: function() { document.addEventListener('click', function(e) { // Close button if (e.target.closest('.modal-close')) { const modal = e.target.closest('.modal'); if (modal) RR.modals.close(modal.id); } // Backdrop click if (e.target.classList.contains('modal')) { RR.modals.close(e.target.id); } }); }, setupKeyboardNavigation: function() { document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { const openModal = document.querySelector('.modal[style*="flex"]'); if (openModal) { RR.modals.close(openModal.id); } } }); }, open: function(modalId) { const modal = document.getElementById(modalId); if (!modal) return; modal.style.display = 'flex'; document.body.style.overflow = 'hidden'; // Focus first input const firstInput = modal.querySelector('input, textarea, select'); if (firstInput) { setTimeout(() => firstInput.focus(), 100); } // Add animation class if needed const modalContent = modal.querySelector('.modal-content'); if (modalContent) { modalContent.style.animation = 'modalAppear 0.3s ease-out'; } }, close: function(modalId) { const modal = document.getElementById(modalId); if (!modal) return; modal.style.display = 'none'; document.body.style.overflow = ''; // Reset form if exists const form = modal.querySelector('form'); if (form) { form.reset(); form.querySelectorAll('.error').forEach(el => el.classList.remove('error')); form.querySelectorAll('.error-message').forEach(el => el.remove()); } } }; // Toast Notifications RR.toast = { show: function(message, type = 'info') { // Remove existing toasts document.querySelectorAll('.toast').forEach(toast => toast.remove()); const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.textContent = message; toast.setAttribute('role', 'alert'); toast.setAttribute('aria-live', 'assertive'); document.body.appendChild(toast); // Show with animation setTimeout(() => toast.classList.add('show'), 100); // Auto hide setTimeout(() => { toast.classList.remove('show'); setTimeout(() => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } }, RR.config.animationDuration); }, RR.config.toastDuration); } }; // Tooltips Module RR.tooltips = { init: function() { this.createTooltips(); }, createTooltips: function() { const elements = document.querySelectorAll('[title]'); elements.forEach(el => { const title = el.getAttribute('title'); el.removeAttribute('title'); // Prevent default tooltip el.addEventListener('mouseenter', () => { RR.tooltips.show(el, title); }); el.addEventListener('mouseleave', () => { RR.tooltips.hide(); }); }); }, show: function(element, text) { const tooltip = document.createElement('div'); tooltip.className = 'tooltip'; tooltip.textContent = text; tooltip.style.cssText = ` position: absolute; background: var(--dark-color); color: var(--white); padding: var(--spacing-sm) var(--spacing-md); border-radius: var(--border-radius); font-size: var(--font-size-sm); z-index: var(--z-tooltip); pointer-events: none; opacity: 0; transition: opacity 0.2s ease; white-space: nowrap; `; document.body.appendChild(tooltip); const rect = element.getBoundingClientRect(); const tooltipRect = tooltip.getBoundingClientRect(); tooltip.style.left = rect.left + (rect.width - tooltipRect.width) / 2 + 'px'; tooltip.style.top = rect.top - tooltipRect.height - 8 + 'px'; setTimeout(() => tooltip.style.opacity = '1', 10); }, hide: function() { const tooltips = document.querySelectorAll('.tooltip'); tooltips.forEach(tooltip => { tooltip.style.opacity = '0'; setTimeout(() => { if (tooltip.parentNode) { tooltip.parentNode.removeChild(tooltip); } }, 200); }); } }; // Lazy Loading Module RR.lazyLoad = { init: function() { if ('IntersectionObserver' in window) { this.setupImageLazyLoading(); this.setupContentLazyLoading(); } }, setupImageLazyLoading: function() { const images = document.querySelectorAll('img[data-src]'); if (images.length === 0) return; const imageObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; img.classList.remove('lazy'); imageObserver.unobserve(img); } }); }); images.forEach(img => imageObserver.observe(img)); }, setupContentLazyLoading: function() { const lazyElements = document.querySelectorAll('.lazy-load'); if (lazyElements.length === 0) return; const contentObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('loaded'); contentObserver.unobserve(entry.target); } }); }); lazyElements.forEach(el => contentObserver.observe(el)); } }; // Performance Module RR.performance = { init: function() { this.monitorPageLoad(); this.setupLinkPrefetching(); }, monitorPageLoad: function() { window.addEventListener('load', function() { if ('performance' in window) { const loadTime = Math.round(performance.now()); console.log(`Page loaded in ${loadTime}ms`); // Warn if load time is slow if (loadTime > 3000) { console.warn('Page load time is above 3 seconds. Consider optimization.'); } // Send to analytics if available if (typeof gtag !== 'undefined') { gtag('event', 'timing_complete', { name: 'load', value: loadTime }); } } }); }, setupLinkPrefetching: function() { // Prefetch important pages on hover const importantLinks = document.querySelectorAll('.nav-link, .btn-primary'); importantLinks.forEach(link => { link.addEventListener('mouseenter', function() { const url = this.href; if (url && url.startsWith(window.location.origin)) { RR.performance.prefetchPage(url); } }); }); }, prefetchPage: function(url) { // Check if already prefetched if (document.querySelector(`link[href="${url}"]`)) return; const link = document.createElement('link'); link.rel = 'prefetch'; link.href = url; document.head.appendChild(link); } }; // Utility Functions RR.utils = { // Debounce function debounce: function(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; }, // Throttle function throttle: function(func, limit) { let inThrottle; return function() { const args = arguments; const context = this; if (!inThrottle) { func.apply(context, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; }, // Format numbers with commas formatNumber: function(num) { return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); }, // Format currency formatCurrency: function(amount, currency = 'USD') { return new Intl.NumberFormat('en-US', { style: 'currency', currency: currency }).format(amount); }, // Format date formatDate: function(date, options = {}) { const defaultOptions = { year: 'numeric', month: 'short', day: 'numeric' }; return new Date(date).toLocaleDateString('en-US', { ...defaultOptions, ...options }); }, // Get relative time getRelativeTime: function(date) { const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' }); const now = new Date(); const targetDate = new Date(date); const diffInMs = targetDate - now; const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24)); if (Math.abs(diffInDays) < 1) { const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60)); return rtf.format(diffInHours, 'hour'); } else { return rtf.format(diffInDays, 'day'); } }, // API helper api: async function(url, options = {}) { const defaultOptions = { headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }; try { const response = await fetch(RR.config.apiUrl + url, { ...defaultOptions, ...options, headers: { ...defaultOptions.headers, ...options.headers } }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); return data; } catch (error) { console.error('API call failed:', error); RR.toast.show('An error occurred. Please try again.', 'error'); throw error; } }, // Local storage helpers (with fallbacks) storage: { get: function(key) { try { return JSON.parse(localStorage.getItem(key)); } catch { return null; } }, set: function(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); return true; } catch { return false; } }, remove: function(key) { try { localStorage.removeItem(key); return true; } catch { return false; } } } }; // Expose utilities globally window.utils = RR.utils; window.showToast = RR.toast.show; })();