1068 lines
43 KiB
JavaScript
1068 lines
43 KiB
JavaScript
// 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 `
|
|
<div class="composer-header">
|
|
<h3>Compose Email to ${recipient.name}</h3>
|
|
<button class="close-btn inline-close" type="button">×</button>
|
|
</div>
|
|
<div class="composer-body">
|
|
<div class="recipient-info">
|
|
<strong>${recipient.name}</strong><br>
|
|
${recipient.office}<br>
|
|
${recipient.district}<br>
|
|
<em>${recipient.email}</em>
|
|
</div>
|
|
|
|
<form class="inline-email-form">
|
|
<input type="hidden" id="inline-recipient-email" value="${recipient.email}">
|
|
|
|
<div class="form-group">
|
|
<label for="inline-sender-name">Your Name *</label>
|
|
<input type="text" id="inline-sender-name" name="sender-name" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="inline-sender-email">Your Email *</label>
|
|
<input type="email" id="inline-sender-email" name="sender-email" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="inline-sender-postal-code">Your Postal Code *</label>
|
|
<input type="text"
|
|
id="inline-sender-postal-code"
|
|
name="sender-postal-code"
|
|
value="${postalCode}"
|
|
pattern="[A-Za-z]\\d[A-Za-z]\\s?\\d[A-Za-z]\\d"
|
|
placeholder="e.g., T5N 4B8"
|
|
required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="inline-email-subject">Subject *</label>
|
|
<input type="text" id="inline-email-subject" name="email-subject" value="${defaultSubject}" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="inline-email-message">Your Message *</label>
|
|
<textarea id="inline-email-message" name="email-message" rows="8" maxlength="5000" required></textarea>
|
|
<span class="char-counter inline-char-counter">5000 characters remaining</span>
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-secondary inline-cancel">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Preview & Send</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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('<br>'), '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 ?
|
|
`<div style="background: #fff3cd; color: #856404; padding: 10px; border-radius: 4px; margin-bottom: 15px;">
|
|
<strong>TEST MODE:</strong> Email will be redirected to ${this.escapeHtml(preview.redirectTo)}
|
|
</div>` : '';
|
|
|
|
// Sanitize the HTML to remove any <style> tags or <body> styles that would affect the page
|
|
const sanitizedHtml = this.sanitizeEmailHTML(html);
|
|
|
|
return `
|
|
${testModeWarning}
|
|
<div class="email-preview-details">
|
|
<div class="detail-row">
|
|
<strong>From:</strong> <span>${this.escapeHtml(preview.from)}</span>
|
|
</div>
|
|
<div class="detail-row">
|
|
<strong>To:</strong> <span>${this.escapeHtml(preview.to)}</span>
|
|
</div>
|
|
<div class="detail-row">
|
|
<strong>Subject:</strong> <span>${this.escapeHtml(preview.subject)}</span>
|
|
</div>
|
|
<div class="detail-row">
|
|
<strong>Timestamp:</strong> <span>${new Date(preview.timestamp).toLocaleString()}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin: 20px 0;">
|
|
<strong style="display: block; margin-bottom: 10px; color: #005a9c;">Message Preview:</strong>
|
|
<div class="email-preview-content">
|
|
${sanitizedHtml}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-secondary preview-edit">← Edit Email</button>
|
|
<button type="button" class="btn btn-primary preview-send">Send Email →</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
sanitizeEmailHTML(html) {
|
|
// Remove DOCTYPE, html, head, body tags and their attributes
|
|
// These can interfere with the parent page's styling
|
|
let sanitized = html
|
|
.replace(/<!DOCTYPE[^>]*>/gi, '')
|
|
.replace(/<html[^>]*>/gi, '')
|
|
.replace(/<\/html>/gi, '')
|
|
.replace(/<head[^>]*>[\s\S]*?<\/head>/gi, '')
|
|
.replace(/<body[^>]*>/gi, '<div>')
|
|
.replace(/<\/body>/gi, '</div>');
|
|
|
|
// Remove or modify style tags that could affect the parent page
|
|
// Wrap them in a scoped container instead of removing entirely
|
|
sanitized = sanitized.replace(/<style[^>]*>([\s\S]*?)<\/style>/gi, (match, styleContent) => {
|
|
// Remove any 'body' selectors and replace with a safe class
|
|
const scopedStyles = styleContent
|
|
.replace(/\bbody\s*{/gi, '.email-preview-body {')
|
|
.replace(/\bhtml\s*{/gi, '.email-preview-body {');
|
|
return `<style>${scopedStyles}</style>`;
|
|
});
|
|
|
|
// Wrap the sanitized content in a container div
|
|
return `<div class="email-preview-body">${sanitized}</div>`;
|
|
}
|
|
|
|
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 `
|
|
<div class="recipient-info">
|
|
<strong>${recipient.name}</strong><br>
|
|
${recipient.office}<br>
|
|
${recipient.district}<br>
|
|
<em>${recipient.email}</em>
|
|
</div>
|
|
|
|
<form class="inline-email-form">
|
|
<input type="hidden" id="inline-recipient-email" value="${recipient.email}">
|
|
|
|
<div class="form-group">
|
|
<label for="inline-sender-name">Your Name *</label>
|
|
<input type="text" id="inline-sender-name" name="sender-name" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="inline-sender-email">Your Email *</label>
|
|
<input type="email" id="inline-sender-email" name="sender-email" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="inline-sender-postal-code">Your Postal Code *</label>
|
|
<input type="text"
|
|
id="inline-sender-postal-code"
|
|
name="sender-postal-code"
|
|
pattern="[A-Za-z]\\d[A-Za-z]\\s?\\d[A-Za-z]\\d"
|
|
placeholder="e.g., T5N 4B8"
|
|
required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="inline-email-subject">Subject *</label>
|
|
<input type="text" id="inline-email-subject" name="email-subject" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="inline-email-message">Your Message *</label>
|
|
<textarea id="inline-email-message" name="email-message" rows="8" maxlength="5000" required></textarea>
|
|
<span class="char-counter inline-char-counter">5000 characters remaining</span>
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-secondary inline-cancel">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Preview & Send</button>
|
|
</div>
|
|
</form>
|
|
`;
|
|
}
|
|
|
|
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 = `
|
|
<strong>${recipient.name}</strong><br>
|
|
${recipient.office}<br>
|
|
${recipient.district}<br>
|
|
<em>${recipient.email}</em>
|
|
`;
|
|
|
|
// 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 = [
|
|
/<script/i,
|
|
/javascript:/i,
|
|
/on\w+\s*=/i,
|
|
/<iframe/i,
|
|
/<object/i,
|
|
/<embed/i
|
|
];
|
|
|
|
return suspiciousPatterns.some(pattern => pattern.test(text));
|
|
}
|
|
|
|
async handlePreview(e) {
|
|
e.preventDefault();
|
|
|
|
// Prevent duplicate calls within 2 seconds
|
|
const currentTime = Date.now();
|
|
if (currentTime - this.lastPreviewTime < 2000) {
|
|
console.log('Preview request blocked - too soon since last request');
|
|
return;
|
|
}
|
|
this.lastPreviewTime = currentTime;
|
|
|
|
const errors = this.validateForm();
|
|
if (errors.length > 0) {
|
|
window.messageDisplay.show(errors.join('<br>'), 'error');
|
|
return;
|
|
}
|
|
|
|
const submitButton = this.form.querySelector('button[type="submit"]');
|
|
const originalText = submitButton.textContent;
|
|
|
|
try {
|
|
submitButton.disabled = true;
|
|
submitButton.textContent = 'Loading Preview...';
|
|
|
|
this.currentEmailData = {
|
|
recipientEmail: document.getElementById('recipient-email').value,
|
|
senderName: document.getElementById('sender-name').value.trim(),
|
|
senderEmail: document.getElementById('sender-email').value.trim(),
|
|
subject: document.getElementById('email-subject').value.trim(),
|
|
message: document.getElementById('email-message').value.trim(),
|
|
postalCode: document.getElementById('sender-postal-code').value,
|
|
recipientName: this.currentRecipient ? this.currentRecipient.name : null
|
|
};
|
|
|
|
const preview = await window.apiClient.previewEmail(this.currentEmailData);
|
|
|
|
if (preview.success) {
|
|
this.showPreview(preview.preview);
|
|
this.previewModal.style.display = 'block'; // Show preview modal first
|
|
this.closeModal(); // Close the compose modal
|
|
} else {
|
|
throw new Error(preview.message || 'Failed to generate preview');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Email preview failed:', error);
|
|
window.messageDisplay.show(`Failed to generate preview: ${error.message}`, 'error');
|
|
} finally {
|
|
submitButton.disabled = false;
|
|
submitButton.textContent = originalText;
|
|
}
|
|
}
|
|
|
|
showPreview(preview) {
|
|
// Populate preview modal with email details
|
|
document.getElementById('preview-recipient').textContent = preview.to;
|
|
document.getElementById('preview-sender').textContent = `${this.currentEmailData.senderName} <${this.currentEmailData.senderEmail}>`;
|
|
document.getElementById('preview-subject').textContent = preview.subject;
|
|
|
|
// Show the HTML preview content using iframe for complete isolation
|
|
const previewContent = document.getElementById('preview-content');
|
|
|
|
if (preview.html) {
|
|
// Use iframe to completely isolate the email HTML from the parent page
|
|
const iframe = document.createElement('iframe');
|
|
iframe.style.width = '100%';
|
|
iframe.style.minHeight = '400px';
|
|
iframe.style.border = '1px solid #dee2e6';
|
|
iframe.style.borderRadius = '6px';
|
|
iframe.style.backgroundColor = '#ffffff';
|
|
iframe.sandbox = 'allow-same-origin'; // Safe sandbox settings
|
|
|
|
// Clear previous content and add iframe
|
|
previewContent.innerHTML = '';
|
|
previewContent.appendChild(iframe);
|
|
|
|
// Write the HTML content to the iframe
|
|
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
|
|
iframeDoc.open();
|
|
iframeDoc.write(preview.html);
|
|
iframeDoc.close();
|
|
|
|
// Auto-resize iframe to content height
|
|
iframe.onload = () => {
|
|
try {
|
|
const body = iframe.contentDocument.body;
|
|
const html = iframe.contentDocument.documentElement;
|
|
const height = Math.max(
|
|
body.scrollHeight,
|
|
body.offsetHeight,
|
|
html.clientHeight,
|
|
html.scrollHeight,
|
|
html.offsetHeight
|
|
);
|
|
iframe.style.height = Math.min(height + 20, 600) + 'px'; // Max height of 600px
|
|
} catch (e) {
|
|
// Fallback height if auto-resize fails
|
|
iframe.style.height = '400px';
|
|
}
|
|
};
|
|
|
|
} else if (preview.text) {
|
|
previewContent.innerHTML = `<pre style="white-space: pre-wrap; font-family: inherit; padding: 20px; background-color: #f8f9fa; border-radius: 6px; border: 1px solid #dee2e6;">${this.escapeHtml(preview.text)}</pre>`;
|
|
} else {
|
|
previewContent.innerHTML = '<p style="padding: 20px; text-align: center; color: #666;">No preview content available</p>';
|
|
}
|
|
}
|
|
|
|
// Escape HTML to prevent injection when showing text content
|
|
escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
async confirmSend() {
|
|
if (!this.currentEmailData) {
|
|
window.messageDisplay.show('No email data to send', 'error');
|
|
return;
|
|
}
|
|
|
|
const confirmButton = this.confirmSendBtn;
|
|
const originalText = confirmButton.textContent;
|
|
|
|
try {
|
|
confirmButton.disabled = true;
|
|
confirmButton.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');
|
|
|
|
// Show campaign conversion prompt if user is not authenticated
|
|
this.showCampaignConversionPrompt(this.currentEmailData);
|
|
|
|
this.closePreviewModal();
|
|
this.currentEmailData = 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.status === 429 && error.data && error.data.rateLimitType === 'per-recipient') {
|
|
const retryMinutes = Math.ceil((error.data.retryAfter || 300) / 60);
|
|
window.messageDisplay.show(
|
|
`Rate limit reached: You can only send one email per representative every 5 minutes. Please wait ${retryMinutes} more minutes before sending another email to this representative. You can still send emails to other representatives.`,
|
|
'warning'
|
|
);
|
|
} else if (error.status === 429) {
|
|
// General rate limit
|
|
window.messageDisplay.show(
|
|
`Too many emails sent. Please wait before sending more emails.`,
|
|
'warning'
|
|
);
|
|
} else {
|
|
window.messageDisplay.show(`Failed to send email: ${error.message}`, 'error');
|
|
}
|
|
} finally {
|
|
confirmButton.disabled = false;
|
|
confirmButton.textContent = originalText;
|
|
}
|
|
}
|
|
|
|
// Helper method to get template messages
|
|
getTemplateMessage(type) {
|
|
const templates = {
|
|
general: `Dear {{name}},
|
|
|
|
I am writing as your constituent from {{postalCode}} to express my views on an important matter.
|
|
|
|
[Please write your message here]
|
|
|
|
I would appreciate your response on this issue and would like to know your position.
|
|
|
|
Thank you for your time and service to our community.
|
|
|
|
Sincerely,
|
|
{{senderName}}`,
|
|
|
|
concern: `Dear {{name}},
|
|
|
|
I am writing to express my concern about [specific issue] as your constituent from {{postalCode}}.
|
|
|
|
[Describe your concern and its impact]
|
|
|
|
I urge you to [specific action you want them to take].
|
|
|
|
Thank you for considering my views on this important matter.
|
|
|
|
Best regards,
|
|
{{senderName}}`,
|
|
|
|
support: `Dear {{name}},
|
|
|
|
I am writing to express my support for [specific issue/bill/policy] as your constituent from {{postalCode}}.
|
|
|
|
[Explain why you support this and its importance]
|
|
|
|
I encourage you to continue supporting this initiative.
|
|
|
|
Thank you for your leadership on this matter.
|
|
|
|
Respectfully,
|
|
{{senderName}}`
|
|
};
|
|
|
|
return templates[type] || templates.general;
|
|
}
|
|
|
|
// Campaign conversion prompt
|
|
showCampaignConversionPrompt(emailData) {
|
|
// Only show if user is not authenticated
|
|
if (window.authManager && window.authManager.isAuthenticated) {
|
|
return;
|
|
}
|
|
|
|
// Create the conversion prompt
|
|
const promptHTML = `
|
|
<div class="campaign-conversion-prompt" style="
|
|
position: fixed;
|
|
bottom: 20px;
|
|
right: 20px;
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
max-width: 400px;
|
|
z-index: 10000;
|
|
animation: slideIn 0.3s ease-out;
|
|
">
|
|
<button class="close-prompt" style="
|
|
position: absolute;
|
|
top: 10px;
|
|
right: 10px;
|
|
background: none;
|
|
border: none;
|
|
font-size: 24px;
|
|
cursor: pointer;
|
|
color: #999;
|
|
">×</button>
|
|
|
|
<h3 style="margin-top: 0; color: #d73027;">🚀 Turn Your Message Into a Campaign!</h3>
|
|
<p style="margin: 15px 0; color: #666;">
|
|
Want others to join your cause? Create a campaign so more people can send similar messages.
|
|
</p>
|
|
|
|
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
|
<button id="convert-to-campaign" class="btn btn-primary" style="flex: 1;">
|
|
Create Campaign
|
|
</button>
|
|
<button id="skip-conversion" class="btn btn-secondary" style="flex: 1;">
|
|
No Thanks
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
@keyframes slideIn {
|
|
from {
|
|
transform: translateX(100%);
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
transform: translateX(0);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
</style>
|
|
`;
|
|
|
|
// Insert the prompt
|
|
const promptDiv = document.createElement('div');
|
|
promptDiv.innerHTML = promptHTML;
|
|
document.body.appendChild(promptDiv);
|
|
|
|
// Attach event listeners
|
|
const convertBtn = promptDiv.querySelector('#convert-to-campaign');
|
|
const skipBtn = promptDiv.querySelector('#skip-conversion');
|
|
const closeBtn = promptDiv.querySelector('.close-prompt');
|
|
|
|
const removePrompt = () => {
|
|
promptDiv.remove();
|
|
};
|
|
|
|
convertBtn.addEventListener('click', () => {
|
|
this.initiateConversion(emailData);
|
|
removePrompt();
|
|
});
|
|
|
|
skipBtn.addEventListener('click', removePrompt);
|
|
closeBtn.addEventListener('click', removePrompt);
|
|
|
|
// Auto-remove after 30 seconds
|
|
setTimeout(removePrompt, 30000);
|
|
}
|
|
|
|
async initiateConversion(emailData) {
|
|
if (!emailData) {
|
|
window.messageDisplay.show('No email data available for conversion', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Show loading message
|
|
window.messageDisplay.show('Sending verification email...', 'info');
|
|
|
|
const conversionData = {
|
|
email: emailData.senderEmail,
|
|
senderName: emailData.senderName,
|
|
subject: emailData.subject,
|
|
message: emailData.message,
|
|
postalCode: emailData.postalCode
|
|
};
|
|
|
|
const response = await window.apiClient.post('/emails/convert-to-campaign', conversionData);
|
|
|
|
if (response.success) {
|
|
window.messageDisplay.show(
|
|
'Verification email sent! Please check your inbox to complete campaign creation.',
|
|
'success',
|
|
10000
|
|
);
|
|
} else {
|
|
throw new Error(response.error || 'Failed to send verification email');
|
|
}
|
|
} catch (error) {
|
|
console.error('Campaign conversion error:', error);
|
|
window.messageDisplay.show(
|
|
`Failed to initiate campaign creation: ${error.message}`,
|
|
'error',
|
|
8000
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize when DOM is loaded
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
window.emailComposer = new EmailComposer();
|
|
}); |