diff --git a/README.md b/README.md index 80df7dd..93278d9 100644 --- a/README.md +++ b/README.md @@ -30,12 +30,21 @@ cd changemaker.lite docker compose up -d ``` -## Map & Production +## Map Instructions on how to build the map are available in the map directory. Instructions on how to build for production are available in the mkdocs/docs/build directory or in the site preview. +### Quick Start for Map + +Update the .env file in the map directory with your NocoDB URLs, and then run: + +```bash +cd map +docker compose up -d +``` + ## Service Access After starting, access services at: @@ -49,6 +58,14 @@ After starting, access services at: - **NocoDB**: http://localhost:8090 - **Map Viewer**: http://localhost:3000 +## Production Deployment + +If you are deploying to production, using Cloudflare, you can use the included 'start-production.sh' script to set up a secure deployment with HTTPS. Ensure your domain and cloudflare settings are correctly configured in the root .env before running. More information on the required API tokens and settings can be found in the mkdocs/docs/build directory or at cmlite.org. + +```bash +./start-production.sh +``` + ## Documentation Complete documentation is available in the MkDocs site, including: diff --git a/config.sh b/config.sh index 655aef7..cef59a5 100755 --- a/config.sh +++ b/config.sh @@ -748,7 +748,7 @@ NODE_ENV=production ALLOWED_ORIGINS=https://map.$new_domain,http://localhost:3000 # Add allowed origin -ALLOWED_ORIGINS=https://map.cmlite.org,http://localhost:3000 +ALLOWED_ORIGINS=https://map.$new_domain,http://localhost:3000 # SMTP Configuration SMTP_HOST=smtp.insert.here @@ -766,7 +766,7 @@ APP_NAME="$new_domain Map" LISTMONK_API_URL=http://listmonk_app:9000/api LISTMONK_PASSWORD=changeme LISTMONK_SYNC_ENABLED=true -LISTMONK_INITIAL_SYNC=true # Set to true only for first run to sync existing data +LISTMONK_INITIAL_SYNC=false # Set to true to sync existing data EOL echo "✅ Created new map .env file at $MAP_ENV_FILE" diff --git a/map/app/controllers/shiftsController.js b/map/app/controllers/shiftsController.js index bec4fb6..0cd5564 100644 --- a/map/app/controllers/shiftsController.js +++ b/map/app/controllers/shiftsController.js @@ -287,7 +287,7 @@ class ShiftsController { // Admin: Create shift async create(req, res) { try { - const { title, description, date, startTime, endTime, location, maxVolunteers } = req.body; + const { title, description, date, startTime, endTime, location, maxVolunteers, isPublic } = req.body; if (!title || !date || !startTime || !endTime || !location || !maxVolunteers) { return res.status(400).json({ @@ -306,6 +306,7 @@ class ShiftsController { 'Max Volunteers': parseInt(maxVolunteers), 'Current Volunteers': 0, Status: 'Open', + 'Is Public': isPublic !== false, // Default to true if not specified 'Created By': req.session.userEmail, 'Created At': new Date().toISOString(), 'Updated At': new Date().toISOString() @@ -340,7 +341,8 @@ class ShiftsController { endTime: 'End Time', location: 'Location', maxVolunteers: 'Max Volunteers', - status: 'Status' + status: 'Status', + isPublic: 'Is Public' }; for (const [key, field] of Object.entries(fieldMap)) { diff --git a/map/app/public/css/admin/cuts-shifts.css b/map/app/public/css/admin/cuts-shifts.css index 43a8d6f..27be13e 100644 --- a/map/app/public/css/admin/cuts-shifts.css +++ b/map/app/public/css/admin/cuts-shifts.css @@ -99,6 +99,26 @@ margin: 0 0 10px 0; } +/* Editing State for Shifts */ +.shift-admin-item.editing { + border: 2px solid #a02c8d; + background-color: #f9f0f8; +} + +.shift-admin-item.editing::before { + content: "EDITING"; + position: absolute; + top: 5px; + right: 5px; + background: #a02c8d; + color: white; + padding: 2px 8px; + font-size: 11px; + border-radius: 3px; + font-weight: bold; + z-index: 1; +} + .shift-admin-item p { margin: 5px 0; color: var(--secondary-color); diff --git a/map/app/public/js/admin.js b/map/app/public/js/admin.js index 9b8d562..2d8fa12 100644 --- a/map/app/public/js/admin.js +++ b/map/app/public/js/admin.js @@ -2,6 +2,7 @@ let adminMap = null; let startMarker = null; let storedQRCodes = {}; +let editingShiftId = null; // Utility function to create a local date from YYYY-MM-DD string // This prevents timezone issues when displaying dates @@ -357,7 +358,13 @@ function setupEventListeners() { // Clear shift form button const clearShiftBtn = document.getElementById('clear-shift-form'); if (clearShiftBtn) { - clearShiftBtn.addEventListener('click', clearShiftForm); + clearShiftBtn.addEventListener('click', function() { + const wasEditing = editingShiftId !== null; + clearShiftForm(); + if (wasEditing) { + showStatus('Edit cancelled', 'info'); + } + }); } // User form submission @@ -1267,47 +1274,133 @@ async function deleteShift(shiftId) { } // Update editShift function (remove window. prefix) -function editShift(shiftId) { - showStatus('Edit functionality coming soon', 'info'); +async function editShift(shiftId) { + try { + // Find the shift in the current data + const response = await fetch('/api/shifts/admin'); + const data = await response.json(); + + if (!data.success) { + showStatus('Failed to load shift data', 'error'); + return; + } + + const shift = data.shifts.find(s => s.ID === parseInt(shiftId)); + if (!shift) { + showStatus('Shift not found', 'error'); + return; + } + + // Set editing mode + editingShiftId = shiftId; + + // Populate the form + document.getElementById('shift-title').value = shift.Title || ''; + document.getElementById('shift-description').value = shift.Description || ''; + document.getElementById('shift-date').value = shift.Date || ''; + document.getElementById('shift-start').value = shift['Start Time'] || ''; + document.getElementById('shift-end').value = shift['End Time'] || ''; + document.getElementById('shift-location').value = shift.Location || ''; + document.getElementById('shift-max-volunteers').value = shift['Max Volunteers'] || ''; + + // Update public checkbox if it exists + const publicCheckbox = document.getElementById('shift-is-public'); + if (publicCheckbox) { + publicCheckbox.checked = shift['Is Public'] !== false; + } + + // Change submit button text + const submitBtn = document.querySelector('#shift-form button[type="submit"]'); + if (submitBtn) { + submitBtn.textContent = 'Update Shift'; + } + + // Remove editing class from any previous item + document.querySelectorAll('.shift-admin-item.editing').forEach(el => { + el.classList.remove('editing'); + }); + + // Add editing class to current item + const shiftElement = document.querySelector(`[data-shift-id="${shiftId}"]`); + if (shiftElement) { + const shiftItem = shiftElement.closest('.shift-admin-item'); + if (shiftItem) { + shiftItem.classList.add('editing'); + } + } + + // Scroll to form + document.getElementById('shift-form').scrollIntoView({ behavior: 'smooth' }); + + showStatus('Editing shift: ' + shift.Title, 'info'); + + } catch (error) { + console.error('Error loading shift for edit:', error); + showStatus('Failed to load shift for editing', 'error'); + } } // Add function to create shift async function createShift(e) { e.preventDefault(); - const formData = { - title: document.getElementById('shift-title').value, - description: document.getElementById('shift-description').value, - date: document.getElementById('shift-date').value, - startTime: document.getElementById('shift-start').value, - endTime: document.getElementById('shift-end').value, - location: document.getElementById('shift-location').value, - maxVolunteers: document.getElementById('shift-max-volunteers').value, - isPublic: document.getElementById('shift-is-public')?.checked !== false + const title = document.getElementById('shift-title').value; + const description = document.getElementById('shift-description').value; + const date = document.getElementById('shift-date').value; + const startTime = document.getElementById('shift-start').value; + const endTime = document.getElementById('shift-end').value; + const location = document.getElementById('shift-location').value; + const maxVolunteers = document.getElementById('shift-max-volunteers').value; + + // Get public checkbox value + const isPublic = document.getElementById('shift-is-public')?.checked ?? true; + + const shiftData = { + title, + description, + date, + startTime, + endTime, + location, + maxVolunteers: parseInt(maxVolunteers), + isPublic }; try { - const response = await fetch('/api/shifts/admin', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(formData) - }); + let response; + if (editingShiftId) { + // Update existing shift + response = await fetch(`/api/shifts/admin/${editingShiftId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(shiftData) + }); + } else { + // Create new shift + response = await fetch('/api/shifts/admin', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(shiftData) + }); + } const data = await response.json(); if (data.success) { - showStatus('Shift created successfully', 'success'); - document.getElementById('shift-form').reset(); + showStatus(editingShiftId ? 'Shift updated successfully' : 'Shift created successfully', 'success'); + clearShiftForm(); await loadAdminShifts(); - console.log('Refreshed shifts list after creating new shift'); + console.log('Refreshed shifts list after saving shift'); } else { - showStatus(data.error || 'Failed to create shift', 'error'); + showStatus(data.error || 'Failed to save shift', 'error'); } } catch (error) { - console.error('Error creating shift:', error); - showStatus('Failed to create shift', 'error'); + console.error('Error saving shift:', error); + showStatus('Failed to save shift', 'error'); } } @@ -1315,6 +1408,21 @@ function clearShiftForm() { const form = document.getElementById('shift-form'); if (form) { form.reset(); + + // Reset editing state + editingShiftId = null; + + // Reset submit button text + const submitBtn = document.querySelector('#shift-form button[type="submit"]'); + if (submitBtn) { + submitBtn.textContent = 'Create Shift'; + } + + // Remove editing class from any shift items + document.querySelectorAll('.shift-admin-item.editing').forEach(el => { + el.classList.remove('editing'); + }); + showStatus('Form cleared', 'info'); } } diff --git a/map/app/server.js b/map/app/server.js index 53f17f0..6962cdf 100644 --- a/map/app/server.js +++ b/map/app/server.js @@ -118,7 +118,9 @@ app.use(cors({ `https://${config.domain}`, `https://map.${config.domain}`, config.mkdocs.url, // Use configured MkDocs URL instead of hardcoded subdomain - `https://admin.${config.domain}` + `https://admin.${config.domain}`, + `http://localhost:${config.port}`, // Allow localhost with configured port + `http://localhost:3000` // Allow default port 3000 as well ]; if (allowedOrigins.includes(origin)) { diff --git a/mkdocs/docs/assets/listmonk.png b/mkdocs/docs/assets/listmonk.png new file mode 100644 index 0000000..014f912 Binary files /dev/null and b/mkdocs/docs/assets/listmonk.png differ diff --git a/mkdocs/docs/assets/repo-data/admin-changemaker.lite.json b/mkdocs/docs/assets/repo-data/admin-changemaker.lite.json index 8a18e54..b9051f5 100644 --- a/mkdocs/docs/assets/repo-data/admin-changemaker.lite.json +++ b/mkdocs/docs/assets/repo-data/admin-changemaker.lite.json @@ -6,11 +6,11 @@ "language": "HTML", "stars_count": 0, "forks_count": 0, - "open_issues_count": 8, - "updated_at": "2025-08-01T15:14:12-06:00", + "open_issues_count": 19, + "updated_at": "2025-09-02T21:31:48-06:00", "created_at": "2025-05-28T14:54:59-06:00", "clone_url": "https://gitea.bnkops.com/admin/changemaker.lite.git", "ssh_url": "git@gitea.bnkops.com:admin/changemaker.lite.git", "default_branch": "main", - "last_build_update": "2025-08-01T15:14:12-06:00" + "last_build_update": "2025-09-02T21:31:48-06:00" } \ No newline at end of file diff --git a/mkdocs/docs/build/index.md b/mkdocs/docs/build/index.md index a77094b..145cdb2 100644 --- a/mkdocs/docs/build/index.md +++ b/mkdocs/docs/build/index.md @@ -2,13 +2,17 @@ Welcome to Changemaker-Lite! You're about to reclaim your digital sovereignty and stop feeding your secrets to corporations. This guide will help you set up your own political infrastructure that you actually own and control. -This documentation is broken into a few sections: +This documentation is broken into a few sections, which you can see in the navigation bar to the left: - **Build:** Instructions on how to build the cm-lite on your own hardware - **Services:** Overview of all the services that are installed when you install cm-lite - **Configuration:** Information on how to configure all the services that you install in cm-lite - **Manuals:** Manuals on how to use the applications inside cm-lite (with videos!) +Of course, everything is also searachable, so if you want to find something specific, just use the search bar at the top right. + +If you come across anything that is unclear, please open an issue in the [Git Repository](https://gitea.bnkops.com/admin/changemaker.lite), reach out to us at [admin@thebunkerops.ca](mailto:admin@thebunkerops.ca), or edit it yourself by clicking the pencil icon at the top right of each page. + ## Quick Start ### Build Changemaker-Lite @@ -20,7 +24,7 @@ cd changemaker.lite ``` !!! warning "Cloudflare Credentials" - The config.sh script will ask you for your Cloudflare credentials to get started. You can find more information on how to find this in the [Cloudlflare Configuration](../config/cloudflare-config.md) + The config.sh script will ask you for your optional Cloudflare credentials to get started. You can find more information on how to find this in the [Cloudlflare Configuration](../config/cloudflare-config.md) ``` @@ -48,6 +52,28 @@ For secure public access, use the production deployment script: ./start-production.sh ``` +### Map + +Map is the canvassing application that is custom view of nocodb data. Map is best built **after production deployment** to reduce duplicate build efforts. + +Instructions on how to build the map are available in the [map manual](../build/map.md) in the build directory. + +#### Quick Start for Map +Get your NocoDB API token and URL, update the .env file in the map directory, and then run: + +``` +cd map +chmod +x build-nocodb.sh # builds the nocodb tables +./build-nocodb.sh +``` +Copy the urls of the newly created nocodb views and update the .env file in the map directory with them, and then run: + +``` +cd map +docker compose up -d +``` + +You Map instance will be available at [http://localhost:3000](http://localhost:3000) or on the domain you set up during production deployment. ## Why Changemaker Lite? @@ -78,8 +104,8 @@ Before we dive into the technical setup, let's be clear about what you're doing - **Ubuntu 24.04 LTS (Noble Numbat)** - Recommended and tested -!!! note "Getting Started on Ubunut" - Want some help getting started with a baseline buildout for a Ubunut server? You can use our [BNKops Server Build Script](./server.md) +!!! note "Getting Started on Ubuntu" + Want some help getting started with a baseline buildout for a Ubuntu server? You can use our [BNKops Server Build Script](./server.md) - Other Linux distributions with systemd support - WSL2 on Windows (limited functionality) @@ -100,8 +126,7 @@ Before we dive into the technical setup, let's be clear about what you're doing ### Software Prerequisites -!!! tip "Getting Started on Docker" - Want some help getting started with a baseline buildout for a Ubunutu server? You can use our [BNKops Server Build Script](./server.md) to roll out a configured server in about 20 mins! +Ensure the following software is installed on your system. The [BNKops Server Build Script](./server.md) can help set these up if you're on Ubuntu. 1. **Docker Engine** (24.0+) diff --git a/mkdocs/docs/build/map.md b/mkdocs/docs/build/map.md index 525fb78..8db405f 100644 --- a/mkdocs/docs/build/map.md +++ b/mkdocs/docs/build/map.md @@ -34,28 +34,80 @@ cd map Update your `.env` file with your NocoDB details, specifically the instance and api token: ```env -# NocoDB API Configuration -NOCODB_API_URL=https://your-nocodb-instance.com/api/v1 -NOCODB_API_TOKEN=your-api-token-here +NOCODB_API_URL=[change me] +NOCODB_API_TOKEN=[change me] -# These will be populated after running build-nocodb.sh -NOCODB_VIEW_URL= -NOCODB_LOGIN_SHEET= -NOCODB_SETTINGS_SHEET= +# NocoDB View URL is the URL to your NocoDB view where the map data is stored. +NOCODB_VIEW_URL=[change me] + +# NOCODB_LOGIN_SHEET is the URL to your NocoDB login sheet. +NOCODB_LOGIN_SHEET=[change me] + +# NOCODB_SETTINGS_SHEET is the URL to your NocoDB settings sheet. +NOCODB_SETTINGS_SHEET=[change me] + +# NOCODB_SHIFTS_SHEET is the URL to your shifts sheet. +NOCODB_SHIFTS_SHEET=[change me] + +# NOCODB_SHIFT_SIGNUPS_SHEET is the URL to your NocoDB shift signups sheet where users can add their own shifts. +NOCODB_SHIFT_SIGNUPS_SHEET=[change me] + +# NOCODB_CUTS_SHEET is the URL to your NocoDB Cuts sheet. +NOCODB_CUTS_SHEET=[change me] + +DOMAIN=[change me] + +# MkDocs Integration +MKDOCS_URL=[change me] +MKDOCS_SEARCH_URL=[change me] +MKDOCS_SITE_SERVER_PORT=4002 # Server Configuration PORT=3000 NODE_ENV=production -SESSION_SECRET=your-secure-random-string -# Map Defaults (Edmonton, AB) +# Session Secret (IMPORTANT: Generate a secure random string for production) +SESSION_SECRET=[change me] + +# Map Defaults (Edmonton, Alberta, Canada) DEFAULT_LAT=53.5461 DEFAULT_LNG=-113.4938 DEFAULT_ZOOM=11 -# Production Settings -COOKIE_DOMAIN=.yourdomain.com -ALLOWED_ORIGINS=https://map.yourdomain.com,http://localhost:3000 +# Optional: Map Boundaries (prevents users from adding points outside area) +# BOUND_NORTH=53.7 +# BOUND_SOUTH=53.4 +# BOUND_EAST=-113.3 +# BOUND_WEST=-113.7 + +# Cloudflare Settings +TRUST_PROXY=true +COOKIE_DOMAIN=[change me] + +# Update NODE_ENV to production for HTTPS +NODE_ENV=production + +# Add allowed origin +ALLOWED_ORIGINS=[change me] + +# SMTP Configuration +SMTP_HOST=[change me] +SMTP_PORT=587 +SMTP_SECURE=false +SMTP_USER=[change me] +SMTP_PASS=[change me] +EMAIL_FROM_NAME="[change me]" +EMAIL_FROM_ADDRESS=[change me] + +# App Configuration +APP_NAME="[change me]" + +# Listmonk Configuration +LISTMONK_API_URL=[change me] +LISTMONK_USERNAME=[change me] +LISTMONK_PASSWORD=[change me] +LISTMONK_SYNC_ENABLED=true +LISTMONK_INITIAL_SYNC=false # Set to true only for first run to sync existing data ``` ### 3. Auto-Create Database Structure @@ -86,9 +138,23 @@ After the script completes: Edit your `.env` file and add the table URLs: ```env -NOCODB_VIEW_URL=https://your-nocodb.com/dashboard/#/nc/project-id/locations-table-id -NOCODB_LOGIN_SHEET=https://your-nocodb.com/dashboard/#/nc/project-id/login-table-id -NOCODB_SETTINGS_SHEET=https://your-nocodb.com/dashboard/#/nc/project-id/settings-table-id +# NocoDB View URL is the URL to your NocoDB view where the map data is stored. +NOCODB_VIEW_URL=[change me] + +# NOCODB_LOGIN_SHEET is the URL to your NocoDB login sheet. +NOCODB_LOGIN_SHEET=[change me] + +# NOCODB_SETTINGS_SHEET is the URL to your NocoDB settings sheet. +NOCODB_SETTINGS_SHEET=[change me] + +# NOCODB_SHIFTS_SHEET is the URL to your shifts sheet. +NOCODB_SHIFTS_SHEET=[change me] + +# NOCODB_SHIFT_SIGNUPS_SHEET is the URL to your NocoDB shift signups sheet where users can add their own shifts. +NOCODB_SHIFT_SIGNUPS_SHEET=[change me] + +# NOCODB_CUTS_SHEET is the URL to your NocoDB Cuts sheet. +NOCODB_CUTS_SHEET=[change me] ``` ### 6. Build and Deploy diff --git a/mkdocs/docs/config/cloudflare-config.md b/mkdocs/docs/config/cloudflare-config.md index d375ae8..a2a6715 100644 --- a/mkdocs/docs/config/cloudflare-config.md +++ b/mkdocs/docs/config/cloudflare-config.md @@ -12,6 +12,7 @@ The `config.sh` and `start-production.sh` scripts require the following Cloudfla - **Required Permissions**: - `Zone.DNS` (Read/Write) - `Account.Cloudflare Tunnel` (Read/Write) + - `Access` (Read/Write) - **How to Obtain**: - Log in to your Cloudflare account. - Go to **My Profile** > **API Tokens** > **Create Token**. diff --git a/mkdocs/docs/overrides/lander.html b/mkdocs/docs/overrides/lander.html index 94b9b36..14d2bd2 100644 --- a/mkdocs/docs/overrides/lander.html +++ b/mkdocs/docs/overrides/lander.html @@ -1282,11 +1282,11 @@
🚀 Hardware Up This Site Served by Changemaker - Lite
-

Power Tools for Modern Campaign Documentation

+

Power Tools for Modern Campaigns

Give your supporters instant answers at the door, on the phone, or in person. Turn your campaign website & knowledge into a searchable, - mobile-first documentation system that actually works in the field or at the party. No corporate middlemen; your data, your servers, your platform. + mobile-first documentation system that actually works in the field or at the party. Add unlimited users, start as many campaigns as you can, and message all your supporters indefinitely with no extra costs. No corporate middlemen; your data, your servers, your platform.

@@ -1383,14 +1383,31 @@
-

Documentation That Works at the Door

-

Everything your team needs, instantly searchable, always accessible

+

Documentation That Works

+

Everything your team needs, instantly searchable, always accessible, and easy to communicate

+
+ +
+
+

Communicate on Scale

+

Full email and messenger campaign systems with unlimited users

+
    +
  • Drop in replacement for mailchimp, sendgrid, etc.
  • +
  • Track emails, clicks, and user actions
  • +
  • Unlimited contact, asset, and file storage
  • +
  • Compatible with all major email providers
  • +
  • Fully extensible with API's & webhooks
  • +
+
+
+ Phone showing mobile-optimized interface with large touch targets, clear typography, and instant search results +

Mobile-First Everything

-

Built for phones first, because that's what your canvassers carry. Every feature, every interface, optimized for one-handed use in the field.

+

Built for phones first, because that's what your supporters carry. Every feature, every interface, optimized for one-handed use in the field.

  • Touch-optimized interfaces
  • Offline-capable after first load
  • @@ -1579,7 +1596,7 @@
    Listmonk Email Platform
-

Professional email campaigns without the professional price tag.

+

Professional email & messenger campaigns without the professional price tag.