/** * UI Components Module * Reusable UI components and utilities */ class UIComponents { constructor() { this.loadingElement = document.getElementById('loading'); this.successMessageElement = document.getElementById('successMessage'); } /** * Show loading spinner * @param {string} message - Custom loading message */ showLoading(message = 'Finding challenges...') { if (this.loadingElement) { // Update loading message const loadingText = this.loadingElement.querySelector('p'); if (loadingText) { loadingText.textContent = message; } this.loadingElement.style.display = 'flex'; } } /** * Hide loading spinner */ hideLoading() { if (this.loadingElement) { this.loadingElement.style.display = 'none'; } } /** * Show success message * @param {string} message - Success message * @param {number} duration - Duration to show message (ms) */ showSuccessMessage(message, duration = 4000) { if (this.successMessageElement) { const messageSpan = this.successMessageElement.querySelector('span'); if (messageSpan) { messageSpan.textContent = message; } this.successMessageElement.style.display = 'flex'; // Auto-hide after duration setTimeout(() => { this.successMessageElement.style.display = 'none'; }, duration); } } /** * Show error message * @param {string} message - Error message * @param {number} duration - Duration to show message (ms) */ showError(message, duration = 3000) { // Create a temporary error message const errorDiv = document.createElement('div'); errorDiv.style.cssText = ` position: fixed; top: 2rem; left: 50%; transform: translateX(-50%); background: var(--danger-color); color: white; padding: 1rem 2rem; border-radius: var(--border-radius); box-shadow: var(--shadow-lg); z-index: 1000; animation: slideIn 0.3s ease-out; `; errorDiv.innerHTML = ` ${message}`; document.body.appendChild(errorDiv); setTimeout(() => { errorDiv.remove(); }, duration); } /** * Show info message * @param {string} message - Info message * @param {number} duration - Duration to show message (ms) */ showInfoMessage(message, duration = 3000) { const infoDiv = document.createElement('div'); infoDiv.style.cssText = ` position: fixed; top: 2rem; left: 50%; transform: translateX(-50%); background: var(--primary-color); color: white; padding: 1rem 2rem; border-radius: var(--border-radius); box-shadow: var(--shadow-lg); z-index: 1000; animation: slideIn 0.3s ease-out; `; infoDiv.innerHTML = ` ${message}`; document.body.appendChild(infoDiv); setTimeout(() => { infoDiv.remove(); }, duration); } /** * Create a modal * @param {string} title - Modal title * @param {string} content - Modal content (HTML) * @param {Array} buttons - Array of button objects {text, class, onclick} */ createModal(title, content, buttons = []) { const modal = document.createElement('div'); modal.className = 'modal'; modal.style.display = 'flex'; const modalContent = `
`; modal.innerHTML = modalContent; document.body.appendChild(modal); // Add event listeners const closeBtn = modal.querySelector('.close-btn'); closeBtn.addEventListener('click', () => this.closeModal(modal)); // Close on background click modal.addEventListener('click', (e) => { if (e.target === modal) { this.closeModal(modal); } }); // Add button event listeners buttons.forEach((btn, index) => { const buttonElement = modal.querySelectorAll('.modal-footer .btn')[index]; if (buttonElement && btn.onclick) { buttonElement.addEventListener('click', (e) => { btn.onclick(e, modal); }); } }); return modal; } /** * Close modal * @param {HTMLElement} modal - Modal element */ closeModal(modal) { modal.remove(); } /** * Validate form field * @param {HTMLElement} field - Form field element * @param {Object} rules - Validation rules */ validateField(field, rules) { const value = field.value.trim(); const errors = []; if (rules.required && !value) { errors.push(`${rules.name || 'Field'} is required`); } if (rules.email && value) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(value)) { errors.push('Please enter a valid email address'); } } if (rules.minLength && value.length < rules.minLength) { errors.push(`${rules.name || 'Field'} must be at least ${rules.minLength} characters`); } // Show/hide validation errors this.updateFieldValidation(field, errors); return errors.length === 0; } /** * Update field validation display * @param {HTMLElement} field - Form field element * @param {Array} errors - Array of error messages */ updateFieldValidation(field, errors) { // Remove existing error display const existingError = field.parentNode.querySelector('.field-error'); if (existingError) { existingError.remove(); } // Add/remove error class field.classList.toggle('error', errors.length > 0); // Add error message if needed if (errors.length > 0) { const errorDiv = document.createElement('div'); errorDiv.className = 'field-error'; errorDiv.style.cssText = ` color: var(--danger-color); font-size: 0.875rem; margin-top: 0.25rem; `; errorDiv.textContent = errors[0]; // Show first error field.parentNode.appendChild(errorDiv); } } /** * Add loading state to button * @param {HTMLElement} button - Button element * @param {string} loadingText - Text to show while loading */ addLoadingState(button, loadingText = 'Loading...') { const originalText = button.innerHTML; button.innerHTML = ` ${loadingText}`; button.disabled = true; return () => { button.innerHTML = originalText; button.disabled = false; }; } /** * Debounce function calls * @param {Function} func - Function to debounce * @param {number} wait - Wait time in ms */ debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } /** * Auto-resize textarea * @param {HTMLElement} textarea - Textarea element */ autoResizeTextarea(textarea) { textarea.style.height = 'auto'; textarea.style.height = textarea.scrollHeight + 'px'; } /** * Add interactive feedback to elements * @param {HTMLElement} element - Element to add feedback to */ addInteractiveFeedback(element) { element.addEventListener('click', function(e) { e.target.style.transform = 'scale(0.98)'; setTimeout(() => { e.target.style.transform = ''; }, 150); }); } /** * Format challenge data for display * @param {Object} challenge - Challenge object */ formatChallenge(challenge) { return { name: challenge.name || 'Unknown Challenge', description: challenge.description || 'No description available', prize: challenge.prize || '$0', url: challenge.url || null, track: challenge.track || 'General', deadline: challenge.deadline || null }; } }