| |
| |
| |
| |
|
|
| class UIComponents { |
| constructor() { |
| this.loadingElement = document.getElementById('loading'); |
| this.successMessageElement = document.getElementById('successMessage'); |
| } |
|
|
| |
| |
| |
| |
| showLoading(message = 'Finding challenges...') { |
| if (this.loadingElement) { |
| |
| const loadingText = this.loadingElement.querySelector('p'); |
| if (loadingText) { |
| loadingText.textContent = message; |
| } |
| |
| this.loadingElement.style.display = 'flex'; |
| } |
| } |
|
|
| |
| |
| |
| hideLoading() { |
| if (this.loadingElement) { |
| this.loadingElement.style.display = 'none'; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| showSuccessMessage(message, duration = 4000) { |
| if (this.successMessageElement) { |
| const messageSpan = this.successMessageElement.querySelector('span'); |
| if (messageSpan) { |
| messageSpan.textContent = message; |
| } |
| |
| this.successMessageElement.style.display = 'flex'; |
| |
| |
| setTimeout(() => { |
| this.successMessageElement.style.display = 'none'; |
| }, duration); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| showError(message, duration = 3000) { |
| |
| 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 = `<i class="fas fa-exclamation-triangle"></i> ${message}`; |
| |
| document.body.appendChild(errorDiv); |
| |
| setTimeout(() => { |
| errorDiv.remove(); |
| }, duration); |
| } |
|
|
| |
| |
| |
| |
| |
| 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 = `<i class="fas fa-info-circle"></i> ${message}`; |
| |
| document.body.appendChild(infoDiv); |
| |
| setTimeout(() => { |
| infoDiv.remove(); |
| }, duration); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| createModal(title, content, buttons = []) { |
| const modal = document.createElement('div'); |
| modal.className = 'modal'; |
| modal.style.display = 'flex'; |
| |
| const modalContent = ` |
| <div class="modal-content"> |
| <div class="modal-header"> |
| <h3><i class="fas fa-info-circle"></i> ${title}</h3> |
| <button class="close-btn">×</button> |
| </div> |
| <div class="modal-body"> |
| ${content} |
| </div> |
| <div class="modal-footer"> |
| ${buttons.map(btn => |
| `<button class="btn ${btn.class || 'btn-secondary'}" data-action="${btn.action || ''}">${btn.text}</button>` |
| ).join('')} |
| </div> |
| </div> |
| `; |
| |
| modal.innerHTML = modalContent; |
| document.body.appendChild(modal); |
| |
| |
| const closeBtn = modal.querySelector('.close-btn'); |
| closeBtn.addEventListener('click', () => this.closeModal(modal)); |
| |
| |
| modal.addEventListener('click', (e) => { |
| if (e.target === modal) { |
| this.closeModal(modal); |
| } |
| }); |
| |
| |
| 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; |
| } |
|
|
| |
| |
| |
| |
| closeModal(modal) { |
| modal.remove(); |
| } |
|
|
| |
| |
| |
| |
| |
| 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`); |
| } |
| |
| |
| this.updateFieldValidation(field, errors); |
| |
| return errors.length === 0; |
| } |
|
|
| |
| |
| |
| |
| |
| updateFieldValidation(field, errors) { |
| |
| const existingError = field.parentNode.querySelector('.field-error'); |
| if (existingError) { |
| existingError.remove(); |
| } |
| |
| |
| field.classList.toggle('error', errors.length > 0); |
| |
| |
| 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]; |
| field.parentNode.appendChild(errorDiv); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| addLoadingState(button, loadingText = 'Loading...') { |
| const originalText = button.innerHTML; |
| button.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${loadingText}`; |
| button.disabled = true; |
| |
| return () => { |
| button.innerHTML = originalText; |
| button.disabled = false; |
| }; |
| } |
|
|
| |
| |
| |
| |
| |
| debounce(func, wait) { |
| let timeout; |
| return function executedFunction(...args) { |
| const later = () => { |
| clearTimeout(timeout); |
| func(...args); |
| }; |
| clearTimeout(timeout); |
| timeout = setTimeout(later, wait); |
| }; |
| } |
|
|
| |
| |
| |
| |
| autoResizeTextarea(textarea) { |
| textarea.style.height = 'auto'; |
| textarea.style.height = textarea.scrollHeight + 'px'; |
| } |
|
|
| |
| |
| |
| |
| addInteractiveFeedback(element) { |
| element.addEventListener('click', function(e) { |
| e.target.style.transform = 'scale(0.98)'; |
| setTimeout(() => { |
| e.target.style.transform = ''; |
| }, 150); |
| }); |
| } |
|
|
| |
| |
| |
| |
| 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 |
| }; |
| } |
| } |
|
|