got config save working

This commit is contained in:
2025-07-06 21:01:27 -06:00
parent 22f2fb510c
commit f1a4c7a9cc
16 changed files with 2279 additions and 429 deletions

View File

@@ -10,7 +10,36 @@ document.addEventListener('DOMContentLoaded', () => {
loadCurrentStartLocation();
setupEventListeners();
setupNavigation();
loadWalkSheetConfig();
// Check if URL has a hash to show specific section
const hash = window.location.hash;
if (hash === '#walk-sheet') {
// Show walk sheet section and load config
const startLocationSection = document.getElementById('start-location');
const walkSheetSection = document.getElementById('walk-sheet');
const walkSheetNav = document.querySelector('.admin-nav a[href="#walk-sheet"]');
const startLocationNav = document.querySelector('.admin-nav a[href="#start-location"]');
if (startLocationSection) startLocationSection.style.display = 'none';
if (walkSheetSection) walkSheetSection.style.display = 'block';
if (startLocationNav) startLocationNav.classList.remove('active');
if (walkSheetNav) walkSheetNav.classList.add('active');
// Load walk sheet config
setTimeout(() => {
loadWalkSheetConfig().then((success) => {
if (success) {
generateWalkSheetPreview();
}
});
}, 200);
} else {
// Even if not showing walk sheet section, load the config so it's available
// This ensures the config is loaded when the page loads, just like map location
setTimeout(() => {
loadWalkSheetConfig();
}, 300);
}
});
// Check if user is authenticated as admin
@@ -247,9 +276,16 @@ function setupNavigation() {
});
link.classList.add('active');
// If switching to walk sheet, generate preview
// If switching to walk sheet, load config first then generate preview
if (targetId === 'walk-sheet') {
generateWalkSheetPreview();
console.log('Switching to walk sheet section, loading config...');
// Always load the latest config when switching to walk sheet
loadWalkSheetConfig().then((success) => {
if (success) {
console.log('Config loaded, generating preview...');
generateWalkSheetPreview();
}
});
}
});
});
@@ -329,18 +365,20 @@ async function saveWalkSheetConfig() {
qr_code_3_url: document.getElementById('qr-code-3-url')?.value || '',
qr_code_3_label: document.getElementById('qr-code-3-label')?.value || ''
};
console.log('Saving walk sheet config:', config);
// Show loading state
const saveButton = document.getElementById('save-walk-sheet');
if (!saveButton) {
showStatus('Save button not found', 'error');
return;
}
const originalText = saveButton.textContent;
saveButton.textContent = 'Saving...';
saveButton.disabled = true;
try {
const response = await fetch('/api/admin/walk-sheet-config', {
method: 'POST',
@@ -349,27 +387,19 @@ async function saveWalkSheetConfig() {
},
body: JSON.stringify(config)
});
const data = await response.json();
console.log('Save response:', data);
if (data.success) {
showStatus('Walk sheet configuration saved successfully!', 'success');
// Update stored QR codes if new ones were generated
if (data.qrCodes) {
for (let i = 1; i <= 3; i++) {
if (data.qrCodes[`qr_code_${i}_image`]) {
storedQRCodes[`qr_code_${i}_image`] = data.qrCodes[`qr_code_${i}_image`];
}
}
}
// Refresh preview with new QR codes
console.log('Configuration saved successfully');
// Don't reload config here - the form already has the latest values
// Just regenerate the preview
generateWalkSheetPreview();
} else {
throw new Error(data.error || 'Failed to save');
}
} catch (error) {
console.error('Save error:', error);
showStatus(error.message || 'Failed to save walk sheet configuration', 'error');
@@ -539,13 +569,31 @@ async function generatePreviewQRCodes() {
function printWalkSheet() {
// First generate fresh preview to ensure QR codes are generated
generateWalkSheetPreview();
// Wait for QR codes to generate, then print
setTimeout(() => {
const previewContent = document.getElementById('walk-sheet-preview-content');
const clonedContent = previewContent.cloneNode(true);
// Convert canvas elements to images for printing
const canvases = previewContent.querySelectorAll('canvas');
const clonedCanvases = clonedContent.querySelectorAll('canvas');
canvases.forEach((canvas, index) => {
if (canvas && clonedCanvases[index]) {
const img = document.createElement('img');
img.src = canvas.toDataURL('image/png');
img.width = canvas.width;
img.height = canvas.height;
img.style.width = canvas.style.width || `${canvas.width}px`;
img.style.height = canvas.style.height || `${canvas.height}px`;
clonedCanvases[index].parentNode.replaceChild(img, clonedCanvases[index]);
}
});
// Create a print-specific window
const printContent = document.getElementById('walk-sheet-preview-content').innerHTML;
const printWindow = window.open('', '_blank');
printWindow.document.write(`
<!DOCTYPE html>
<html>
@@ -570,73 +618,187 @@ function printWalkSheet() {
margin: 0 !important;
box-shadow: none !important;
page-break-after: avoid !important;
transform: none !important;
}
.ws-qr-code img {
width: 100px !important;
height: 100px !important;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}
@media screen {
body {
margin: 20px;
background: #f0f0f0;
}
.walk-sheet-page {
width: 8.5in;
height: 11in;
padding: 0.5in;
margin: 0 auto;
background: white;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
}
</style>
</head>
<body>
<div class="walk-sheet-page">
${printContent}
${clonedContent.innerHTML}
</div>
</body>
</html>
`);
printWindow.document.close();
// Wait for images to load
printWindow.onload = function() {
setTimeout(() => {
printWindow.print();
printWindow.close();
}, 250);
// User can close manually after printing
}, 500);
};
}, 500);
}, 1000); // Give QR codes time to generate
}
// Load walk sheet configuration
async function loadWalkSheetConfig() {
try {
console.log('Loading walk sheet config...');
const response = await fetch('/api/admin/walk-sheet-config');
const data = await response.json();
if (data.success && data.data) {
// Populate form fields
console.log('Loaded walk sheet config:', data);
if (data.success) {
// The config object contains the actual configuration
const config = data.config || {};
console.log('Config object:', config);
// Populate form fields - use the exact field names from the backend
const titleInput = document.getElementById('walk-sheet-title');
const subtitleInput = document.getElementById('walk-sheet-subtitle');
const footerInput = document.getElementById('walk-sheet-footer');
if (titleInput) titleInput.value = data.data.walk_sheet_title || '';
if (subtitleInput) subtitleInput.value = data.data.walk_sheet_subtitle || '';
if (footerInput) footerInput.value = data.data.walk_sheet_footer || '';
// Store QR code images if they exist
console.log('Found form elements:', {
title: !!titleInput,
subtitle: !!subtitleInput,
footer: !!footerInput
});
if (titleInput) {
titleInput.value = config.walk_sheet_title || 'Campaign Walk Sheet';
console.log('Set title to:', titleInput.value);
}
if (subtitleInput) {
subtitleInput.value = config.walk_sheet_subtitle || 'Door-to-Door Canvassing Form';
console.log('Set subtitle to:', subtitleInput.value);
}
if (footerInput) {
footerInput.value = config.walk_sheet_footer || 'Thank you for your support!';
console.log('Set footer to:', footerInput.value);
}
// Populate QR code fields
for (let i = 1; i <= 3; i++) {
const urlField = document.getElementById(`qr-code-${i}-url`);
const labelField = document.getElementById(`qr-code-${i}-label`);
if (urlField && data.data[`qr_code_${i}_url`]) {
urlField.value = data.data[`qr_code_${i}_url`];
console.log(`QR ${i} fields found:`, {
url: !!urlField,
label: !!labelField
});
if (urlField) {
urlField.value = config[`qr_code_${i}_url`] || '';
console.log(`Set QR ${i} URL to:`, urlField.value);
}
if (labelField && data.data[`qr_code_${i}_label`]) {
labelField.value = data.data[`qr_code_${i}_label`];
}
// Store the QR code image URL if it exists
if (data.data[`qr_code_${i}_image`]) {
storedQRCodes[`qr_code_${i}_image`] = data.data[`qr_code_${i}_image`];
if (labelField) {
labelField.value = config[`qr_code_${i}_label`] || '';
console.log(`Set QR ${i} label to:`, labelField.value);
}
}
console.log('Walk sheet config loaded successfully');
// Generate preview
generateWalkSheetPreview();
// Show status message about data source
if (data.source) {
const sourceText = data.source === 'database' ? 'Walk sheet config loaded from database' :
data.source === 'defaults' ? 'Using walk sheet defaults' :
'Walk sheet config loaded';
showStatus(sourceText, 'info');
}
return true;
} else {
console.error('Failed to load config:', data.error);
showStatus('Failed to load walk sheet configuration', 'error');
return false;
}
} catch (error) {
console.error('Failed to load walk sheet config:', error);
showStatus('Failed to load walk sheet configuration', 'error');
return false;
}
}
// Check if walk sheet section is visible and load config if needed
function checkAndLoadWalkSheetConfig() {
const walkSheetSection = document.getElementById('walk-sheet');
if (walkSheetSection && walkSheetSection.style.display !== 'none') {
console.log('Walk sheet section is visible, loading config...');
loadWalkSheetConfig().then((success) => {
if (success) {
generateWalkSheetPreview();
}
});
}
}
// Add a function to force load config when walk sheet section is accessed
function showWalkSheetSection() {
const walkSheetSection = document.getElementById('walk-sheet');
const startLocationSection = document.getElementById('start-location');
if (startLocationSection) {
startLocationSection.style.display = 'none';
}
if (walkSheetSection) {
walkSheetSection.style.display = 'block';
// Load config after section is shown
setTimeout(() => {
loadWalkSheetConfig().then((success) => {
if (success) {
generateWalkSheetPreview();
}
});
}, 100); // Small delay to ensure DOM is ready
}
}
// Add event listener to trigger config load when walking sheet nav is clicked
document.addEventListener('DOMContentLoaded', function() {
// Add additional event listener for walk sheet nav
const walkSheetNav = document.querySelector('.admin-nav a[href="#walk-sheet"]');
if (walkSheetNav) {
walkSheetNav.addEventListener('click', function(e) {
e.preventDefault();
showWalkSheetSection();
// Update nav state
document.querySelectorAll('.admin-nav a').forEach(link => {
link.classList.remove('active');
});
this.classList.add('active');
});
}
});
// Handle logout
async function handleLogout() {
if (!confirm('Are you sure you want to logout?')) {

81
map/app/public/js/auth.js Normal file
View File

@@ -0,0 +1,81 @@
// Authentication related functions
import { showStatus } from './utils.js';
export let currentUser = null;
export async function checkAuth() {
try {
const response = await fetch('/api/auth/check');
const data = await response.json();
if (!data.authenticated) {
window.location.href = '/login.html';
throw new Error('Not authenticated');
}
currentUser = data.user;
updateUserInterface();
} catch (error) {
console.error('Auth check failed:', error);
window.location.href = '/login.html';
throw error;
}
}
export function updateUserInterface() {
if (!currentUser) return;
// Update user email in both desktop and mobile
const userEmailElement = document.getElementById('user-email');
const mobileUserEmailElement = document.getElementById('mobile-user-email');
if (userEmailElement) {
userEmailElement.textContent = currentUser.email;
}
if (mobileUserEmailElement) {
mobileUserEmailElement.textContent = currentUser.email;
}
// Add admin link if user is admin
if (currentUser.isAdmin) {
addAdminLinks();
}
}
function addAdminLinks() {
// Add admin link to desktop header
const headerActions = document.querySelector('.header-actions');
if (headerActions) {
const adminLink = document.createElement('a');
adminLink.href = '/admin.html';
adminLink.className = 'btn btn-secondary';
adminLink.textContent = '⚙️ Admin';
headerActions.insertBefore(adminLink, headerActions.firstChild);
}
// Add admin link to mobile dropdown
const mobileDropdownContent = document.getElementById('mobile-dropdown-content');
if (mobileDropdownContent) {
// Check if admin link already exists
if (!mobileDropdownContent.querySelector('.admin-link-mobile')) {
const adminItem = document.createElement('div');
adminItem.className = 'mobile-dropdown-item admin-link-mobile';
const adminLink = document.createElement('a');
adminLink.href = '/admin.html';
adminLink.style.color = 'inherit';
adminLink.style.textDecoration = 'none';
adminLink.textContent = '⚙️ Admin Panel';
adminItem.appendChild(adminLink);
// Insert admin link at the top of the dropdown
if (mobileDropdownContent.firstChild) {
mobileDropdownContent.insertBefore(adminItem, mobileDropdownContent.firstChild);
} else {
mobileDropdownContent.appendChild(adminItem);
}
}
}
}

View File

@@ -0,0 +1,9 @@
// Global configuration
export const CONFIG = {
DEFAULT_LAT: parseFloat(document.querySelector('meta[name="default-lat"]')?.content) || 53.5461,
DEFAULT_LNG: parseFloat(document.querySelector('meta[name="default-lng"]')?.content) || -113.4938,
DEFAULT_ZOOM: parseInt(document.querySelector('meta[name="default-zoom"]')?.content) || 11,
REFRESH_INTERVAL: 30000, // 30 seconds
MAX_ZOOM: 19,
MIN_ZOOM: 2
};

View File

@@ -0,0 +1,333 @@
// Location management (CRUD operations)
import { map } from './map-manager.js';
import { showStatus, updateLocationCount, escapeHtml } from './utils.js';
import { currentUser } from './auth.js';
export let markers = [];
export let currentEditingLocation = null;
export async function loadLocations() {
try {
const response = await fetch('/api/locations');
const data = await response.json();
if (data.success) {
displayLocations(data.locations);
updateLocationCount(data.locations.length);
} else {
throw new Error(data.error || 'Failed to load locations');
}
} catch (error) {
console.error('Error loading locations:', error);
showStatus('Failed to load locations', 'error');
}
}
export function displayLocations(locations) {
// Clear existing markers
markers.forEach(marker => {
if (marker && map) {
map.removeLayer(marker);
}
});
markers = [];
// Add new markers
locations.forEach(location => {
if (location.latitude && location.longitude) {
const marker = createLocationMarker(location);
if (marker) {
markers.push(marker);
}
}
});
console.log(`Displayed ${markers.length} locations`);
}
function createLocationMarker(location) {
if (!map) {
console.warn('Map not initialized, skipping marker creation');
return null;
}
const lat = parseFloat(location.latitude);
const lng = parseFloat(location.longitude);
// Determine marker color based on support level
let markerColor = 'blue';
if (location['Support Level']) {
const level = parseInt(location['Support Level']);
switch(level) {
case 1: markerColor = 'green'; break;
case 2: markerColor = 'yellow'; break;
case 3: markerColor = 'orange'; break;
case 4: markerColor = 'red'; break;
}
}
const marker = L.circleMarker([lat, lng], {
radius: 8,
fillColor: markerColor,
color: '#fff',
weight: 2,
opacity: 1,
fillOpacity: 0.8
}).addTo(map);
const popupContent = createPopupContent(location);
marker.bindPopup(popupContent);
marker._locationData = location;
return marker;
}
function createPopupContent(location) {
const locationId = location.Id || location.id || location.ID || location._id;
const name = [location['First Name'], location['Last Name']]
.filter(Boolean).join(' ') || 'Unknown';
const address = location.Address || 'No address';
const supportLevel = location['Support Level'] ?
`Level ${location['Support Level']}` : 'Not specified';
return `
<div class="popup-content">
<h3>${escapeHtml(name)}</h3>
<p><strong>Address:</strong> ${escapeHtml(address)}</p>
<p><strong>Support:</strong> ${escapeHtml(supportLevel)}</p>
${location.Sign ? '<p>🏁 Has campaign sign</p>' : ''}
${location.Notes ? `<p><strong>Notes:</strong> ${escapeHtml(location.Notes)}</p>` : ''}
<div class="popup-meta">
<p>ID: ${locationId || 'Unknown'}</p>
</div>
${currentUser ? `
<div class="popup-actions">
<button class="btn btn-primary btn-sm edit-location-popup-btn"
data-location='${escapeHtml(JSON.stringify(location))}'>
✏️ Edit
</button>
</div>
` : ''}
</div>
`;
}
export async function handleAddLocation(e) {
e.preventDefault();
const formData = new FormData(e.target);
const data = {};
// Convert form data to object
for (let [key, value] of formData.entries()) {
// Map form field names to NocoDB column names
if (key === 'latitude') data.latitude = value.trim();
else if (key === 'longitude') data.longitude = value.trim();
else if (key === 'Geo-Location') data['Geo-Location'] = value.trim();
else if (value.trim() !== '') {
data[key] = value.trim();
}
}
// Ensure geo-location is set
if (data.latitude && data.longitude) {
data['Geo-Location'] = `${data.latitude};${data.longitude}`;
}
// Handle checkbox
data.Sign = document.getElementById('sign').checked;
try {
const response = await fetch('/api/locations', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
showStatus('Location added successfully!', 'success');
closeAddModal();
loadLocations();
} else {
throw new Error(result.error || 'Failed to add location');
}
} catch (error) {
console.error('Error adding location:', error);
showStatus(error.message || 'Failed to add location', 'error');
}
}
export function openEditForm(location) {
currentEditingLocation = location;
// Extract ID - check multiple possible field names
const locationId = location.Id || location.id || location.ID || location._id;
if (!locationId) {
console.error('No ID found in location object. Available fields:', Object.keys(location));
showStatus('Error: Location ID not found. Check console for details.', 'error');
return;
}
// Store the ID in a data attribute for later use
document.getElementById('edit-location-id').value = locationId;
document.getElementById('edit-location-id').setAttribute('data-location-id', locationId);
// Populate form fields
document.getElementById('edit-first-name').value = location['First Name'] || '';
document.getElementById('edit-last-name').value = location['Last Name'] || '';
document.getElementById('edit-location-email').value = location.Email || '';
document.getElementById('edit-location-phone').value = location.Phone || '';
document.getElementById('edit-location-unit').value = location['Unit Number'] || '';
document.getElementById('edit-support-level').value = location['Support Level'] || '';
document.getElementById('edit-location-address').value = location.Address || '';
document.getElementById('edit-sign').checked = location.Sign === true || location.Sign === 'true' || location.Sign === 1;
document.getElementById('edit-sign-size').value = location['Sign Size'] || '';
document.getElementById('edit-location-notes').value = location.Notes || '';
document.getElementById('edit-location-lat').value = location.latitude || '';
document.getElementById('edit-location-lng').value = location.longitude || '';
document.getElementById('edit-geo-location').value = location['Geo-Location'] || '';
// Show edit footer
document.getElementById('edit-footer').classList.remove('hidden');
}
export function closeEditForm() {
document.getElementById('edit-footer').classList.add('hidden');
currentEditingLocation = null;
}
export async function handleEditLocation(e) {
e.preventDefault();
if (!currentEditingLocation) return;
// Get the stored location ID
const locationIdElement = document.getElementById('edit-location-id');
const locationId = locationIdElement.getAttribute('data-location-id') || locationIdElement.value;
if (!locationId || locationId === 'undefined') {
showStatus('Error: Location ID not found', 'error');
return;
}
const formData = new FormData(e.target);
const data = {};
// Convert form data to object
for (let [key, value] of formData.entries()) {
// Skip the ID field
if (key === 'id' || key === 'Id' || key === 'ID') continue;
if (value !== null && value !== undefined) {
data[key] = value.trim();
}
}
// Ensure geo-location is set
if (data.latitude && data.longitude) {
data['Geo-Location'] = `${data.latitude};${data.longitude}`;
}
// Handle checkbox
data.Sign = document.getElementById('edit-sign').checked;
try {
const response = await fetch(`/api/locations/${locationId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const responseText = await response.text();
let result;
try {
result = JSON.parse(responseText);
} catch (e) {
console.error('Failed to parse response:', responseText);
throw new Error(`Server response error: ${response.status} ${response.statusText}`);
}
if (result.success) {
showStatus('Location updated successfully!', 'success');
closeEditForm();
loadLocations();
} else {
throw new Error(result.error || 'Failed to update location');
}
} catch (error) {
console.error('Error updating location:', error);
showStatus(`Update failed: ${error.message}`, 'error');
}
}
export async function handleDeleteLocation() {
if (!currentEditingLocation) return;
// Get the stored location ID
const locationIdElement = document.getElementById('edit-location-id');
const locationId = locationIdElement.getAttribute('data-location-id') || locationIdElement.value;
if (!locationId || locationId === 'undefined') {
showStatus('Error: Location ID not found', 'error');
return;
}
if (!confirm('Are you sure you want to delete this location?')) {
return;
}
try {
const response = await fetch(`/api/locations/${locationId}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
showStatus('Location deleted successfully!', 'success');
closeEditForm();
loadLocations();
} else {
throw new Error(result.error || 'Failed to delete location');
}
} catch (error) {
console.error('Error deleting location:', error);
showStatus(error.message || 'Failed to delete location', 'error');
}
}
export function closeAddModal() {
const modal = document.getElementById('add-modal');
modal.classList.add('hidden');
document.getElementById('location-form').reset();
}
export function openAddModal(lat, lng) {
const modal = document.getElementById('add-modal');
const latInput = document.getElementById('location-lat');
const lngInput = document.getElementById('location-lng');
const geoInput = document.getElementById('geo-location');
// Set coordinates
latInput.value = lat.toFixed(8);
lngInput.value = lng.toFixed(8);
geoInput.value = `${lat.toFixed(8)};${lng.toFixed(8)}`;
// Clear other fields
document.getElementById('location-form').reset();
latInput.value = lat.toFixed(8);
lngInput.value = lng.toFixed(8);
geoInput.value = `${lat.toFixed(8)};${lng.toFixed(8)}`;
// Show modal
modal.classList.remove('hidden');
}

49
map/app/public/js/main.js Normal file
View File

@@ -0,0 +1,49 @@
// Main application entry point
import { CONFIG } from './config.js';
import { hideLoading, showStatus } from './utils.js';
import { checkAuth } from './auth.js';
import { initializeMap } from './map-manager.js';
import { loadLocations } from './location-manager.js';
import { setupEventListeners } from './ui-controls.js';
// Application state
let refreshInterval = null;
// Initialize the application
document.addEventListener('DOMContentLoaded', async () => {
console.log('DOM loaded, initializing application...');
try {
// First check authentication
await checkAuth();
// Then initialize the map
await initializeMap();
// Only load locations after map is ready
await loadLocations();
// Setup other features
setupEventListeners();
setupAutoRefresh();
} catch (error) {
console.error('Initialization error:', error);
showStatus('Failed to initialize application', 'error');
} finally {
hideLoading();
}
});
function setupAutoRefresh() {
refreshInterval = setInterval(() => {
loadLocations();
}, CONFIG.REFRESH_INTERVAL);
}
// Clean up on page unload
window.addEventListener('beforeunload', () => {
if (refreshInterval) {
clearInterval(refreshInterval);
}
});

View File

@@ -0,0 +1,107 @@
// Map initialization and management
import { CONFIG } from './config.js';
import { showStatus } from './utils.js';
import { currentUser } from './auth.js';
export let map = null;
export let startLocationMarker = null;
export let isStartLocationVisible = true;
export async function initializeMap() {
try {
// Get start location from server
const response = await fetch('/api/admin/start-location');
const data = await response.json();
let startLat = CONFIG.DEFAULT_LAT;
let startLng = CONFIG.DEFAULT_LNG;
let startZoom = CONFIG.DEFAULT_ZOOM;
if (data.success && data.location) {
startLat = data.location.latitude;
startLng = data.location.longitude;
startZoom = data.location.zoom;
}
// Initialize map
map = L.map('map').setView([startLat, startLng], startZoom);
// Add tile layer
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: CONFIG.MAX_ZOOM,
minZoom: CONFIG.MIN_ZOOM
}).addTo(map);
// Add start location marker
addStartLocationMarker(startLat, startLng);
console.log('Map initialized successfully');
} catch (error) {
console.error('Failed to initialize map:', error);
showStatus('Failed to initialize map', 'error');
}
}
function addStartLocationMarker(lat, lng) {
console.log(`Adding start location marker at: ${lat}, ${lng}`);
// Remove existing start location marker if it exists
if (startLocationMarker) {
map.removeLayer(startLocationMarker);
}
// Create a very distinctive custom icon
const startIcon = L.divIcon({
html: `
<div class="start-location-marker-wrapper">
<div class="start-location-marker-pin">
<div class="start-location-marker-inner">
<svg width="24" height="24" viewBox="0 0 24 24" fill="white">
<path d="M12 2L2 7v10c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V7l-10-5z"/>
</svg>
</div>
</div>
<div class="start-location-marker-pulse"></div>
</div>
`,
className: 'start-location-custom-marker',
iconSize: [48, 48],
iconAnchor: [24, 48],
popupAnchor: [0, -48]
});
// Create the marker
startLocationMarker = L.marker([lat, lng], {
icon: startIcon,
zIndexOffset: 1000
}).addTo(map);
// Add popup
startLocationMarker.bindPopup(`
<div class="popup-content start-location-popup-enhanced">
<h3>📍 Map Start Location</h3>
<p>This is todays starting location!</p>
${currentUser?.isAdmin ? '<p><a href="/admin.html">Edit in Admin Panel</a></p>' : ''}
</div>
`);
}
export function toggleStartLocationVisibility() {
if (!startLocationMarker) return;
isStartLocationVisible = !isStartLocationVisible;
if (isStartLocationVisible) {
map.addLayer(startLocationMarker);
// Update both desktop and mobile button text
const desktopBtn = document.querySelector('#toggle-start-location-btn .btn-text');
if (desktopBtn) desktopBtn.textContent = 'Hide Start Location';
} else {
map.removeLayer(startLocationMarker);
// Update both desktop and mobile button text
const desktopBtn = document.querySelector('#toggle-start-location-btn .btn-text');
if (desktopBtn) desktopBtn.textContent = 'Show Start Location';
}
}

View File

@@ -72,14 +72,53 @@ async function checkAuth() {
function updateUserInterface() {
if (!currentUser) return;
// Add user info and admin link to header if admin
const headerActions = document.querySelector('.header-actions');
if (currentUser.isAdmin && headerActions) {
const adminLink = document.createElement('a');
adminLink.href = '/admin.html';
adminLink.className = 'btn btn-secondary';
adminLink.textContent = '⚙️ Admin';
headerActions.insertBefore(adminLink, headerActions.firstChild);
// Update user email in both desktop and mobile
const userEmailElement = document.getElementById('user-email');
const mobileUserEmailElement = document.getElementById('mobile-user-email');
if (userEmailElement) {
userEmailElement.textContent = currentUser.email;
}
if (mobileUserEmailElement) {
mobileUserEmailElement.textContent = currentUser.email;
}
// Add admin link if user is admin
if (currentUser.isAdmin) {
// Add admin link to desktop header
const headerActions = document.querySelector('.header-actions');
if (headerActions) {
const adminLink = document.createElement('a');
adminLink.href = '/admin.html';
adminLink.className = 'btn btn-secondary';
adminLink.textContent = '⚙️ Admin';
headerActions.insertBefore(adminLink, headerActions.firstChild);
}
// Add admin link to mobile dropdown
const mobileDropdownContent = document.getElementById('mobile-dropdown-content');
if (mobileDropdownContent) {
// Check if admin link already exists
if (!mobileDropdownContent.querySelector('.admin-link-mobile')) {
const adminItem = document.createElement('div');
adminItem.className = 'mobile-dropdown-item admin-link-mobile';
const adminLink = document.createElement('a');
adminLink.href = '/admin.html';
adminLink.style.color = 'inherit';
adminLink.style.textDecoration = 'none';
adminLink.textContent = '⚙️ Admin Panel';
adminItem.appendChild(adminLink);
// Insert admin link at the top of the dropdown
if (mobileDropdownContent.firstChild) {
mobileDropdownContent.insertBefore(adminItem, mobileDropdownContent.firstChild);
} else {
mobileDropdownContent.appendChild(adminItem);
}
}
}
}
}
@@ -174,10 +213,14 @@ function toggleStartLocationVisibility() {
if (isStartLocationVisible) {
map.addLayer(startLocationMarker);
document.querySelector('#toggle-start-location-btn .btn-text').textContent = 'Hide Start Location';
// Update both desktop and mobile button text
const desktopBtn = document.querySelector('#toggle-start-location-btn .btn-text');
if (desktopBtn) desktopBtn.textContent = 'Hide Start Location';
} else {
map.removeLayer(startLocationMarker);
document.querySelector('#toggle-start-location-btn .btn-text').textContent = 'Show Start Location';
// Update both desktop and mobile button text
const desktopBtn = document.querySelector('#toggle-start-location-btn .btn-text');
if (desktopBtn) desktopBtn.textContent = 'Show Start Location';
}
}
@@ -299,24 +342,43 @@ function createPopupContent(location) {
// Setup event listeners
function setupEventListeners() {
// Refresh button
// Desktop controls
document.getElementById('refresh-btn')?.addEventListener('click', () => {
loadLocations();
showStatus('Locations refreshed', 'success');
});
// Geolocate button
document.getElementById('geolocate-btn')?.addEventListener('click', getUserLocation);
// Toggle start location button
document.getElementById('toggle-start-location-btn')?.addEventListener('click', toggleStartLocationVisibility);
// Add location button
document.getElementById('add-location-btn')?.addEventListener('click', toggleAddLocationMode);
// Fullscreen button
document.getElementById('fullscreen-btn')?.addEventListener('click', toggleFullscreen);
// Mobile controls
document.getElementById('mobile-refresh-btn')?.addEventListener('click', () => {
loadLocations();
showStatus('Locations refreshed', 'success');
});
document.getElementById('mobile-geolocate-btn')?.addEventListener('click', getUserLocation);
document.getElementById('mobile-toggle-start-location-btn')?.addEventListener('click', toggleStartLocationVisibility);
document.getElementById('mobile-add-location-btn')?.addEventListener('click', toggleAddLocationMode);
document.getElementById('mobile-fullscreen-btn')?.addEventListener('click', toggleFullscreen);
// Mobile dropdown toggle
document.getElementById('mobile-dropdown-toggle')?.addEventListener('click', (e) => {
e.stopPropagation();
const dropdown = document.getElementById('mobile-dropdown');
dropdown.classList.toggle('active');
});
// Close mobile dropdown when clicking outside
document.addEventListener('click', (e) => {
const dropdown = document.getElementById('mobile-dropdown');
if (!dropdown.contains(e.target)) {
dropdown.classList.remove('active');
}
});
// Modal controls
document.getElementById('close-modal-btn')?.addEventListener('click', closeAddModal);
document.getElementById('cancel-modal-btn')?.addEventListener('click', closeAddModal);
@@ -498,16 +560,41 @@ function toggleAddLocationMode() {
const crosshair = document.getElementById('crosshair');
const addBtn = document.getElementById('add-location-btn');
const mobileAddBtn = document.getElementById('mobile-add-location-btn');
if (isAddingLocation) {
crosshair.classList.remove('hidden');
addBtn.classList.add('active');
addBtn.innerHTML = '<span class="btn-icon">✕</span><span class="btn-text">Cancel</span>';
// Update desktop button
if (addBtn) {
addBtn.classList.add('active');
addBtn.innerHTML = '<span class="btn-icon">✕</span><span class="btn-text">Cancel</span>';
}
// Update mobile button
if (mobileAddBtn) {
mobileAddBtn.classList.add('active');
mobileAddBtn.innerHTML = '✕';
mobileAddBtn.title = 'Cancel';
}
map.on('click', handleMapClick);
} else {
crosshair.classList.add('hidden');
addBtn.classList.remove('active');
addBtn.innerHTML = '<span class="btn-icon"></span><span class="btn-text">Add Location Here</span>';
// Update desktop button
if (addBtn) {
addBtn.classList.remove('active');
addBtn.innerHTML = '<span class="btn-icon"></span><span class="btn-text">Add Location Here</span>';
}
// Update mobile button
if (mobileAddBtn) {
mobileAddBtn.classList.remove('active');
mobileAddBtn.innerHTML = '';
mobileAddBtn.title = 'Add Location';
}
map.off('click', handleMapClick);
}
}
@@ -842,11 +929,22 @@ async function lookupAddress(mode) {
function toggleFullscreen() {
const app = document.getElementById('app');
const btn = document.getElementById('fullscreen-btn');
const mobileBtn = document.getElementById('mobile-fullscreen-btn');
if (!document.fullscreenElement) {
app.requestFullscreen().then(() => {
app.classList.add('fullscreen');
btn.innerHTML = '<span class="btn-icon">◱</span><span class="btn-text">Exit Fullscreen</span>';
// Update desktop button
if (btn) {
btn.innerHTML = '<span class="btn-icon">◱</span><span class="btn-text">Exit Fullscreen</span>';
}
// Update mobile button
if (mobileBtn) {
mobileBtn.innerHTML = '◱';
mobileBtn.title = 'Exit Fullscreen';
}
}).catch(err => {
console.error('Error entering fullscreen:', err);
showStatus('Unable to enter fullscreen', 'error');
@@ -854,7 +952,17 @@ function toggleFullscreen() {
} else {
document.exitFullscreen().then(() => {
app.classList.remove('fullscreen');
btn.innerHTML = '<span class="btn-icon">⛶</span><span class="btn-text">Fullscreen</span>';
// Update desktop button
if (btn) {
btn.innerHTML = '<span class="btn-icon">⛶</span><span class="btn-text">Fullscreen</span>';
}
// Update mobile button
if (mobileBtn) {
mobileBtn.innerHTML = '⛶';
mobileBtn.title = 'Fullscreen';
}
});
}
}
@@ -862,8 +970,15 @@ function toggleFullscreen() {
// Update location count
function updateLocationCount(count) {
const countElement = document.getElementById('location-count');
const mobileCountElement = document.getElementById('mobile-location-count');
const countText = `${count} location${count !== 1 ? 's' : ''}`;
if (countElement) {
countElement.textContent = `${count} location${count !== 1 ? 's' : ''}`;
countElement.textContent = countText;
}
if (mobileCountElement) {
mobileCountElement.textContent = countText;
}
}

View File

@@ -0,0 +1,364 @@
// UI interaction handlers
import { showStatus, parseGeoLocation } from './utils.js';
import { map, toggleStartLocationVisibility } from './map-manager.js';
import { loadLocations, handleAddLocation, handleEditLocation, handleDeleteLocation, openEditForm, closeEditForm, closeAddModal, openAddModal } from './location-manager.js';
export let userLocationMarker = null;
export let isAddingLocation = false;
export function getUserLocation() {
if (!navigator.geolocation) {
showStatus('Geolocation is not supported by your browser', 'error');
return;
}
showStatus('Getting your location...', 'info');
navigator.geolocation.getCurrentPosition(
(position) => {
const lat = position.coords.latitude;
const lng = position.coords.longitude;
// Center map on user location
map.setView([lat, lng], 15);
// Add or update user location marker
if (userLocationMarker) {
userLocationMarker.setLatLng([lat, lng]);
} else {
userLocationMarker = L.circleMarker([lat, lng], {
radius: 10,
fillColor: '#2196F3',
color: '#fff',
weight: 3,
opacity: 1,
fillOpacity: 0.8
}).addTo(map);
userLocationMarker.bindPopup('<strong>Your Location</strong>');
}
showStatus('Location found!', 'success');
},
(error) => {
let message = 'Unable to get your location';
switch(error.code) {
case error.PERMISSION_DENIED:
message = 'Location permission denied';
break;
case error.POSITION_UNAVAILABLE:
message = 'Location information unavailable';
break;
case error.TIMEOUT:
message = 'Location request timed out';
break;
}
showStatus(message, 'error');
},
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0
}
);
}
export function toggleAddLocationMode() {
isAddingLocation = !isAddingLocation;
const crosshair = document.getElementById('crosshair');
const addBtn = document.getElementById('add-location-btn');
const mobileAddBtn = document.getElementById('mobile-add-location-btn');
if (isAddingLocation) {
crosshair.classList.remove('hidden');
// Update desktop button
if (addBtn) {
addBtn.classList.add('active');
addBtn.innerHTML = '<span class="btn-icon">✕</span><span class="btn-text">Cancel</span>';
}
// Update mobile button
if (mobileAddBtn) {
mobileAddBtn.classList.add('active');
mobileAddBtn.innerHTML = '✕';
mobileAddBtn.title = 'Cancel';
}
map.on('click', handleMapClick);
} else {
crosshair.classList.add('hidden');
// Update desktop button
if (addBtn) {
addBtn.classList.remove('active');
addBtn.innerHTML = '<span class="btn-icon"></span><span class="btn-text">Add Location Here</span>';
}
// Update mobile button
if (mobileAddBtn) {
mobileAddBtn.classList.remove('active');
mobileAddBtn.innerHTML = '';
mobileAddBtn.title = 'Add Location';
}
map.off('click', handleMapClick);
}
}
function handleMapClick(e) {
if (!isAddingLocation) return;
const { lat, lng } = e.latlng;
openAddModal(lat, lng);
toggleAddLocationMode();
}
export function toggleFullscreen() {
const app = document.getElementById('app');
const btn = document.getElementById('fullscreen-btn');
const mobileBtn = document.getElementById('mobile-fullscreen-btn');
if (!document.fullscreenElement) {
app.requestFullscreen().then(() => {
app.classList.add('fullscreen');
// Update desktop button
if (btn) {
btn.innerHTML = '<span class="btn-icon">◱</span><span class="btn-text">Exit Fullscreen</span>';
}
// Update mobile button
if (mobileBtn) {
mobileBtn.innerHTML = '◱';
mobileBtn.title = 'Exit Fullscreen';
}
}).catch(err => {
console.error('Error entering fullscreen:', err);
showStatus('Unable to enter fullscreen', 'error');
});
} else {
document.exitFullscreen().then(() => {
app.classList.remove('fullscreen');
// Update desktop button
if (btn) {
btn.innerHTML = '<span class="btn-icon">⛶</span><span class="btn-text">Fullscreen</span>';
}
// Update mobile button
if (mobileBtn) {
mobileBtn.innerHTML = '⛶';
mobileBtn.title = 'Fullscreen';
}
});
}
}
export async function lookupAddress(mode) {
let latInput, lngInput, addressInput;
if (mode === 'add') {
latInput = document.getElementById('location-lat');
lngInput = document.getElementById('location-lng');
addressInput = document.getElementById('location-address');
} else if (mode === 'edit') {
latInput = document.getElementById('edit-location-lat');
lngInput = document.getElementById('edit-location-lng');
addressInput = document.getElementById('edit-location-address');
} else {
console.error('Invalid lookup mode:', mode);
return;
}
if (!latInput || !lngInput || !addressInput) {
showStatus('Form elements not found', 'error');
return;
}
const lat = parseFloat(latInput.value);
const lng = parseFloat(lngInput.value);
if (isNaN(lat) || isNaN(lng)) {
showStatus('Please enter valid coordinates first', 'warning');
return;
}
// Show loading state
const button = mode === 'add' ?
document.getElementById('lookup-address-add-btn') :
document.getElementById('lookup-address-edit-btn');
const originalText = button ? button.textContent : '';
if (button) {
button.disabled = true;
button.textContent = 'Looking up...';
}
try {
console.log(`Looking up address for: ${lat}, ${lng}`);
const response = await fetch(`/api/geocode/reverse?lat=${lat}&lng=${lng}`);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Geocoding failed: ${response.status} ${errorText}`);
}
const data = await response.json();
if (data.success && data.data) {
// Use the formatted address or full address
const address = data.data.formattedAddress || data.data.fullAddress;
if (address) {
addressInput.value = address;
showStatus('Address found!', 'success');
} else {
showStatus('No address found for these coordinates', 'warning');
}
} else {
showStatus('Address lookup failed', 'warning');
}
} catch (error) {
console.error('Address lookup error:', error);
showStatus(`Address lookup failed: ${error.message}`, 'error');
} finally {
// Restore button state
if (button) {
button.disabled = false;
button.textContent = originalText;
}
}
}
export function setupGeoLocationSync() {
// For add form
const addLatInput = document.getElementById('location-lat');
const addLngInput = document.getElementById('location-lng');
const addGeoInput = document.getElementById('geo-location');
if (addLatInput && addLngInput && addGeoInput) {
[addLatInput, addLngInput].forEach(input => {
input.addEventListener('input', () => {
const lat = addLatInput.value;
const lng = addLngInput.value;
if (lat && lng) {
addGeoInput.value = `${lat};${lng}`;
}
});
});
addGeoInput.addEventListener('input', () => {
const coords = parseGeoLocation(addGeoInput.value);
if (coords) {
addLatInput.value = coords.lat;
addLngInput.value = coords.lng;
}
});
}
// For edit form
const editLatInput = document.getElementById('edit-location-lat');
const editLngInput = document.getElementById('edit-location-lng');
const editGeoInput = document.getElementById('edit-geo-location');
if (editLatInput && editLngInput && editGeoInput) {
[editLatInput, editLngInput].forEach(input => {
input.addEventListener('input', () => {
const lat = editLatInput.value;
const lng = editLngInput.value;
if (lat && lng) {
editGeoInput.value = `${lat};${lng}`;
}
});
});
editGeoInput.addEventListener('input', () => {
const coords = parseGeoLocation(editGeoInput.value);
if (coords) {
editLatInput.value = coords.lat;
editLngInput.value = coords.lng;
}
});
}
}
export function setupEventListeners() {
// Desktop controls
document.getElementById('refresh-btn')?.addEventListener('click', () => {
loadLocations();
showStatus('Locations refreshed', 'success');
});
document.getElementById('geolocate-btn')?.addEventListener('click', getUserLocation);
document.getElementById('toggle-start-location-btn')?.addEventListener('click', toggleStartLocationVisibility);
document.getElementById('add-location-btn')?.addEventListener('click', toggleAddLocationMode);
document.getElementById('fullscreen-btn')?.addEventListener('click', toggleFullscreen);
// Mobile controls
document.getElementById('mobile-refresh-btn')?.addEventListener('click', () => {
loadLocations();
showStatus('Locations refreshed', 'success');
});
document.getElementById('mobile-geolocate-btn')?.addEventListener('click', getUserLocation);
document.getElementById('mobile-toggle-start-location-btn')?.addEventListener('click', toggleStartLocationVisibility);
document.getElementById('mobile-add-location-btn')?.addEventListener('click', toggleAddLocationMode);
document.getElementById('mobile-fullscreen-btn')?.addEventListener('click', toggleFullscreen);
// Mobile dropdown toggle
document.getElementById('mobile-dropdown-toggle')?.addEventListener('click', (e) => {
e.stopPropagation();
const dropdown = document.getElementById('mobile-dropdown');
dropdown.classList.toggle('active');
});
// Close mobile dropdown when clicking outside
document.addEventListener('click', (e) => {
const dropdown = document.getElementById('mobile-dropdown');
if (!dropdown.contains(e.target)) {
dropdown.classList.remove('active');
}
});
// Modal controls
document.getElementById('close-modal-btn')?.addEventListener('click', closeAddModal);
document.getElementById('cancel-modal-btn')?.addEventListener('click', closeAddModal);
// Edit footer controls
document.getElementById('close-edit-footer-btn')?.addEventListener('click', closeEditForm);
// Forms
document.getElementById('location-form')?.addEventListener('submit', handleAddLocation);
document.getElementById('edit-location-form')?.addEventListener('submit', handleEditLocation);
// Delete button
document.getElementById('delete-location-btn')?.addEventListener('click', handleDeleteLocation);
// Address lookup buttons
document.getElementById('lookup-address-add-btn')?.addEventListener('click', () => {
lookupAddress('add');
});
document.getElementById('lookup-address-edit-btn')?.addEventListener('click', () => {
lookupAddress('edit');
});
// Geo-location field sync
setupGeoLocationSync();
// Add event delegation for popup edit buttons
document.addEventListener('click', (e) => {
if (e.target.classList.contains('edit-location-popup-btn')) {
e.preventDefault();
try {
const locationData = JSON.parse(e.target.getAttribute('data-location'));
openEditForm(locationData);
} catch (error) {
console.error('Error parsing location data:', error);
showStatus('Error opening edit form', 'error');
}
}
});
}

View File

@@ -0,0 +1,67 @@
// Utility functions
export function escapeHtml(text) {
if (text === null || text === undefined) {
return '';
}
const div = document.createElement('div');
div.textContent = String(text);
return div.innerHTML;
}
export function parseGeoLocation(value) {
if (!value) return null;
// Try semicolon separator first
let parts = value.split(';');
if (parts.length !== 2) {
// Try comma separator
parts = value.split(',');
}
if (parts.length === 2) {
const lat = parseFloat(parts[0].trim());
const lng = parseFloat(parts[1].trim());
if (!isNaN(lat) && !isNaN(lng)) {
return { lat, lng };
}
}
return null;
}
export function showStatus(message, type = 'info') {
const container = document.getElementById('status-container');
const messageDiv = document.createElement('div');
messageDiv.className = `status-message ${type}`;
messageDiv.textContent = message;
container.appendChild(messageDiv);
// Auto-remove after 5 seconds
setTimeout(() => {
messageDiv.remove();
}, 5000);
}
export function hideLoading() {
const loading = document.getElementById('loading');
if (loading) {
loading.classList.add('hidden');
}
}
export function updateLocationCount(count) {
const countElement = document.getElementById('location-count');
const mobileCountElement = document.getElementById('mobile-location-count');
const countText = `${count} location${count !== 1 ? 's' : ''}`;
if (countElement) {
countElement.textContent = countText;
}
if (mobileCountElement) {
mobileCountElement.textContent = countText;
}
}