tonne of imporvements, debugs, new ui

This commit is contained in:
2025-09-20 15:58:55 -06:00
parent f49306a96f
commit b6960e45e8
13 changed files with 673 additions and 87 deletions

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Panel - Alberta Influence Campaign Tool</title>
<title>Admin Panel - BNKops Influence Campaign Tool</title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="css/styles.css">
<style>
@@ -286,6 +286,187 @@
cursor: pointer;
}
/* Enhanced Form Styling */
.form-group label {
display: block;
font-weight: 600;
color: #2c3e50;
margin-bottom: 0.5rem;
font-size: 0.95rem;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 0.75rem;
border: 2px solid #e0e6ed;
border-radius: 8px;
font-size: 0.95rem;
transition: border-color 0.3s, box-shadow 0.3s;
background: #f8f9fa;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
background: white;
}
/* Enhanced Status Select */
.status-select {
position: relative;
}
.status-select select {
appearance: none;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-weight: 600;
padding: 1rem 3rem 1rem 1rem;
font-size: 1rem;
border: none;
cursor: pointer;
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
}
.status-select select option {
background: white;
color: #2c3e50;
font-weight: 500;
padding: 0.75rem;
text-shadow: none;
}
.status-select select option:hover,
.status-select select option:checked {
background: #3498db;
color: white;
}
.status-select::after {
content: '▼';
position: absolute;
right: 1rem;
top: 50%;
transform: translateY(-50%);
color: white;
pointer-events: none;
font-size: 0.8rem;
}
.status-select select:focus {
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.3);
}
/* Enhanced Checkbox Styling */
.checkbox-group {
background: #f8f9fa;
padding: 1.5rem;
border-radius: 12px;
border: 2px solid #e9ecef;
margin-top: 0.5rem;
}
.checkbox-item {
background: white;
padding: 1rem;
border-radius: 8px;
border: 2px solid #e9ecef;
margin-bottom: 0.75rem;
transition: all 0.3s;
cursor: pointer;
}
.checkbox-item:hover {
border-color: #3498db;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(52, 152, 219, 0.15);
}
.checkbox-item input[type="checkbox"] {
width: auto;
margin-right: 0.75rem;
transform: scale(1.2);
accent-color: #3498db;
}
.checkbox-item label {
margin: 0;
font-weight: 500;
color: #495057;
cursor: pointer;
flex: 1;
}
.checkbox-item:has(input:checked) {
background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%);
border-color: #3498db;
}
.checkbox-item:has(input:checked) label {
color: #2c3e50;
font-weight: 600;
}
/* Enhanced Form Grid */
.form-grid {
background: white;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
margin-bottom: 2rem;
}
/* Section Headers */
.section-header {
color: #2c3e50;
font-size: 1.1rem;
font-weight: 600;
margin: 2rem 0 1rem 0;
padding-bottom: 0.5rem;
border-bottom: 2px solid #e9ecef;
}
/* Enhanced Buttons */
.btn {
padding: 1rem 2rem;
border: none;
border-radius: 8px;
cursor: pointer;
text-decoration: none;
display: inline-block;
font-size: 1rem;
font-weight: 600;
transition: all 0.3s;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
transform: translateY(-1px);
}
@media (max-width: 768px) {
.admin-nav {
flex-direction: column;
@@ -324,7 +505,6 @@
<div id="campaigns-tab" class="tab-content active">
<div class="form-row" style="align-items: center; margin-bottom: 2rem;">
<h2 style="margin: 0;">Active Campaigns</h2>
<button class="btn btn-primary" data-action="create-campaign">Create New Campaign</button>
</div>
<div id="campaigns-loading" class="loading hidden">
@@ -349,6 +529,16 @@
placeholder="Save Alberta Parks">
</div>
<div class="form-group status-select">
<label for="create-status">Campaign Status</label>
<select id="create-status" name="status">
<option value="draft">📝 Draft</option>
<option value="active">🚀 Active</option>
<option value="paused">⏸️ Paused</option>
<option value="archived">📦 Archived</option>
</select>
</div>
<div class="form-group">
<label for="create-description">Description</label>
<textarea id="create-description" name="description" rows="3"
@@ -379,46 +569,46 @@ Sincerely,
placeholder="Join thousands of Albertans in protecting our provincial parks. Send an email to your representatives today!"></textarea>
</div>
<div class="section-header">⚙️ Campaign Settings</div>
<div class="form-group">
<label>Campaign Settings</label>
<div class="checkbox-group">
<div class="checkbox-item">
<input type="checkbox" id="create-allow-smtp" name="allow_smtp_email" checked>
<label for="create-allow-smtp">Allow SMTP Email Sending</label>
<label for="create-allow-smtp">📧 Allow SMTP Email Sending</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="create-allow-mailto" name="allow_mailto_link" checked>
<label for="create-allow-mailto">Allow Mailto Links</label>
<label for="create-allow-mailto">🔗 Allow Mailto Links</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="create-collect-info" name="collect_user_info" checked>
<label for="create-collect-info">Collect User Information</label>
<label for="create-collect-info">👤 Collect User Information</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="create-show-count" name="show_email_count" checked>
<label for="create-show-count">Show Email Count</label>
<label for="create-show-count">📊 Show Email Count</label>
</div>
</div>
</div>
<div class="section-header">🏛️ Target Government Levels</div>
<div class="form-group">
<label>Target Government Levels</label>
<div class="checkbox-group">
<div class="checkbox-item">
<input type="checkbox" id="create-federal" name="target_government_levels" value="Federal" checked>
<label for="create-federal">Federal (MPs)</label>
<label for="create-federal">🍁 Federal (MPs)</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="create-provincial" name="target_government_levels" value="Provincial" checked>
<label for="create-provincial">Provincial (MLAs)</label>
<label for="create-provincial">🏛️ Provincial (MLAs)</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="create-municipal" name="target_government_levels" value="Municipal" checked>
<label for="create-municipal">Municipal (Mayors, Councillors)</label>
<label for="create-municipal">🏙️ Municipal (Mayors, Councillors)</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="create-school" name="target_government_levels" value="School Board">
<label for="create-school">School Board</label>
<label for="create-school">🎓 School Board</label>
</div>
</div>
</div>

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title id="page-title">Campaign - Alberta Influence Tool</title>
<title id="page-title">Campaign - BNKops Influence Tool</title>
<link rel="stylesheet" href="/css/styles.css">
<style>
.campaign-header {
@@ -266,12 +266,12 @@
<div id="optional-fields" style="display: none;">
<div class="form-group">
<label for="user-name">Your Name (Optional)</label>
<label for="user-name">Your Name</label>
<input type="text" id="user-name" name="userName" placeholder="Your full name">
</div>
<div class="form-group">
<label for="user-email">Your Email (Optional)</label>
<label for="user-email">Your Email (Optional - If you would like a reply)</label>
<input type="email" id="user-email" name="userEmail" placeholder="your@email.com">
</div>
</div>

View File

@@ -37,6 +37,75 @@ header p {
font-size: 1.1em;
}
/* Buttons */
.btn {
display: inline-block;
padding: 10px 16px;
margin: 2px;
font-size: 14px;
font-weight: 500;
line-height: 1.5;
text-align: center;
text-decoration: none;
white-space: nowrap;
vertical-align: middle;
cursor: pointer;
border: 1px solid transparent;
border-radius: 4px;
transition: all 0.2s ease-in-out;
user-select: none;
}
.btn:hover {
text-decoration: none;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.btn:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.btn-primary {
color: white;
background-color: #005a9c;
border-color: #005a9c;
}
.btn-primary:hover {
background-color: #004a7c;
border-color: #004a7c;
}
.btn-secondary {
color: #005a9c;
background-color: white;
border-color: #005a9c;
}
.btn-secondary:hover {
color: white;
background-color: #005a9c;
border-color: #005a9c;
}
.btn-success {
color: white;
background-color: #28a745;
border-color: #28a745;
}
.btn-success:hover {
background-color: #218838;
border-color: #1e7e34;
}
.text-muted {
color: #6c757d;
font-style: italic;
}
/* Forms */
.form-group {
margin-bottom: 20px;
@@ -278,6 +347,17 @@ header p {
flex-wrap: wrap;
}
.call-representative {
display: inline-flex;
align-items: center;
gap: 6px;
}
.call-representative:hover {
background-color: #218838;
border-color: #1e7e34;
}
/* Modal Styles */
.modal {
position: fixed;

View File

@@ -0,0 +1,5 @@
<!-- Minimal favicon to prevent 404 errors -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<circle cx="16" cy="16" r="14" fill="#007bff"/>
<text x="16" y="20" text-anchor="middle" fill="white" font-family="Arial" font-size="16" font-weight="bold">I</text>
</svg>

After

Width:  |  Height:  |  Size: 283 B

View File

@@ -3,14 +3,14 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Alberta Influence Campaign Tool</title>
<title>BNKops Influence Campaign Tool</title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<div class="container">
<header>
<h1>Alberta Influence Campaign Tool</h1>
<h1>BNKops Influence Tool</h1>
<p>Connect with your elected representatives across all levels of government</p>
</header>
@@ -20,7 +20,7 @@
<h2>Find Your Representatives</h2>
<form id="postal-form">
<div class="form-group">
<label for="postal-code">Enter your Alberta postal code:</label>
<label for="postal-code">Enter your postal code:</label>
<div class="input-group">
<input
type="text"

View File

@@ -49,8 +49,8 @@ class CampaignPage {
renderCampaign() {
// Update page title and header
document.title = `${this.campaign.title} - Alberta Influence Tool`;
document.getElementById('page-title').textContent = `${this.campaign.title} - Alberta Influence Tool`;
document.title = `${this.campaign.title} - BNKops Influence Tool`;
document.getElementById('page-title').textContent = `${this.campaign.title} - BNKops Influence Tool`;
document.getElementById('campaign-title').textContent = this.campaign.title;
document.getElementById('campaign-description').textContent = this.campaign.description;
@@ -74,6 +74,15 @@ class CampaignPage {
// Set up email method options
this.setupEmailMethodOptions();
// Show optional fields if user info collection is enabled
if (this.campaign.collect_user_info) {
const optionalFields = document.getElementById('optional-fields');
if (optionalFields) {
optionalFields.style.display = 'block';
console.log('Showing optional user info fields');
}
}
// Set initial step
this.setStep(1);
}
@@ -131,9 +140,38 @@ class CampaignPage {
userEmail: formData.get('userEmail') || ''
};
// Track user info when they click "Find My Representatives"
await this.trackUserInfo();
await this.loadRepresentatives();
}
async trackUserInfo() {
try {
const response = await fetch(`/api/campaigns/${this.campaignSlug}/track-user`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
userEmail: this.userInfo.userEmail,
userName: this.userInfo.userName,
postalCode: this.userInfo.postalCode
})
});
const data = await response.json();
if (!data.success) {
console.warn('Failed to track user info:', data.error);
// Don't throw error - this is just tracking, shouldn't block the user
}
} catch (error) {
console.warn('Failed to track user info:', error.message);
// Don't throw error - this is just tracking, shouldn't block the user
}
}
async loadRepresentatives() {
this.showLoading('Finding your representatives...');
@@ -180,10 +218,11 @@ class CampaignPage {
<p>${rep.elected_office || 'Representative'}</p>
<p>${rep.party_name || ''}</p>
${rep.email ? `<p>📧 ${rep.email}</p>` : ''}
${this.getPhoneNumber(rep) ? `<p>📞 ${this.getPhoneNumber(rep)}</p>` : ''}
</div>
</div>
${rep.email ? `
<div class="rep-actions">
<div class="rep-actions">
${rep.email ? `
<button class="btn btn-primary" data-action="send-email"
data-email="${rep.email}"
data-name="${rep.name}"
@@ -191,8 +230,18 @@ class CampaignPage {
data-level="${this.getGovernmentLevel(rep)}">
Send Email
</button>
</div>
` : '<p style="text-align: center; color: #6c757d;">No email available</p>'}
` : ''}
${this.getPhoneNumber(rep) ? `
<button class="btn btn-success" data-action="call-representative"
data-phone="${this.getPhoneNumber(rep)}"
data-name="${rep.name}"
data-title="${rep.elected_office || ''}"
data-office-type="${this.getPhoneOfficeType(rep)}">
📞 Call
</button>
` : ''}
${!rep.email && !this.getPhoneNumber(rep) ? '<p style="text-align: center; color: #6c757d;">No contact information available</p>' : ''}
</div>
</div>
`).join('');
@@ -214,6 +263,17 @@ class CampaignPage {
});
});
// Call buttons
document.querySelectorAll('[data-action="call-representative"]').forEach(btn => {
btn.addEventListener('click', (e) => {
const phone = e.target.dataset.phone;
const name = e.target.dataset.name;
const title = e.target.dataset.title;
const officeType = e.target.dataset.officeType;
this.callRepresentative(phone, name, title, officeType);
});
});
// Reload page button
const reloadBtn = document.querySelector('[data-action="reload-page"]');
if (reloadBtn) {
@@ -232,6 +292,67 @@ class CampaignPage {
return 'Other';
}
getPhoneNumber(rep) {
if (!rep.offices || !Array.isArray(rep.offices)) {
return null;
}
// Find the first office with a phone number
const officeWithPhone = rep.offices.find(office => office.tel);
return officeWithPhone ? officeWithPhone.tel : null;
}
getPhoneOfficeType(rep) {
if (!rep.offices || !Array.isArray(rep.offices)) {
return 'office';
}
const officeWithPhone = rep.offices.find(office => office.tel);
return officeWithPhone ? (officeWithPhone.type || 'office') : 'office';
}
callRepresentative(phone, name, title, officeType) {
// Clean the phone number for tel: link (remove spaces, dashes, parentheses)
const cleanPhone = phone.replace(/[\s\-\(\)]/g, '');
// Create tel: link
const telLink = `tel:${cleanPhone}`;
// Show confirmation dialog with formatted information
const officeInfo = officeType ? ` (${officeType} office)` : '';
const message = `Call ${name}${title ? ` - ${title}` : ''}${officeInfo}?\n\nPhone: ${phone}`;
if (confirm(message)) {
// Attempt to initiate the call
window.location.href = telLink;
// Track the call attempt
this.trackCall(phone, name, title, officeType);
}
}
async trackCall(phone, name, title, officeType) {
try {
await fetch(`/api/campaigns/${this.campaignSlug}/track-call`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
userEmail: this.userInfo.userEmail,
userName: this.userInfo.userName,
postalCode: this.userInfo.postalCode,
recipientPhone: phone,
recipientName: name,
recipientTitle: title,
officeType: officeType
})
});
} catch (error) {
console.error('Failed to track call:', error);
}
}
async sendEmail(recipientEmail, recipientName, recipientTitle, recipientLevel) {
const emailMethod = document.querySelector('input[name="emailMethod"]:checked').value;

View File

@@ -107,6 +107,10 @@ class RepresentativesDisplay {
const party = rep.party_name || 'Party not specified';
const photoUrl = rep.photo_url || null;
// Extract phone numbers from offices array
const phoneNumbers = this.extractPhoneNumbers(rep.offices || []);
const primaryPhone = phoneNumbers.length > 0 ? phoneNumbers[0] : null;
const emailButton = email ?
`<button class="btn btn-primary compose-email"
data-email="${email}"
@@ -117,6 +121,16 @@ class RepresentativesDisplay {
</button>` :
'<span class="text-muted">No email available</span>';
// Add call button if phone number is available
const callButton = primaryPhone ?
`<button class="btn btn-success call-representative"
data-phone="${primaryPhone.number}"
data-office-type="${primaryPhone.type}"
data-name="${name}"
data-office="${office}">
📞 Call
</button>` : '';
const profileUrl = rep.url ?
`<a href="${rep.url}" target="_blank" class="btn btn-secondary">View Profile</a>` : '';
@@ -155,9 +169,11 @@ class RepresentativesDisplay {
<p><strong>District:</strong> ${district}</p>
${party !== 'Party not specified' ? `<p><strong>Party:</strong> ${party}</p>` : ''}
${email ? `<p><strong>Email:</strong> ${email}</p>` : ''}
${primaryPhone ? `<p><strong>Phone:</strong> ${primaryPhone.number} ${primaryPhone.type ? `(${primaryPhone.type})` : ''}</p>` : ''}
</div>
<div class="rep-actions">
${emailButton}
${callButton}
${profileUrl}
</div>
</div>
@@ -165,6 +181,23 @@ class RepresentativesDisplay {
`;
}
extractPhoneNumbers(offices) {
const phoneNumbers = [];
if (Array.isArray(offices)) {
offices.forEach(office => {
if (office.tel) {
phoneNumbers.push({
number: office.tel,
type: office.type || 'office'
});
}
});
}
return phoneNumbers;
}
attachEventListeners() {
// Add event listeners for compose email buttons
const composeButtons = this.container.querySelectorAll('.compose-email');
@@ -183,6 +216,36 @@ class RepresentativesDisplay {
});
});
});
// Add event listeners for call buttons
const callButtons = this.container.querySelectorAll('.call-representative');
callButtons.forEach(button => {
button.addEventListener('click', (e) => {
const phone = e.target.dataset.phone;
const name = e.target.dataset.name;
const office = e.target.dataset.office;
const officeType = e.target.dataset.officeType;
this.handleCallClick(phone, name, office, officeType);
});
});
}
handleCallClick(phone, name, office, officeType) {
// Clean the phone number for tel: link (remove spaces, dashes, parentheses)
const cleanPhone = phone.replace(/[\s\-\(\)]/g, '');
// Create tel: link
const telLink = `tel:${cleanPhone}`;
// Show confirmation dialog with formatted information
const officeInfo = officeType ? ` (${officeType} office)` : '';
const message = `Call ${name}${officeInfo}?\n\nPhone: ${phone}`;
if (confirm(message)) {
// Attempt to initiate the call
window.location.href = telLink;
}
}
}

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Login - Alberta Influence Campaign Tool</title>
<title>Admin Login - BNKops Influence Campaign Tool</title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="css/styles.css">
<style>