Add venue taxonomies and filter /find-training to show only approved labs: - Create venue_type, venue_equipment, venue_amenities taxonomies - Filter venue markers by mq-approved-lab taxonomy term - Add equipment and amenities badges to venue modal - Add venue contact form with AJAX handler and email notification - Include POC (Point of Contact) meta for each training lab 9 approved training labs configured: - Fast Track Learning Lab, Progressive Training Lab, NAVAC Technical Training Center - Stevens Equipment Phoenix/Johnstown, San Jacinto College, Johnstone Supply - TruTech Tools Training Center (new), Auer Steel & Heating Supply (new) Note: Venues not displaying on map - to be debugged next session. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1113 lines
37 KiB
JavaScript
1113 lines
37 KiB
JavaScript
/**
|
|
* Find Training Map - Google Maps Integration
|
|
*
|
|
* Handles Google Maps initialization, markers, clustering,
|
|
* and interaction for the Find Training page.
|
|
*
|
|
* @package HVAC_Community_Events
|
|
* @since 2.2.0
|
|
*/
|
|
|
|
(function($) {
|
|
'use strict';
|
|
|
|
// Namespace for the module
|
|
window.HVACTrainingMap = {
|
|
|
|
// Map instance
|
|
map: null,
|
|
|
|
// Marker collections
|
|
trainerMarkers: [],
|
|
venueMarkers: [],
|
|
|
|
// MarkerClusterer instance
|
|
markerClusterer: null,
|
|
|
|
// Info window instance (reused)
|
|
infoWindow: null,
|
|
|
|
// Current data
|
|
trainers: [],
|
|
venues: [],
|
|
|
|
// Visible trainers (filtered by map bounds)
|
|
visibleTrainers: [],
|
|
|
|
// Configuration
|
|
config: {
|
|
mapElementId: 'hvac-training-map',
|
|
defaultCenter: { lat: 39.8283, lng: -98.5795 },
|
|
defaultZoom: 4,
|
|
clusterZoom: 8
|
|
},
|
|
|
|
/**
|
|
* Initialize the map
|
|
*/
|
|
init: function() {
|
|
const self = this;
|
|
|
|
// Check if API key is configured
|
|
if (typeof hvacFindTraining === 'undefined' || !hvacFindTraining.api_key_configured) {
|
|
console.warn('Google Maps API key not configured');
|
|
// Still load trainer directory data
|
|
this.loadTrainerDirectory();
|
|
return;
|
|
}
|
|
|
|
// Check if Google Maps is loaded
|
|
if (typeof google === 'undefined' || typeof google.maps === 'undefined') {
|
|
console.error('Google Maps API not loaded');
|
|
this.showMapError('Google Maps failed to load. Please refresh the page.');
|
|
// Still load trainer directory data
|
|
this.loadTrainerDirectory();
|
|
return;
|
|
}
|
|
|
|
// Override config with localized data
|
|
if (hvacFindTraining.map_center) {
|
|
this.config.defaultCenter = hvacFindTraining.map_center;
|
|
}
|
|
if (hvacFindTraining.default_zoom) {
|
|
this.config.defaultZoom = parseInt(hvacFindTraining.default_zoom);
|
|
}
|
|
|
|
// Create the map
|
|
this.createMap();
|
|
|
|
// Create info window
|
|
this.infoWindow = new google.maps.InfoWindow();
|
|
|
|
// Load initial data
|
|
this.loadMapData();
|
|
|
|
// Bind events
|
|
this.bindEvents();
|
|
|
|
// Initialize responsive features
|
|
this.handleWindowResize();
|
|
this.initSidebarToggle();
|
|
},
|
|
|
|
/**
|
|
* Load just the trainer directory (no map)
|
|
*/
|
|
loadTrainerDirectory: function() {
|
|
const self = this;
|
|
|
|
$.ajax({
|
|
url: hvacFindTraining.ajax_url,
|
|
type: 'POST',
|
|
data: {
|
|
action: 'hvac_get_training_map_data',
|
|
nonce: hvacFindTraining.nonce
|
|
},
|
|
success: function(response) {
|
|
if (response.success && response.data) {
|
|
self.trainers = response.data.trainers || [];
|
|
self.updateTrainerGrid(self.trainers);
|
|
self.updateCounts(self.trainers.length, 0);
|
|
}
|
|
},
|
|
error: function() {
|
|
self.hideLoading();
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Create the Google Map instance
|
|
*/
|
|
createMap: function() {
|
|
const mapElement = document.getElementById(this.config.mapElementId);
|
|
|
|
if (!mapElement) {
|
|
console.error('Map element not found');
|
|
return;
|
|
}
|
|
|
|
// Remove loading indicator
|
|
mapElement.innerHTML = '';
|
|
|
|
// Create map with options
|
|
this.map = new google.maps.Map(mapElement, {
|
|
center: this.config.defaultCenter,
|
|
zoom: this.config.defaultZoom,
|
|
mapTypeControl: true,
|
|
mapTypeControlOptions: {
|
|
position: google.maps.ControlPosition.TOP_RIGHT
|
|
},
|
|
streetViewControl: false,
|
|
fullscreenControl: true,
|
|
zoomControl: true,
|
|
zoomControlOptions: {
|
|
position: google.maps.ControlPosition.RIGHT_CENTER
|
|
},
|
|
styles: this.getMapStyles()
|
|
});
|
|
|
|
// Close info window on map click
|
|
this.map.addListener('click', () => {
|
|
this.infoWindow.close();
|
|
});
|
|
|
|
// Sync sidebar with map viewport on pan/zoom
|
|
const self = this;
|
|
this.map.addListener('idle', function() {
|
|
self.syncSidebarWithViewport();
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Get custom map styles
|
|
*/
|
|
getMapStyles: function() {
|
|
return [
|
|
{
|
|
featureType: 'poi',
|
|
elementType: 'labels',
|
|
stylers: [{ visibility: 'off' }]
|
|
},
|
|
{
|
|
featureType: 'transit',
|
|
elementType: 'labels',
|
|
stylers: [{ visibility: 'off' }]
|
|
}
|
|
];
|
|
},
|
|
|
|
/**
|
|
* Load map data via AJAX
|
|
*/
|
|
loadMapData: function(filters) {
|
|
const self = this;
|
|
|
|
const data = {
|
|
action: 'hvac_get_training_map_data',
|
|
nonce: hvacFindTraining.nonce
|
|
};
|
|
|
|
// Add filters if provided
|
|
if (filters) {
|
|
Object.assign(data, filters);
|
|
}
|
|
|
|
$.ajax({
|
|
url: hvacFindTraining.ajax_url,
|
|
type: 'POST',
|
|
data: data,
|
|
beforeSend: function() {
|
|
self.showLoading();
|
|
},
|
|
success: function(response) {
|
|
if (response.success) {
|
|
self.trainers = response.data.trainers || [];
|
|
self.venues = response.data.venues || [];
|
|
self.visibleTrainers = self.trainers.slice(); // Initially all trainers are "visible"
|
|
|
|
self.updateMarkers();
|
|
self.updateCounts(self.trainers.length);
|
|
self.updateTrainerGrid();
|
|
// Note: syncSidebarWithViewport will be called by map 'idle' event
|
|
// to filter trainers to current viewport
|
|
} else {
|
|
self.showMapError(response.data?.message || 'Failed to load data');
|
|
}
|
|
},
|
|
error: function() {
|
|
self.showMapError('Network error. Please try again.');
|
|
},
|
|
complete: function() {
|
|
self.hideLoading();
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Update markers on the map
|
|
*/
|
|
updateMarkers: function() {
|
|
// Clear existing markers
|
|
this.clearMarkers();
|
|
|
|
// Check toggle states
|
|
const showTrainers = $('#hvac-show-trainers').is(':checked');
|
|
const showVenues = $('#hvac-show-venues').is(':checked');
|
|
|
|
// Add trainer markers
|
|
if (showTrainers && this.trainers.length > 0) {
|
|
this.trainers.forEach(trainer => {
|
|
if (trainer.lat && trainer.lng) {
|
|
this.addTrainerMarker(trainer);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Add venue markers
|
|
if (showVenues && this.venues.length > 0) {
|
|
this.venues.forEach(venue => {
|
|
if (venue.lat && venue.lng) {
|
|
this.addVenueMarker(venue);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Initialize clustering
|
|
this.initClustering();
|
|
|
|
// Fit bounds if we have markers
|
|
this.fitBounds();
|
|
},
|
|
|
|
/**
|
|
* Add a trainer marker
|
|
*/
|
|
addTrainerMarker: function(trainer) {
|
|
const self = this;
|
|
|
|
// Create marker with custom icon
|
|
const marker = new google.maps.Marker({
|
|
position: { lat: trainer.lat, lng: trainer.lng },
|
|
map: this.map,
|
|
title: trainer.name,
|
|
icon: this.getTrainerIcon(),
|
|
optimized: false // Required for reliable hover events
|
|
});
|
|
|
|
// Store trainer data on marker
|
|
marker.trainerData = trainer;
|
|
marker.markerType = 'trainer';
|
|
|
|
// Add hover listener to show info window preview
|
|
marker.addListener('mouseover', function() {
|
|
self.showTrainerInfoWindow(this);
|
|
});
|
|
|
|
// Add click listener (also shows info window, for touch devices)
|
|
marker.addListener('click', function() {
|
|
self.showTrainerInfoWindow(this);
|
|
});
|
|
|
|
this.trainerMarkers.push(marker);
|
|
},
|
|
|
|
/**
|
|
* Add a venue marker
|
|
*/
|
|
addVenueMarker: function(venue) {
|
|
const self = this;
|
|
|
|
// Create marker with custom icon
|
|
const marker = new google.maps.Marker({
|
|
position: { lat: venue.lat, lng: venue.lng },
|
|
map: this.map,
|
|
title: venue.name,
|
|
icon: this.getVenueIcon(),
|
|
optimized: false // Required for reliable hover events
|
|
});
|
|
|
|
// Store venue data on marker
|
|
marker.venueData = venue;
|
|
marker.markerType = 'venue';
|
|
|
|
// Add hover listener to show info window preview
|
|
marker.addListener('mouseover', function() {
|
|
self.showVenueInfoWindow(this);
|
|
});
|
|
|
|
// Add click listener (also shows info window, for touch devices)
|
|
marker.addListener('click', function() {
|
|
self.showVenueInfoWindow(this);
|
|
});
|
|
|
|
this.venueMarkers.push(marker);
|
|
},
|
|
|
|
/**
|
|
* Get trainer marker icon
|
|
*/
|
|
getTrainerIcon: function() {
|
|
// Use custom icon if available, otherwise use SVG circle
|
|
if (hvacFindTraining.marker_icons?.trainer) {
|
|
return {
|
|
url: hvacFindTraining.marker_icons.trainer,
|
|
scaledSize: new google.maps.Size(32, 32)
|
|
};
|
|
}
|
|
|
|
// SVG circle marker (teal)
|
|
return {
|
|
path: google.maps.SymbolPath.CIRCLE,
|
|
fillColor: '#00b3a4',
|
|
fillOpacity: 1,
|
|
strokeColor: '#ffffff',
|
|
strokeWeight: 2,
|
|
scale: 10
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Get venue marker icon
|
|
*/
|
|
getVenueIcon: function() {
|
|
// Use custom icon if available, otherwise use SVG marker
|
|
if (hvacFindTraining.marker_icons?.venue) {
|
|
return {
|
|
url: hvacFindTraining.marker_icons.venue,
|
|
scaledSize: new google.maps.Size(32, 32)
|
|
};
|
|
}
|
|
|
|
// SVG marker (orange)
|
|
return {
|
|
path: google.maps.SymbolPath.BACKWARD_CLOSED_ARROW,
|
|
fillColor: '#f5a623',
|
|
fillOpacity: 1,
|
|
strokeColor: '#ffffff',
|
|
strokeWeight: 2,
|
|
scale: 6
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Initialize marker clustering
|
|
*/
|
|
initClustering: function() {
|
|
// Clear existing clusterer
|
|
if (this.markerClusterer) {
|
|
this.markerClusterer.clearMarkers();
|
|
}
|
|
|
|
// Combine all markers
|
|
const allMarkers = [...this.trainerMarkers, ...this.venueMarkers];
|
|
|
|
if (allMarkers.length === 0) {
|
|
return;
|
|
}
|
|
|
|
// Check if MarkerClusterer is available
|
|
if (typeof markerClusterer !== 'undefined' && markerClusterer.MarkerClusterer) {
|
|
this.markerClusterer = new markerClusterer.MarkerClusterer({
|
|
map: this.map,
|
|
markers: allMarkers,
|
|
algorithmOptions: {
|
|
maxZoom: this.config.clusterZoom
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Clear all markers from the map
|
|
*/
|
|
clearMarkers: function() {
|
|
// Clear trainer markers
|
|
this.trainerMarkers.forEach(marker => marker.setMap(null));
|
|
this.trainerMarkers = [];
|
|
|
|
// Clear venue markers
|
|
this.venueMarkers.forEach(marker => marker.setMap(null));
|
|
this.venueMarkers = [];
|
|
|
|
// Clear clusterer
|
|
if (this.markerClusterer) {
|
|
this.markerClusterer.clearMarkers();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Fit map bounds to show all markers
|
|
*/
|
|
fitBounds: function() {
|
|
const allMarkers = [...this.trainerMarkers, ...this.venueMarkers];
|
|
|
|
if (allMarkers.length === 0) {
|
|
// Reset to default view
|
|
this.map.setCenter(this.config.defaultCenter);
|
|
this.map.setZoom(this.config.defaultZoom);
|
|
return;
|
|
}
|
|
|
|
if (allMarkers.length === 1) {
|
|
// Single marker - center on it
|
|
this.map.setCenter(allMarkers[0].getPosition());
|
|
this.map.setZoom(10);
|
|
return;
|
|
}
|
|
|
|
// Calculate bounds
|
|
const bounds = new google.maps.LatLngBounds();
|
|
allMarkers.forEach(marker => {
|
|
bounds.extend(marker.getPosition());
|
|
});
|
|
|
|
this.map.fitBounds(bounds, { padding: 50 });
|
|
},
|
|
|
|
/**
|
|
* Show trainer info window
|
|
*/
|
|
showTrainerInfoWindow: function(marker) {
|
|
const self = this;
|
|
const trainer = marker.trainerData;
|
|
|
|
// Create DOM elements safely to avoid XSS
|
|
const container = document.createElement('div');
|
|
container.className = 'hvac-info-window';
|
|
|
|
const title = document.createElement('div');
|
|
title.className = 'hvac-info-window-title';
|
|
title.textContent = trainer.name;
|
|
container.appendChild(title);
|
|
|
|
const location = document.createElement('div');
|
|
location.className = 'hvac-info-window-location';
|
|
location.textContent = (trainer.city || '') + ', ' + (trainer.state || '');
|
|
container.appendChild(location);
|
|
|
|
if (trainer.certification) {
|
|
const certBadge = document.createElement('span');
|
|
certBadge.className = 'hvac-info-window-cert';
|
|
certBadge.textContent = trainer.certification;
|
|
container.appendChild(certBadge);
|
|
}
|
|
|
|
const button = document.createElement('button');
|
|
button.className = 'hvac-info-window-btn';
|
|
button.textContent = 'View Profile';
|
|
button.addEventListener('click', function() {
|
|
self.openTrainerModal(trainer.profile_id);
|
|
});
|
|
container.appendChild(button);
|
|
|
|
this.infoWindow.setContent(container);
|
|
this.infoWindow.open(this.map, marker);
|
|
},
|
|
|
|
/**
|
|
* Show venue info window
|
|
*/
|
|
showVenueInfoWindow: function(marker) {
|
|
const self = this;
|
|
const venue = marker.venueData;
|
|
|
|
const eventsText = venue.upcoming_events > 0
|
|
? venue.upcoming_events + ' upcoming event' + (venue.upcoming_events > 1 ? 's' : '')
|
|
: 'No upcoming events';
|
|
|
|
// Create DOM elements safely to avoid XSS
|
|
const container = document.createElement('div');
|
|
container.className = 'hvac-info-window-venue';
|
|
|
|
const title = document.createElement('div');
|
|
title.className = 'hvac-info-window-title';
|
|
title.textContent = venue.name;
|
|
container.appendChild(title);
|
|
|
|
const address = document.createElement('div');
|
|
address.className = 'hvac-info-window-address';
|
|
address.innerHTML = this.escapeHtml(venue.address || '') + '<br>' +
|
|
this.escapeHtml(venue.city || '') + ', ' + this.escapeHtml(venue.state || '');
|
|
container.appendChild(address);
|
|
|
|
const eventsCount = document.createElement('div');
|
|
eventsCount.className = 'hvac-info-window-events-count';
|
|
eventsCount.textContent = eventsText;
|
|
container.appendChild(eventsCount);
|
|
|
|
const button = document.createElement('button');
|
|
button.className = 'hvac-info-window-btn';
|
|
button.textContent = 'View Details';
|
|
button.addEventListener('click', function() {
|
|
self.openVenueModal(venue.id);
|
|
});
|
|
container.appendChild(button);
|
|
|
|
this.infoWindow.setContent(container);
|
|
this.infoWindow.open(this.map, marker);
|
|
},
|
|
|
|
/**
|
|
* Open trainer profile modal
|
|
*/
|
|
openTrainerModal: function(profileId) {
|
|
const self = this;
|
|
const $modal = $('#hvac-trainer-modal');
|
|
const $body = $modal.find('.hvac-modal-body');
|
|
const $loading = $modal.find('.hvac-modal-loading');
|
|
|
|
// Show modal with loading
|
|
$modal.show();
|
|
$loading.show();
|
|
$body.hide();
|
|
|
|
// Close info window
|
|
this.infoWindow.close();
|
|
|
|
// Fetch profile data
|
|
$.ajax({
|
|
url: hvacFindTraining.ajax_url,
|
|
type: 'POST',
|
|
data: {
|
|
action: 'hvac_get_trainer_profile_modal',
|
|
nonce: hvacFindTraining.nonce,
|
|
profile_id: profileId
|
|
},
|
|
success: function(response) {
|
|
if (response.success) {
|
|
$body.html(response.data.html);
|
|
$loading.hide();
|
|
$body.show();
|
|
self.bindContactForm();
|
|
} else {
|
|
$body.html('<p class="hvac-error">Failed to load profile.</p>');
|
|
$loading.hide();
|
|
$body.show();
|
|
}
|
|
},
|
|
error: function() {
|
|
$body.html('<p class="hvac-error">Network error. Please try again.</p>');
|
|
$loading.hide();
|
|
$body.show();
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Open venue details modal
|
|
*/
|
|
openVenueModal: function(venueId) {
|
|
const self = this;
|
|
const $modal = $('#hvac-venue-modal');
|
|
|
|
// Close info window
|
|
this.infoWindow.close();
|
|
|
|
// Fetch venue data
|
|
$.ajax({
|
|
url: hvacFindTraining.ajax_url,
|
|
type: 'POST',
|
|
data: {
|
|
action: 'hvac_get_venue_info',
|
|
nonce: hvacFindTraining.nonce,
|
|
venue_id: venueId
|
|
},
|
|
success: function(response) {
|
|
if (response.success) {
|
|
const venue = response.data.venue;
|
|
self.populateVenueModal(venue);
|
|
$modal.show();
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Populate venue modal with data
|
|
*/
|
|
populateVenueModal: function(venue) {
|
|
const self = this;
|
|
const $modal = $('#hvac-venue-modal');
|
|
|
|
// Title
|
|
$modal.find('#venue-modal-title').text(venue.name);
|
|
|
|
// Address
|
|
const addressParts = [venue.address, venue.city, venue.state, venue.zip].filter(Boolean);
|
|
$modal.find('.hvac-venue-address').text(addressParts.join(', '));
|
|
|
|
// Phone
|
|
if (venue.phone) {
|
|
$modal.find('.hvac-venue-phone').html('<strong>Phone:</strong> ' + this.escapeHtml(venue.phone)).show();
|
|
} else {
|
|
$modal.find('.hvac-venue-phone').hide();
|
|
}
|
|
|
|
// Capacity
|
|
if (venue.capacity) {
|
|
$modal.find('.hvac-venue-capacity').html('<strong>Capacity:</strong> ' + this.escapeHtml(venue.capacity)).show();
|
|
} else {
|
|
$modal.find('.hvac-venue-capacity').hide();
|
|
}
|
|
|
|
// Description
|
|
if (venue.description) {
|
|
$modal.find('.hvac-venue-description').html(venue.description).show();
|
|
} else {
|
|
$modal.find('.hvac-venue-description').hide();
|
|
}
|
|
|
|
// Equipment badges
|
|
const $equipmentSection = $modal.find('.hvac-venue-equipment');
|
|
const $equipmentBadges = $modal.find('.hvac-equipment-badges');
|
|
$equipmentBadges.empty();
|
|
|
|
if (venue.equipment && venue.equipment.length > 0) {
|
|
venue.equipment.forEach(item => {
|
|
$equipmentBadges.append(`<span class="hvac-badge hvac-badge-equipment">${this.escapeHtml(item)}</span>`);
|
|
});
|
|
$equipmentSection.show();
|
|
} else {
|
|
$equipmentSection.hide();
|
|
}
|
|
|
|
// Amenities badges
|
|
const $amenitiesSection = $modal.find('.hvac-venue-amenities');
|
|
const $amenitiesBadges = $modal.find('.hvac-amenities-badges');
|
|
$amenitiesBadges.empty();
|
|
|
|
if (venue.amenities && venue.amenities.length > 0) {
|
|
venue.amenities.forEach(item => {
|
|
$amenitiesBadges.append(`<span class="hvac-badge hvac-badge-amenity">${this.escapeHtml(item)}</span>`);
|
|
});
|
|
$amenitiesSection.show();
|
|
} else {
|
|
$amenitiesSection.hide();
|
|
}
|
|
|
|
// Events list
|
|
const $eventsList = $modal.find('.hvac-venue-events-list');
|
|
const $noEvents = $modal.find('.hvac-venue-no-events');
|
|
$eventsList.empty();
|
|
|
|
if (venue.events && venue.events.length > 0) {
|
|
venue.events.forEach(event => {
|
|
$eventsList.append(`
|
|
<li>
|
|
<a href="${this.escapeHtml(event.url)}" target="_blank">${this.escapeHtml(event.title)}</a>
|
|
<span class="hvac-event-date">${this.escapeHtml(event.date)}</span>
|
|
</li>
|
|
`);
|
|
});
|
|
$eventsList.show();
|
|
$noEvents.hide();
|
|
} else {
|
|
$eventsList.hide();
|
|
$noEvents.show();
|
|
}
|
|
|
|
// Directions link
|
|
const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${venue.lat},${venue.lng}`;
|
|
$modal.find('.hvac-venue-directions').attr('href', directionsUrl);
|
|
|
|
// Set up contact form
|
|
const $form = $modal.find('.hvac-venue-contact-form');
|
|
$form.data('venue-id', venue.id);
|
|
$form.attr('data-venue-id', venue.id);
|
|
$form.show();
|
|
$modal.find('.hvac-form-success').hide();
|
|
$modal.find('.hvac-form-error').hide();
|
|
$form[0].reset();
|
|
|
|
// Bind contact form submission
|
|
this.bindVenueContactForm();
|
|
},
|
|
|
|
/**
|
|
* Bind venue contact form submission
|
|
*/
|
|
bindVenueContactForm: function() {
|
|
const self = this;
|
|
|
|
$('.hvac-venue-contact-form').off('submit').on('submit', function(e) {
|
|
e.preventDefault();
|
|
self.submitVenueContactForm($(this));
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Submit venue contact form
|
|
*/
|
|
submitVenueContactForm: function($form) {
|
|
const venueId = $form.data('venue-id');
|
|
const $successMsg = $form.siblings('.hvac-form-success');
|
|
const $errorMsg = $form.siblings('.hvac-form-error');
|
|
const $submit = $form.find('button[type="submit"]');
|
|
|
|
// Collect form data
|
|
const formData = {
|
|
action: 'hvac_submit_venue_contact',
|
|
nonce: hvacFindTraining.nonce,
|
|
venue_id: venueId
|
|
};
|
|
|
|
$form.serializeArray().forEach(field => {
|
|
formData[field.name] = field.value;
|
|
});
|
|
|
|
// Submit
|
|
$submit.prop('disabled', true).text('Sending...');
|
|
$successMsg.hide();
|
|
$errorMsg.hide();
|
|
|
|
$.ajax({
|
|
url: hvacFindTraining.ajax_url,
|
|
type: 'POST',
|
|
data: formData,
|
|
success: function(response) {
|
|
if (response.success) {
|
|
$form.hide();
|
|
$successMsg.show();
|
|
} else {
|
|
$errorMsg.find('p').text(response.data?.message || 'There was a problem sending your message.');
|
|
$errorMsg.show();
|
|
}
|
|
},
|
|
error: function() {
|
|
$errorMsg.show();
|
|
},
|
|
complete: function() {
|
|
$submit.prop('disabled', false).text('Send Message');
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Update trainer directory grid
|
|
*/
|
|
updateTrainerGrid: function() {
|
|
const $grid = $('#hvac-trainer-grid');
|
|
$grid.empty();
|
|
|
|
// Use visible trainers if available (viewport sync), otherwise use all trainers
|
|
const trainersToShow = this.visibleTrainers.length > 0 || this.map
|
|
? this.visibleTrainers
|
|
: this.trainers;
|
|
|
|
if (trainersToShow.length === 0) {
|
|
const message = this.trainers.length > 0
|
|
? 'No trainers visible in this area. Zoom out or pan the map to see more.'
|
|
: 'No trainers found matching your criteria.';
|
|
$grid.html('<div class="hvac-no-results"><p>' + message + '</p></div>');
|
|
$('.hvac-load-more-wrapper').hide();
|
|
return;
|
|
}
|
|
|
|
// Display trainers (show first 6 for compact sidebar, load more on request)
|
|
const displayCount = Math.min(trainersToShow.length, 6);
|
|
|
|
for (let i = 0; i < displayCount; i++) {
|
|
const trainer = trainersToShow[i];
|
|
$grid.append(this.createTrainerCard(trainer));
|
|
}
|
|
|
|
// Show load more if there are more trainers
|
|
if (trainersToShow.length > 6) {
|
|
$('.hvac-load-more-wrapper').show();
|
|
} else {
|
|
$('.hvac-load-more-wrapper').hide();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Create trainer card HTML
|
|
*/
|
|
createTrainerCard: function(trainer) {
|
|
const imageHtml = trainer.image
|
|
? `<img src="${this.escapeHtml(trainer.image)}" alt="${this.escapeHtml(trainer.name)}">`
|
|
: '<div class="hvac-trainer-card-avatar"><span class="dashicons dashicons-businessperson"></span></div>';
|
|
|
|
const certHtml = trainer.certifications && trainer.certifications.length > 0
|
|
? trainer.certifications.map(cert => `<span class="hvac-cert-badge">${this.escapeHtml(cert)}</span>`).join('')
|
|
: '';
|
|
|
|
return `
|
|
<div class="hvac-trainer-card" data-profile-id="${trainer.profile_id}">
|
|
<div class="hvac-trainer-card-image">${imageHtml}</div>
|
|
<div class="hvac-trainer-card-info">
|
|
<div class="hvac-trainer-card-name">${this.escapeHtml(trainer.name)}</div>
|
|
<div class="hvac-trainer-card-location">${this.escapeHtml(trainer.city)}, ${this.escapeHtml(trainer.state)}</div>
|
|
<div class="hvac-trainer-card-certs">${certHtml}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
},
|
|
|
|
/**
|
|
* Update counts display
|
|
* @param {number} visible - Number of visible trainers (or total if no viewport filtering)
|
|
* @param {number} total - Total number of trainers (optional, for "X of Y" format)
|
|
*/
|
|
updateCounts: function(visible, total) {
|
|
const $count = $('#hvac-trainer-count');
|
|
|
|
if (total && total !== visible) {
|
|
// Show "X of Y" format when viewport is filtering
|
|
$count.text(visible + ' of ' + total);
|
|
} else {
|
|
// Show just the count
|
|
$count.text(visible || 0);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Bind event handlers
|
|
*/
|
|
bindEvents: function() {
|
|
const self = this;
|
|
|
|
// Trainer card click
|
|
$(document).on('click', '.hvac-trainer-card', function() {
|
|
const profileId = $(this).data('profile-id');
|
|
if (profileId) {
|
|
self.openTrainerModal(profileId);
|
|
}
|
|
});
|
|
|
|
// Modal close
|
|
$(document).on('click', '.hvac-modal-close, .hvac-modal-overlay', function() {
|
|
$('.hvac-training-modal').hide();
|
|
});
|
|
|
|
// ESC key to close modal
|
|
$(document).on('keydown', function(e) {
|
|
if (e.key === 'Escape') {
|
|
$('.hvac-training-modal').hide();
|
|
}
|
|
});
|
|
|
|
// Marker toggles
|
|
$('#hvac-show-trainers, #hvac-show-venues').on('change', function() {
|
|
self.updateMarkers();
|
|
});
|
|
|
|
// Load more button
|
|
$('#hvac-load-more').on('click', function() {
|
|
self.loadMoreTrainers();
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Bind contact form in modal
|
|
*/
|
|
bindContactForm: function() {
|
|
const self = this;
|
|
|
|
$('.hvac-training-contact-form').off('submit').on('submit', function(e) {
|
|
e.preventDefault();
|
|
self.submitContactForm($(this));
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Submit contact form
|
|
*/
|
|
submitContactForm: function($form) {
|
|
const trainerId = $form.data('trainer-id');
|
|
const profileId = $form.data('profile-id');
|
|
const $successMsg = $form.siblings('.hvac-form-success');
|
|
const $errorMsg = $form.siblings('.hvac-form-error');
|
|
const $submit = $form.find('button[type="submit"]');
|
|
|
|
// Collect form data
|
|
const formData = {
|
|
action: 'hvac_submit_contact_form',
|
|
nonce: hvacFindTraining.nonce,
|
|
trainer_id: trainerId,
|
|
trainer_profile_id: profileId
|
|
};
|
|
|
|
$form.serializeArray().forEach(field => {
|
|
formData[field.name] = field.value;
|
|
});
|
|
|
|
// Submit
|
|
$submit.prop('disabled', true).text('Sending...');
|
|
$successMsg.hide();
|
|
$errorMsg.hide();
|
|
|
|
$.ajax({
|
|
url: hvacFindTraining.ajax_url,
|
|
type: 'POST',
|
|
data: formData,
|
|
success: function(response) {
|
|
if (response.success) {
|
|
$form.hide();
|
|
$successMsg.show();
|
|
} else {
|
|
$errorMsg.show();
|
|
}
|
|
},
|
|
error: function() {
|
|
$errorMsg.show();
|
|
},
|
|
complete: function() {
|
|
$submit.prop('disabled', false).text('Send Message');
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Load more trainers
|
|
*/
|
|
loadMoreTrainers: function() {
|
|
const $grid = $('#hvac-trainer-grid');
|
|
const currentCount = $grid.find('.hvac-trainer-card').length;
|
|
const loadMore = 6; // Load 6 more at a time for sidebar
|
|
|
|
// Use visible trainers if available (viewport sync), otherwise use all trainers
|
|
const trainersToShow = this.visibleTrainers.length > 0 || this.map
|
|
? this.visibleTrainers
|
|
: this.trainers;
|
|
|
|
for (let i = currentCount; i < currentCount + loadMore && i < trainersToShow.length; i++) {
|
|
$grid.append(this.createTrainerCard(trainersToShow[i]));
|
|
}
|
|
|
|
if ($grid.find('.hvac-trainer-card').length >= trainersToShow.length) {
|
|
$('.hvac-load-more-wrapper').hide();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Handle window resize for map
|
|
*/
|
|
handleWindowResize: function() {
|
|
const self = this;
|
|
let resizeTimer;
|
|
|
|
window.addEventListener('resize', function() {
|
|
clearTimeout(resizeTimer);
|
|
resizeTimer = setTimeout(function() {
|
|
if (self.map) {
|
|
google.maps.event.trigger(self.map, 'resize');
|
|
}
|
|
}, 250);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Initialize sidebar toggle for mobile
|
|
*/
|
|
initSidebarToggle: function() {
|
|
const self = this;
|
|
|
|
$(document).on('click', '.hvac-sidebar-toggle', function() {
|
|
const $sidebar = $('.hvac-sidebar');
|
|
const $toggle = $(this);
|
|
const isCollapsed = $sidebar.hasClass('collapsed');
|
|
|
|
$sidebar.toggleClass('collapsed');
|
|
|
|
// Update ARIA attributes
|
|
$toggle.attr('aria-expanded', isCollapsed ? 'true' : 'false');
|
|
|
|
// Trigger map resize after sidebar animation
|
|
setTimeout(function() {
|
|
if (self.map) {
|
|
google.maps.event.trigger(self.map, 'resize');
|
|
}
|
|
}, 300);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Get user's location
|
|
*/
|
|
getUserLocation: function(callback) {
|
|
if (!navigator.geolocation) {
|
|
callback(null, hvacFindTraining.messages.geolocation_unsupported);
|
|
return;
|
|
}
|
|
|
|
navigator.geolocation.getCurrentPosition(
|
|
function(position) {
|
|
callback({
|
|
lat: position.coords.latitude,
|
|
lng: position.coords.longitude
|
|
});
|
|
},
|
|
function(error) {
|
|
callback(null, hvacFindTraining.messages.geolocation_error);
|
|
}
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Center map on location
|
|
*/
|
|
centerOnLocation: function(lat, lng, zoom) {
|
|
this.map.setCenter({ lat: lat, lng: lng });
|
|
this.map.setZoom(zoom || 10);
|
|
},
|
|
|
|
/**
|
|
* Sync sidebar trainer list with visible map viewport
|
|
*/
|
|
syncSidebarWithViewport: function() {
|
|
if (!this.map || this.trainers.length === 0) {
|
|
return;
|
|
}
|
|
|
|
// Get current map bounds
|
|
const bounds = this.map.getBounds();
|
|
if (!bounds) {
|
|
return;
|
|
}
|
|
|
|
// Filter trainers to those visible in current viewport
|
|
this.visibleTrainers = this.trainers.filter(trainer => {
|
|
if (!trainer.lat || !trainer.lng) {
|
|
return false;
|
|
}
|
|
const position = new google.maps.LatLng(trainer.lat, trainer.lng);
|
|
return bounds.contains(position);
|
|
});
|
|
|
|
// Update the sidebar grid with visible trainers
|
|
this.updateTrainerGrid();
|
|
|
|
// Update count to show visible vs total
|
|
this.updateCounts(this.visibleTrainers.length, this.trainers.length);
|
|
},
|
|
|
|
/**
|
|
* Show loading state
|
|
*/
|
|
showLoading: function() {
|
|
$('#hvac-trainer-grid').html('<div class="hvac-grid-loading"><span class="dashicons dashicons-update-alt hvac-spin"></span> Loading trainers...</div>');
|
|
},
|
|
|
|
/**
|
|
* Hide loading state
|
|
*/
|
|
hideLoading: function() {
|
|
$('#hvac-trainer-grid .hvac-grid-loading').remove();
|
|
},
|
|
|
|
/**
|
|
* Show map error
|
|
*/
|
|
showMapError: function(message) {
|
|
const $map = $('#' + this.config.mapElementId);
|
|
$map.html(`
|
|
<div class="hvac-map-loading">
|
|
<span class="dashicons dashicons-warning"></span>
|
|
<p>${this.escapeHtml(message)}</p>
|
|
</div>
|
|
`);
|
|
},
|
|
|
|
/**
|
|
* Escape HTML for safe output
|
|
*/
|
|
escapeHtml: function(text) {
|
|
if (!text) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
};
|
|
|
|
// Initialize when document is ready
|
|
$(document).ready(function() {
|
|
// Wait a moment for Google Maps to fully load
|
|
if ($('#hvac-training-map').length) {
|
|
setTimeout(function() {
|
|
HVACTrainingMap.init();
|
|
}, 100);
|
|
}
|
|
});
|
|
|
|
})(jQuery);
|