tc-agent / static /js /modules /ui-components.js
togitoon's picture
Initial
bf5f290
/**
* 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 = `<i class="fas fa-exclamation-triangle"></i> ${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 = `<i class="fas fa-info-circle"></i> ${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 = `
<div class="modal-content">
<div class="modal-header">
<h3><i class="fas fa-info-circle"></i> ${title}</h3>
<button class="close-btn">&times;</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);
// 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 = `<i class="fas fa-spinner fa-spin"></i> ${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
};
}
}