Pushing the new influence app in its current state
This commit is contained in:
511
influence/app/public/js/admin.js
Normal file
511
influence/app/public/js/admin.js
Normal file
@@ -0,0 +1,511 @@
|
||||
// Admin Panel JavaScript
|
||||
class AdminPanel {
|
||||
constructor() {
|
||||
this.currentCampaign = null;
|
||||
this.campaigns = [];
|
||||
this.authManager = null;
|
||||
}
|
||||
|
||||
async init() {
|
||||
// Check authentication first
|
||||
if (typeof authManager !== 'undefined') {
|
||||
this.authManager = authManager;
|
||||
const isAuth = await this.authManager.checkSession();
|
||||
if (!isAuth || !this.authManager.user?.isAdmin) {
|
||||
window.location.href = '/login.html';
|
||||
return;
|
||||
}
|
||||
this.setupUserInterface();
|
||||
} else {
|
||||
// Fallback if authManager not loaded
|
||||
window.location.href = '/login.html';
|
||||
return;
|
||||
}
|
||||
|
||||
this.setupEventListeners();
|
||||
this.setupFormInteractions();
|
||||
this.loadCampaigns();
|
||||
}
|
||||
|
||||
setupUserInterface() {
|
||||
// Add user info to header
|
||||
const adminHeader = document.querySelector('.admin-header .admin-container');
|
||||
if (adminHeader && this.authManager.user) {
|
||||
const userInfo = document.createElement('div');
|
||||
userInfo.style.cssText = 'position: absolute; top: 1rem; right: 2rem; color: white; font-size: 0.9rem;';
|
||||
userInfo.innerHTML = `
|
||||
Welcome, ${this.authManager.user.name || this.authManager.user.email}
|
||||
<button id="logout-btn" style="margin-left: 1rem; padding: 0.5rem 1rem; background: rgba(255,255,255,0.2); border: 1px solid rgba(255,255,255,0.3); color: white; border-radius: 4px; cursor: pointer;">Logout</button>
|
||||
`;
|
||||
adminHeader.style.position = 'relative';
|
||||
adminHeader.appendChild(userInfo);
|
||||
|
||||
// Add logout event listener
|
||||
document.getElementById('logout-btn').addEventListener('click', () => {
|
||||
this.authManager.logout();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Tab navigation
|
||||
document.querySelectorAll('.nav-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const tab = e.target.dataset.tab;
|
||||
this.switchTab(tab);
|
||||
});
|
||||
});
|
||||
|
||||
// Form submissions
|
||||
document.getElementById('create-campaign-form').addEventListener('submit', (e) => {
|
||||
this.handleCreateCampaign(e);
|
||||
});
|
||||
|
||||
document.getElementById('edit-campaign-form').addEventListener('submit', (e) => {
|
||||
this.handleUpdateCampaign(e);
|
||||
});
|
||||
|
||||
// Cancel buttons - using event delegation for proper handling
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.matches('[data-action="cancel-create"]')) {
|
||||
this.switchTab('campaigns');
|
||||
}
|
||||
if (e.target.matches('[data-action="cancel-edit"]')) {
|
||||
this.switchTab('campaigns');
|
||||
}
|
||||
});
|
||||
this.loadCampaigns();
|
||||
}
|
||||
|
||||
setupFormInteractions() {
|
||||
// Create campaign button
|
||||
const createBtn = document.querySelector('[data-action="create-campaign"]');
|
||||
if (createBtn) {
|
||||
createBtn.addEventListener('click', () => this.switchTab('create'));
|
||||
}
|
||||
|
||||
// Cancel buttons
|
||||
const cancelCreateBtn = document.querySelector('[data-action="cancel-create"]');
|
||||
if (cancelCreateBtn) {
|
||||
cancelCreateBtn.addEventListener('click', () => this.switchTab('campaigns'));
|
||||
}
|
||||
|
||||
const cancelEditBtn = document.querySelector('[data-action="cancel-edit"]');
|
||||
if (cancelEditBtn) {
|
||||
cancelEditBtn.addEventListener('click', () => this.switchTab('campaigns'));
|
||||
}
|
||||
|
||||
// Handle checkbox changes for government levels
|
||||
document.querySelectorAll('input[name="target_government_levels"]').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', () => {
|
||||
this.updateGovernmentLevelsPreview();
|
||||
});
|
||||
});
|
||||
|
||||
// Handle settings toggles
|
||||
document.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', () => {
|
||||
this.handleSettingsChange(checkbox);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
switchTab(tabName) {
|
||||
// Hide all tabs
|
||||
document.querySelectorAll('.tab-content').forEach(tab => {
|
||||
tab.classList.remove('active');
|
||||
});
|
||||
|
||||
// Remove active class from nav buttons
|
||||
document.querySelectorAll('.nav-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
|
||||
// Show selected tab
|
||||
const targetTab = document.getElementById(`${tabName}-tab`);
|
||||
if (targetTab) {
|
||||
targetTab.classList.add('active');
|
||||
}
|
||||
|
||||
// Update nav button
|
||||
const targetNavBtn = document.querySelector(`[data-tab="${tabName}"]`);
|
||||
if (targetNavBtn) {
|
||||
targetNavBtn.classList.add('active');
|
||||
}
|
||||
|
||||
// Special handling for different tabs
|
||||
if (tabName === 'campaigns') {
|
||||
this.loadCampaigns();
|
||||
} else if (tabName === 'edit' && this.currentCampaign) {
|
||||
this.populateEditForm();
|
||||
}
|
||||
}
|
||||
|
||||
async loadCampaigns() {
|
||||
const loadingDiv = document.getElementById('campaigns-loading');
|
||||
const listDiv = document.getElementById('campaigns-list');
|
||||
|
||||
loadingDiv.classList.remove('hidden');
|
||||
listDiv.innerHTML = '';
|
||||
|
||||
try {
|
||||
const response = await window.apiClient.get('/admin/campaigns');
|
||||
|
||||
if (response.success) {
|
||||
this.campaigns = response.campaigns;
|
||||
this.renderCampaignList();
|
||||
} else {
|
||||
throw new Error(response.error || 'Failed to load campaigns');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Load campaigns error:', error);
|
||||
this.showMessage('Failed to load campaigns: ' + error.message, 'error');
|
||||
} finally {
|
||||
loadingDiv.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
renderCampaignList() {
|
||||
const listDiv = document.getElementById('campaigns-list');
|
||||
|
||||
if (this.campaigns.length === 0) {
|
||||
listDiv.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<h3>No campaigns yet</h3>
|
||||
<p>Create your first campaign to get started.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
listDiv.innerHTML = this.campaigns.map(campaign => `
|
||||
<div class="campaign-card" data-campaign-id="${campaign.id}">
|
||||
<div class="campaign-header">
|
||||
<h3>${this.escapeHtml(campaign.title)}</h3>
|
||||
<span class="status-badge status-${campaign.status}">${campaign.status}</span>
|
||||
</div>
|
||||
|
||||
<div class="campaign-meta">
|
||||
<p><strong>Slug:</strong> <code>/campaign/${campaign.slug}</code></p>
|
||||
<p><strong>Email Count:</strong> ${campaign.emailCount || 0}</p>
|
||||
<p><strong>Created:</strong> ${this.formatDate(campaign.created_at)}</p>
|
||||
</div>
|
||||
|
||||
<div class="campaign-actions">
|
||||
<button class="btn btn-secondary" data-action="edit-campaign" data-campaign-id="${campaign.id}">
|
||||
Edit
|
||||
</button>
|
||||
<button class="btn btn-secondary" data-action="view-analytics" data-campaign-id="${campaign.id}">
|
||||
Analytics
|
||||
</button>
|
||||
<a href="/campaign/${campaign.slug}" target="_blank" class="btn btn-secondary">
|
||||
View Public Page
|
||||
</a>
|
||||
<button class="btn btn-danger" data-action="delete-campaign" data-campaign-id="${campaign.id}">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Attach event listeners to campaign actions
|
||||
this.attachCampaignActionListeners();
|
||||
}
|
||||
|
||||
attachCampaignActionListeners() {
|
||||
// Edit campaign buttons
|
||||
document.querySelectorAll('[data-action="edit-campaign"]').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const campaignId = parseInt(e.target.dataset.campaignId);
|
||||
this.editCampaign(campaignId);
|
||||
});
|
||||
});
|
||||
|
||||
// Delete campaign buttons
|
||||
document.querySelectorAll('[data-action="delete-campaign"]').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const campaignId = parseInt(e.target.dataset.campaignId);
|
||||
this.deleteCampaign(campaignId);
|
||||
});
|
||||
});
|
||||
|
||||
// Analytics buttons
|
||||
document.querySelectorAll('[data-action="view-analytics"]').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const campaignId = parseInt(e.target.dataset.campaignId);
|
||||
this.viewAnalytics(campaignId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async handleCreateCampaign(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.target);
|
||||
const campaignData = {
|
||||
title: formData.get('title'),
|
||||
description: formData.get('description'),
|
||||
email_subject: formData.get('email_subject'),
|
||||
email_body: formData.get('email_body'),
|
||||
call_to_action: formData.get('call_to_action'),
|
||||
allow_smtp_email: formData.get('allow_smtp_email') === 'on',
|
||||
allow_mailto_link: formData.get('allow_mailto_link') === 'on',
|
||||
collect_user_info: formData.get('collect_user_info') === 'on',
|
||||
show_email_count: formData.get('show_email_count') === 'on',
|
||||
target_government_levels: Array.from(formData.getAll('target_government_levels'))
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await window.apiClient.post('/admin/campaigns', campaignData);
|
||||
|
||||
if (response.success) {
|
||||
this.showMessage('Campaign created successfully!', 'success');
|
||||
e.target.reset();
|
||||
this.switchTab('campaigns');
|
||||
} else {
|
||||
throw new Error(response.error || 'Failed to create campaign');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Create campaign error:', error);
|
||||
this.showMessage('Failed to create campaign: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
editCampaign(campaignId) {
|
||||
this.currentCampaign = this.campaigns.find(c => c.id === campaignId);
|
||||
if (this.currentCampaign) {
|
||||
this.switchTab('edit');
|
||||
}
|
||||
}
|
||||
|
||||
populateEditForm() {
|
||||
if (!this.currentCampaign) return;
|
||||
|
||||
const form = document.getElementById('edit-campaign-form');
|
||||
const campaign = this.currentCampaign;
|
||||
|
||||
// Populate form fields
|
||||
form.querySelector('[name="title"]').value = campaign.title || '';
|
||||
form.querySelector('[name="description"]').value = campaign.description || '';
|
||||
form.querySelector('[name="email_subject"]').value = campaign.email_subject || '';
|
||||
form.querySelector('[name="email_body"]').value = campaign.email_body || '';
|
||||
form.querySelector('[name="call_to_action"]').value = campaign.call_to_action || '';
|
||||
|
||||
// Status select
|
||||
form.querySelector('[name="status"]').value = campaign.status || 'draft';
|
||||
|
||||
// Checkboxes
|
||||
form.querySelector('[name="allow_smtp_email"]').checked = campaign.allow_smtp_email;
|
||||
form.querySelector('[name="allow_mailto_link"]').checked = campaign.allow_mailto_link;
|
||||
form.querySelector('[name="collect_user_info"]').checked = campaign.collect_user_info;
|
||||
form.querySelector('[name="show_email_count"]').checked = campaign.show_email_count;
|
||||
|
||||
// Government levels
|
||||
const targetLevels = campaign.target_government_levels ?
|
||||
campaign.target_government_levels.split(',').map(l => l.trim()) : [];
|
||||
|
||||
form.querySelectorAll('[name="target_government_levels"]').forEach(checkbox => {
|
||||
checkbox.checked = targetLevels.includes(checkbox.value);
|
||||
});
|
||||
}
|
||||
|
||||
async handleUpdateCampaign(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.currentCampaign) return;
|
||||
|
||||
const formData = new FormData(e.target);
|
||||
const updates = {
|
||||
title: formData.get('title'),
|
||||
description: formData.get('description'),
|
||||
email_subject: formData.get('email_subject'),
|
||||
email_body: formData.get('email_body'),
|
||||
call_to_action: formData.get('call_to_action'),
|
||||
status: formData.get('status'),
|
||||
allow_smtp_email: formData.get('allow_smtp_email') === 'on',
|
||||
allow_mailto_link: formData.get('allow_mailto_link') === 'on',
|
||||
collect_user_info: formData.get('collect_user_info') === 'on',
|
||||
show_email_count: formData.get('show_email_count') === 'on',
|
||||
target_government_levels: Array.from(formData.getAll('target_government_levels'))
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await window.apiClient.makeRequest(`/admin/campaigns/${this.currentCampaign.id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(updates)
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
this.showMessage('Campaign updated successfully!', 'success');
|
||||
this.switchTab('campaigns');
|
||||
} else {
|
||||
throw new Error(response.error || 'Failed to update campaign');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Update campaign error:', error);
|
||||
this.showMessage('Failed to update campaign: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async deleteCampaign(campaignId) {
|
||||
const campaign = this.campaigns.find(c => c.id === campaignId);
|
||||
if (!campaign) return;
|
||||
|
||||
if (!confirm(`Are you sure you want to delete the campaign "${campaign.title}"? This action cannot be undone.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await window.apiClient.makeRequest(`/admin/campaigns/${campaignId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
this.showMessage('Campaign deleted successfully!', 'success');
|
||||
this.loadCampaigns();
|
||||
} else {
|
||||
throw new Error(response.error || 'Failed to delete campaign');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Delete campaign error:', error);
|
||||
this.showMessage('Failed to delete campaign: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async viewAnalytics(campaignId) {
|
||||
try {
|
||||
const response = await window.apiClient.get(`/admin/campaigns/${campaignId}/analytics`);
|
||||
|
||||
if (response.success) {
|
||||
this.showAnalyticsModal(response.analytics);
|
||||
} else {
|
||||
throw new Error(response.error || 'Failed to load analytics');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Analytics error:', error);
|
||||
this.showMessage('Failed to load analytics: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
showAnalyticsModal(analytics) {
|
||||
// Create a simple analytics modal
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'modal-overlay';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content" style="max-width: 800px;">
|
||||
<div class="modal-header">
|
||||
<h2>Campaign Analytics</h2>
|
||||
<button class="modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="analytics-grid">
|
||||
<div class="analytics-stat">
|
||||
<h3>${analytics.totalEmails}</h3>
|
||||
<p>Total Emails</p>
|
||||
</div>
|
||||
<div class="analytics-stat">
|
||||
<h3>${analytics.smtpEmails}</h3>
|
||||
<p>SMTP Emails</p>
|
||||
</div>
|
||||
<div class="analytics-stat">
|
||||
<h3>${analytics.mailtoClicks}</h3>
|
||||
<p>Mailto Clicks</p>
|
||||
</div>
|
||||
<div class="analytics-stat">
|
||||
<h3>${analytics.successfulEmails}</h3>
|
||||
<p>Successful</p>
|
||||
</div>
|
||||
</div>
|
||||
${Object.keys(analytics.byLevel).length > 0 ? `
|
||||
<h3>By Government Level</h3>
|
||||
<div class="level-stats">
|
||||
${Object.entries(analytics.byLevel).map(([level, count]) =>
|
||||
`<div class="level-stat"><strong>${level}:</strong> ${count}</div>`
|
||||
).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
${analytics.recentEmails.length > 0 ? `
|
||||
<h3>Recent Activity</h3>
|
||||
<div class="recent-activity">
|
||||
${analytics.recentEmails.slice(0, 5).map(email => `
|
||||
<div class="activity-item">
|
||||
<strong>${email.user_name || 'Anonymous'}</strong>
|
||||
→ ${email.recipient_name} (${email.recipient_level})
|
||||
<span class="timestamp">${this.formatDate(email.timestamp)}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Close modal handlers
|
||||
modal.querySelector('.modal-close').addEventListener('click', () => {
|
||||
document.body.removeChild(modal);
|
||||
});
|
||||
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
document.body.removeChild(modal);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateGovernmentLevelsPreview() {
|
||||
const checkboxes = document.querySelectorAll('input[name="target_government_levels"]:checked');
|
||||
const levels = Array.from(checkboxes).map(cb => cb.value);
|
||||
|
||||
// Could update a preview somewhere if needed
|
||||
console.log('Selected government levels:', levels);
|
||||
}
|
||||
|
||||
handleSettingsChange(checkbox) {
|
||||
// Handle real-time settings changes if needed
|
||||
console.log(`Setting ${checkbox.name} changed to:`, checkbox.checked);
|
||||
}
|
||||
|
||||
showMessage(message, type = 'info') {
|
||||
const container = document.getElementById('message-container');
|
||||
container.className = `message-${type}`;
|
||||
container.textContent = message;
|
||||
container.classList.remove('hidden');
|
||||
|
||||
setTimeout(() => {
|
||||
container.classList.add('hidden');
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return 'N/A';
|
||||
|
||||
try {
|
||||
return new Date(dateString).toLocaleDateString('en-CA', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
} catch (error) {
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize admin panel when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
window.adminPanel = new AdminPanel();
|
||||
await window.adminPanel.init();
|
||||
});
|
||||
73
influence/app/public/js/api-client.js
Normal file
73
influence/app/public/js/api-client.js
Normal file
@@ -0,0 +1,73 @@
|
||||
// API Client for making requests to the backend
|
||||
class APIClient {
|
||||
constructor() {
|
||||
this.baseURL = '/api';
|
||||
}
|
||||
|
||||
async makeRequest(endpoint, options = {}) {
|
||||
const config = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
},
|
||||
...options
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.baseURL}${endpoint}`, config);
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || data.message || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('API request failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async get(endpoint) {
|
||||
return this.makeRequest(endpoint, {
|
||||
method: 'GET'
|
||||
});
|
||||
}
|
||||
|
||||
async post(endpoint, data) {
|
||||
return this.makeRequest(endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
|
||||
// Health check
|
||||
async checkHealth() {
|
||||
return this.get('/health');
|
||||
}
|
||||
|
||||
// Test Represent API connection
|
||||
async testRepresent() {
|
||||
return this.get('/test-represent');
|
||||
}
|
||||
|
||||
// Get representatives by postal code
|
||||
async getRepresentativesByPostalCode(postalCode) {
|
||||
const cleanPostalCode = postalCode.replace(/\s/g, '').toUpperCase();
|
||||
return this.get(`/representatives/by-postal/${cleanPostalCode}`);
|
||||
}
|
||||
|
||||
// Refresh representatives for postal code
|
||||
async refreshRepresentatives(postalCode) {
|
||||
const cleanPostalCode = postalCode.replace(/\s/g, '').toUpperCase();
|
||||
return this.post(`/representatives/refresh-postal/${cleanPostalCode}`);
|
||||
}
|
||||
|
||||
// Send email to representative
|
||||
async sendEmail(emailData) {
|
||||
return this.post('/emails/send', emailData);
|
||||
}
|
||||
}
|
||||
|
||||
// Create global instance
|
||||
window.apiClient = new APIClient();
|
||||
196
influence/app/public/js/auth.js
Normal file
196
influence/app/public/js/auth.js
Normal file
@@ -0,0 +1,196 @@
|
||||
// Authentication module for handling login/logout and session management
|
||||
class AuthManager {
|
||||
constructor() {
|
||||
this.user = null;
|
||||
this.isAuthenticated = false;
|
||||
}
|
||||
|
||||
// Initialize authentication state
|
||||
async init() {
|
||||
await this.checkSession();
|
||||
this.setupAuthListeners();
|
||||
}
|
||||
|
||||
// Check current session status
|
||||
async checkSession() {
|
||||
try {
|
||||
const response = await apiClient.get('/auth/session');
|
||||
|
||||
if (response.authenticated) {
|
||||
this.isAuthenticated = true;
|
||||
this.user = response.user;
|
||||
this.updateUI();
|
||||
return true;
|
||||
} else {
|
||||
this.isAuthenticated = false;
|
||||
this.user = null;
|
||||
this.updateUI();
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Session check failed:', error);
|
||||
this.isAuthenticated = false;
|
||||
this.user = null;
|
||||
this.updateUI();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Login with email and password
|
||||
async login(email, password) {
|
||||
try {
|
||||
const response = await apiClient.post('/auth/login', {
|
||||
email,
|
||||
password
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
this.isAuthenticated = true;
|
||||
this.user = response.user;
|
||||
this.updateUI();
|
||||
return { success: true };
|
||||
} else {
|
||||
return { success: false, error: response.error };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
return { success: false, error: error.message || 'Login failed' };
|
||||
}
|
||||
}
|
||||
|
||||
// Logout current user
|
||||
async logout() {
|
||||
try {
|
||||
await apiClient.post('/auth/logout');
|
||||
this.isAuthenticated = false;
|
||||
this.user = null;
|
||||
this.updateUI();
|
||||
|
||||
// Redirect to login page
|
||||
window.location.href = '/login.html';
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
// Force logout on client side even if server request fails
|
||||
this.isAuthenticated = false;
|
||||
this.user = null;
|
||||
this.updateUI();
|
||||
window.location.href = '/login.html';
|
||||
}
|
||||
}
|
||||
|
||||
// Update UI based on authentication state
|
||||
updateUI() {
|
||||
// Update user info display
|
||||
const userInfo = document.getElementById('user-info');
|
||||
if (userInfo) {
|
||||
if (this.isAuthenticated && this.user) {
|
||||
userInfo.innerHTML = `
|
||||
<span>Welcome, ${this.user.name || this.user.email}</span>
|
||||
<button id="logout-btn" class="btn btn-secondary">Logout</button>
|
||||
`;
|
||||
|
||||
// Add logout button listener
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
if (logoutBtn) {
|
||||
logoutBtn.addEventListener('click', () => this.logout());
|
||||
}
|
||||
} else {
|
||||
userInfo.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Show/hide admin elements
|
||||
const adminElements = document.querySelectorAll('.admin-only');
|
||||
adminElements.forEach(element => {
|
||||
if (this.isAuthenticated && this.user?.isAdmin) {
|
||||
element.style.display = 'block';
|
||||
} else {
|
||||
element.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Show/hide authenticated elements
|
||||
const authElements = document.querySelectorAll('.auth-only');
|
||||
authElements.forEach(element => {
|
||||
if (this.isAuthenticated) {
|
||||
element.style.display = 'block';
|
||||
} else {
|
||||
element.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Set up event listeners for auth-related actions
|
||||
setupAuthListeners() {
|
||||
// Global logout button
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.matches('[data-action="logout"]')) {
|
||||
e.preventDefault();
|
||||
this.logout();
|
||||
}
|
||||
});
|
||||
|
||||
// Login form submission
|
||||
const loginForm = document.getElementById('login-form');
|
||||
if (loginForm) {
|
||||
loginForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const email = document.getElementById('email').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
|
||||
const result = await this.login(email, password);
|
||||
|
||||
if (result.success) {
|
||||
// Redirect to admin panel
|
||||
window.location.href = '/admin.html';
|
||||
} else {
|
||||
// Show error message
|
||||
const errorElement = document.getElementById('error-message');
|
||||
if (errorElement) {
|
||||
errorElement.textContent = result.error;
|
||||
errorElement.style.display = 'block';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Require authentication for current page
|
||||
requireAuth() {
|
||||
if (!this.isAuthenticated) {
|
||||
window.location.href = '/login.html';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Require admin access for current page
|
||||
requireAdmin() {
|
||||
if (!this.isAuthenticated) {
|
||||
window.location.href = '/login.html';
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.user?.isAdmin) {
|
||||
alert('Admin access required');
|
||||
window.location.href = '/';
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Create global auth manager instance
|
||||
const authManager = new AuthManager();
|
||||
|
||||
// Initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
authManager.init();
|
||||
});
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = AuthManager;
|
||||
}
|
||||
237
influence/app/public/js/email-composer.js
Normal file
237
influence/app/public/js/email-composer.js
Normal file
@@ -0,0 +1,237 @@
|
||||
// Email Composer Module
|
||||
class EmailComposer {
|
||||
constructor() {
|
||||
this.modal = document.getElementById('email-modal');
|
||||
this.form = document.getElementById('email-form');
|
||||
this.closeBtn = document.getElementById('close-modal');
|
||||
this.cancelBtn = document.getElementById('cancel-email');
|
||||
this.messageTextarea = document.getElementById('email-message');
|
||||
this.charCounter = document.querySelector('.char-counter');
|
||||
|
||||
this.currentRecipient = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Modal controls
|
||||
this.closeBtn.addEventListener('click', () => this.closeModal());
|
||||
this.cancelBtn.addEventListener('click', () => this.closeModal());
|
||||
this.modal.addEventListener('click', (e) => {
|
||||
if (e.target === this.modal) this.closeModal();
|
||||
});
|
||||
|
||||
// Form handling
|
||||
this.form.addEventListener('submit', (e) => this.handleSubmit(e));
|
||||
|
||||
// Character counter
|
||||
this.messageTextarea.addEventListener('input', () => this.updateCharCounter());
|
||||
|
||||
// Escape key to close modal
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && this.modal.style.display === 'block') {
|
||||
this.closeModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openModal(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 in ${postalCode}`;
|
||||
|
||||
this.updateCharCounter();
|
||||
this.modal.style.display = 'block';
|
||||
|
||||
// Focus on first input
|
||||
document.getElementById('sender-name').focus();
|
||||
}
|
||||
|
||||
closeModal() {
|
||||
this.modal.style.display = 'none';
|
||||
this.currentRecipient = null;
|
||||
}
|
||||
|
||||
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 handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
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 = 'Sending...';
|
||||
|
||||
const emailData = {
|
||||
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
|
||||
};
|
||||
|
||||
const result = await window.apiClient.sendEmail(emailData);
|
||||
|
||||
if (result.success) {
|
||||
window.messageDisplay.show('Email sent successfully! Your representative will receive your message.', 'success');
|
||||
this.closeModal();
|
||||
} else {
|
||||
throw new Error(result.message || 'Failed to send email');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Email send failed:', error);
|
||||
window.messageDisplay.show(`Failed to send email: ${error.message}`, 'error');
|
||||
} finally {
|
||||
submitButton.disabled = false;
|
||||
submitButton.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;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.emailComposer = new EmailComposer();
|
||||
});
|
||||
79
influence/app/public/js/login.js
Normal file
79
influence/app/public/js/login.js
Normal file
@@ -0,0 +1,79 @@
|
||||
// Login page specific functionality
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const loginForm = document.getElementById('login-form');
|
||||
const loginBtn = document.getElementById('login-btn');
|
||||
const loginText = document.getElementById('login-text');
|
||||
const loading = document.querySelector('.loading');
|
||||
const errorMessage = document.getElementById('error-message');
|
||||
|
||||
// Check if already logged in
|
||||
checkSession();
|
||||
|
||||
loginForm.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const email = document.getElementById('email').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
|
||||
if (!email || !password) {
|
||||
showError('Please enter both email and password');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
hideError();
|
||||
|
||||
try {
|
||||
const response = await apiClient.post('/auth/login', {
|
||||
email,
|
||||
password
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
// Redirect to admin panel
|
||||
window.location.href = '/admin.html';
|
||||
} else {
|
||||
showError(response.error || 'Login failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
showError(error.message || 'Login failed. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
async function checkSession() {
|
||||
try {
|
||||
const response = await apiClient.get('/auth/session');
|
||||
if (response.authenticated) {
|
||||
// Already logged in, redirect to admin
|
||||
window.location.href = '/admin.html';
|
||||
}
|
||||
} catch (error) {
|
||||
// Not logged in, continue with login form
|
||||
console.log('Not logged in');
|
||||
}
|
||||
}
|
||||
|
||||
function setLoading(isLoading) {
|
||||
loginBtn.disabled = isLoading;
|
||||
loginText.style.display = isLoading ? 'none' : 'inline';
|
||||
loading.style.display = isLoading ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
errorMessage.textContent = message;
|
||||
errorMessage.style.display = 'block';
|
||||
}
|
||||
|
||||
function hideError() {
|
||||
errorMessage.style.display = 'none';
|
||||
}
|
||||
|
||||
// Check for URL parameters
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.get('expired') === 'true') {
|
||||
showError('Your session has expired. Please log in again.');
|
||||
}
|
||||
});
|
||||
152
influence/app/public/js/main.js
Normal file
152
influence/app/public/js/main.js
Normal file
@@ -0,0 +1,152 @@
|
||||
// Main Application Module
|
||||
class MainApp {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Initialize message display system
|
||||
window.messageDisplay = new MessageDisplay();
|
||||
|
||||
// Check API health on startup
|
||||
this.checkAPIHealth();
|
||||
|
||||
// Add global error handling
|
||||
window.addEventListener('error', (e) => {
|
||||
console.error('Global error:', e.error);
|
||||
window.messageDisplay.show('An unexpected error occurred. Please refresh the page and try again.', 'error');
|
||||
});
|
||||
|
||||
// Add unhandled promise rejection handling
|
||||
window.addEventListener('unhandledrejection', (e) => {
|
||||
console.error('Unhandled promise rejection:', e.reason);
|
||||
window.messageDisplay.show('An unexpected error occurred. Please try again.', 'error');
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
async checkAPIHealth() {
|
||||
try {
|
||||
await window.apiClient.checkHealth();
|
||||
console.log('API health check passed');
|
||||
} catch (error) {
|
||||
console.error('API health check failed:', error);
|
||||
window.messageDisplay.show('Connection to server failed. Please check your internet connection and try again.', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Message Display System
|
||||
class MessageDisplay {
|
||||
constructor() {
|
||||
this.container = document.getElementById('message-display');
|
||||
this.timeouts = new Map();
|
||||
}
|
||||
|
||||
show(message, type = 'info', duration = 5000) {
|
||||
// Clear existing timeout for this container
|
||||
if (this.timeouts.has(this.container)) {
|
||||
clearTimeout(this.timeouts.get(this.container));
|
||||
}
|
||||
|
||||
// Set message content and type
|
||||
this.container.innerHTML = message;
|
||||
this.container.className = `message-display ${type}`;
|
||||
this.container.style.display = 'block';
|
||||
|
||||
// Auto-hide after duration
|
||||
const timeout = setTimeout(() => {
|
||||
this.hide();
|
||||
}, duration);
|
||||
|
||||
this.timeouts.set(this.container, timeout);
|
||||
|
||||
// Add click to dismiss
|
||||
this.container.style.cursor = 'pointer';
|
||||
this.container.onclick = () => this.hide();
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.container.style.display = 'none';
|
||||
this.container.onclick = null;
|
||||
|
||||
// Clear timeout
|
||||
if (this.timeouts.has(this.container)) {
|
||||
clearTimeout(this.timeouts.get(this.container));
|
||||
this.timeouts.delete(this.container);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
const Utils = {
|
||||
// Format postal code consistently
|
||||
formatPostalCode(postalCode) {
|
||||
const cleaned = postalCode.replace(/\s/g, '').toUpperCase();
|
||||
if (cleaned.length === 6) {
|
||||
return `${cleaned.slice(0, 3)} ${cleaned.slice(3)}`;
|
||||
}
|
||||
return cleaned;
|
||||
},
|
||||
|
||||
// Sanitize text input
|
||||
sanitizeText(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
},
|
||||
|
||||
// Debounce function for input handling
|
||||
debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
},
|
||||
|
||||
// Check if we're on mobile
|
||||
isMobile() {
|
||||
return window.innerWidth <= 768;
|
||||
},
|
||||
|
||||
// Format date for display
|
||||
formatDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-CA', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Make utils globally available
|
||||
window.Utils = Utils;
|
||||
|
||||
// Initialize app when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.mainApp = new MainApp();
|
||||
|
||||
// Add some basic accessibility improvements
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// Allow Escape to close modals (handled in individual modules)
|
||||
// Add tab navigation improvements if needed
|
||||
});
|
||||
|
||||
// Add responsive behavior
|
||||
window.addEventListener('resize', Utils.debounce(() => {
|
||||
// Handle responsive layout changes if needed
|
||||
const isMobile = Utils.isMobile();
|
||||
document.body.classList.toggle('mobile', isMobile);
|
||||
}, 250));
|
||||
|
||||
// Initial mobile class
|
||||
document.body.classList.toggle('mobile', Utils.isMobile());
|
||||
});
|
||||
158
influence/app/public/js/postal-lookup.js
Normal file
158
influence/app/public/js/postal-lookup.js
Normal file
@@ -0,0 +1,158 @@
|
||||
// Postal Code Lookup Module
|
||||
class PostalLookup {
|
||||
constructor() {
|
||||
this.form = document.getElementById('postal-form');
|
||||
this.input = document.getElementById('postal-code');
|
||||
this.refreshBtn = document.getElementById('refresh-btn');
|
||||
this.loadingDiv = document.getElementById('loading');
|
||||
this.errorDiv = document.getElementById('error-message');
|
||||
this.representativesSection = document.getElementById('representatives-section');
|
||||
this.locationDetails = document.getElementById('location-details');
|
||||
|
||||
this.currentPostalCode = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.form.addEventListener('submit', (e) => this.handleSubmit(e));
|
||||
this.refreshBtn.addEventListener('click', () => this.handleRefresh());
|
||||
this.input.addEventListener('input', (e) => this.formatPostalCode(e));
|
||||
}
|
||||
|
||||
formatPostalCode(e) {
|
||||
let value = e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, '');
|
||||
|
||||
// Format as A1A 1A1
|
||||
if (value.length > 3) {
|
||||
value = value.slice(0, 3) + ' ' + value.slice(3, 6);
|
||||
}
|
||||
|
||||
e.target.value = value;
|
||||
}
|
||||
|
||||
validatePostalCode(postalCode) {
|
||||
const cleaned = postalCode.replace(/\s/g, '');
|
||||
|
||||
// Check format: Letter-Number-Letter Number-Letter-Number
|
||||
const regex = /^[A-Z]\d[A-Z]\d[A-Z]\d$/;
|
||||
if (!regex.test(cleaned)) {
|
||||
return { valid: false, message: 'Please enter a valid postal code format (A1A 1A1)' };
|
||||
}
|
||||
|
||||
// Check if it's an Alberta postal code (starts with T)
|
||||
if (!cleaned.startsWith('T')) {
|
||||
return { valid: false, message: 'This tool is designed for Alberta postal codes only (starting with T)' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
this.errorDiv.textContent = message;
|
||||
this.errorDiv.style.display = 'block';
|
||||
this.representativesSection.style.display = 'none';
|
||||
}
|
||||
|
||||
hideError() {
|
||||
this.errorDiv.style.display = 'none';
|
||||
}
|
||||
|
||||
showLoading() {
|
||||
this.loadingDiv.style.display = 'block';
|
||||
this.hideError();
|
||||
this.representativesSection.style.display = 'none';
|
||||
}
|
||||
|
||||
hideLoading() {
|
||||
this.loadingDiv.style.display = 'none';
|
||||
}
|
||||
|
||||
async handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const postalCode = this.input.value.trim();
|
||||
if (!postalCode) {
|
||||
this.showError('Please enter a postal code');
|
||||
return;
|
||||
}
|
||||
|
||||
const validation = this.validatePostalCode(postalCode);
|
||||
if (!validation.valid) {
|
||||
this.showError(validation.message);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.lookupRepresentatives(postalCode);
|
||||
}
|
||||
|
||||
async handleRefresh() {
|
||||
if (!this.currentPostalCode) return;
|
||||
|
||||
try {
|
||||
this.showLoading();
|
||||
this.refreshBtn.disabled = true;
|
||||
|
||||
const data = await window.apiClient.refreshRepresentatives(this.currentPostalCode);
|
||||
this.displayResults(data);
|
||||
|
||||
window.messageDisplay.show('Representatives data refreshed successfully!', 'success');
|
||||
} catch (error) {
|
||||
console.error('Refresh failed:', error);
|
||||
this.showError(`Failed to refresh data: ${error.message}`);
|
||||
} finally {
|
||||
this.hideLoading();
|
||||
this.refreshBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async lookupRepresentatives(postalCode) {
|
||||
try {
|
||||
this.showLoading();
|
||||
|
||||
const data = await window.apiClient.getRepresentativesByPostalCode(postalCode);
|
||||
this.currentPostalCode = postalCode;
|
||||
this.displayResults(data);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Lookup failed:', error);
|
||||
this.showError(`Failed to find representatives: ${error.message}`);
|
||||
} finally {
|
||||
this.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
displayResults(apiResponse) {
|
||||
this.hideError();
|
||||
this.hideLoading();
|
||||
|
||||
// Handle the API response structure
|
||||
const data = apiResponse.data || apiResponse; // Handle both new and old response formats
|
||||
|
||||
// Update location info
|
||||
let locationText = `Postal Code: ${data.postalCode}`;
|
||||
if (data.location && data.location.city && data.location.province) {
|
||||
locationText += ` • ${data.location.city}, ${data.location.province}`;
|
||||
} else if (data.city && data.province) {
|
||||
locationText += ` • ${data.city}, ${data.province}`;
|
||||
}
|
||||
if (data.source || apiResponse.source) {
|
||||
locationText += ` • Data source: ${data.source || apiResponse.source}`;
|
||||
}
|
||||
locationText += ` • Data source: api`;
|
||||
this.locationDetails.textContent = locationText;
|
||||
|
||||
// Show representatives
|
||||
const representatives = data.representatives || [];
|
||||
console.log('Displaying representatives:', representatives.length, representatives);
|
||||
window.representativesDisplay.displayRepresentatives(representatives);
|
||||
|
||||
// Show section and refresh button
|
||||
this.representativesSection.style.display = 'block';
|
||||
this.refreshBtn.style.display = 'inline-block';
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.postalLookup = new PostalLookup();
|
||||
});
|
||||
192
influence/app/public/js/representatives-display.js
Normal file
192
influence/app/public/js/representatives-display.js
Normal file
@@ -0,0 +1,192 @@
|
||||
// Representatives Display Module
|
||||
class RepresentativesDisplay {
|
||||
constructor() {
|
||||
this.container = document.getElementById('representatives-container');
|
||||
}
|
||||
|
||||
displayRepresentatives(representatives) {
|
||||
if (!representatives || representatives.length === 0) {
|
||||
this.container.innerHTML = `
|
||||
<div class="rep-category">
|
||||
<h3>No Representatives Found</h3>
|
||||
<p>No representatives were found for this postal code. This might be due to:</p>
|
||||
<ul>
|
||||
<li>The postal code is not in our database</li>
|
||||
<li>Temporary API issues</li>
|
||||
<li>The postal code is not currently assigned to electoral districts</li>
|
||||
</ul>
|
||||
<p>Please try again later or verify your postal code.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Group representatives by level/type
|
||||
const grouped = this.groupRepresentatives(representatives);
|
||||
|
||||
let html = '';
|
||||
|
||||
// Order of importance for display
|
||||
const displayOrder = [
|
||||
'Federal',
|
||||
'Provincial',
|
||||
'Municipal',
|
||||
'School Board',
|
||||
'Other'
|
||||
];
|
||||
|
||||
displayOrder.forEach(level => {
|
||||
if (grouped[level] && grouped[level].length > 0) {
|
||||
html += this.renderRepresentativeCategory(level, grouped[level]);
|
||||
}
|
||||
});
|
||||
|
||||
this.container.innerHTML = html;
|
||||
this.attachEventListeners();
|
||||
}
|
||||
|
||||
groupRepresentatives(representatives) {
|
||||
const groups = {
|
||||
'Federal': [],
|
||||
'Provincial': [],
|
||||
'Municipal': [],
|
||||
'School Board': [],
|
||||
'Other': []
|
||||
};
|
||||
|
||||
representatives.forEach(rep => {
|
||||
const setName = rep.representative_set_name || '';
|
||||
const office = rep.elected_office || '';
|
||||
|
||||
if (setName.toLowerCase().includes('house of commons') ||
|
||||
setName.toLowerCase().includes('federal') ||
|
||||
office.toLowerCase().includes('member of parliament') ||
|
||||
office.toLowerCase().includes('mp')) {
|
||||
groups['Federal'].push(rep);
|
||||
} else if (setName.toLowerCase().includes('provincial') ||
|
||||
setName.toLowerCase().includes('legislative assembly') ||
|
||||
setName.toLowerCase().includes('mla') ||
|
||||
office.toLowerCase().includes('mla')) {
|
||||
groups['Provincial'].push(rep);
|
||||
} else if (setName.toLowerCase().includes('municipal') ||
|
||||
setName.toLowerCase().includes('city council') ||
|
||||
setName.toLowerCase().includes('mayor') ||
|
||||
office.toLowerCase().includes('councillor') ||
|
||||
office.toLowerCase().includes('mayor')) {
|
||||
groups['Municipal'].push(rep);
|
||||
} else if (setName.toLowerCase().includes('school') ||
|
||||
office.toLowerCase().includes('school') ||
|
||||
office.toLowerCase().includes('trustee')) {
|
||||
groups['School Board'].push(rep);
|
||||
} else {
|
||||
groups['Other'].push(rep);
|
||||
}
|
||||
});
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
renderRepresentativeCategory(categoryName, representatives) {
|
||||
const cards = representatives.map(rep => this.renderRepresentativeCard(rep)).join('');
|
||||
|
||||
return `
|
||||
<div class="rep-category">
|
||||
<h3>${categoryName} Representatives</h3>
|
||||
<div class="rep-cards">
|
||||
${cards}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
renderRepresentativeCard(rep) {
|
||||
const name = rep.name || 'Name not available';
|
||||
const email = rep.email || null;
|
||||
const office = rep.elected_office || 'Office not specified';
|
||||
const district = rep.district_name || 'District not specified';
|
||||
const party = rep.party_name || 'Party not specified';
|
||||
const photoUrl = rep.photo_url || null;
|
||||
|
||||
const emailButton = email ?
|
||||
`<button class="btn btn-primary compose-email"
|
||||
data-email="${email}"
|
||||
data-name="${name}"
|
||||
data-office="${office}"
|
||||
data-district="${district}">
|
||||
Send Email
|
||||
</button>` :
|
||||
'<span class="text-muted">No email available</span>';
|
||||
|
||||
const profileUrl = rep.url ?
|
||||
`<a href="${rep.url}" target="_blank" class="btn btn-secondary">View Profile</a>` : '';
|
||||
|
||||
// Generate initials for fallback
|
||||
const initials = name.split(' ')
|
||||
.map(word => word.charAt(0))
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
.slice(0, 2);
|
||||
|
||||
const photoElement = photoUrl ?
|
||||
`<div class="rep-photo">
|
||||
<img src="${photoUrl}"
|
||||
alt="${name}"
|
||||
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';"
|
||||
loading="lazy">
|
||||
<div class="rep-photo-fallback" style="display: none;">
|
||||
${initials}
|
||||
</div>
|
||||
</div>` :
|
||||
`<div class="rep-photo">
|
||||
<div class="rep-photo-fallback">
|
||||
${initials}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
return `
|
||||
<div class="rep-card">
|
||||
${photoElement}
|
||||
<div class="rep-content">
|
||||
<div class="rep-header">
|
||||
<h4>${name}</h4>
|
||||
</div>
|
||||
<div class="rep-info">
|
||||
<p><strong>Office:</strong> ${office}</p>
|
||||
<p><strong>District:</strong> ${district}</p>
|
||||
${party !== 'Party not specified' ? `<p><strong>Party:</strong> ${party}</p>` : ''}
|
||||
${email ? `<p><strong>Email:</strong> ${email}</p>` : ''}
|
||||
</div>
|
||||
<div class="rep-actions">
|
||||
${emailButton}
|
||||
${profileUrl}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
attachEventListeners() {
|
||||
// Add event listeners for compose email buttons
|
||||
const composeButtons = this.container.querySelectorAll('.compose-email');
|
||||
composeButtons.forEach(button => {
|
||||
button.addEventListener('click', (e) => {
|
||||
const email = e.target.dataset.email;
|
||||
const name = e.target.dataset.name;
|
||||
const office = e.target.dataset.office;
|
||||
const district = e.target.dataset.district;
|
||||
|
||||
window.emailComposer.openModal({
|
||||
email,
|
||||
name,
|
||||
office,
|
||||
district
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.representativesDisplay = new RepresentativesDisplay();
|
||||
});
|
||||
Reference in New Issue
Block a user