Updates to the html for url construction throughout and a bunch of upgrades to how the response wall works
This commit is contained in:
parent
9da13d6d3d
commit
b71a6e4ff3
12
README.md
12
README.md
@ -96,4 +96,14 @@ Complete documentation is available in the MkDocs site, including:
|
|||||||
- Map application setup and usage
|
- Map application setup and usage
|
||||||
- Troubleshooting guides
|
- Troubleshooting guides
|
||||||
|
|
||||||
Visit http://localhost:4000 after starting services to access the full documentation.
|
Visit http://localhost:4000 after starting services to access the full documentation.
|
||||||
|
|
||||||
|
## Licensing
|
||||||
|
|
||||||
|
This project is licensed under the Apache License 2.0 - https://opensource.org/license/apache-2-0
|
||||||
|
|
||||||
|
## AI Disclaimer
|
||||||
|
|
||||||
|
This project used AI tools to assist in its creation and large amounts of the boilerplate code was reviewed using AI. AI tools (although not activated or connected) are pre-installed in the Coder docker image. See `docker.code-server` for more details.
|
||||||
|
|
||||||
|
While these tools can help generate code and documentation, they may also introduce errors or inaccuracies. Users should review and test all content to ensure it meets their requirements and standards.
|
||||||
@ -1,191 +0,0 @@
|
|||||||
# Response Wall Admin Panel - Inline Handler Fix Summary
|
|
||||||
|
|
||||||
## Issue
|
|
||||||
Content Security Policy (CSP) violation when clicking buttons in the Response Moderation tab of the admin panel.
|
|
||||||
|
|
||||||
## Error Messages
|
|
||||||
```
|
|
||||||
Refused to execute inline event handler because it violates the following Content Security Policy directive: "script-src-attr 'none'"
|
|
||||||
TypeError: window.apiClient.patch is not a function
|
|
||||||
```
|
|
||||||
|
|
||||||
## Root Causes
|
|
||||||
|
|
||||||
### 1. Inline Event Handlers (CSP Violation)
|
|
||||||
The admin panel's Response Moderation tab was using inline `onclick` handlers:
|
|
||||||
```javascript
|
|
||||||
<button onclick="adminPanel.approveResponse(${response.id})">✓ Approve</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
This violates:
|
|
||||||
- Browser Content Security Policy (CSP)
|
|
||||||
- Project development guidelines in `instruct.md`: **"No inline event handlers. Always use addEventListener in JS files."**
|
|
||||||
|
|
||||||
### 2. Missing API Client Methods
|
|
||||||
The `APIClient` class only had `get()` and `post()` methods, but admin operations needed `patch()`, `put()`, and `delete()`.
|
|
||||||
|
|
||||||
## Solutions Implemented
|
|
||||||
|
|
||||||
### Fix 1: Removed All Inline Handlers in admin.js
|
|
||||||
|
|
||||||
**Before:**
|
|
||||||
```javascript
|
|
||||||
<button class="btn btn-success btn-sm" onclick="adminPanel.approveResponse(${response.id})">
|
|
||||||
✓ Approve
|
|
||||||
</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
**After:**
|
|
||||||
```javascript
|
|
||||||
<button class="btn btn-success btn-sm"
|
|
||||||
data-action="approve-response"
|
|
||||||
data-response-id="${response.id}">
|
|
||||||
✓ Approve
|
|
||||||
</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Fix 2: Added Event Delegation
|
|
||||||
|
|
||||||
Created new `setupResponseActionListeners()` method in `AdminPanel` class:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
setupResponseActionListeners() {
|
|
||||||
const container = document.getElementById('admin-responses-container');
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
// Remove old listener if exists to avoid duplicates
|
|
||||||
const oldListener = container._responseActionListener;
|
|
||||||
if (oldListener) {
|
|
||||||
container.removeEventListener('click', oldListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new listener with event delegation
|
|
||||||
const listener = (e) => {
|
|
||||||
const target = e.target;
|
|
||||||
const action = target.dataset.action;
|
|
||||||
const responseId = target.dataset.responseId;
|
|
||||||
|
|
||||||
if (!action || !responseId) return;
|
|
||||||
|
|
||||||
switch (action) {
|
|
||||||
case 'approve-response':
|
|
||||||
this.approveResponse(parseInt(responseId));
|
|
||||||
break;
|
|
||||||
case 'reject-response':
|
|
||||||
this.rejectResponse(parseInt(responseId));
|
|
||||||
break;
|
|
||||||
case 'verify-response':
|
|
||||||
const isVerified = target.dataset.verified === 'true';
|
|
||||||
this.toggleVerified(parseInt(responseId), isVerified);
|
|
||||||
break;
|
|
||||||
case 'delete-response':
|
|
||||||
this.deleteResponse(parseInt(responseId));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Store listener reference and add it
|
|
||||||
container._responseActionListener = listener;
|
|
||||||
container.addEventListener('click', listener);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This method is called at the end of `renderAdminResponses()` to set up listeners after the HTML is rendered.
|
|
||||||
|
|
||||||
### Fix 3: Added Missing HTTP Methods to api-client.js
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
async put(endpoint, data) {
|
|
||||||
return this.makeRequest(endpoint, {
|
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async patch(endpoint, data) {
|
|
||||||
return this.makeRequest(endpoint, {
|
|
||||||
method: 'PATCH',
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(endpoint) {
|
|
||||||
return this.makeRequest(endpoint, {
|
|
||||||
method: 'DELETE'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Buttons Fixed
|
|
||||||
|
|
||||||
All response moderation buttons now use proper event delegation:
|
|
||||||
|
|
||||||
1. **Approve** - `data-action="approve-response"`
|
|
||||||
2. **Reject** - `data-action="reject-response"`
|
|
||||||
3. **Mark as Verified** - `data-action="verify-response" data-verified="true"`
|
|
||||||
4. **Remove Verification** - `data-action="verify-response" data-verified="false"`
|
|
||||||
5. **Unpublish** - `data-action="reject-response"` (reuses reject action)
|
|
||||||
6. **Delete** - `data-action="delete-response"`
|
|
||||||
|
|
||||||
## Files Modified
|
|
||||||
|
|
||||||
1. **app/public/js/admin.js**
|
|
||||||
- Modified `renderAdminResponses()` - Replaced inline onclick with data attributes
|
|
||||||
- Added `setupResponseActionListeners()` - Event delegation implementation
|
|
||||||
- Total changes: ~85 lines modified/added
|
|
||||||
|
|
||||||
2. **app/public/js/api-client.js**
|
|
||||||
- Added `put()` method
|
|
||||||
- Added `patch()` method
|
|
||||||
- Added `delete()` method
|
|
||||||
- Total changes: ~18 lines added
|
|
||||||
|
|
||||||
## Benefits of This Approach
|
|
||||||
|
|
||||||
### 1. Security
|
|
||||||
- ✅ Complies with Content Security Policy (CSP)
|
|
||||||
- ✅ Prevents script injection attacks
|
|
||||||
- ✅ Follows modern web security best practices
|
|
||||||
|
|
||||||
### 2. Performance
|
|
||||||
- ✅ Single event listener instead of N listeners (one per button)
|
|
||||||
- ✅ Better memory usage
|
|
||||||
- ✅ Faster page rendering
|
|
||||||
|
|
||||||
### 3. Maintainability
|
|
||||||
- ✅ Follows project coding standards (instruct.md)
|
|
||||||
- ✅ Centralized event handling logic
|
|
||||||
- ✅ Easy to add new actions without HTML changes
|
|
||||||
|
|
||||||
### 4. Reliability
|
|
||||||
- ✅ Prevents duplicate listeners
|
|
||||||
- ✅ Clean listener removal/re-attachment
|
|
||||||
- ✅ Works with dynamically rendered content
|
|
||||||
|
|
||||||
## Testing Checklist
|
|
||||||
|
|
||||||
- [x] Load admin panel → Response Moderation tab
|
|
||||||
- [x] Click "Approve" button → Should approve response without CSP error
|
|
||||||
- [x] Click "Reject" button → Should reject response
|
|
||||||
- [x] Click "Mark as Verified" → Should add verification badge
|
|
||||||
- [x] Click "Remove Verification" → Should remove badge
|
|
||||||
- [x] Click "Delete" → Should delete response after confirmation
|
|
||||||
- [x] Filter responses by status → Should reload list
|
|
||||||
- [x] No console errors related to inline handlers
|
|
||||||
- [x] No CSP violations
|
|
||||||
|
|
||||||
## Lessons Learned
|
|
||||||
|
|
||||||
1. **Always Follow Project Guidelines**: The instruct.md file explicitly prohibits inline event handlers - following it from the start would have prevented this issue
|
|
||||||
|
|
||||||
2. **Complete API Client**: When building a REST client, implement all HTTP methods (GET, POST, PUT, PATCH, DELETE) from the beginning
|
|
||||||
|
|
||||||
3. **Event Delegation for Dynamic Content**: When rendering content dynamically with buttons/links, always use event delegation on a parent container
|
|
||||||
|
|
||||||
4. **CSP is Your Friend**: Content Security Policy errors point to real security issues - fix them rather than disabling CSP
|
|
||||||
|
|
||||||
## Related Documentation
|
|
||||||
|
|
||||||
- See `instruct.md` - Development Rules section: "No inline event handlers"
|
|
||||||
- See `RESPONSE_WALL_FIXES.md` - Full list of all Response Wall bug fixes
|
|
||||||
- See `RESPONSE_WALL_USAGE.md` - How to use the Response Wall feature
|
|
||||||
@ -1,181 +0,0 @@
|
|||||||
# Response Wall Bug Fixes
|
|
||||||
|
|
||||||
## Issues Identified and Fixed
|
|
||||||
|
|
||||||
### 1. **TypeError: responses.filter is not a function**
|
|
||||||
**Error Location**: `app/controllers/responses.js:292` in `getResponseStats()`
|
|
||||||
|
|
||||||
**Root Cause**: The `getRepresentativeResponses()` method in `nocodb.js` was returning the raw NocoDB API response object `{list: [...], pageInfo: {...}}` instead of an array. The controller code expected an array and tried to call `.filter()` on an object.
|
|
||||||
|
|
||||||
**Fix Applied**: Modified `getRepresentativeResponses()` to extract the `list` array from the response and normalize each item:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
async getRepresentativeResponses(params = {}) {
|
|
||||||
if (!this.tableIds.representativeResponses) {
|
|
||||||
throw new Error('Representative responses table not configured');
|
|
||||||
}
|
|
||||||
const result = await this.getAll(this.tableIds.representativeResponses, params);
|
|
||||||
// NocoDB returns {list: [...]} or {pageInfo: {...}, list: [...]}
|
|
||||||
const list = result.list || [];
|
|
||||||
return list.map(item => this.normalizeResponse(item));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. **TypeError: responses.map is not a function**
|
|
||||||
**Error Location**: `app/controllers/responses.js:51` in `getCampaignResponses()`
|
|
||||||
|
|
||||||
**Root Cause**: Same as issue #1 - the method was returning an object instead of an array.
|
|
||||||
|
|
||||||
**Fix Applied**: Same fix as above ensures an array is always returned.
|
|
||||||
|
|
||||||
### 3. **Database Error: "A value is required for this field" (code 23502)**
|
|
||||||
**Error Location**: NocoDB database constraint violation when creating a response
|
|
||||||
|
|
||||||
**Root Cause**: The `campaign_id` field was being set to `null` or `undefined`. Investigation revealed that:
|
|
||||||
- The campaign object from NocoDB uses `Id` (capital I) as the primary key field
|
|
||||||
- The controller was trying to access `campaign.id` (lowercase) which returned `undefined`
|
|
||||||
- NocoDB's representative_responses table has `campaign_id` marked as required (`"rqd": true`)
|
|
||||||
|
|
||||||
**Fix Applied**:
|
|
||||||
1. Updated `submitResponse()` controller to check multiple possible field names:
|
|
||||||
```javascript
|
|
||||||
campaign_id: campaign.Id || campaign.id || campaign['Campaign ID'],
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Added validation in `createRepresentativeResponse()` to fail fast if campaign_id is missing:
|
|
||||||
```javascript
|
|
||||||
if (!responseData.campaign_id) {
|
|
||||||
throw new Error('Campaign ID is required for creating a response');
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Added debug logging to track the campaign_id value:
|
|
||||||
```javascript
|
|
||||||
console.log('Submitting response with campaign_id:', newResponse.campaign_id, 'from campaign:', campaign);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. **Array Handling in Response Upvotes**
|
|
||||||
**Potential Issue**: The `getResponseUpvotes()` method had the same array vs object issue
|
|
||||||
|
|
||||||
**Fix Applied**: Updated the method to return a normalized array:
|
|
||||||
```javascript
|
|
||||||
async getResponseUpvotes(params = {}) {
|
|
||||||
if (!this.tableIds.responseUpvotes) {
|
|
||||||
throw new Error('Response upvotes table not configured');
|
|
||||||
}
|
|
||||||
const result = await this.getAll(this.tableIds.responseUpvotes, params);
|
|
||||||
// NocoDB returns {list: [...]} or {pageInfo: {...}, list: [...]}
|
|
||||||
const list = result.list || [];
|
|
||||||
return list.map(item => this.normalizeUpvote(item));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Files Modified
|
|
||||||
|
|
||||||
1. **app/services/nocodb.js**
|
|
||||||
- Modified `getRepresentativeResponses()` - Extract and normalize list
|
|
||||||
- Modified `getResponseUpvotes()` - Extract and normalize list
|
|
||||||
- Modified `createRepresentativeResponse()` - Add campaign_id validation and logging
|
|
||||||
|
|
||||||
2. **app/controllers/responses.js**
|
|
||||||
- Modified `submitResponse()` - Handle multiple campaign ID field name variations (ID, Id, id)
|
|
||||||
- Added debug logging for campaign_id
|
|
||||||
|
|
||||||
3. **app/public/js/admin.js**
|
|
||||||
- Modified `renderAdminResponses()` - Removed all inline onclick handlers, replaced with data-action attributes
|
|
||||||
- Added `setupResponseActionListeners()` - Event delegation for response moderation buttons
|
|
||||||
- Follows instruct.md guidelines: "No inline event handlers. Always use addEventListener in JS files."
|
|
||||||
|
|
||||||
4. **app/public/js/api-client.js**
|
|
||||||
- Added `put()` method for HTTP PUT requests
|
|
||||||
- Added `patch()` method for HTTP PATCH requests
|
|
||||||
- Added `delete()` method for HTTP DELETE requests
|
|
||||||
- These methods were missing and causing errors in admin panel operations
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
After applying these fixes, test the following:
|
|
||||||
|
|
||||||
1. **Load Response Wall** - Visit `http://localhost:3333/response-wall.html?campaign=test-page`
|
|
||||||
- Stats should load without errors
|
|
||||||
- Response list should load without errors
|
|
||||||
|
|
||||||
2. **Submit Response** - Fill out and submit the response form
|
|
||||||
- Should successfully create a response in pending status
|
|
||||||
- Should return a success message
|
|
||||||
- Check logs for "Submitting response with campaign_id: [number]"
|
|
||||||
|
|
||||||
3. **Upvote Response** - Click the upvote button on an approved response
|
|
||||||
- Should increment the upvote count
|
|
||||||
- Should prevent duplicate upvotes
|
|
||||||
|
|
||||||
4. **Admin Moderation** - Visit `http://localhost:3333/admin.html` → Response Moderation tab
|
|
||||||
- Should see pending responses
|
|
||||||
- Should be able to approve/reject responses
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
The application container has been restarted with:
|
|
||||||
```bash
|
|
||||||
docker compose restart app
|
|
||||||
```
|
|
||||||
|
|
||||||
All fixes are now live and ready for testing.
|
|
||||||
|
|
||||||
## Root Cause Analysis
|
|
||||||
|
|
||||||
The main issue was a misunderstanding of NocoDB's API response structure:
|
|
||||||
- **Expected**: Array of records directly
|
|
||||||
- **Actual**: Object with `{list: [records], pageInfo: {...}}`
|
|
||||||
|
|
||||||
This is a common pattern in REST APIs for pagination support. The fix ensures all service methods return properly normalized arrays for consistent usage throughout the application.
|
|
||||||
|
|
||||||
The secondary issue was field naming inconsistency:
|
|
||||||
- **NocoDB Primary Key**: Uses `ID` (all caps) not `Id` or `id`
|
|
||||||
- **Application Code**: Expected `id` (lowercase)
|
|
||||||
|
|
||||||
The fix handles all three variations to ensure compatibility: `campaign.ID || campaign.Id || campaign.id`
|
|
||||||
|
|
||||||
### 5. **CSP Violation: Inline Event Handlers in Admin Panel**
|
|
||||||
**Error**: "Refused to execute inline event handler because it violates the following Content Security Policy directive: 'script-src-attr 'none''"
|
|
||||||
|
|
||||||
**Root Cause**: The `renderAdminResponses()` method in admin.js was using inline `onclick` handlers like:
|
|
||||||
```javascript
|
|
||||||
<button onclick="adminPanel.approveResponse(${response.id})">
|
|
||||||
```
|
|
||||||
|
|
||||||
This violates the development guidelines in instruct.md which explicitly state: **"No inline event handlers. Always use addEventListener in JS files."**
|
|
||||||
|
|
||||||
**Fix Applied**:
|
|
||||||
- Replaced all inline onclick handlers with data-action attributes
|
|
||||||
- Created `setupResponseActionListeners()` method using event delegation
|
|
||||||
- All buttons now use pattern: `data-action="approve-response" data-response-id="${response.id}"`
|
|
||||||
- Event delegation listens on container and routes actions based on data attributes
|
|
||||||
|
|
||||||
### 6. **Missing HTTP Methods in API Client**
|
|
||||||
**Error**: "TypeError: window.apiClient.patch is not a function"
|
|
||||||
|
|
||||||
**Root Cause**: The APIClient class in api-client.js only had `get()` and `post()` methods. Admin panel operations needed `patch()`, `put()`, and `delete()` methods for updating and deleting responses.
|
|
||||||
|
|
||||||
**Fix Applied**: Added three new methods to APIClient:
|
|
||||||
```javascript
|
|
||||||
async put(endpoint, data) { ... }
|
|
||||||
async patch(endpoint, data) { ... }
|
|
||||||
async delete(endpoint) { ... }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Prevention
|
|
||||||
|
|
||||||
To prevent similar issues in the future:
|
|
||||||
|
|
||||||
1. **Type Safety**: Consider adding TypeScript or JSDoc type annotations
|
|
||||||
2. **Consistent Normalization**: Always normalize data at the service layer
|
|
||||||
3. **Field Name Standards**: Document NocoDB field naming conventions
|
|
||||||
4. **Validation**: Add validation for required fields early in the request flow
|
|
||||||
5. **Logging**: Continue adding debug logging for data transformations
|
|
||||||
|
|
||||||
## Related Documentation
|
|
||||||
|
|
||||||
- See `RESPONSE_WALL_USAGE.md` for usage instructions
|
|
||||||
- See `RESPONSE_WALL_IMPLEMENTATION.md` for feature implementation details
|
|
||||||
- See `scripts/build-nocodb.sh` for database schema definitions
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,204 +0,0 @@
|
|||||||
# Response Wall - Usage Guide
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
The Response Wall is now fully implemented! It allows campaign participants to share and vote on responses they receive from elected representatives.
|
|
||||||
|
|
||||||
## How to Access the Response Wall
|
|
||||||
|
|
||||||
The Response Wall requires a campaign slug to function. You must access it with a URL parameter:
|
|
||||||
|
|
||||||
### URL Format
|
|
||||||
```
|
|
||||||
http://localhost:3333/response-wall.html?campaign=YOUR-CAMPAIGN-SLUG
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example URLs
|
|
||||||
```
|
|
||||||
http://localhost:3333/response-wall.html?campaign=climate-action
|
|
||||||
http://localhost:3333/response-wall.html?campaign=healthcare-reform
|
|
||||||
http://localhost:3333/response-wall.html?campaign=education-funding
|
|
||||||
```
|
|
||||||
|
|
||||||
## Setup Instructions
|
|
||||||
|
|
||||||
### 1. Create a Campaign First
|
|
||||||
Before using the Response Wall, you need to have an active campaign:
|
|
||||||
|
|
||||||
1. Go to http://localhost:3333/admin.html
|
|
||||||
2. Create a new campaign with a slug (e.g., "climate-action")
|
|
||||||
3. Note the campaign slug you created
|
|
||||||
|
|
||||||
### 2. Access the Response Wall
|
|
||||||
Use the campaign slug in the URL:
|
|
||||||
```
|
|
||||||
http://localhost:3333/response-wall.html?campaign=YOUR-SLUG-HERE
|
|
||||||
```
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
### For Public Users
|
|
||||||
- **View Responses**: See all approved responses from representatives
|
|
||||||
- **Filter & Sort**:
|
|
||||||
- Filter by government level (Federal, Provincial, Municipal, School Board)
|
|
||||||
- Sort by Most Recent, Most Upvoted, or Verified First
|
|
||||||
- **Submit Responses**: Share responses you've received from representatives
|
|
||||||
- Required: Representative name, level, response type, response text
|
|
||||||
- Optional: Representative title, your comment, screenshot, your name/email
|
|
||||||
- Can post anonymously
|
|
||||||
- **Upvote Responses**: Show appreciation for helpful responses
|
|
||||||
- **View Statistics**: See total responses, verified count, and total upvotes
|
|
||||||
|
|
||||||
### For Administrators
|
|
||||||
Access via the admin panel at http://localhost:3333/admin.html:
|
|
||||||
|
|
||||||
1. Navigate to the "Response Moderation" tab
|
|
||||||
2. Filter by status: Pending, Approved, Rejected, or All
|
|
||||||
3. Moderate submissions:
|
|
||||||
- **Approve**: Make response visible to public
|
|
||||||
- **Reject**: Hide inappropriate responses
|
|
||||||
- **Verify**: Add verification badge to authentic responses
|
|
||||||
- **Edit**: Modify response content if needed
|
|
||||||
- **Delete**: Remove responses permanently
|
|
||||||
|
|
||||||
## Moderation Workflow
|
|
||||||
|
|
||||||
1. User submits a response (status: "pending")
|
|
||||||
2. Admin reviews in admin panel → "Response Moderation" tab
|
|
||||||
3. Admin approves → Response appears on public Response Wall
|
|
||||||
4. Admin can mark as "verified" for authenticity badge
|
|
||||||
5. Public can upvote helpful responses
|
|
||||||
|
|
||||||
## Integration with Campaigns
|
|
||||||
|
|
||||||
### Link from Campaign Pages
|
|
||||||
You can add links to the Response Wall from your campaign pages:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<a href="/response-wall.html?campaign=YOUR-SLUG">
|
|
||||||
View Community Responses
|
|
||||||
</a>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Embed in Campaign Flow
|
|
||||||
Consider adding a "Share Your Response" call-to-action after users send emails to representatives.
|
|
||||||
|
|
||||||
## Testing the Feature
|
|
||||||
|
|
||||||
### 1. Create Test Campaign
|
|
||||||
```bash
|
|
||||||
# Via admin panel or API
|
|
||||||
POST /api/admin/campaigns
|
|
||||||
{
|
|
||||||
"title": "Test Campaign",
|
|
||||||
"slug": "test-campaign",
|
|
||||||
"email_subject": "Test",
|
|
||||||
"email_body": "Test body"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Submit Test Response
|
|
||||||
Navigate to:
|
|
||||||
```
|
|
||||||
http://localhost:3333/response-wall.html?campaign=test-campaign
|
|
||||||
```
|
|
||||||
|
|
||||||
Click "Share a Response" and fill out the form.
|
|
||||||
|
|
||||||
### 3. Moderate Response
|
|
||||||
1. Go to admin panel: http://localhost:3333/admin.html
|
|
||||||
2. Click "Response Moderation" tab
|
|
||||||
3. Change filter to "Pending"
|
|
||||||
4. Approve the test response
|
|
||||||
|
|
||||||
### 4. View Public Response
|
|
||||||
Refresh the Response Wall page to see your approved response.
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
### Public Endpoints
|
|
||||||
- `GET /api/campaigns/:slug/responses` - Get approved responses
|
|
||||||
- `GET /api/campaigns/:slug/response-stats` - Get statistics
|
|
||||||
- `POST /api/campaigns/:slug/responses` - Submit new response
|
|
||||||
- `POST /api/responses/:id/upvote` - Upvote a response
|
|
||||||
- `DELETE /api/responses/:id/upvote` - Remove upvote
|
|
||||||
|
|
||||||
### Admin Endpoints (require authentication)
|
|
||||||
- `GET /api/admin/responses` - Get all responses (any status)
|
|
||||||
- `PATCH /api/admin/responses/:id/status` - Update status
|
|
||||||
- `PATCH /api/admin/responses/:id` - Update response details
|
|
||||||
- `DELETE /api/admin/responses/:id` - Delete response
|
|
||||||
|
|
||||||
## Database Tables
|
|
||||||
|
|
||||||
The feature uses two new NocoDB tables:
|
|
||||||
|
|
||||||
### `influence_representative_responses`
|
|
||||||
Stores submitted responses with fields:
|
|
||||||
- Representative details (name, title, level)
|
|
||||||
- Response content (type, text, screenshot)
|
|
||||||
- Submitter info (name, email, anonymous flag)
|
|
||||||
- Moderation (status, verified flag)
|
|
||||||
- Engagement (upvote count)
|
|
||||||
|
|
||||||
### `influence_response_upvotes`
|
|
||||||
Tracks upvotes with fields:
|
|
||||||
- Response ID
|
|
||||||
- User ID / Email (if authenticated)
|
|
||||||
- IP address (for anonymous upvotes)
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### "No campaign specified" Error
|
|
||||||
**Problem**: Accessing `/response-wall.html` without campaign parameter
|
|
||||||
|
|
||||||
**Solution**: Add `?campaign=YOUR-SLUG` to the URL
|
|
||||||
|
|
||||||
### No Responses Showing
|
|
||||||
**Problem**: Responses exist but not visible
|
|
||||||
|
|
||||||
**Possible Causes**:
|
|
||||||
1. Responses are in "pending" status (not yet approved by admin)
|
|
||||||
2. Wrong campaign slug in URL
|
|
||||||
3. Responses filtered out by current filter settings
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
- Check admin panel → Response Moderation tab
|
|
||||||
- Verify campaign slug matches
|
|
||||||
- Clear filters (set to "All Levels")
|
|
||||||
|
|
||||||
### Button Not Working
|
|
||||||
**Problem**: "Share a Response" button doesn't open modal
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
- Check browser console for JavaScript errors
|
|
||||||
- Verify you're accessing with a valid campaign slug
|
|
||||||
- Hard refresh the page (Ctrl+F5)
|
|
||||||
|
|
||||||
### Images Not Uploading
|
|
||||||
**Problem**: Screenshot upload fails
|
|
||||||
|
|
||||||
**Possible Causes**:
|
|
||||||
1. File too large (max 5MB)
|
|
||||||
2. Wrong file type (only JPEG/PNG/GIF/WebP allowed)
|
|
||||||
3. Upload directory permissions
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
- Resize image to under 5MB
|
|
||||||
- Convert to supported format
|
|
||||||
- Check `/app/public/uploads/responses/` directory exists with write permissions
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
1. **Run Database Setup**: `./scripts/build-nocodb.sh` (if not already done)
|
|
||||||
2. **Restart Application**: `docker compose down && docker compose up --build`
|
|
||||||
3. **Create Test Campaign**: Via admin panel
|
|
||||||
4. **Test Response Submission**: Submit and moderate a test response
|
|
||||||
5. **Integrate with Campaigns**: Add Response Wall links to your campaign pages
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- All submissions require admin moderation (status: "pending" → "approved")
|
|
||||||
- Anonymous upvotes are tracked by IP address to prevent duplicates
|
|
||||||
- Authenticated upvotes are tracked by user ID
|
|
||||||
- Screenshots are stored in `/app/public/uploads/responses/`
|
|
||||||
- The feature respects NocoDB best practices (column titles, system fields, etc.)
|
|
||||||
@ -144,6 +144,7 @@ class CampaignsController {
|
|||||||
cover_photo: campaign['Cover Photo'] || campaign.cover_photo,
|
cover_photo: campaign['Cover Photo'] || campaign.cover_photo,
|
||||||
show_email_count: showEmailCount,
|
show_email_count: showEmailCount,
|
||||||
show_call_count: showCallCount,
|
show_call_count: showCallCount,
|
||||||
|
show_response_wall: campaign['Show Response Wall Button'] || campaign.show_response_wall,
|
||||||
target_government_levels: normalizedTargetLevels,
|
target_government_levels: normalizedTargetLevels,
|
||||||
created_at: campaign.CreatedAt || campaign.created_at,
|
created_at: campaign.CreatedAt || campaign.created_at,
|
||||||
emailCount,
|
emailCount,
|
||||||
@ -198,6 +199,7 @@ class CampaignsController {
|
|||||||
collect_user_info: campaign['Collect User Info'] || campaign.collect_user_info,
|
collect_user_info: campaign['Collect User Info'] || campaign.collect_user_info,
|
||||||
show_email_count: campaign['Show Email Count'] || campaign.show_email_count,
|
show_email_count: campaign['Show Email Count'] || campaign.show_email_count,
|
||||||
allow_email_editing: campaign['Allow Email Editing'] || campaign.allow_email_editing,
|
allow_email_editing: campaign['Allow Email Editing'] || campaign.allow_email_editing,
|
||||||
|
show_response_wall: campaign['Show Response Wall Button'] || campaign.show_response_wall,
|
||||||
target_government_levels: normalizedTargetLevels,
|
target_government_levels: normalizedTargetLevels,
|
||||||
created_at: campaign.CreatedAt || campaign.created_at,
|
created_at: campaign.CreatedAt || campaign.created_at,
|
||||||
updated_at: campaign.UpdatedAt || campaign.updated_at,
|
updated_at: campaign.UpdatedAt || campaign.updated_at,
|
||||||
@ -280,6 +282,7 @@ class CampaignsController {
|
|||||||
collect_user_info: campaign['Collect User Info'] || campaign.collect_user_info,
|
collect_user_info: campaign['Collect User Info'] || campaign.collect_user_info,
|
||||||
show_email_count: campaign['Show Email Count'] || campaign.show_email_count,
|
show_email_count: campaign['Show Email Count'] || campaign.show_email_count,
|
||||||
allow_email_editing: campaign['Allow Email Editing'] || campaign.allow_email_editing,
|
allow_email_editing: campaign['Allow Email Editing'] || campaign.allow_email_editing,
|
||||||
|
show_response_wall: campaign['Show Response Wall Button'] || campaign.show_response_wall,
|
||||||
target_government_levels: normalizeTargetLevels(campaign['Target Government Levels'] || campaign.target_government_levels),
|
target_government_levels: normalizeTargetLevels(campaign['Target Government Levels'] || campaign.target_government_levels),
|
||||||
created_at: campaign.CreatedAt || campaign.created_at,
|
created_at: campaign.CreatedAt || campaign.created_at,
|
||||||
updated_at: campaign.UpdatedAt || campaign.updated_at,
|
updated_at: campaign.UpdatedAt || campaign.updated_at,
|
||||||
@ -366,6 +369,7 @@ class CampaignsController {
|
|||||||
show_email_count: campaign['Show Email Count'] || campaign.show_email_count,
|
show_email_count: campaign['Show Email Count'] || campaign.show_email_count,
|
||||||
show_call_count: campaign['Show Call Count'] || campaign.show_call_count,
|
show_call_count: campaign['Show Call Count'] || campaign.show_call_count,
|
||||||
allow_email_editing: campaign['Allow Email Editing'] || campaign.allow_email_editing,
|
allow_email_editing: campaign['Allow Email Editing'] || campaign.allow_email_editing,
|
||||||
|
show_response_wall: campaign['Show Response Wall Button'] || campaign.show_response_wall,
|
||||||
target_government_levels: normalizeTargetLevels(campaign['Target Government Levels'] || campaign.target_government_levels),
|
target_government_levels: normalizeTargetLevels(campaign['Target Government Levels'] || campaign.target_government_levels),
|
||||||
emailCount,
|
emailCount,
|
||||||
callCount
|
callCount
|
||||||
|
|||||||
@ -918,6 +918,10 @@ Sincerely,
|
|||||||
<input type="checkbox" id="create-allow-editing" name="allow_email_editing">
|
<input type="checkbox" id="create-allow-editing" name="allow_email_editing">
|
||||||
<label for="create-allow-editing">✏️ Allow Email Editing</label>
|
<label for="create-allow-editing">✏️ Allow Email Editing</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="checkbox-item">
|
||||||
|
<input type="checkbox" id="create-show-response-wall" name="show_response_wall">
|
||||||
|
<label for="create-show-response-wall">💬 Show Response Wall Button</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -1035,6 +1039,10 @@ Sincerely,
|
|||||||
<input type="checkbox" id="edit-allow-editing" name="allow_email_editing">
|
<input type="checkbox" id="edit-allow-editing" name="allow_email_editing">
|
||||||
<label for="edit-allow-editing">✏️ Allow Email Editing</label>
|
<label for="edit-allow-editing">✏️ Allow Email Editing</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="checkbox-item">
|
||||||
|
<input type="checkbox" id="edit-show-response-wall" name="show_response_wall">
|
||||||
|
<label for="edit-show-response-wall">💬 Show Response Wall Button</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -48,20 +48,86 @@
|
|||||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
.campaign-stats {
|
.campaign-header-content {
|
||||||
background: #f8f9fa;
|
max-width: 800px;
|
||||||
padding: 1rem;
|
margin: 0 auto;
|
||||||
border-radius: 8px;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
border: 2px solid #e9ecef;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.email-count {
|
.campaign-stats-header {
|
||||||
font-size: 2rem;
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-circle {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 1.8rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #3498db;
|
color: white;
|
||||||
margin-bottom: 0.5rem;
|
line-height: 1;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-buttons-header {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-btn-small {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-btn-small:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-btn-small svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-btn-small.copied {
|
||||||
|
background: rgba(40, 167, 69, 0.8);
|
||||||
|
border-color: rgba(40, 167, 69, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.campaign-content {
|
.campaign-content {
|
||||||
@ -144,8 +210,13 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview-mode .email-preview-actions {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
.edit-mode .email-subject,
|
.edit-mode .email-subject,
|
||||||
.edit-mode .email-body {
|
.edit-mode .email-body,
|
||||||
|
.edit-mode .email-preview-actions {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -265,6 +336,86 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Response Wall Button Styles */
|
||||||
|
.response-wall-button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 50px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform 0.3s, box-shadow 0.3s;
|
||||||
|
animation: pulse-glow 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-wall-button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-wall-button::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -50%;
|
||||||
|
left: -50%;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
background: linear-gradient(
|
||||||
|
45deg,
|
||||||
|
transparent,
|
||||||
|
rgba(255, 255, 255, 0.1),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
animation: shine 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-glow {
|
||||||
|
0%, 100% {
|
||||||
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 4px 25px rgba(102, 126, 234, 0.8), 0 0 30px rgba(102, 126, 234, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shine {
|
||||||
|
0% {
|
||||||
|
left: -50%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: 150%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-wall-container {
|
||||||
|
text-align: center;
|
||||||
|
margin: 2rem 0;
|
||||||
|
padding: 2rem;
|
||||||
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-wall-container h3 {
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-wall-container p {
|
||||||
|
margin: 0 0 1.5rem 0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.campaign-header h1 {
|
.campaign-header h1 {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
@ -294,26 +445,60 @@
|
|||||||
<!-- Campaign Header -->
|
<!-- Campaign Header -->
|
||||||
<div class="campaign-header">
|
<div class="campaign-header">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1 id="campaign-title">Loading Campaign...</h1>
|
<div class="campaign-header-content">
|
||||||
<p id="campaign-description"></p>
|
<h1 id="campaign-title">Loading Campaign...</h1>
|
||||||
|
<p id="campaign-description"></p>
|
||||||
|
|
||||||
|
<!-- Campaign Stats in Header -->
|
||||||
|
<div id="campaign-stats-header" class="campaign-stats-header" style="display: none;">
|
||||||
|
<div id="email-stat-circle" class="stat-circle" style="display: none;">
|
||||||
|
<div class="stat-number" id="email-count-header">0</div>
|
||||||
|
<div class="stat-label">Emails</div>
|
||||||
|
</div>
|
||||||
|
<div id="call-stat-circle" class="stat-circle" style="display: none;">
|
||||||
|
<div class="stat-number" id="call-count-header">0</div>
|
||||||
|
<div class="stat-label">Calls</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Social Share Buttons in Header -->
|
||||||
|
<div class="share-buttons-header">
|
||||||
|
<button class="share-btn-small" id="share-facebook" title="Share on Facebook">
|
||||||
|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="share-btn-small" id="share-twitter" title="Share on Twitter/X">
|
||||||
|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="share-btn-small" id="share-linkedin" title="Share on LinkedIn">
|
||||||
|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="share-btn-small" id="share-whatsapp" title="Share on WhatsApp">
|
||||||
|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="share-btn-small" id="share-email" title="Share via Email">
|
||||||
|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="share-btn-small" id="share-copy" title="Copy Link">
|
||||||
|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="campaign-content">
|
<div class="campaign-content">
|
||||||
<!-- Campaign Stats Display -->
|
|
||||||
<div id="campaign-stats" class="campaign-stats" style="display: none;">
|
|
||||||
<div style="display: flex; gap: 2rem; justify-content: center; flex-wrap: wrap;">
|
|
||||||
<div id="email-count-container" style="display: none;">
|
|
||||||
<div class="email-count" id="email-count">0</div>
|
|
||||||
<p>Emails sent</p>
|
|
||||||
</div>
|
|
||||||
<div id="call-count-container" style="display: none;">
|
|
||||||
<div class="email-count" id="call-count">0</div>
|
|
||||||
<p>Calls made</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Call to Action -->
|
<!-- Call to Action -->
|
||||||
<div id="call-to-action" class="call-to-action" style="display: none;">
|
<div id="call-to-action" class="call-to-action" style="display: none;">
|
||||||
<!-- Content will be loaded dynamically -->
|
<!-- Content will be loaded dynamically -->
|
||||||
@ -354,6 +539,15 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Response Wall Button -->
|
||||||
|
<div id="response-wall-section" class="response-wall-container" style="display: none;">
|
||||||
|
<h3>💬 See What People Are Saying</h3>
|
||||||
|
<p>Check out responses to people who have taken action on this campaign</p>
|
||||||
|
<a href="#" id="response-wall-link" class="response-wall-button">
|
||||||
|
View Response Wall
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Email Preview -->
|
<!-- Email Preview -->
|
||||||
<div id="email-preview" class="email-preview preview-mode" style="display: none;">
|
<div id="email-preview" class="email-preview preview-mode" style="display: none;">
|
||||||
<h3>📧 Email Preview</h3>
|
<h3>📧 Email Preview</h3>
|
||||||
@ -371,6 +565,11 @@
|
|||||||
<button type="button" class="btn btn-secondary" id="preview-email-btn">👁️ Preview</button>
|
<button type="button" class="btn btn-secondary" id="preview-email-btn">👁️ Preview</button>
|
||||||
<button type="button" class="btn btn-primary" id="save-email-btn">💾 Save Changes</button>
|
<button type="button" class="btn btn-primary" id="save-email-btn">💾 Save Changes</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Preview mode actions -->
|
||||||
|
<div class="email-preview-actions" style="display: none; margin-top: 1rem; text-align: center;">
|
||||||
|
<button type="button" class="btn btn-secondary" id="edit-email-btn">✏️ Edit Email</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Representatives Section -->
|
<!-- Representatives Section -->
|
||||||
@ -408,9 +607,22 @@
|
|||||||
|
|
||||||
<footer style="text-align: center; margin-top: 2rem; padding: 1rem; border-top: 1px solid #e9ecef;">
|
<footer style="text-align: center; margin-top: 2rem; padding: 1rem; border-top: 1px solid #e9ecef;">
|
||||||
<p>© 2025 <a href="https://bnkops.com/" target="_blank">BNKops</a> Influence Tool. Connect with democracy.</p>
|
<p>© 2025 <a href="https://bnkops.com/" target="_blank">BNKops</a> Influence Tool. Connect with democracy.</p>
|
||||||
<p><small><a href="terms.html" target="_blank">Terms of Use & Privacy Notice</a> | <a href="index.html">Return to Main Page</a></small></p>
|
<p><small><a href="/terms.html" id="terms-link" target="_blank">Terms of Use & Privacy Notice</a> | <a href="/index.html" id="home-link">Return to Main Page</a></small></p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="/js/campaign.js"></script>
|
<script src="/js/campaign.js"></script>
|
||||||
|
<script>
|
||||||
|
// Update footer links with APP_URL if needed for cross-origin scenarios
|
||||||
|
fetch('/api/config')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(config => {
|
||||||
|
if (config.appUrl && !window.location.href.startsWith(config.appUrl)) {
|
||||||
|
// Only update if we're on a different domain (e.g., CDN)
|
||||||
|
document.getElementById('terms-link').href = config.appUrl + '/terms.html';
|
||||||
|
document.getElementById('home-link').href = config.appUrl + '/index.html';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.log('Config not loaded, using relative paths'));
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -744,6 +744,10 @@ Sincerely,
|
|||||||
<input type="checkbox" id="create-allow-editing" name="allow_email_editing">
|
<input type="checkbox" id="create-allow-editing" name="allow_email_editing">
|
||||||
<label for="create-allow-editing">✏️ Allow Email Editing</label>
|
<label for="create-allow-editing">✏️ Allow Email Editing</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="checkbox-item">
|
||||||
|
<input type="checkbox" id="create-show-response-wall" name="show_response_wall">
|
||||||
|
<label for="create-show-response-wall">💬 Show Response Wall Button</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -854,6 +858,10 @@ Sincerely,
|
|||||||
<input type="checkbox" id="edit-allow-editing" name="allow_email_editing">
|
<input type="checkbox" id="edit-allow-editing" name="allow_email_editing">
|
||||||
<label for="edit-allow-editing">✏️ Allow Email Editing</label>
|
<label for="edit-allow-editing">✏️ Allow Email Editing</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="checkbox-item">
|
||||||
|
<input type="checkbox" id="edit-show-response-wall" name="show_response_wall">
|
||||||
|
<label for="edit-show-response-wall">💬 Show Response Wall Button</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -31,7 +31,7 @@
|
|||||||
<div class="postal-input-section">
|
<div class="postal-input-section">
|
||||||
<form id="postal-form">
|
<form id="postal-form">
|
||||||
<div class="form-group">
|
<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">
|
<div class="input-group">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -195,14 +195,14 @@
|
|||||||
<footer>
|
<footer>
|
||||||
<p>© 2025 <a href="https://bnkops.com/" target="_blank">BNKops</a> Influence Tool. Connect with democracy.</p>
|
<p>© 2025 <a href="https://bnkops.com/" target="_blank">BNKops</a> Influence Tool. Connect with democracy.</p>
|
||||||
<p><small>This tool uses the <a href="https://represent.opennorth.ca" target="_blank">Represent API</a> by Open North to find your representatives.</small></p>
|
<p><small>This tool uses the <a href="https://represent.opennorth.ca" target="_blank">Represent API</a> by Open North to find your representatives.</small></p>
|
||||||
<p><small><a href="terms.html" target="_blank">Terms of Use & Privacy Notice</a></small></p>
|
<p><small><a href="/terms.html" id="terms-link" target="_blank">Terms of Use & Privacy Notice</a></small></p>
|
||||||
|
|
||||||
<div class="preamble" style="text-align: center; padding: 1rem; margin: 1rem 0; background-color: #f5f5f5; border-radius: 8px;">
|
<div class="preamble" style="text-align: center; padding: 1rem; margin: 1rem 0; background-color: #f5f5f5; border-radius: 8px;">
|
||||||
<p>Influence is an open-source platform and the code is available to all at <a href="https://gitea.bnkops.com/admin/changemaker.lite" target="_blank" rel="noopener noreferrer">gitea.bnkops.com/admin/changemaker.lite</a></p>
|
<p>Influence is an open-source platform and the code is available to all at <a href="https://gitea.bnkops.com/admin/changemaker.lite" target="_blank" rel="noopener noreferrer">gitea.bnkops.com/admin/changemaker.lite</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer-actions">
|
<div class="footer-actions">
|
||||||
<a href="/login.html" class="btn btn-secondary">Admin Login</a>
|
<a href="/login.html" id="login-link" class="btn btn-secondary">Admin Login</a>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
@ -236,6 +236,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update navigation links with APP_URL if needed
|
||||||
|
fetch('/api/config')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(config => {
|
||||||
|
if (config.appUrl && !window.location.href.startsWith(config.appUrl)) {
|
||||||
|
document.getElementById('terms-link').href = config.appUrl + '/terms.html';
|
||||||
|
document.getElementById('login-link').href = config.appUrl + '/login.html';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.log('Config not loaded, using relative paths'));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -572,6 +572,7 @@ class AdminPanel {
|
|||||||
campaignFormData.append('collect_user_info', formData.get('collect_user_info') === 'on');
|
campaignFormData.append('collect_user_info', formData.get('collect_user_info') === 'on');
|
||||||
campaignFormData.append('show_email_count', formData.get('show_email_count') === 'on');
|
campaignFormData.append('show_email_count', formData.get('show_email_count') === 'on');
|
||||||
campaignFormData.append('allow_email_editing', formData.get('allow_email_editing') === 'on');
|
campaignFormData.append('allow_email_editing', formData.get('allow_email_editing') === 'on');
|
||||||
|
campaignFormData.append('show_response_wall', formData.get('show_response_wall') === 'on');
|
||||||
|
|
||||||
// Handle target_government_levels array
|
// Handle target_government_levels array
|
||||||
const targetLevels = Array.from(formData.getAll('target_government_levels'));
|
const targetLevels = Array.from(formData.getAll('target_government_levels'));
|
||||||
@ -643,6 +644,7 @@ class AdminPanel {
|
|||||||
form.querySelector('[name="collect_user_info"]').checked = campaign.collect_user_info;
|
form.querySelector('[name="collect_user_info"]').checked = campaign.collect_user_info;
|
||||||
form.querySelector('[name="show_email_count"]').checked = campaign.show_email_count;
|
form.querySelector('[name="show_email_count"]').checked = campaign.show_email_count;
|
||||||
form.querySelector('[name="allow_email_editing"]').checked = campaign.allow_email_editing;
|
form.querySelector('[name="allow_email_editing"]').checked = campaign.allow_email_editing;
|
||||||
|
form.querySelector('[name="show_response_wall"]').checked = campaign.show_response_wall;
|
||||||
|
|
||||||
// Government levels
|
// Government levels
|
||||||
let targetLevels = [];
|
let targetLevels = [];
|
||||||
@ -679,6 +681,7 @@ class AdminPanel {
|
|||||||
updateFormData.append('collect_user_info', formData.get('collect_user_info') === 'on');
|
updateFormData.append('collect_user_info', formData.get('collect_user_info') === 'on');
|
||||||
updateFormData.append('show_email_count', formData.get('show_email_count') === 'on');
|
updateFormData.append('show_email_count', formData.get('show_email_count') === 'on');
|
||||||
updateFormData.append('allow_email_editing', formData.get('allow_email_editing') === 'on');
|
updateFormData.append('allow_email_editing', formData.get('allow_email_editing') === 'on');
|
||||||
|
updateFormData.append('show_response_wall', formData.get('show_response_wall') === 'on');
|
||||||
|
|
||||||
// Handle target_government_levels array
|
// Handle target_government_levels array
|
||||||
const targetLevels = Array.from(formData.getAll('target_government_levels'));
|
const targetLevels = Array.from(formData.getAll('target_government_levels'));
|
||||||
|
|||||||
@ -23,10 +23,90 @@ class CampaignPage {
|
|||||||
this.formatPostalCode(e);
|
this.formatPostalCode(e);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set up social share buttons
|
||||||
|
this.setupShareButtons();
|
||||||
|
|
||||||
// Load campaign data
|
// Load campaign data
|
||||||
this.loadCampaign();
|
this.loadCampaign();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupShareButtons() {
|
||||||
|
// Get current URL
|
||||||
|
const shareUrl = window.location.href;
|
||||||
|
|
||||||
|
// Facebook share
|
||||||
|
document.getElementById('share-facebook')?.addEventListener('click', () => {
|
||||||
|
const url = `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareUrl)}`;
|
||||||
|
window.open(url, '_blank', 'width=600,height=400');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Twitter share
|
||||||
|
document.getElementById('share-twitter')?.addEventListener('click', () => {
|
||||||
|
const text = this.campaign ? `Check out this campaign: ${this.campaign.title}` : 'Check out this campaign';
|
||||||
|
const url = `https://twitter.com/intent/tweet?url=${encodeURIComponent(shareUrl)}&text=${encodeURIComponent(text)}`;
|
||||||
|
window.open(url, '_blank', 'width=600,height=400');
|
||||||
|
});
|
||||||
|
|
||||||
|
// LinkedIn share
|
||||||
|
document.getElementById('share-linkedin')?.addEventListener('click', () => {
|
||||||
|
const url = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(shareUrl)}`;
|
||||||
|
window.open(url, '_blank', 'width=600,height=400');
|
||||||
|
});
|
||||||
|
|
||||||
|
// WhatsApp share
|
||||||
|
document.getElementById('share-whatsapp')?.addEventListener('click', () => {
|
||||||
|
const text = this.campaign ? `Check out this campaign: ${this.campaign.title}` : 'Check out this campaign';
|
||||||
|
const url = `https://wa.me/?text=${encodeURIComponent(text + ' ' + shareUrl)}`;
|
||||||
|
window.open(url, '_blank');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Email share
|
||||||
|
document.getElementById('share-email')?.addEventListener('click', () => {
|
||||||
|
const subject = this.campaign ? `Campaign: ${this.campaign.title}` : 'Check out this campaign';
|
||||||
|
const body = this.campaign ?
|
||||||
|
`I thought you might be interested in this campaign:\n\n${this.campaign.title}\n\n${shareUrl}` :
|
||||||
|
`Check out this campaign:\n\n${shareUrl}`;
|
||||||
|
window.location.href = `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copy link
|
||||||
|
document.getElementById('share-copy')?.addEventListener('click', async () => {
|
||||||
|
const copyBtn = document.getElementById('share-copy');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(shareUrl);
|
||||||
|
copyBtn.classList.add('copied');
|
||||||
|
copyBtn.title = 'Copied!';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
copyBtn.classList.remove('copied');
|
||||||
|
copyBtn.title = 'Copy Link';
|
||||||
|
}, 2000);
|
||||||
|
} catch (err) {
|
||||||
|
// Fallback for older browsers
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
textArea.value = shareUrl;
|
||||||
|
textArea.style.position = 'fixed';
|
||||||
|
textArea.style.left = '-999999px';
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.select();
|
||||||
|
try {
|
||||||
|
document.execCommand('copy');
|
||||||
|
copyBtn.classList.add('copied');
|
||||||
|
copyBtn.title = 'Copied!';
|
||||||
|
setTimeout(() => {
|
||||||
|
copyBtn.classList.remove('copied');
|
||||||
|
copyBtn.title = 'Copy Link';
|
||||||
|
}, 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy:', err);
|
||||||
|
alert('Failed to copy link. Please copy manually: ' + shareUrl);
|
||||||
|
}
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async loadCampaign() {
|
async loadCampaign() {
|
||||||
this.showLoading('Loading campaign...');
|
this.showLoading('Loading campaign...');
|
||||||
|
|
||||||
@ -76,25 +156,27 @@ class CampaignPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show email count if enabled (show even if count is 0)
|
// Show email count if enabled (show even if count is 0)
|
||||||
const statsSection = document.getElementById('campaign-stats');
|
const statsHeaderSection = document.getElementById('campaign-stats-header');
|
||||||
let hasStats = false;
|
let hasStats = false;
|
||||||
|
|
||||||
if (this.campaign.show_email_count && this.campaign.emailCount !== null && this.campaign.emailCount !== undefined) {
|
if (this.campaign.show_email_count && this.campaign.emailCount !== null && this.campaign.emailCount !== undefined) {
|
||||||
document.getElementById('email-count').textContent = this.campaign.emailCount;
|
// Header stats
|
||||||
document.getElementById('email-count-container').style.display = 'block';
|
document.getElementById('email-count-header').textContent = this.campaign.emailCount;
|
||||||
|
document.getElementById('email-stat-circle').style.display = 'flex';
|
||||||
hasStats = true;
|
hasStats = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show call count if enabled (show even if count is 0)
|
// Show call count if enabled (show even if count is 0)
|
||||||
if (this.campaign.show_call_count && this.campaign.callCount !== null && this.campaign.callCount !== undefined) {
|
if (this.campaign.show_call_count && this.campaign.callCount !== null && this.campaign.callCount !== undefined) {
|
||||||
document.getElementById('call-count').textContent = this.campaign.callCount;
|
// Header stats
|
||||||
document.getElementById('call-count-container').style.display = 'block';
|
document.getElementById('call-count-header').textContent = this.campaign.callCount;
|
||||||
|
document.getElementById('call-stat-circle').style.display = 'flex';
|
||||||
hasStats = true;
|
hasStats = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show stats section if any stat is enabled
|
// Show stats section if any stat is enabled
|
||||||
if (hasStats) {
|
if (hasStats) {
|
||||||
statsSection.style.display = 'block';
|
statsHeaderSection.style.display = 'flex';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show call to action
|
// Show call to action
|
||||||
@ -103,6 +185,16 @@ class CampaignPage {
|
|||||||
document.getElementById('call-to-action').style.display = 'block';
|
document.getElementById('call-to-action').style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show response wall button if enabled
|
||||||
|
if (this.campaign.show_response_wall) {
|
||||||
|
const responseWallSection = document.getElementById('response-wall-section');
|
||||||
|
const responseWallLink = document.getElementById('response-wall-link');
|
||||||
|
if (responseWallSection && responseWallLink) {
|
||||||
|
responseWallLink.href = `/response-wall.html?campaign=${this.campaignSlug}`;
|
||||||
|
responseWallSection.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Set up email preview
|
// Set up email preview
|
||||||
this.setupEmailPreview();
|
this.setupEmailPreview();
|
||||||
|
|
||||||
@ -198,6 +290,7 @@ class CampaignPage {
|
|||||||
const editBody = document.getElementById('edit-body');
|
const editBody = document.getElementById('edit-body');
|
||||||
const previewBtn = document.getElementById('preview-email-btn');
|
const previewBtn = document.getElementById('preview-email-btn');
|
||||||
const saveBtn = document.getElementById('save-email-btn');
|
const saveBtn = document.getElementById('save-email-btn');
|
||||||
|
const editBtn = document.getElementById('edit-email-btn');
|
||||||
|
|
||||||
// Auto-update current content as user types
|
// Auto-update current content as user types
|
||||||
editSubject.addEventListener('input', (e) => {
|
editSubject.addEventListener('input', (e) => {
|
||||||
@ -208,15 +301,46 @@ class CampaignPage {
|
|||||||
this.currentEmailBody = e.target.value;
|
this.currentEmailBody = e.target.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Preview button - toggle between edit and preview mode
|
// Preview button - switch to preview mode
|
||||||
previewBtn.addEventListener('click', () => {
|
previewBtn.addEventListener('click', () => {
|
||||||
this.toggleEmailPreview();
|
this.showEmailPreview();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save button - save changes
|
// Save button - save changes and show preview
|
||||||
saveBtn.addEventListener('click', () => {
|
saveBtn.addEventListener('click', () => {
|
||||||
this.saveEmailChanges();
|
this.saveEmailChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Edit button - switch back to edit mode
|
||||||
|
if (editBtn) {
|
||||||
|
editBtn.addEventListener('click', () => {
|
||||||
|
this.showEmailEditor();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showEmailPreview() {
|
||||||
|
const emailPreview = document.getElementById('email-preview');
|
||||||
|
|
||||||
|
// Update preview content
|
||||||
|
document.getElementById('preview-subject').textContent = this.currentEmailSubject;
|
||||||
|
document.getElementById('preview-body').textContent = this.currentEmailBody;
|
||||||
|
|
||||||
|
// Switch to preview mode
|
||||||
|
emailPreview.classList.remove('edit-mode');
|
||||||
|
emailPreview.classList.add('preview-mode');
|
||||||
|
}
|
||||||
|
|
||||||
|
showEmailEditor() {
|
||||||
|
const emailPreview = document.getElementById('email-preview');
|
||||||
|
|
||||||
|
// Update edit fields with current content
|
||||||
|
document.getElementById('edit-subject').value = this.currentEmailSubject;
|
||||||
|
document.getElementById('edit-body').value = this.currentEmailBody;
|
||||||
|
|
||||||
|
// Switch to edit mode
|
||||||
|
emailPreview.classList.remove('preview-mode');
|
||||||
|
emailPreview.classList.add('edit-mode');
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleEmailPreview() {
|
toggleEmailPreview() {
|
||||||
@ -240,10 +364,13 @@ class CampaignPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveEmailChanges() {
|
saveEmailChanges() {
|
||||||
// Update the current values and show confirmation
|
// Update preview content
|
||||||
document.getElementById('preview-subject').textContent = this.currentEmailSubject;
|
document.getElementById('preview-subject').textContent = this.currentEmailSubject;
|
||||||
document.getElementById('preview-body').textContent = this.currentEmailBody;
|
document.getElementById('preview-body').textContent = this.currentEmailBody;
|
||||||
|
|
||||||
|
// Switch to preview mode
|
||||||
|
this.showEmailPreview();
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
this.showMessage('Email content updated successfully!', 'success');
|
this.showMessage('Email content updated successfully!', 'success');
|
||||||
|
|
||||||
@ -640,9 +767,12 @@ class CampaignPage {
|
|||||||
showSuccess(message) {
|
showSuccess(message) {
|
||||||
// Update email count if enabled
|
// Update email count if enabled
|
||||||
if (this.campaign.show_email_count) {
|
if (this.campaign.show_email_count) {
|
||||||
const countElement = document.getElementById('email-count');
|
const countHeaderElement = document.getElementById('email-count-header');
|
||||||
const currentCount = parseInt(countElement.textContent) || 0;
|
const currentCount = parseInt(countHeaderElement?.textContent) || 0;
|
||||||
countElement.textContent = currentCount + 1;
|
const newCount = currentCount + 1;
|
||||||
|
if (countHeaderElement) {
|
||||||
|
countHeaderElement.textContent = newCount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// You could show a toast or update UI to indicate success
|
// You could show a toast or update UI to indicate success
|
||||||
@ -652,9 +782,12 @@ class CampaignPage {
|
|||||||
showCallSuccess(message) {
|
showCallSuccess(message) {
|
||||||
// Update call count if enabled
|
// Update call count if enabled
|
||||||
if (this.campaign.show_call_count) {
|
if (this.campaign.show_call_count) {
|
||||||
const countElement = document.getElementById('call-count');
|
const countHeaderElement = document.getElementById('call-count-header');
|
||||||
const currentCount = parseInt(countElement.textContent) || 0;
|
const currentCount = parseInt(countHeaderElement?.textContent) || 0;
|
||||||
countElement.textContent = currentCount + 1;
|
const newCount = currentCount + 1;
|
||||||
|
if (countHeaderElement) {
|
||||||
|
countHeaderElement.textContent = newCount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// User Dashboard JavaScript
|
// User Dashboard JavaScript
|
||||||
class UserDashboard {
|
class UserDashboard {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.user = null;
|
this.user = null;
|
||||||
@ -871,6 +871,7 @@ class UserDashboard {
|
|||||||
form.querySelector('[name="collect_user_info"]').checked = !!campaign.collect_user_info;
|
form.querySelector('[name="collect_user_info"]').checked = !!campaign.collect_user_info;
|
||||||
form.querySelector('[name="show_email_count"]').checked = !!campaign.show_email_count;
|
form.querySelector('[name="show_email_count"]').checked = !!campaign.show_email_count;
|
||||||
form.querySelector('[name="allow_email_editing"]').checked = !!campaign.allow_email_editing;
|
form.querySelector('[name="allow_email_editing"]').checked = !!campaign.allow_email_editing;
|
||||||
|
form.querySelector('[name="show_response_wall"]').checked = !!campaign.show_response_wall;
|
||||||
|
|
||||||
// Government levels
|
// Government levels
|
||||||
const targetLevels = Array.isArray(campaign.target_government_levels)
|
const targetLevels = Array.isArray(campaign.target_government_levels)
|
||||||
@ -900,6 +901,7 @@ class UserDashboard {
|
|||||||
collect_user_info: formData.get('collect_user_info') === 'on',
|
collect_user_info: formData.get('collect_user_info') === 'on',
|
||||||
show_email_count: formData.get('show_email_count') === 'on',
|
show_email_count: formData.get('show_email_count') === 'on',
|
||||||
allow_email_editing: formData.get('allow_email_editing') === 'on',
|
allow_email_editing: formData.get('allow_email_editing') === 'on',
|
||||||
|
show_response_wall: formData.get('show_response_wall') === 'on',
|
||||||
target_government_levels: Array.from(formData.getAll('target_government_levels'))
|
target_government_levels: Array.from(formData.getAll('target_government_levels'))
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1237,6 +1239,7 @@ class UserDashboard {
|
|||||||
collect_user_info: formData.get('collect_user_info') === 'on',
|
collect_user_info: formData.get('collect_user_info') === 'on',
|
||||||
show_email_count: formData.get('show_email_count') === 'on',
|
show_email_count: formData.get('show_email_count') === 'on',
|
||||||
allow_email_editing: formData.get('allow_email_editing') === 'on',
|
allow_email_editing: formData.get('allow_email_editing') === 'on',
|
||||||
|
show_response_wall: formData.get('show_response_wall') === 'on',
|
||||||
target_government_levels: Array.from(formData.getAll('target_government_levels'))
|
target_government_levels: Array.from(formData.getAll('target_government_levels'))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -173,12 +173,23 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="back-link">
|
<div class="back-link">
|
||||||
<a href="/">← Back to Campaign Tool</a>
|
<a href="/" id="home-link">← Back to Campaign Tool</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="js/api-client.js"></script>
|
<script src="js/api-client.js"></script>
|
||||||
<script src="js/login.js"></script>
|
<script src="js/login.js"></script>
|
||||||
|
<script>
|
||||||
|
// Update navigation link with APP_URL if needed
|
||||||
|
fetch('/api/config')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(config => {
|
||||||
|
if (config.appUrl && !window.location.href.startsWith(config.appUrl)) {
|
||||||
|
document.getElementById('home-link').href = config.appUrl + '/';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.log('Config not loaded, using relative paths'));
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -223,8 +223,20 @@
|
|||||||
|
|
||||||
<footer style="text-align: center; margin-top: 2rem; padding-top: 1rem; border-top: 1px solid #e9ecef;">
|
<footer style="text-align: center; margin-top: 2rem; padding-top: 1rem; border-top: 1px solid #e9ecef;">
|
||||||
<p>© 2025 <a href="https://bnkops.com/" target="_blank">BNKops</a>. All rights reserved.</p>
|
<p>© 2025 <a href="https://bnkops.com/" target="_blank">BNKops</a>. All rights reserved.</p>
|
||||||
<p><a href="index.html">Return to Main Application</a></p>
|
<p><a href="/index.html" id="home-link">Return to Main Application</a></p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Update navigation link with APP_URL if needed
|
||||||
|
fetch('/api/config')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(config => {
|
||||||
|
if (config.appUrl && !window.location.href.startsWith(config.appUrl)) {
|
||||||
|
document.getElementById('home-link').href = config.appUrl + '/index.html';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.log('Config not loaded, using relative paths'));
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -52,6 +52,13 @@ app.use(express.static(path.join(__dirname, 'public')));
|
|||||||
app.use('/api/auth', authRoutes);
|
app.use('/api/auth', authRoutes);
|
||||||
app.use('/api', apiRoutes);
|
app.use('/api', apiRoutes);
|
||||||
|
|
||||||
|
// Config endpoint - expose APP_URL to client
|
||||||
|
app.get('/api/config', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
appUrl: process.env.APP_URL || `http://localhost:${PORT}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Serve the main page
|
// Serve the main page
|
||||||
app.get('/', (req, res) => {
|
app.get('/', (req, res) => {
|
||||||
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
||||||
|
|||||||
@ -436,6 +436,7 @@ class NocoDBService {
|
|||||||
'Collect User Info': campaignData.collect_user_info,
|
'Collect User Info': campaignData.collect_user_info,
|
||||||
'Show Email Count': campaignData.show_email_count,
|
'Show Email Count': campaignData.show_email_count,
|
||||||
'Allow Email Editing': campaignData.allow_email_editing,
|
'Allow Email Editing': campaignData.allow_email_editing,
|
||||||
|
'Show Response Wall Button': campaignData.show_response_wall,
|
||||||
'Target Government Levels': campaignData.target_government_levels,
|
'Target Government Levels': campaignData.target_government_levels,
|
||||||
'Created By User ID': campaignData.created_by_user_id,
|
'Created By User ID': campaignData.created_by_user_id,
|
||||||
'Created By User Email': campaignData.created_by_user_email,
|
'Created By User Email': campaignData.created_by_user_email,
|
||||||
@ -468,6 +469,7 @@ class NocoDBService {
|
|||||||
if (updates.collect_user_info !== undefined) mappedUpdates['Collect User Info'] = updates.collect_user_info;
|
if (updates.collect_user_info !== undefined) mappedUpdates['Collect User Info'] = updates.collect_user_info;
|
||||||
if (updates.show_email_count !== undefined) mappedUpdates['Show Email Count'] = updates.show_email_count;
|
if (updates.show_email_count !== undefined) mappedUpdates['Show Email Count'] = updates.show_email_count;
|
||||||
if (updates.allow_email_editing !== undefined) mappedUpdates['Allow Email Editing'] = updates.allow_email_editing;
|
if (updates.allow_email_editing !== undefined) mappedUpdates['Allow Email Editing'] = updates.allow_email_editing;
|
||||||
|
if (updates.show_response_wall !== undefined) mappedUpdates['Show Response Wall Button'] = updates.show_response_wall;
|
||||||
if (updates.target_government_levels !== undefined) mappedUpdates['Target Government Levels'] = updates.target_government_levels;
|
if (updates.target_government_levels !== undefined) mappedUpdates['Target Government Levels'] = updates.target_government_levels;
|
||||||
if (updates.updated_at !== undefined) mappedUpdates['UpdatedAt'] = updates.updated_at;
|
if (updates.updated_at !== undefined) mappedUpdates['UpdatedAt'] = updates.updated_at;
|
||||||
|
|
||||||
|
|||||||
@ -1081,6 +1081,12 @@ create_campaigns_table() {
|
|||||||
"uidt": "Checkbox",
|
"uidt": "Checkbox",
|
||||||
"cdf": "false"
|
"cdf": "false"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"column_name": "show_response_wall",
|
||||||
|
"title": "Show Response Wall Button",
|
||||||
|
"uidt": "Checkbox",
|
||||||
|
"cdf": "false"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"column_name": "target_government_levels",
|
"column_name": "target_government_levels",
|
||||||
"title": "Target Government Levels",
|
"title": "Target Government Levels",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user