462 lines
14 KiB
JavaScript
462 lines
14 KiB
JavaScript
/**
|
|
* UI Helper Functions and Components
|
|
* Handles DOM manipulation, notifications, modals, and UI state
|
|
*/
|
|
|
|
class UIManager {
|
|
constructor() {
|
|
this.currentTab = 'lists';
|
|
this.currentEditingItem = null;
|
|
this.isLoading = false;
|
|
this.confirmCallback = null;
|
|
|
|
this.initializeEventListeners();
|
|
}
|
|
|
|
/**
|
|
* Initialize all event listeners
|
|
*/
|
|
initializeEventListeners() {
|
|
// Tab navigation
|
|
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
btn.addEventListener('click', (e) => {
|
|
this.switchTab(e.target.dataset.tab);
|
|
});
|
|
});
|
|
|
|
// Modal close buttons
|
|
document.querySelectorAll('.modal-close, .modal .btn-secondary').forEach(btn => {
|
|
btn.addEventListener('click', (e) => {
|
|
this.closeModal(e.target.closest('.modal'));
|
|
});
|
|
});
|
|
|
|
// Click outside modal to close
|
|
document.querySelectorAll('.modal').forEach(modal => {
|
|
modal.addEventListener('click', (e) => {
|
|
if (e.target === modal) {
|
|
this.closeModal(modal);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Notification close
|
|
document.getElementById('notificationClose').addEventListener('click', () => {
|
|
this.hideNotification();
|
|
});
|
|
|
|
// Add buttons
|
|
document.getElementById('addListBtn').addEventListener('click', () => {
|
|
this.showListModal();
|
|
});
|
|
|
|
document.getElementById('addMemberBtn').addEventListener('click', () => {
|
|
this.showMemberModal();
|
|
});
|
|
|
|
document.getElementById('addSubscriptionBtn').addEventListener('click', () => {
|
|
this.showSubscriptionModal();
|
|
});
|
|
|
|
// Form submissions
|
|
document.getElementById('listForm').addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
this.handleListFormSubmit();
|
|
});
|
|
|
|
document.getElementById('memberForm').addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
this.handleMemberFormSubmit();
|
|
});
|
|
|
|
document.getElementById('subscriptionForm').addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
this.handleSubscriptionFormSubmit();
|
|
});
|
|
|
|
// Confirmation modal
|
|
document.getElementById('confirmOkBtn').addEventListener('click', () => {
|
|
if (this.confirmCallback) {
|
|
this.confirmCallback();
|
|
this.confirmCallback = null;
|
|
}
|
|
this.closeModal(document.getElementById('confirmModal'));
|
|
});
|
|
|
|
document.getElementById('confirmCancelBtn').addEventListener('click', () => {
|
|
this.confirmCallback = null;
|
|
this.closeModal(document.getElementById('confirmModal'));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Show/hide loading overlay
|
|
*/
|
|
setLoading(loading) {
|
|
this.isLoading = loading;
|
|
const overlay = document.getElementById('loadingOverlay');
|
|
if (loading) {
|
|
overlay.style.display = 'flex';
|
|
} else {
|
|
overlay.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show notification
|
|
*/
|
|
showNotification(message, type = 'info') {
|
|
const notification = document.getElementById('notification');
|
|
const messageEl = document.getElementById('notificationMessage');
|
|
|
|
notification.className = `notification ${type}`;
|
|
messageEl.textContent = message;
|
|
notification.style.display = 'flex';
|
|
|
|
// Auto-hide after 5 seconds
|
|
setTimeout(() => {
|
|
this.hideNotification();
|
|
}, 5000);
|
|
}
|
|
|
|
/**
|
|
* Hide notification
|
|
*/
|
|
hideNotification() {
|
|
document.getElementById('notification').style.display = 'none';
|
|
}
|
|
|
|
/**
|
|
* Show confirmation dialog
|
|
*/
|
|
showConfirmation(message, callback) {
|
|
document.getElementById('confirmMessage').textContent = message;
|
|
this.confirmCallback = callback;
|
|
this.showModal(document.getElementById('confirmModal'));
|
|
}
|
|
|
|
/**
|
|
* Switch between tabs
|
|
*/
|
|
switchTab(tabName) {
|
|
this.currentTab = tabName;
|
|
|
|
// Update tab buttons
|
|
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
btn.classList.toggle('active', btn.dataset.tab === tabName);
|
|
});
|
|
|
|
// Update tab content
|
|
document.querySelectorAll('.tab-content').forEach(content => {
|
|
content.classList.toggle('active', content.id === `${tabName}-tab`);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Show modal
|
|
*/
|
|
showModal(modal) {
|
|
modal.classList.add('active');
|
|
// Focus first input
|
|
const firstInput = modal.querySelector('input, select, textarea');
|
|
if (firstInput) {
|
|
setTimeout(() => firstInput.focus(), 100);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Close modal
|
|
*/
|
|
closeModal(modal) {
|
|
modal.classList.remove('active');
|
|
this.currentEditingItem = null;
|
|
|
|
// Reset forms
|
|
const form = modal.querySelector('form');
|
|
if (form) {
|
|
form.reset();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show list modal (add/edit)
|
|
*/
|
|
showListModal(listData = null) {
|
|
const modal = document.getElementById('listModal');
|
|
const title = document.getElementById('listModalTitle');
|
|
const form = document.getElementById('listForm');
|
|
|
|
if (listData) {
|
|
// Edit mode
|
|
title.textContent = 'Edit Mailing List';
|
|
document.getElementById('listName').value = listData.list_name;
|
|
document.getElementById('listEmail').value = listData.list_email;
|
|
document.getElementById('listDescription').value = listData.description || '';
|
|
document.getElementById('listActive').checked = listData.active;
|
|
this.currentEditingItem = listData;
|
|
} else {
|
|
// Add mode
|
|
title.textContent = 'Add Mailing List';
|
|
form.reset();
|
|
document.getElementById('listActive').checked = true;
|
|
this.currentEditingItem = null;
|
|
}
|
|
|
|
this.showModal(modal);
|
|
}
|
|
|
|
/**
|
|
* Show member modal (add/edit)
|
|
*/
|
|
showMemberModal(memberData = null) {
|
|
const modal = document.getElementById('memberModal');
|
|
const title = document.getElementById('memberModalTitle');
|
|
const form = document.getElementById('memberForm');
|
|
|
|
if (memberData) {
|
|
// Edit mode
|
|
title.textContent = 'Edit Member';
|
|
document.getElementById('memberName').value = memberData.name;
|
|
document.getElementById('memberEmail').value = memberData.email;
|
|
document.getElementById('memberActive').checked = memberData.active;
|
|
this.currentEditingItem = memberData;
|
|
} else {
|
|
// Add mode
|
|
title.textContent = 'Add Member';
|
|
form.reset();
|
|
document.getElementById('memberActive').checked = true;
|
|
this.currentEditingItem = null;
|
|
}
|
|
|
|
this.showModal(modal);
|
|
}
|
|
|
|
/**
|
|
* Show subscription modal
|
|
*/
|
|
async showSubscriptionModal() {
|
|
const modal = document.getElementById('subscriptionModal');
|
|
|
|
try {
|
|
// Populate dropdowns
|
|
await this.populateSubscriptionDropdowns();
|
|
this.showModal(modal);
|
|
} catch (error) {
|
|
this.showNotification('Failed to load subscription data', 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Populate subscription modal dropdowns
|
|
*/
|
|
async populateSubscriptionDropdowns() {
|
|
const listSelect = document.getElementById('subscriptionList');
|
|
const memberSelect = document.getElementById('subscriptionMember');
|
|
|
|
// Clear existing options
|
|
listSelect.innerHTML = '<option value="">Select a list...</option>';
|
|
memberSelect.innerHTML = '<option value="">Select a member...</option>';
|
|
|
|
try {
|
|
const [lists, members] = await Promise.all([
|
|
apiClient.getLists(),
|
|
apiClient.getMembers()
|
|
]);
|
|
|
|
// Populate lists
|
|
lists.forEach(list => {
|
|
if (list.active) {
|
|
const option = document.createElement('option');
|
|
option.value = list.list_email;
|
|
option.textContent = `${list.list_name} (${list.list_email})`;
|
|
listSelect.appendChild(option);
|
|
}
|
|
});
|
|
|
|
// Populate members
|
|
members.forEach(member => {
|
|
if (member.active) {
|
|
const option = document.createElement('option');
|
|
option.value = member.email;
|
|
option.textContent = `${member.name} (${member.email})`;
|
|
memberSelect.appendChild(option);
|
|
}
|
|
});
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle list form submission
|
|
*/
|
|
async handleListFormSubmit() {
|
|
const form = document.getElementById('listForm');
|
|
const formData = new FormData(form);
|
|
|
|
const listData = {
|
|
list_name: formData.get('listName'),
|
|
list_email: formData.get('listEmail'),
|
|
description: formData.get('listDescription') || null,
|
|
active: formData.get('listActive') === 'on'
|
|
};
|
|
|
|
try {
|
|
this.setLoading(true);
|
|
|
|
if (this.currentEditingItem) {
|
|
// Update existing list
|
|
await apiClient.updateList(this.currentEditingItem.list_id, listData);
|
|
this.showNotification('Mailing list updated successfully', 'success');
|
|
} else {
|
|
// Create new list
|
|
await apiClient.createList(listData);
|
|
this.showNotification('Mailing list created successfully', 'success');
|
|
}
|
|
|
|
this.closeModal(document.getElementById('listModal'));
|
|
await window.app.loadData();
|
|
} catch (error) {
|
|
this.handleError(error, 'Failed to save mailing list');
|
|
} finally {
|
|
this.setLoading(false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle member form submission
|
|
*/
|
|
async handleMemberFormSubmit() {
|
|
const form = document.getElementById('memberForm');
|
|
const formData = new FormData(form);
|
|
|
|
const memberData = {
|
|
name: formData.get('memberName'),
|
|
email: formData.get('memberEmail'),
|
|
active: formData.get('memberActive') === 'on'
|
|
};
|
|
|
|
try {
|
|
this.setLoading(true);
|
|
|
|
if (this.currentEditingItem) {
|
|
// Update existing member
|
|
await apiClient.updateMember(this.currentEditingItem.member_id, memberData);
|
|
this.showNotification('Member updated successfully', 'success');
|
|
} else {
|
|
// Create new member
|
|
await apiClient.createMember(memberData);
|
|
this.showNotification('Member created successfully', 'success');
|
|
}
|
|
|
|
this.closeModal(document.getElementById('memberModal'));
|
|
await window.app.loadData();
|
|
} catch (error) {
|
|
this.handleError(error, 'Failed to save member');
|
|
} finally {
|
|
this.setLoading(false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle subscription form submission
|
|
*/
|
|
async handleSubscriptionFormSubmit() {
|
|
const form = document.getElementById('subscriptionForm');
|
|
const formData = new FormData(form);
|
|
|
|
const subscriptionData = {
|
|
list_email: formData.get('subscriptionList'),
|
|
member_email: formData.get('subscriptionMember'),
|
|
active: true
|
|
};
|
|
|
|
try {
|
|
this.setLoading(true);
|
|
|
|
await apiClient.createSubscription(subscriptionData);
|
|
this.showNotification('Subscription created successfully', 'success');
|
|
|
|
this.closeModal(document.getElementById('subscriptionModal'));
|
|
await window.app.loadData();
|
|
} catch (error) {
|
|
this.handleError(error, 'Failed to create subscription');
|
|
} finally {
|
|
this.setLoading(false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle API errors
|
|
*/
|
|
handleError(error, defaultMessage = 'An error occurred') {
|
|
let message = defaultMessage;
|
|
|
|
if (error instanceof APIError) {
|
|
if (error.isAuthError()) {
|
|
message = 'Authentication failed. Please check your API token.';
|
|
window.app.logout();
|
|
} else if (error.isBadRequest()) {
|
|
message = error.message || 'Invalid request data';
|
|
} else if (error.isNotFound()) {
|
|
message = 'Resource not found';
|
|
} else if (error.isServerError()) {
|
|
message = 'Server error. Please try again later.';
|
|
} else {
|
|
message = error.message || defaultMessage;
|
|
}
|
|
} else {
|
|
message = error.message || defaultMessage;
|
|
}
|
|
|
|
this.showNotification(message, 'error');
|
|
console.error('Error:', error);
|
|
}
|
|
|
|
/**
|
|
* Create action button
|
|
*/
|
|
createActionButton(text, icon, className, onClick) {
|
|
const button = document.createElement('button');
|
|
button.className = `btn btn-sm ${className}`;
|
|
button.innerHTML = `<i class="fas fa-${icon}"></i> ${text}`;
|
|
button.addEventListener('click', onClick);
|
|
return button;
|
|
}
|
|
|
|
/**
|
|
* Create status badge
|
|
*/
|
|
createStatusBadge(active) {
|
|
const badge = document.createElement('span');
|
|
badge.className = `status-badge ${active ? 'active' : 'inactive'}`;
|
|
badge.innerHTML = `
|
|
<i class="fas fa-${active ? 'check' : 'times'}"></i>
|
|
${active ? 'Active' : 'Inactive'}
|
|
`;
|
|
return badge;
|
|
}
|
|
|
|
/**
|
|
* Format email as mailto link
|
|
*/
|
|
createEmailLink(email) {
|
|
const link = document.createElement('a');
|
|
link.href = `mailto:${email}`;
|
|
link.textContent = email;
|
|
link.style.color = 'var(--primary-color)';
|
|
return link;
|
|
}
|
|
|
|
/**
|
|
* Escape HTML to prevent XSS
|
|
*/
|
|
escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
}
|
|
|
|
// Create global UI manager instance
|
|
window.uiManager = new UIManager(); |