/** * 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(); }); // Member subscriptions modal document.getElementById('memberSubscriptionsModalClose').addEventListener('click', () => { this.closeModal(document.getElementById('memberSubscriptionsModal')); }); document.getElementById('memberSubscriptionsCancelBtn').addEventListener('click', () => { this.closeModal(document.getElementById('memberSubscriptionsModal')); }); document.getElementById('memberSubscriptionsSaveBtn').addEventListener('click', () => { this.handleMemberSubscriptionsSave(); }); // 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 = ''; memberSelect.innerHTML = ''; 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); } } /** * Show member subscriptions modal */ async showMemberSubscriptionsModal(member) { const modal = document.getElementById('memberSubscriptionsModal'); // Update member info document.getElementById('memberSubscriptionsName').textContent = member.name; document.getElementById('memberSubscriptionsEmail').textContent = member.email; document.getElementById('memberSubscriptionsTitle').textContent = `Manage Subscriptions - ${member.name}`; try { // Load all lists and member's current subscriptions const [lists, memberSubscriptions] = await Promise.all([ apiClient.getLists(), this.getMemberSubscriptions(member.member_id) ]); this.currentMemberForSubscriptions = member; this.renderSubscriptionCheckboxes(lists, memberSubscriptions); this.showModal(modal); } catch (error) { this.handleError(error, 'Failed to load subscription data'); } } /** * Get member's current subscriptions */ async getMemberSubscriptions(memberId) { const subscriptions = []; // Check each list to see if the member is subscribed for (const [listId, members] of window.app.subscriptions) { if (members.some(m => m.member_id === memberId)) { const list = window.app.lists.find(l => l.list_id === listId); if (list) { subscriptions.push(list); } } } return subscriptions; } /** * Render subscription checkboxes */ renderSubscriptionCheckboxes(lists, memberSubscriptions) { const container = document.getElementById('subscriptionCheckboxList'); container.innerHTML = ''; this.subscriptionChanges = new Map(); // Track changes lists.forEach(list => { const isSubscribed = memberSubscriptions.some(sub => sub.list_id === list.list_id); const item = document.createElement('div'); item.className = `subscription-item ${isSubscribed ? 'subscribed' : ''}`; item.dataset.listId = list.list_id; item.dataset.subscribed = isSubscribed.toString(); item.innerHTML = `
${this.escapeHtml(list.list_name)}
${this.escapeHtml(list.list_email)}
${isSubscribed ? 'Subscribed' : 'Not subscribed'}
`; // Add click handlers const toggleSwitch = item.querySelector('.toggle-switch'); const statusSpan = item.querySelector('.subscription-status'); const toggleSubscription = () => { const currentlySubscribed = item.dataset.subscribed === 'true'; const newSubscribed = !currentlySubscribed; // Update UI item.dataset.subscribed = newSubscribed.toString(); item.className = `subscription-item ${newSubscribed ? 'subscribed' : ''}`; toggleSwitch.className = `toggle-switch ${newSubscribed ? 'active' : ''}`; statusSpan.className = `subscription-status ${newSubscribed ? 'subscribed' : 'not-subscribed'}`; statusSpan.textContent = newSubscribed ? 'Subscribed' : 'Not subscribed'; // Track the change const originallySubscribed = memberSubscriptions.some(sub => sub.list_id === list.list_id); if (newSubscribed !== originallySubscribed) { this.subscriptionChanges.set(list.list_id, { list: list, action: newSubscribed ? 'subscribe' : 'unsubscribe' }); } else { this.subscriptionChanges.delete(list.list_id); } // Update save button state this.updateSubscriptionsSaveButton(); }; item.addEventListener('click', toggleSubscription); toggleSwitch.addEventListener('click', (e) => { e.stopPropagation(); toggleSubscription(); }); container.appendChild(item); }); this.updateSubscriptionsSaveButton(); } /** * Update save button state based on changes */ updateSubscriptionsSaveButton() { const saveBtn = document.getElementById('memberSubscriptionsSaveBtn'); const hasChanges = this.subscriptionChanges && this.subscriptionChanges.size > 0; saveBtn.disabled = !hasChanges; saveBtn.innerHTML = hasChanges ? ` Save Changes (${this.subscriptionChanges.size})` : ` No Changes`; } /** * Handle saving subscription changes */ async handleMemberSubscriptionsSave() { if (!this.subscriptionChanges || this.subscriptionChanges.size === 0) { return; } try { this.setLoading(true); const promises = []; for (const [listId, change] of this.subscriptionChanges) { if (change.action === 'subscribe') { promises.push( apiClient.createSubscription({ list_email: change.list.list_email, member_email: this.currentMemberForSubscriptions.email, active: true }) ); } else { promises.push( apiClient.deleteSubscription( change.list.list_email, this.currentMemberForSubscriptions.email ) ); } } await Promise.all(promises); this.showNotification(`Successfully updated ${this.subscriptionChanges.size} subscription(s)`, 'success'); this.closeModal(document.getElementById('memberSubscriptionsModal')); await window.app.loadData(); } catch (error) { this.handleError(error, 'Failed to save subscription changes'); } 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 = ` ${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 = ` ${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();