538 lines
16 KiB
JavaScript

/**
* Utility functions for the Discord Bot Dashboard
*/
// Toast notification system
const Toast = {
container: null,
/**
* Initialize the toast container
*/
init() {
// Create toast container if it doesn't exist
if (!this.container) {
this.container = document.createElement('div');
this.container.className = 'toast-container';
document.body.appendChild(this.container);
}
},
/**
* Show a toast notification
* @param {string} message - The message to display
* @param {string} type - The type of toast (success, error, warning, info)
* @param {string} title - Optional title for the toast
* @param {number} duration - Duration in milliseconds before auto-hiding
*/
show(message, type = 'info', title = '', duration = 5000) {
this.init();
// Create toast element
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
// Create icon based on type
let iconSvg = '';
switch (type) {
case 'success':
iconSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#48BB78" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>';
break;
case 'error':
iconSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#F56565" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>';
break;
case 'warning':
iconSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#F6AD55" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>';
break;
default: // info
iconSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#5865F2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>';
}
// Set toast content
toast.innerHTML = `
<div class="toast-icon">${iconSvg}</div>
<div class="toast-content">
${title ? `<div class="toast-title">${title}</div>` : ''}
<div class="toast-message">${message}</div>
</div>
<button class="toast-close">&times;</button>
`;
// Add to container
this.container.appendChild(toast);
// Add close event
const closeBtn = toast.querySelector('.toast-close');
closeBtn.addEventListener('click', () => this.hide(toast));
// Auto-hide after duration
if (duration) {
setTimeout(() => this.hide(toast), duration);
}
return toast;
},
/**
* Hide a toast notification
* @param {HTMLElement} toast - The toast element to hide
*/
hide(toast) {
toast.classList.add('toast-hiding');
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300); // Match animation duration
},
/**
* Show a success toast
* @param {string} message - The message to display
* @param {string} title - Optional title
*/
success(message, title = 'Success') {
return this.show(message, 'success', title);
},
/**
* Show an error toast
* @param {string} message - The message to display
* @param {string} title - Optional title
*/
error(message, title = 'Error') {
return this.show(message, 'error', title);
},
/**
* Show a warning toast
* @param {string} message - The message to display
* @param {string} title - Optional title
*/
warning(message, title = 'Warning') {
return this.show(message, 'warning', title);
},
/**
* Show an info toast
* @param {string} message - The message to display
* @param {string} title - Optional title
*/
info(message, title = 'Info') {
return this.show(message, 'info', title);
}
};
// API utilities
const API = {
/**
* Make an API request with loading state
* @param {string} url - The API endpoint URL
* @param {Object} options - Fetch options
* @param {HTMLElement} loadingElement - Element to show loading state on
* @returns {Promise} - The fetch promise
*/
async request(url, options = {}, loadingElement = null) {
// Set default headers
options.headers = options.headers || {};
options.headers['Content-Type'] = options.headers['Content-Type'] || 'application/json';
// Add loading state
if (loadingElement) {
if (loadingElement.tagName === 'BUTTON') {
loadingElement.disabled = true;
loadingElement.classList.add('btn-loading');
} else {
// Create or use existing loading overlay
let overlay = loadingElement.querySelector('.loading-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.className = 'loading-overlay';
overlay.innerHTML = '<div class="loading-spinner"></div>';
// Make sure the element has position relative for absolute positioning
const computedStyle = window.getComputedStyle(loadingElement);
if (computedStyle.position === 'static') {
loadingElement.style.position = 'relative';
}
loadingElement.appendChild(overlay);
} else {
overlay.style.display = 'flex';
}
}
}
try {
const response = await fetch(url, options);
// Parse JSON response
let data;
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
data = await response.json();
} else {
data = await response.text();
}
// Remove loading state
if (loadingElement) {
if (loadingElement.tagName === 'BUTTON') {
loadingElement.disabled = false;
loadingElement.classList.remove('btn-loading');
} else {
const overlay = loadingElement.querySelector('.loading-overlay');
if (overlay) {
overlay.style.display = 'none';
}
}
}
// Handle error responses
if (!response.ok) {
const error = new Error(data.message || data.error || 'API request failed');
error.status = response.status;
error.data = data;
throw error;
}
return data;
} catch (error) {
// Remove loading state
if (loadingElement) {
if (loadingElement.tagName === 'BUTTON') {
loadingElement.disabled = false;
loadingElement.classList.remove('btn-loading');
} else {
const overlay = loadingElement.querySelector('.loading-overlay');
if (overlay) {
overlay.style.display = 'none';
}
}
}
// Show error toast
Toast.error(error.message || 'An error occurred');
throw error;
}
},
/**
* Make a GET request
* @param {string} url - The API endpoint URL
* @param {HTMLElement} loadingElement - Element to show loading state on
* @returns {Promise} - The fetch promise
*/
async get(url, loadingElement = null) {
return this.request(url, { method: 'GET' }, loadingElement);
},
/**
* Make a POST request
* @param {string} url - The API endpoint URL
* @param {Object} data - The data to send
* @param {HTMLElement} loadingElement - Element to show loading state on
* @returns {Promise} - The fetch promise
*/
async post(url, data, loadingElement = null) {
return this.request(
url,
{
method: 'POST',
body: JSON.stringify(data)
},
loadingElement
);
},
/**
* Make a PUT request
* @param {string} url - The API endpoint URL
* @param {Object} data - The data to send
* @param {HTMLElement} loadingElement - Element to show loading state on
* @returns {Promise} - The fetch promise
*/
async put(url, data, loadingElement = null) {
return this.request(
url,
{
method: 'PUT',
body: JSON.stringify(data)
},
loadingElement
);
},
/**
* Make a DELETE request
* @param {string} url - The API endpoint URL
* @param {HTMLElement} loadingElement - Element to show loading state on
* @returns {Promise} - The fetch promise
*/
async delete(url, loadingElement = null) {
return this.request(url, { method: 'DELETE' }, loadingElement);
}
};
// Modal utilities
const Modal = {
/**
* Open a modal
* @param {string} modalId - The ID of the modal to open
*/
open(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.add('active');
document.body.style.overflow = 'hidden'; // Prevent scrolling
}
},
/**
* Close a modal
* @param {string} modalId - The ID of the modal to close
*/
close(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.remove('active');
document.body.style.overflow = ''; // Restore scrolling
}
},
/**
* Initialize modal close buttons
*/
init() {
// Close modal when clicking the close button
document.querySelectorAll('.modal-close').forEach(button => {
button.addEventListener('click', () => {
const modal = button.closest('.modal');
if (modal) {
modal.classList.remove('active');
document.body.style.overflow = ''; // Restore scrolling
}
});
});
// Close modal when clicking outside the modal content
document.querySelectorAll('.modal').forEach(modal => {
modal.addEventListener('click', (event) => {
if (event.target === modal) {
modal.classList.remove('active');
document.body.style.overflow = ''; // Restore scrolling
}
});
});
}
};
// Form utilities
const Form = {
/**
* Serialize form data to an object
* @param {HTMLFormElement} form - The form element
* @returns {Object} - The serialized form data
*/
serialize(form) {
const formData = new FormData(form);
const data = {};
for (const [key, value] of formData.entries()) {
// Handle checkboxes
if (form.elements[key].type === 'checkbox') {
data[key] = value === 'on';
} else {
data[key] = value;
}
}
return data;
},
/**
* Populate a form with data
* @param {HTMLFormElement} form - The form element
* @param {Object} data - The data to populate the form with
*/
populate(form, data) {
for (const key in data) {
if (form.elements[key]) {
const element = form.elements[key];
if (element.type === 'checkbox') {
element.checked = Boolean(data[key]);
} else if (element.type === 'radio') {
const radio = form.querySelector(`input[name="${key}"][value="${data[key]}"]`);
if (radio) {
radio.checked = true;
}
} else {
element.value = data[key];
}
// Trigger change event for elements like select
const event = new Event('change');
element.dispatchEvent(event);
}
}
},
/**
* Validate a form
* @param {HTMLFormElement} form - The form element
* @returns {boolean} - Whether the form is valid
*/
validate(form) {
let isValid = true;
// Remove existing error messages
form.querySelectorAll('.form-error').forEach(error => {
error.remove();
});
// Check required fields
form.querySelectorAll('[required]').forEach(field => {
if (!field.value.trim()) {
isValid = false;
this.showError(field, 'This field is required');
}
});
// Check email fields
form.querySelectorAll('input[type="email"]').forEach(field => {
if (field.value && !this.isValidEmail(field.value)) {
isValid = false;
this.showError(field, 'Please enter a valid email address');
}
});
return isValid;
},
/**
* Show an error message for a form field
* @param {HTMLElement} field - The form field
* @param {string} message - The error message
*/
showError(field, message) {
// Create error message element
const error = document.createElement('div');
error.className = 'form-error';
error.textContent = message;
error.style.color = 'var(--danger-color)';
error.style.fontSize = '0.875rem';
error.style.marginTop = '0.25rem';
// Add error class to field
field.classList.add('is-invalid');
// Insert error after field
field.parentNode.insertBefore(error, field.nextSibling);
// Add event listener to remove error when field is changed
field.addEventListener('input', () => {
field.classList.remove('is-invalid');
if (error.parentNode) {
error.parentNode.removeChild(error);
}
}, { once: true });
},
/**
* Check if an email is valid
* @param {string} email - The email to check
* @returns {boolean} - Whether the email is valid
*/
isValidEmail(email) {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
}
};
// DOM utilities
const DOM = {
/**
* Create an element with attributes and children
* @param {string} tag - The tag name
* @param {Object} attrs - The attributes
* @param {Array|string} children - The children
* @returns {HTMLElement} - The created element
*/
createElement(tag, attrs = {}, children = []) {
const element = document.createElement(tag);
// Set attributes
for (const key in attrs) {
if (key === 'className') {
element.className = attrs[key];
} else if (key === 'style' && typeof attrs[key] === 'object') {
Object.assign(element.style, attrs[key]);
} else if (key.startsWith('on') && typeof attrs[key] === 'function') {
const eventName = key.substring(2).toLowerCase();
element.addEventListener(eventName, attrs[key]);
} else {
element.setAttribute(key, attrs[key]);
}
}
// Add children
if (Array.isArray(children)) {
children.forEach(child => {
if (typeof child === 'string') {
element.appendChild(document.createTextNode(child));
} else if (child instanceof Node) {
element.appendChild(child);
}
});
} else if (typeof children === 'string') {
element.textContent = children;
}
return element;
},
/**
* Create a loading spinner
* @param {string} size - The size of the spinner (sm, md, lg)
* @returns {HTMLElement} - The spinner element
*/
createSpinner(size = 'md') {
const spinner = document.createElement('div');
spinner.className = `loading-spinner loading-spinner-${size}`;
return spinner;
},
/**
* Show a loading spinner in a container
* @param {HTMLElement} container - The container element
* @param {string} size - The size of the spinner
* @returns {HTMLElement} - The spinner container element
*/
showSpinner(container, size = 'md') {
// Clear container
container.innerHTML = '';
// Create spinner container
const spinnerContainer = document.createElement('div');
spinnerContainer.className = 'loading-spinner-container';
// Create spinner
const spinner = this.createSpinner(size);
spinnerContainer.appendChild(spinner);
// Add to container
container.appendChild(spinnerContainer);
return spinnerContainer;
}
};
// Export utilities
window.Toast = Toast;
window.API = API;
window.Modal = Modal;
window.Form = Form;
window.DOM = DOM;