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>
517 lines
18 KiB
PHP
517 lines
18 KiB
PHP
<?php
|
|
/**
|
|
* Find Training Page Handler
|
|
*
|
|
* Manages the Find Training page with Google Maps integration
|
|
* showing trainers and training venues on an interactive map.
|
|
*
|
|
* @package HVAC_Community_Events
|
|
* @since 2.2.0
|
|
*/
|
|
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Class HVAC_Find_Training_Page
|
|
*
|
|
* Main controller for the Find Training page functionality.
|
|
* Uses singleton pattern consistent with other HVAC plugin classes.
|
|
*/
|
|
class HVAC_Find_Training_Page {
|
|
|
|
/**
|
|
* Singleton instance
|
|
*
|
|
* @var HVAC_Find_Training_Page|null
|
|
*/
|
|
private static ?self $instance = null;
|
|
|
|
/**
|
|
* Page slug
|
|
*
|
|
* @var string
|
|
*/
|
|
private string $page_slug = 'find-training';
|
|
|
|
/**
|
|
* Google Maps API key
|
|
*
|
|
* @var string
|
|
*/
|
|
private string $api_key = '';
|
|
|
|
/**
|
|
* Get singleton instance
|
|
*
|
|
* @return HVAC_Find_Training_Page
|
|
*/
|
|
public static function get_instance(): self {
|
|
if (self::$instance === null) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
private function __construct() {
|
|
$this->load_api_key();
|
|
$this->init_hooks();
|
|
}
|
|
|
|
/**
|
|
* Load Google Maps API key from secure storage
|
|
*/
|
|
private function load_api_key(): void {
|
|
if (class_exists('HVAC_Secure_Storage')) {
|
|
$this->api_key = HVAC_Secure_Storage::get_credential('hvac_google_maps_api_key', '');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize WordPress hooks
|
|
*/
|
|
private function init_hooks(): void {
|
|
// Page registration
|
|
add_action('init', [$this, 'register_page'], 15);
|
|
|
|
// Asset enqueuing
|
|
add_action('wp_enqueue_scripts', [$this, 'enqueue_assets']);
|
|
|
|
// Body classes
|
|
add_filter('body_class', [$this, 'add_body_classes']);
|
|
|
|
// AJAX handlers
|
|
add_action('wp_ajax_hvac_get_training_map_data', [$this, 'ajax_get_map_data']);
|
|
add_action('wp_ajax_nopriv_hvac_get_training_map_data', [$this, 'ajax_get_map_data']);
|
|
|
|
add_action('wp_ajax_hvac_filter_training_map', [$this, 'ajax_filter_map']);
|
|
add_action('wp_ajax_nopriv_hvac_filter_training_map', [$this, 'ajax_filter_map']);
|
|
|
|
add_action('wp_ajax_hvac_get_trainer_profile_modal', [$this, 'ajax_get_trainer_profile']);
|
|
add_action('wp_ajax_nopriv_hvac_get_trainer_profile_modal', [$this, 'ajax_get_trainer_profile']);
|
|
|
|
add_action('wp_ajax_hvac_get_venue_info', [$this, 'ajax_get_venue_info']);
|
|
add_action('wp_ajax_nopriv_hvac_get_venue_info', [$this, 'ajax_get_venue_info']);
|
|
|
|
// Redirect from old page
|
|
add_action('template_redirect', [$this, 'maybe_redirect_from_old_page']);
|
|
}
|
|
|
|
/**
|
|
* Register the Find Training page
|
|
*/
|
|
public function register_page(): void {
|
|
$page = get_page_by_path($this->page_slug);
|
|
|
|
if (!$page) {
|
|
$this->create_page();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create the Find Training page in WordPress
|
|
*/
|
|
private function create_page(): void {
|
|
$page_data = [
|
|
'post_title' => 'Find Training',
|
|
'post_name' => $this->page_slug,
|
|
'post_content' => '<!-- This page uses a custom template -->',
|
|
'post_status' => 'publish',
|
|
'post_type' => 'page',
|
|
'post_author' => 1,
|
|
'comment_status' => 'closed',
|
|
'ping_status' => 'closed',
|
|
'meta_input' => [
|
|
'_wp_page_template' => 'page-find-training.php',
|
|
'ast-site-content-layout' => 'page-builder',
|
|
'site-post-title' => 'disabled',
|
|
'site-sidebar-layout' => 'no-sidebar',
|
|
'theme-transparent-header-meta' => 'disabled'
|
|
]
|
|
];
|
|
|
|
$page_id = wp_insert_post($page_data);
|
|
|
|
if ($page_id && !is_wp_error($page_id)) {
|
|
update_option('hvac_find_training_page_id', $page_id);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if current page is the Find Training page
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function is_find_training_page(): bool {
|
|
return is_page($this->page_slug) || is_page(get_option('hvac_find_training_page_id'));
|
|
}
|
|
|
|
/**
|
|
* Enqueue page assets
|
|
*/
|
|
public function enqueue_assets(): void {
|
|
if (!$this->is_find_training_page()) {
|
|
return;
|
|
}
|
|
|
|
// Enqueue CSS
|
|
wp_enqueue_style(
|
|
'hvac-find-training',
|
|
HVAC_PLUGIN_URL . 'assets/css/find-training-map.css',
|
|
['astra-theme-css'],
|
|
HVAC_VERSION
|
|
);
|
|
|
|
// Enqueue Google Maps API with MarkerClusterer
|
|
if (!empty($this->api_key)) {
|
|
wp_enqueue_script(
|
|
'google-maps-api',
|
|
'https://maps.googleapis.com/maps/api/js?key=' . esc_attr($this->api_key) . '&libraries=places&callback=Function.prototype',
|
|
[],
|
|
null,
|
|
true
|
|
);
|
|
|
|
// MarkerClusterer library
|
|
wp_enqueue_script(
|
|
'google-maps-markerclusterer',
|
|
'https://unpkg.com/@googlemaps/markerclusterer/dist/index.min.js',
|
|
['google-maps-api'],
|
|
'2.5.3',
|
|
true
|
|
);
|
|
}
|
|
|
|
// Enqueue main map JavaScript
|
|
wp_enqueue_script(
|
|
'hvac-find-training-map',
|
|
HVAC_PLUGIN_URL . 'assets/js/find-training-map.js',
|
|
['jquery', 'google-maps-api', 'google-maps-markerclusterer'],
|
|
HVAC_VERSION,
|
|
true
|
|
);
|
|
|
|
// Enqueue filter JavaScript
|
|
wp_enqueue_script(
|
|
'hvac-find-training-filters',
|
|
HVAC_PLUGIN_URL . 'assets/js/find-training-filters.js',
|
|
['jquery', 'hvac-find-training-map'],
|
|
HVAC_VERSION,
|
|
true
|
|
);
|
|
|
|
// Localize script with data
|
|
wp_localize_script('hvac-find-training-map', 'hvacFindTraining', [
|
|
'ajax_url' => admin_url('admin-ajax.php'),
|
|
'nonce' => wp_create_nonce('hvac_find_training'),
|
|
'api_key' => !empty($this->api_key) ? 'configured' : '', // Don't expose actual key
|
|
'map_center' => [
|
|
'lat' => 39.8283, // US center
|
|
'lng' => -98.5795
|
|
],
|
|
'default_zoom' => 4,
|
|
'cluster_zoom' => 8,
|
|
'messages' => [
|
|
'loading' => __('Loading...', 'hvac-community-events'),
|
|
'error' => __('An error occurred. Please try again.', 'hvac-community-events'),
|
|
'no_results' => __('No trainers or venues found matching your criteria.', 'hvac-community-events'),
|
|
'geolocation_error' => __('Unable to get your location. Please check your browser settings.', 'hvac-community-events'),
|
|
'geolocation_unsupported' => __('Geolocation is not supported by your browser.', 'hvac-community-events')
|
|
],
|
|
'marker_icons' => [
|
|
'trainer' => HVAC_PLUGIN_URL . 'assets/images/marker-trainer.svg',
|
|
'venue' => HVAC_PLUGIN_URL . 'assets/images/marker-venue.svg'
|
|
]
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Add body classes for the page
|
|
*
|
|
* @param array $classes Existing body classes
|
|
* @return array Modified body classes
|
|
*/
|
|
public function add_body_classes(array $classes): array {
|
|
if ($this->is_find_training_page()) {
|
|
$classes[] = 'hvac-find-training-page';
|
|
$classes[] = 'hvac-full-width';
|
|
$classes[] = 'hvac-page';
|
|
}
|
|
return $classes;
|
|
}
|
|
|
|
/**
|
|
* Redirect from old /find-a-trainer page to /find-training
|
|
*/
|
|
public function maybe_redirect_from_old_page(): void {
|
|
if (is_page('find-a-trainer')) {
|
|
wp_safe_redirect(home_url('/find-training/'), 301);
|
|
exit;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* AJAX: Get all map data (trainers and venues)
|
|
*/
|
|
public function ajax_get_map_data(): void {
|
|
// Verify nonce
|
|
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'hvac_find_training')) {
|
|
wp_send_json_error(['message' => 'Invalid security token']);
|
|
return;
|
|
}
|
|
|
|
$data_provider = HVAC_Training_Map_Data::get_instance();
|
|
|
|
$trainers = $data_provider->get_trainer_markers();
|
|
$venues = $data_provider->get_venue_markers();
|
|
|
|
wp_send_json_success([
|
|
'trainers' => $trainers,
|
|
'venues' => $venues,
|
|
'total_trainers' => count($trainers),
|
|
'total_venues' => count($venues)
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* AJAX: Filter map markers
|
|
*/
|
|
public function ajax_filter_map(): void {
|
|
// Verify nonce
|
|
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'hvac_find_training')) {
|
|
wp_send_json_error(['message' => 'Invalid security token']);
|
|
return;
|
|
}
|
|
|
|
$filters = [
|
|
'state' => sanitize_text_field($_POST['state'] ?? ''),
|
|
'certification' => sanitize_text_field($_POST['certification'] ?? ''),
|
|
'training_format' => sanitize_text_field($_POST['training_format'] ?? ''),
|
|
'search' => sanitize_text_field($_POST['search'] ?? ''),
|
|
'show_trainers' => filter_var($_POST['show_trainers'] ?? true, FILTER_VALIDATE_BOOLEAN),
|
|
'show_venues' => filter_var($_POST['show_venues'] ?? true, FILTER_VALIDATE_BOOLEAN),
|
|
'lat' => isset($_POST['lat']) ? floatval($_POST['lat']) : null,
|
|
'lng' => isset($_POST['lng']) ? floatval($_POST['lng']) : null,
|
|
'radius' => isset($_POST['radius']) ? intval($_POST['radius']) : 100 // km
|
|
];
|
|
|
|
$data_provider = HVAC_Training_Map_Data::get_instance();
|
|
|
|
$result = [
|
|
'trainers' => [],
|
|
'venues' => []
|
|
];
|
|
|
|
if ($filters['show_trainers']) {
|
|
$result['trainers'] = $data_provider->get_trainer_markers($filters);
|
|
}
|
|
|
|
if ($filters['show_venues']) {
|
|
$result['venues'] = $data_provider->get_venue_markers($filters);
|
|
}
|
|
|
|
$result['total_trainers'] = count($result['trainers']);
|
|
$result['total_venues'] = count($result['venues']);
|
|
$result['filters_applied'] = array_filter($filters, function($v) {
|
|
return !empty($v) && $v !== true;
|
|
});
|
|
|
|
wp_send_json_success($result);
|
|
}
|
|
|
|
/**
|
|
* AJAX: Get trainer profile for modal
|
|
*/
|
|
public function ajax_get_trainer_profile(): void {
|
|
// Verify nonce
|
|
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'hvac_find_training')) {
|
|
wp_send_json_error(['message' => 'Invalid security token']);
|
|
return;
|
|
}
|
|
|
|
$profile_id = absint($_POST['profile_id'] ?? 0);
|
|
|
|
if (!$profile_id) {
|
|
wp_send_json_error(['message' => 'Invalid profile ID']);
|
|
return;
|
|
}
|
|
|
|
$data_provider = HVAC_Training_Map_Data::get_instance();
|
|
$trainer_data = $data_provider->get_trainer_full_profile($profile_id);
|
|
|
|
if (!$trainer_data) {
|
|
wp_send_json_error(['message' => 'Trainer not found']);
|
|
return;
|
|
}
|
|
|
|
// Generate modal HTML
|
|
ob_start();
|
|
$this->render_trainer_modal_content($trainer_data);
|
|
$html = ob_get_clean();
|
|
|
|
wp_send_json_success([
|
|
'trainer' => $trainer_data,
|
|
'html' => $html
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* AJAX: Get venue info for info window
|
|
*/
|
|
public function ajax_get_venue_info(): void {
|
|
// Verify nonce
|
|
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'hvac_find_training')) {
|
|
wp_send_json_error(['message' => 'Invalid security token']);
|
|
return;
|
|
}
|
|
|
|
$venue_id = absint($_POST['venue_id'] ?? 0);
|
|
|
|
if (!$venue_id) {
|
|
wp_send_json_error(['message' => 'Invalid venue ID']);
|
|
return;
|
|
}
|
|
|
|
$data_provider = HVAC_Training_Map_Data::get_instance();
|
|
$venue_data = $data_provider->get_venue_full_info($venue_id);
|
|
|
|
if (!$venue_data) {
|
|
wp_send_json_error(['message' => 'Venue not found']);
|
|
return;
|
|
}
|
|
|
|
wp_send_json_success(['venue' => $venue_data]);
|
|
}
|
|
|
|
/**
|
|
* Render trainer modal content
|
|
*
|
|
* @param array $trainer Trainer data
|
|
*/
|
|
private function render_trainer_modal_content(array $trainer): void {
|
|
?>
|
|
<div class="hvac-training-modal-header">
|
|
<h2><?php echo esc_html($trainer['name']); ?></h2>
|
|
<button class="hvac-modal-close" aria-label="Close modal">×</button>
|
|
</div>
|
|
|
|
<div class="hvac-training-modal-body">
|
|
<div class="hvac-training-profile-section">
|
|
<div class="hvac-training-profile-header">
|
|
<?php if (!empty($trainer['image'])): ?>
|
|
<img src="<?php echo esc_url($trainer['image']); ?>" alt="<?php echo esc_attr($trainer['name']); ?>" class="hvac-training-profile-image">
|
|
<?php else: ?>
|
|
<div class="hvac-training-profile-avatar">
|
|
<span class="dashicons dashicons-businessperson"></span>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="hvac-training-profile-info">
|
|
<p class="hvac-training-location">
|
|
<?php echo esc_html($trainer['city'] . ', ' . $trainer['state']); ?>
|
|
</p>
|
|
|
|
<?php if (!empty($trainer['certifications'])): ?>
|
|
<div class="hvac-training-certifications">
|
|
<?php foreach ($trainer['certifications'] as $cert): ?>
|
|
<span class="hvac-cert-badge"><?php echo esc_html($cert); ?></span>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if (!empty($trainer['company'])): ?>
|
|
<p class="hvac-training-company"><?php echo esc_html($trainer['company']); ?></p>
|
|
<?php endif; ?>
|
|
|
|
<p class="hvac-training-events-count">
|
|
Total Training Events: <strong><?php echo intval($trainer['event_count']); ?></strong>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if (!empty($trainer['training_formats'])): ?>
|
|
<div class="hvac-training-detail">
|
|
<strong>Training Formats:</strong> <?php echo esc_html(implode(', ', $trainer['training_formats'])); ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if (!empty($trainer['upcoming_events'])): ?>
|
|
<div class="hvac-training-events">
|
|
<h4>Upcoming Events</h4>
|
|
<ul class="hvac-events-list">
|
|
<?php foreach (array_slice($trainer['upcoming_events'], 0, 5) as $event): ?>
|
|
<li>
|
|
<a href="<?php echo esc_url($event['url']); ?>" target="_blank">
|
|
<?php echo esc_html($event['title']); ?>
|
|
</a>
|
|
<span class="hvac-event-date"><?php echo esc_html($event['date']); ?></span>
|
|
</li>
|
|
<?php endforeach; ?>
|
|
</ul>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="hvac-training-contact-section">
|
|
<h4>Contact Trainer</h4>
|
|
<form class="hvac-training-contact-form" data-trainer-id="<?php echo esc_attr($trainer['user_id']); ?>" data-profile-id="<?php echo esc_attr($trainer['profile_id']); ?>">
|
|
<div class="hvac-form-row">
|
|
<input type="text" name="first_name" placeholder="First Name" required>
|
|
<input type="text" name="last_name" placeholder="Last Name" required>
|
|
</div>
|
|
<div class="hvac-form-row">
|
|
<input type="email" name="email" placeholder="Email" required>
|
|
<input type="tel" name="phone" placeholder="Phone Number">
|
|
</div>
|
|
<div class="hvac-form-row">
|
|
<input type="text" name="city" placeholder="City">
|
|
<input type="text" name="state_province" placeholder="State/Province">
|
|
</div>
|
|
<div class="hvac-form-field">
|
|
<input type="text" name="company" placeholder="Company">
|
|
</div>
|
|
<div class="hvac-form-field">
|
|
<textarea name="message" placeholder="Message" rows="3"></textarea>
|
|
</div>
|
|
<button type="submit" class="hvac-btn-primary">Send Message</button>
|
|
</form>
|
|
|
|
<div class="hvac-form-message hvac-form-success" style="display: none;">
|
|
Your message has been sent! Check your inbox for more details.
|
|
</div>
|
|
<div class="hvac-form-message hvac-form-error" style="display: none;">
|
|
There was an error sending your message. Please try again.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Get filter options for dropdowns
|
|
*
|
|
* @return array Filter options
|
|
*/
|
|
public function get_filter_options(): array {
|
|
$data_provider = HVAC_Training_Map_Data::get_instance();
|
|
|
|
return [
|
|
'states' => $data_provider->get_state_options(),
|
|
'certifications' => $data_provider->get_certification_options(),
|
|
'training_formats' => $data_provider->get_training_format_options()
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get page slug
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_page_slug(): string {
|
|
return $this->page_slug;
|
|
}
|
|
}
|