// Email Composer Module class EmailComposer { constructor() { this.modal = document.getElementById('email-modal'); this.previewModal = document.getElementById('email-preview-modal'); this.form = document.getElementById('email-form'); this.closeBtn = document.getElementById('close-modal'); this.closePreviewBtn = document.getElementById('close-preview-modal'); this.cancelBtn = document.getElementById('cancel-email'); this.cancelPreviewBtn = document.getElementById('cancel-preview'); this.editBtn = document.getElementById('edit-email'); this.confirmSendBtn = document.getElementById('confirm-send'); this.messageTextarea = document.getElementById('email-message'); this.charCounter = document.querySelector('.char-counter'); this.currentRecipient = null; this.currentEmailData = null; this.lastPreviewTime = 0; // Track last preview request time this.currentInlineComposer = null; // Track currently open inline composer this.currentRepCard = null; // Track which rep card has the composer open this.init(); } init() { // Modal controls this.closeBtn.addEventListener('click', () => this.closeModal()); this.closePreviewBtn.addEventListener('click', () => this.closePreviewModal()); this.cancelBtn.addEventListener('click', () => this.closeModal()); this.cancelPreviewBtn.addEventListener('click', () => this.closePreviewModal()); this.editBtn.addEventListener('click', () => this.editEmail()); this.confirmSendBtn.addEventListener('click', () => this.confirmSend()); // Click outside modal to close this.modal.addEventListener('click', (e) => { if (e.target === this.modal) this.closeModal(); }); this.previewModal.addEventListener('click', (e) => { if (e.target === this.previewModal) this.closePreviewModal(); }); // Form handling - now shows preview instead of sending directly this.form.addEventListener('submit', (e) => this.handlePreview(e)); // Character counter this.messageTextarea.addEventListener('input', () => this.updateCharCounter()); // Add event listener to sender name field to update subject dynamically const senderNameField = document.getElementById('sender-name'); const subjectField = document.getElementById('email-subject'); const postalCodeField = document.getElementById('sender-postal-code'); senderNameField.addEventListener('input', () => { const senderName = senderNameField.value.trim() || 'your constituent'; const postalCode = postalCodeField.value.trim(); if (postalCode) { subjectField.value = `Message from ${senderName} from ${postalCode}`; } }); // Escape key to close modals document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { if (this.previewModal.style.display === 'block') { this.closePreviewModal(); } else if (this.modal.style.display === 'block') { this.closeModal(); } } }); } openModal(recipient, repCard = null) { // Close any existing inline composers first this.closeInlineComposer(); this.currentRecipient = recipient; this.currentRepCard = repCard; // If repCard is provided, create inline composer if (repCard) { this.createInlineComposer(repCard, recipient); return; } // Otherwise, use modal (fallback) this.openModalDialog(recipient); } createInlineComposer(repCard, recipient) { // Explicitly hide old modals if (this.modal) this.modal.style.display = 'none'; if (this.previewModal) this.previewModal.style.display = 'none'; // Create the inline composer HTML const composerHTML = this.getComposerHTML(recipient); // Create a container div const composerDiv = document.createElement('div'); composerDiv.className = 'inline-email-composer'; composerDiv.innerHTML = composerHTML; // Find the parent category container (not the grid) // The structure is: rep-category > rep-cards (grid) > rep-card const repCardsGrid = repCard.parentElement; // The grid container const repCategory = repCardsGrid.parentElement; // The category container // Insert after the grid, not inside it repCategory.insertAdjacentElement('beforeend', composerDiv); this.currentInlineComposer = composerDiv; // Trigger animation after a small delay setTimeout(() => { composerDiv.classList.add('active'); }, 10); // Attach event listeners to the inline form this.attachInlineFormListeners(composerDiv); // Focus on first input setTimeout(() => { const firstInput = composerDiv.querySelector('#inline-sender-name'); if (firstInput) firstInput.focus(); }, 400); // Scroll the composer into view setTimeout(() => { composerDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }, 100); } getComposerHTML(recipient) { const postalCode = window.postalLookup ? window.postalLookup.currentPostalCode : ''; const defaultSubject = `Message from your constituent from ${postalCode}`; return `

Compose Email to ${recipient.name}

${recipient.name}
${recipient.office}
${recipient.district}
${recipient.email}
5000 characters remaining
`; } attachInlineFormListeners(composerDiv) { // Close button const closeBtn = composerDiv.querySelector('.inline-close'); closeBtn.addEventListener('click', () => this.closeInlineComposer()); // Cancel button const cancelBtn = composerDiv.querySelector('.inline-cancel'); cancelBtn.addEventListener('click', () => this.closeInlineComposer()); // Form submission const form = composerDiv.querySelector('.inline-email-form'); form.addEventListener('submit', (e) => this.handleInlinePreview(e, composerDiv)); // Character counter const messageTextarea = composerDiv.querySelector('#inline-email-message'); const charCounter = composerDiv.querySelector('.inline-char-counter'); messageTextarea.addEventListener('input', () => { const maxLength = 5000; const currentLength = messageTextarea.value.length; const remaining = maxLength - currentLength; charCounter.textContent = `${remaining} characters remaining`; if (remaining < 100) { charCounter.style.color = '#d9534f'; } else { charCounter.style.color = '#666'; } }); // Update subject with sender name const senderNameField = composerDiv.querySelector('#inline-sender-name'); const subjectField = composerDiv.querySelector('#inline-email-subject'); const postalCodeField = composerDiv.querySelector('#inline-sender-postal-code'); senderNameField.addEventListener('input', () => { const senderName = senderNameField.value.trim(); const postalCode = postalCodeField.value.trim(); if (senderName && postalCode) { subjectField.value = `Message from ${senderName} - ${postalCode}`; } }); } closeInlineComposer() { if (this.currentInlineComposer) { // Add closing class for animation this.currentInlineComposer.classList.remove('active'); this.currentInlineComposer.classList.add('closing'); // Remove from DOM after animation setTimeout(() => { if (this.currentInlineComposer && this.currentInlineComposer.parentNode) { this.currentInlineComposer.parentNode.removeChild(this.currentInlineComposer); } this.currentInlineComposer = null; this.currentRepCard = null; }, 400); } } async handleInlinePreview(e, composerDiv) { e.preventDefault(); // Prevent duplicate calls const currentTime = Date.now(); if (currentTime - this.lastPreviewTime < 2000) { return; } this.lastPreviewTime = currentTime; // Get form data from inline form const formData = this.getInlineFormData(composerDiv); const errors = this.validateInlineForm(formData); if (errors.length > 0) { window.messageDisplay.show(errors.join('
'), 'error'); return; } const submitButton = composerDiv.querySelector('button[type="submit"]'); const originalText = submitButton.textContent; try { submitButton.disabled = true; submitButton.textContent = 'Loading preview...'; // Store form data for later use - match API expected field names this.currentEmailData = { recipientEmail: formData.recipientEmail, senderName: formData.senderName, senderEmail: formData.senderEmail, postalCode: formData.postalCode, // API expects 'postalCode' not 'senderPostalCode' subject: formData.subject, message: formData.message }; // Get preview from backend const response = await window.apiClient.previewEmail(this.currentEmailData); if (response.success) { // Replace inline composer with inline preview this.showInlinePreview(response.preview, composerDiv); } else { window.messageDisplay.show(response.error || 'Failed to generate preview', 'error'); } } catch (error) { console.error('Preview error:', error); window.messageDisplay.show('Failed to generate email preview. Please try again.', 'error'); } finally { submitButton.disabled = false; submitButton.textContent = originalText; } } getInlineFormData(composerDiv) { return { recipientEmail: composerDiv.querySelector('#inline-recipient-email').value.trim(), senderName: composerDiv.querySelector('#inline-sender-name').value.trim(), senderEmail: composerDiv.querySelector('#inline-sender-email').value.trim(), postalCode: composerDiv.querySelector('#inline-sender-postal-code').value.trim(), subject: composerDiv.querySelector('#inline-email-subject').value.trim(), message: composerDiv.querySelector('#inline-email-message').value.trim() }; } validateInlineForm(formData) { const errors = []; if (!formData.senderName) { errors.push('Please enter your name'); } if (!formData.senderEmail) { errors.push('Please enter your email'); } else if (!this.validateEmail(formData.senderEmail)) { errors.push('Please enter a valid email address'); } if (!formData.subject) { errors.push('Please enter a subject'); } if (!formData.message) { errors.push('Please enter a message'); } else if (formData.message.length < 10) { errors.push('Message must be at least 10 characters long'); } // Validate postal code format if provided (required by API) if (!formData.postalCode || formData.postalCode.trim() === '') { errors.push('Postal code is required'); } else { // Check postal code format: A1A 1A1 or A1A1A1 const postalCodePattern = /^[A-Za-z]\d[A-Za-z]\s?\d[A-Za-z]\d$/; if (!postalCodePattern.test(formData.postalCode)) { errors.push('Please enter a valid Canadian postal code (e.g., T5N 4B8)'); } } // Check for suspicious content if (this.containsSuspiciousContent(formData.message) || this.containsSuspiciousContent(formData.subject)) { errors.push('Message contains potentially malicious content'); } return errors; } showInlinePreview(preview, composerDiv) { // Ensure old preview modal is hidden if (this.previewModal) { this.previewModal.style.display = 'none'; } // Replace the composer content with preview content const previewHTML = this.getPreviewHTML(preview); // Fade out composer body const composerBody = composerDiv.querySelector('.composer-body'); composerBody.style.opacity = '0'; composerBody.style.transition = 'opacity 0.2s ease'; setTimeout(() => { composerBody.innerHTML = previewHTML; composerBody.style.opacity = '1'; // Attach event listeners to preview buttons this.attachPreviewListeners(composerDiv); // Update header const header = composerDiv.querySelector('.composer-header h3'); header.textContent = 'Email Preview'; // Scroll to make sure preview is visible setTimeout(() => { composerDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }, 100); }, 200); } getPreviewHTML(preview) { const html = preview.html || ''; const testModeWarning = preview.testMode ? `
TEST MODE: Email will be redirected to ${this.escapeHtml(preview.redirectTo)}
` : ''; // Sanitize the HTML to remove any `; }); // Wrap the sanitized content in a container div return `
${sanitized}
`; } attachPreviewListeners(composerDiv) { // Edit button - go back to composer const editBtn = composerDiv.querySelector('.preview-edit'); editBtn.addEventListener('click', () => { this.returnToComposer(composerDiv); }); // Send button - send the email const sendBtn = composerDiv.querySelector('.preview-send'); sendBtn.addEventListener('click', async () => { await this.sendFromInlinePreview(sendBtn, composerDiv); }); } returnToComposer(composerDiv) { // Restore the composer form with the current data const composerFormHTML = this.getComposerFormHTML(); const composerBody = composerDiv.querySelector('.composer-body'); composerBody.style.opacity = '0'; setTimeout(() => { composerBody.innerHTML = composerFormHTML; // Re-populate form fields with saved data if (this.currentEmailData) { composerDiv.querySelector('#inline-sender-name').value = this.currentEmailData.senderName || ''; composerDiv.querySelector('#inline-sender-email').value = this.currentEmailData.senderEmail || ''; composerDiv.querySelector('#inline-sender-postal-code').value = this.currentEmailData.postalCode || ''; composerDiv.querySelector('#inline-email-subject').value = this.currentEmailData.subject || ''; composerDiv.querySelector('#inline-email-message').value = this.currentEmailData.message || ''; // Update character counter const messageField = composerDiv.querySelector('#inline-email-message'); const charCounter = composerDiv.querySelector('.inline-char-counter'); const remaining = 5000 - messageField.value.length; charCounter.textContent = `${remaining} characters remaining`; } composerBody.style.opacity = '1'; // Re-attach form listeners this.attachInlineFormListeners(composerDiv); // Update header const header = composerDiv.querySelector('.composer-header h3'); header.textContent = `Compose Email to ${this.currentRecipient.name}`; }, 200); } getComposerFormHTML() { const recipient = this.currentRecipient; return `
${recipient.name}
${recipient.office}
${recipient.district}
${recipient.email}
5000 characters remaining
`; } async sendFromInlinePreview(sendBtn, composerDiv) { if (!this.currentEmailData) { window.messageDisplay.show('No email data to send', 'error'); return; } const originalText = sendBtn.textContent; try { sendBtn.disabled = true; sendBtn.textContent = 'Sending...'; const result = await window.apiClient.sendEmail(this.currentEmailData); if (result.success) { window.messageDisplay.show('Email sent successfully! Your representative will receive your message.', 'success', 7000); // Show campaign conversion prompt if user is not authenticated this.showCampaignConversionPrompt(this.currentEmailData); // Close the inline composer after successful send this.closeInlineComposer(); this.currentEmailData = null; this.currentRecipient = null; } else { throw new Error(result.message || 'Failed to send email'); } } catch (error) { console.error('Email send failed:', error); // Handle rate limit errors specifically if (error.message && error.message.includes('rate limit')) { window.messageDisplay.show('You are sending emails too quickly. Please wait a few minutes and try again.', 'error', 8000); } else { window.messageDisplay.show(`Failed to send email: ${error.message}`, 'error', 6000); } } finally { sendBtn.disabled = false; sendBtn.textContent = originalText; } } openModalDialog(recipient) { this.currentRecipient = recipient; // Populate recipient info document.getElementById('recipient-email').value = recipient.email; document.getElementById('recipient-info').innerHTML = ` ${recipient.name}
${recipient.office}
${recipient.district}
${recipient.email} `; // Set postal code from current lookup const postalCode = window.postalLookup ? window.postalLookup.currentPostalCode : ''; document.getElementById('sender-postal-code').value = postalCode; // Clear form fields document.getElementById('sender-name').value = ''; document.getElementById('sender-email').value = ''; document.getElementById('email-subject').value = ''; document.getElementById('email-message').value = ''; // Set default subject document.getElementById('email-subject').value = `Message from your constituent from ${postalCode}`; this.updateCharCounter(); this.modal.style.display = 'block'; // Focus on first input document.getElementById('sender-name').focus(); } closeModal() { this.modal.style.display = 'none'; // Only clear data if we're not showing preview (user is canceling) if (this.previewModal.style.display !== 'block') { this.currentRecipient = null; this.currentEmailData = null; } } closePreviewModal() { this.previewModal.style.display = 'none'; // Clear email data when closing preview (user canceling) this.currentRecipient = null; this.currentEmailData = null; } editEmail() { // Close preview modal and return to compose modal without clearing data this.previewModal.style.display = 'none'; this.modal.style.display = 'block'; } updateCharCounter() { const maxLength = 5000; const currentLength = this.messageTextarea.value.length; const remaining = maxLength - currentLength; this.charCounter.textContent = `${remaining} characters remaining`; if (remaining < 100) { this.charCounter.style.color = '#dc3545'; // Red } else if (remaining < 500) { this.charCounter.style.color = '#ffc107'; // Yellow } else { this.charCounter.style.color = '#666'; // Default } } validateForm() { const errors = []; const senderName = document.getElementById('sender-name').value.trim(); const senderEmail = document.getElementById('sender-email').value.trim(); const subject = document.getElementById('email-subject').value.trim(); const message = document.getElementById('email-message').value.trim(); if (!senderName) { errors.push('Your name is required'); } if (!senderEmail) { errors.push('Your email is required'); } else if (!this.validateEmail(senderEmail)) { errors.push('Please enter a valid email address'); } if (!subject) { errors.push('Subject is required'); } if (!message) { errors.push('Message is required'); } else if (message.length < 10) { errors.push('Message must be at least 10 characters long'); } // Check for suspicious content if (this.containsSuspiciousContent(message) || this.containsSuspiciousContent(subject)) { errors.push('Your message contains content that may not be appropriate'); } return errors; } validateEmail(email) { const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return regex.test(email); } containsSuspiciousContent(text) { const suspiciousPatterns = [ /