// 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 `
${recipient.name}
${recipient.office}
${recipient.district}
${recipient.email}
`;
}
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}
`;
}
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);
// 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 = [
/