Implements /find-training page with Google Maps JavaScript API: - Interactive map showing trainers (teal) and venues (orange) markers - MarkerClusterer for dense areas - Filter by State, Certification, Training Format - Search by name/location - "Near Me" geolocation with proximity filtering - Trainer profile modal with contact form - Venue info modal with upcoming events - 301 redirect from /find-a-trainer to /find-training - Auto-geocoding for new TEC venues via Google API Multi-model code review fixes (GPT-5, Gemini 3, Zen MCP): - Added missing contact form AJAX handler with rate limiting - Fixed XSS risk in InfoWindow (DOM creation vs inline onclick) - Added caching for filter dropdown queries (1-hour TTL) - Added AJAX abort handling to prevent race conditions - Replaced alert() with inline error notifications New files: - includes/find-training/class-hvac-find-training-page.php - includes/find-training/class-hvac-training-map-data.php - includes/find-training/class-hvac-venue-geocoding.php - templates/page-find-training.php - assets/js/find-training-map.js - assets/js/find-training-filters.js - assets/css/find-training-map.css - assets/images/marker-trainer.svg - assets/images/marker-venue.svg Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
388 lines
13 KiB
JavaScript
388 lines
13 KiB
JavaScript
/**
|
|
* Find Training Filters
|
|
*
|
|
* Handles filtering, searching, and geolocation functionality
|
|
* for the Find Training page.
|
|
*
|
|
* @package HVAC_Community_Events
|
|
* @since 2.2.0
|
|
*/
|
|
|
|
(function($) {
|
|
'use strict';
|
|
|
|
// Namespace for filters module
|
|
window.HVACTrainingFilters = {
|
|
|
|
// Debounce timer for search
|
|
searchTimer: null,
|
|
|
|
// Current AJAX request (for aborting)
|
|
currentRequest: null,
|
|
|
|
// Active filters
|
|
activeFilters: {
|
|
state: '',
|
|
certification: '',
|
|
training_format: '',
|
|
search: ''
|
|
},
|
|
|
|
// User location (if obtained)
|
|
userLocation: null,
|
|
|
|
/**
|
|
* Initialize filters
|
|
*/
|
|
init: function() {
|
|
this.bindEvents();
|
|
},
|
|
|
|
/**
|
|
* Bind event handlers
|
|
*/
|
|
bindEvents: function() {
|
|
const self = this;
|
|
|
|
// Search input with debounce
|
|
$('#hvac-training-search').on('input', function() {
|
|
clearTimeout(self.searchTimer);
|
|
const value = $(this).val();
|
|
|
|
self.searchTimer = setTimeout(function() {
|
|
self.activeFilters.search = value;
|
|
self.applyFilters();
|
|
}, 300);
|
|
});
|
|
|
|
// State filter
|
|
$('#hvac-filter-state').on('change', function() {
|
|
self.activeFilters.state = $(this).val();
|
|
self.applyFilters();
|
|
self.updateActiveFiltersDisplay();
|
|
});
|
|
|
|
// Certification filter
|
|
$('#hvac-filter-certification').on('change', function() {
|
|
self.activeFilters.certification = $(this).val();
|
|
self.applyFilters();
|
|
self.updateActiveFiltersDisplay();
|
|
});
|
|
|
|
// Training format filter
|
|
$('#hvac-filter-format').on('change', function() {
|
|
self.activeFilters.training_format = $(this).val();
|
|
self.applyFilters();
|
|
self.updateActiveFiltersDisplay();
|
|
});
|
|
|
|
// Near Me button
|
|
$('#hvac-near-me-btn').on('click', function() {
|
|
self.handleNearMeClick($(this));
|
|
});
|
|
|
|
// Clear all filters
|
|
$('.hvac-clear-filters').on('click', function() {
|
|
self.clearAllFilters();
|
|
});
|
|
|
|
// Remove individual filter
|
|
$(document).on('click', '.hvac-active-filter button', function() {
|
|
const filterType = $(this).parent().data('filter');
|
|
self.removeFilter(filterType);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Apply current filters
|
|
*/
|
|
applyFilters: function() {
|
|
const self = this;
|
|
|
|
// Abort any pending request to prevent race conditions
|
|
if (this.currentRequest && this.currentRequest.readyState !== 4) {
|
|
this.currentRequest.abort();
|
|
}
|
|
|
|
// Build filter data
|
|
const filterData = {
|
|
action: 'hvac_filter_training_map',
|
|
nonce: hvacFindTraining.nonce,
|
|
state: this.activeFilters.state,
|
|
certification: this.activeFilters.certification,
|
|
training_format: this.activeFilters.training_format,
|
|
search: this.activeFilters.search,
|
|
show_trainers: $('#hvac-show-trainers').is(':checked'),
|
|
show_venues: $('#hvac-show-venues').is(':checked')
|
|
};
|
|
|
|
// Add user location if available
|
|
if (this.userLocation) {
|
|
filterData.lat = this.userLocation.lat;
|
|
filterData.lng = this.userLocation.lng;
|
|
filterData.radius = 100; // km
|
|
}
|
|
|
|
// Send filter request and store reference
|
|
this.currentRequest = $.ajax({
|
|
url: hvacFindTraining.ajax_url,
|
|
type: 'POST',
|
|
data: filterData,
|
|
success: function(response) {
|
|
if (response.success) {
|
|
// Update map data
|
|
HVACTrainingMap.trainers = response.data.trainers || [];
|
|
HVACTrainingMap.venues = response.data.venues || [];
|
|
HVACTrainingMap.updateMarkers();
|
|
HVACTrainingMap.updateCounts(
|
|
response.data.total_trainers,
|
|
response.data.total_venues
|
|
);
|
|
HVACTrainingMap.updateTrainerGrid();
|
|
}
|
|
},
|
|
complete: function() {
|
|
self.currentRequest = null;
|
|
}
|
|
});
|
|
|
|
// Show/hide clear button
|
|
this.updateClearButtonVisibility();
|
|
},
|
|
|
|
/**
|
|
* Handle Near Me button click
|
|
*/
|
|
handleNearMeClick: function($button) {
|
|
const self = this;
|
|
|
|
// Show loading state
|
|
$button.prop('disabled', true);
|
|
$button.html('<span class="dashicons dashicons-update-alt hvac-spin"></span> Locating...');
|
|
|
|
// Clear any previous error message
|
|
this.clearLocationError();
|
|
|
|
// Get user location
|
|
HVACTrainingMap.getUserLocation(function(location, error) {
|
|
if (location) {
|
|
self.userLocation = location;
|
|
|
|
// Center map on user location
|
|
HVACTrainingMap.centerOnLocation(location.lat, location.lng, 9);
|
|
|
|
// Apply filters with location
|
|
self.applyFilters();
|
|
|
|
// Update button state
|
|
$button.html('<span class="dashicons dashicons-yes-alt"></span> Near Me');
|
|
$button.addClass('active');
|
|
|
|
// Add to active filters display
|
|
self.addActiveFilter('location', 'Near Me');
|
|
} else {
|
|
// Show inline error instead of alert
|
|
self.showLocationError(error || 'Unable to get your location. Please check browser permissions.');
|
|
|
|
// Reset button
|
|
$button.html('<span class="dashicons dashicons-location-alt"></span> Near Me');
|
|
$button.prop('disabled', false);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Show location error message inline
|
|
*/
|
|
showLocationError: function(message) {
|
|
// Remove any existing error
|
|
this.clearLocationError();
|
|
|
|
// Create error element
|
|
const $error = $('<div class="hvac-location-error">' +
|
|
'<span class="dashicons dashicons-warning"></span> ' +
|
|
this.escapeHtml(message) +
|
|
'<button type="button" class="hvac-dismiss-error" aria-label="Dismiss">×</button>' +
|
|
'</div>');
|
|
|
|
// Insert after Near Me button
|
|
$('#hvac-near-me-btn').after($error);
|
|
|
|
// Auto-dismiss after 5 seconds
|
|
setTimeout(function() {
|
|
$error.fadeOut(300, function() { $(this).remove(); });
|
|
}, 5000);
|
|
|
|
// Click to dismiss
|
|
$error.find('.hvac-dismiss-error').on('click', function() {
|
|
$error.remove();
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Clear location error message
|
|
*/
|
|
clearLocationError: function() {
|
|
$('.hvac-location-error').remove();
|
|
},
|
|
|
|
/**
|
|
* Clear all filters
|
|
*/
|
|
clearAllFilters: function() {
|
|
// Reset filter values
|
|
this.activeFilters = {
|
|
state: '',
|
|
certification: '',
|
|
training_format: '',
|
|
search: ''
|
|
};
|
|
|
|
// Reset user location
|
|
this.userLocation = null;
|
|
|
|
// Reset form elements
|
|
$('#hvac-filter-state').val('');
|
|
$('#hvac-filter-certification').val('');
|
|
$('#hvac-filter-format').val('');
|
|
$('#hvac-training-search').val('');
|
|
|
|
// Reset Near Me button
|
|
$('#hvac-near-me-btn')
|
|
.removeClass('active')
|
|
.html('<span class="dashicons dashicons-location-alt"></span> Near Me')
|
|
.prop('disabled', false);
|
|
|
|
// Clear active filters display
|
|
$('.hvac-active-filters').empty();
|
|
|
|
// Hide clear button
|
|
$('.hvac-clear-filters').hide();
|
|
|
|
// Reset map to default view
|
|
HVACTrainingMap.map.setCenter(HVACTrainingMap.config.defaultCenter);
|
|
HVACTrainingMap.map.setZoom(HVACTrainingMap.config.defaultZoom);
|
|
|
|
// Reload data without filters
|
|
HVACTrainingMap.loadMapData();
|
|
},
|
|
|
|
/**
|
|
* Remove a specific filter
|
|
*/
|
|
removeFilter: function(filterType) {
|
|
switch (filterType) {
|
|
case 'state':
|
|
this.activeFilters.state = '';
|
|
$('#hvac-filter-state').val('');
|
|
break;
|
|
case 'certification':
|
|
this.activeFilters.certification = '';
|
|
$('#hvac-filter-certification').val('');
|
|
break;
|
|
case 'training_format':
|
|
this.activeFilters.training_format = '';
|
|
$('#hvac-filter-format').val('');
|
|
break;
|
|
case 'search':
|
|
this.activeFilters.search = '';
|
|
$('#hvac-training-search').val('');
|
|
break;
|
|
case 'location':
|
|
this.userLocation = null;
|
|
$('#hvac-near-me-btn')
|
|
.removeClass('active')
|
|
.html('<span class="dashicons dashicons-location-alt"></span> Near Me')
|
|
.prop('disabled', false);
|
|
break;
|
|
}
|
|
|
|
this.applyFilters();
|
|
this.updateActiveFiltersDisplay();
|
|
},
|
|
|
|
/**
|
|
* Update active filters display
|
|
*/
|
|
updateActiveFiltersDisplay: function() {
|
|
const $container = $('.hvac-active-filters');
|
|
$container.empty();
|
|
|
|
// State filter
|
|
if (this.activeFilters.state) {
|
|
this.addActiveFilter('state', `State: ${this.activeFilters.state}`);
|
|
}
|
|
|
|
// Certification filter
|
|
if (this.activeFilters.certification) {
|
|
this.addActiveFilter('certification', this.activeFilters.certification);
|
|
}
|
|
|
|
// Training format filter
|
|
if (this.activeFilters.training_format) {
|
|
this.addActiveFilter('training_format', this.activeFilters.training_format);
|
|
}
|
|
|
|
// Search filter
|
|
if (this.activeFilters.search) {
|
|
this.addActiveFilter('search', `"${this.activeFilters.search}"`);
|
|
}
|
|
|
|
// Location filter
|
|
if (this.userLocation) {
|
|
this.addActiveFilter('location', 'Near Me');
|
|
}
|
|
|
|
this.updateClearButtonVisibility();
|
|
},
|
|
|
|
/**
|
|
* Add an active filter chip
|
|
*/
|
|
addActiveFilter: function(type, label) {
|
|
const $container = $('.hvac-active-filters');
|
|
const $chip = $(`
|
|
<span class="hvac-active-filter" data-filter="${type}">
|
|
${this.escapeHtml(label)}
|
|
<button type="button" aria-label="Remove filter">×</button>
|
|
</span>
|
|
`);
|
|
$container.append($chip);
|
|
},
|
|
|
|
/**
|
|
* Update clear button visibility
|
|
*/
|
|
updateClearButtonVisibility: function() {
|
|
const hasFilters = this.activeFilters.state ||
|
|
this.activeFilters.certification ||
|
|
this.activeFilters.training_format ||
|
|
this.activeFilters.search ||
|
|
this.userLocation;
|
|
|
|
if (hasFilters) {
|
|
$('.hvac-clear-filters').show();
|
|
} else {
|
|
$('.hvac-clear-filters').hide();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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() {
|
|
if ($('#hvac-training-map').length) {
|
|
HVACTrainingFilters.init();
|
|
}
|
|
});
|
|
|
|
})(jQuery);
|