${route.steps.map((step, index) => {
let stepClass = '';
if (index === 0) stepClass = 'start';
else if (index === route.steps.length - 1) stepClass = 'end';
return `
${index + 1}
${this.escapeHtml(step.instruction)}
${step.distanceText}
`;
}).join('')}
`;
window.print();
}
async openUpdateModal(resourceId) {
try {
const response = await fetch(`/api/resources/${resourceId}`);
const data = await response.json();
const resource = data.resource;
if (!resource) {
alert('Resource not found');
return;
}
// Store resource ID
document.getElementById('updateResourceId').value = resourceId;
// Clear form
document.getElementById('updateForm').reset();
// Pre-populate fields with current values as placeholders
document.getElementById('proposedName').placeholder = resource.name || 'Resource name';
document.getElementById('proposedResourceType').value = '';
document.getElementById('proposedCity').placeholder = resource.city || 'City';
document.getElementById('proposedAddress').placeholder = resource.address || 'Street address';
document.getElementById('proposedPhone').placeholder = resource.phone || 'Phone number';
document.getElementById('proposedEmail').placeholder = resource.email || 'Contact email';
document.getElementById('proposedWebsite').placeholder = resource.website || 'https://...';
document.getElementById('proposedHours').placeholder = resource.hours_of_operation || 'Hours of operation';
document.getElementById('proposedDescription').placeholder = resource.description || 'Description';
document.getElementById('proposedEligibility').placeholder = resource.eligibility || 'Eligibility requirements';
document.getElementById('proposedServices').placeholder = resource.services_offered || 'Services offered';
// Show modal
document.getElementById('updateModal').classList.remove('hidden');
document.body.style.overflow = 'hidden';
} catch (error) {
console.error('Failed to open update modal:', error);
alert('Failed to load resource details');
}
}
async submitUpdateRequest() {
const resourceId = document.getElementById('updateResourceId').value;
const submitterEmail = document.getElementById('submitterEmail').value.trim();
const submitterName = document.getElementById('submitterName').value.trim();
if (!submitterEmail) {
alert('Please enter your email address');
return;
}
const data = {
submitter_email: submitterEmail,
submitter_name: submitterName || null,
proposed_name: document.getElementById('proposedName').value.trim() || null,
proposed_resource_type: document.getElementById('proposedResourceType').value || null,
proposed_city: document.getElementById('proposedCity').value.trim() || null,
proposed_address: document.getElementById('proposedAddress').value.trim() || null,
proposed_phone: document.getElementById('proposedPhone').value.trim() || null,
proposed_email: document.getElementById('proposedEmail').value.trim() || null,
proposed_website: document.getElementById('proposedWebsite').value.trim() || null,
proposed_hours_of_operation: document.getElementById('proposedHours').value.trim() || null,
proposed_description: document.getElementById('proposedDescription').value.trim() || null,
proposed_eligibility: document.getElementById('proposedEligibility').value.trim() || null,
proposed_services_offered: document.getElementById('proposedServices').value.trim() || null,
additional_notes: document.getElementById('additionalNotes').value.trim() || null
};
// Check if at least one change is proposed
const hasChanges = Object.entries(data)
.filter(([key]) => key.startsWith('proposed_'))
.some(([, value]) => value !== null);
if (!hasChanges && !data.additional_notes) {
alert('Please enter at least one proposed change or note');
return;
}
try {
const response = await fetch(`/api/resources/${resourceId}/update-request`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (response.ok) {
alert('Thank you! Your update request has been submitted and will be reviewed by our team.');
this.closeModal('updateModal');
} else {
alert(result.error || 'Failed to submit update request');
}
} catch (error) {
console.error('Failed to submit update request:', error);
alert('Failed to submit update request. Please try again.');
}
}
openAddListingModal() {
// Clear form
document.getElementById('addListingForm').reset();
// Show modal
document.getElementById('addListingModal').classList.remove('hidden');
document.body.style.overflow = 'hidden';
}
async submitNewListing() {
const submitterEmail = document.getElementById('newListingSubmitterEmail').value.trim();
const name = document.getElementById('newListingName').value.trim();
if (!submitterEmail) {
alert('Please enter your email address');
return;
}
if (!name) {
alert('Please enter a name for the listing');
return;
}
const data = {
submitter_email: submitterEmail,
submitter_name: document.getElementById('newListingSubmitterName').value.trim() || null,
name: name,
resource_type: document.getElementById('newListingResourceType').value || 'other',
city: document.getElementById('newListingCity').value.trim() || null,
address: document.getElementById('newListingAddress').value.trim() || null,
phone: document.getElementById('newListingPhone').value.trim() || null,
email: document.getElementById('newListingEmail').value.trim() || null,
website: document.getElementById('newListingWebsite').value.trim() || null,
hours_of_operation: document.getElementById('newListingHours').value.trim() || null,
description: document.getElementById('newListingDescription').value.trim() || null,
eligibility: document.getElementById('newListingEligibility').value.trim() || null,
services_offered: document.getElementById('newListingServices').value.trim() || null,
additional_notes: document.getElementById('newListingNotes').value.trim() || null
};
try {
const response = await fetch('/api/listings/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (response.ok) {
alert('Thank you! Your listing has been submitted and will be reviewed by our team.');
this.closeModal('addListingModal');
} else {
alert(result.error || 'Failed to submit listing');
}
} catch (error) {
console.error('Failed to submit listing:', error);
alert('Failed to submit listing. Please try again.');
}
}
closeModal(modalId) {
document.getElementById(modalId).classList.add('hidden');
document.body.style.overflow = '';
// Clean up directions map
if (modalId === 'directionsModal' && this.directionsMap) {
this.directionsMap.remove();
this.directionsMap = null;
}
}
updateResultCount() {
const count = document.getElementById('resultCount');
if (this.totalResources > 0 && this.resources.length < this.totalResources) {
count.textContent = `Showing ${this.resources.length} of ${this.totalResources} resources`;
} else {
count.textContent = `${this.resources.length} resource${this.resources.length !== 1 ? 's' : ''}`;
}
}
showLoading(show) {
const loading = document.getElementById('loadingIndicator');
const list = document.getElementById('resourceList');
if (show) {
loading.classList.remove('hidden');
list.innerHTML = '';
} else {
loading.classList.add('hidden');
}
}
showError(message) {
const container = document.getElementById('resourceList');
container.innerHTML = `
${message}
`;
}
formatType(type) {
const types = {
'food_bank': 'Food Bank',
'community_meal': 'Community Meal',
'hamper': 'Food Hamper',
'pantry': 'Food Pantry',
'soup_kitchen': 'Soup Kitchen',
'mobile_food': 'Mobile Food',
'grocery_program': 'Grocery Program',
'other': 'Other'
};
return types[type] || type;
}
escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
hasLocationData(resource) {
// Check if resource has enough location data for directions/maps
// Must have a valid street address (with number) or valid coordinates
// Check for valid coordinates
if (resource.latitude && resource.longitude) {
const lat = parseFloat(resource.latitude);
const lng = parseFloat(resource.longitude);
// Validate coordinates are in Alberta region (roughly)
if (!isNaN(lat) && !isNaN(lng) && lat >= 48 && lat <= 60 && lng >= -120 && lng <= -110) {
return true;
}
}
// Check for valid street address
if (resource.address && resource.city) {
const addr = resource.address.trim();
// Skip PO Boxes, general delivery, and addresses without street numbers
if (addr &&
!addr.toLowerCase().startsWith('po box') &&
!addr.toLowerCase().startsWith('p.o. box') &&
!addr.toLowerCase().startsWith('general delivery') &&
/\d/.test(addr)) { // Must contain at least one digit (street number)
return true;
}
}
return false;
}
hasIncompleteInfo(resource) {
// Check if resource is missing important information
return !resource.phone ||
!resource.address ||
!this.hasLocationData(resource);
}
getMapsUrl(resource) {
// Build a search query for maps - prefer address for better label in Maps app
const parts = [];
if (resource.address) parts.push(resource.address);
if (resource.city) parts.push(resource.city);
parts.push(resource.province || 'Alberta');
if (resource.postal_code) parts.push(resource.postal_code);
// Use address if we have at least city
if (parts.length >= 2) {
const query = encodeURIComponent(parts.join(', '));
return `https://www.google.com/maps/search/?api=1&query=${query}`;
}
// Fall back to coordinates if no address info
if (resource.latitude && resource.longitude) {
return `https://www.google.com/maps/search/?api=1&query=${resource.latitude},${resource.longitude}`;
}
// Last resort - just the name
return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(resource.name + ', Alberta')}`;
}
}
// Initialize app
const app = new FoodResourceApp();