/** * 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: [], eventMarkers: [], // MarkerClusterer instance markerClusterer: null, // Info window instance (reused) infoWindow: null, // Current data trainers: [], venues: [], events: [], // Visible items (filtered by map bounds) visibleTrainers: [], visibleVenues: [], visibleEvents: [], // Active tab activeTab: 'trainers', // Items per page for load more itemsPerPage: 6, displayedCounts: { trainers: 0, venues: 0, events: 0 }, // 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'); // Initialize tabs even without map this.initTabs(); // 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.'); // Initialize tabs even without map this.initTabs(); // 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 tabs this.initTabs(); // 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.venues = response.data.venues || []; self.events = response.data.events || []; self.visibleTrainers = self.trainers.slice(); self.visibleVenues = self.venues.slice(); self.visibleEvents = self.events.slice(); self.displayedCounts = { trainers: 0, venues: 0, events: 0 }; self.updateAllCounts(); self.renderActiveTabList(); } }, 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.events = response.data.events || []; // Initially all items are "visible" self.visibleTrainers = self.trainers.slice(); self.visibleVenues = self.venues.slice(); self.visibleEvents = self.events.slice(); // Reset displayed counts self.displayedCounts = { trainers: 0, venues: 0, events: 0 }; self.updateMarkers(); self.updateAllCounts(); self.renderActiveTabList(); // Note: syncSidebarWithViewport will be called by map 'idle' event // to filter items 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'); const showEvents = $('#hvac-show-events').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); } }); } // Add event markers if (showEvents && this.events.length > 0) { this.events.forEach(event => { if (event.lat && event.lng) { this.addEventMarker(event); } }); } // 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); }, /** * Add an event marker */ addEventMarker: function(event) { const self = this; // Create marker with custom icon const marker = new google.maps.Marker({ position: { lat: event.lat, lng: event.lng }, map: this.map, title: event.title, icon: this.getEventIcon(), optimized: false // Required for reliable hover events }); // Store event data on marker marker.eventData = event; marker.markerType = 'event'; // Add hover listener to show info window preview marker.addListener('mouseover', function() { self.showEventInfoWindow(this); }); // Add click listener (also shows info window, for touch devices) marker.addListener('click', function() { self.showEventInfoWindow(this); }); this.eventMarkers.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 (light green with dark outline) return { path: google.maps.SymbolPath.CIRCLE, fillColor: '#f0f7e8', fillOpacity: 1, strokeColor: '#5a8a1a', 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 (green for mQ Approved) return { path: google.maps.SymbolPath.BACKWARD_CLOSED_ARROW, fillColor: '#89c92e', fillOpacity: 1, strokeColor: '#ffffff', strokeWeight: 2, scale: 6 }; }, /** * Get event marker icon */ getEventIcon: function() { // Use custom icon if available, otherwise use SVG circle if (hvacFindTraining.marker_icons?.event) { return { url: hvacFindTraining.marker_icons.event, scaledSize: new google.maps.Size(32, 32) }; } // SVG circle marker (teal for events) return { path: google.maps.SymbolPath.CIRCLE, fillColor: '#0ebaa6', fillOpacity: 1, strokeColor: '#ffffff', strokeWeight: 2, scale: 8 }; }, /** * Initialize marker clustering */ initClustering: function() { // Clear existing clusterer if (this.markerClusterer) { this.markerClusterer.clearMarkers(); } // Combine all markers const allMarkers = [...this.trainerMarkers, ...this.venueMarkers, ...this.eventMarkers]; 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 event markers this.eventMarkers.forEach(marker => marker.setMap(null)); this.eventMarkers = []; // Clear clusterer if (this.markerClusterer) { this.markerClusterer.clearMarkers(); } }, /** * Fit map bounds to show all markers */ fitBounds: function() { const allMarkers = [...this.trainerMarkers, ...this.venueMarkers, ...this.eventMarkers]; 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 || '') + '
' + 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); }, /** * Show event info window */ showEventInfoWindow: function(marker) { const event = marker.eventData; // Build date/time string let dateTimeStr = event.start_date; if (event.end_date && event.end_date !== event.start_date) { dateTimeStr += ' - ' + event.end_date; } if (event.is_all_day) { dateTimeStr += ' (All Day)'; } else if (event.start_time) { dateTimeStr += ' at ' + event.start_time; } // Build venue location string const venueLocation = [event.venue_city, event.venue_state].filter(Boolean).join(', '); // Create DOM elements safely to avoid XSS const container = document.createElement('div'); container.className = 'hvac-info-window-event'; const title = document.createElement('div'); title.className = 'hvac-info-window-title'; title.textContent = event.title; container.appendChild(title); const dateDiv = document.createElement('div'); dateDiv.className = 'hvac-info-window-date'; const calIcon = document.createElement('span'); calIcon.className = 'dashicons dashicons-calendar-alt'; dateDiv.appendChild(calIcon); dateDiv.appendChild(document.createTextNode(' ' + dateTimeStr)); container.appendChild(dateDiv); if (event.venue_name) { const venueDiv = document.createElement('div'); venueDiv.className = 'hvac-info-window-venue-name'; venueDiv.textContent = event.venue_name; if (venueLocation) { venueDiv.textContent += ' (' + venueLocation + ')'; } container.appendChild(venueDiv); } // Cost badge const costSpan = document.createElement('span'); costSpan.className = 'hvac-info-window-cost'; if (event.cost === 'Free' || event.cost.toLowerCase() === 'free') { costSpan.classList.add('hvac-free'); } costSpan.textContent = event.cost; container.appendChild(costSpan); // Past event badge if (event.is_past) { const pastBadge = document.createElement('span'); pastBadge.className = 'hvac-info-window-past-badge'; pastBadge.textContent = 'Past Event'; container.appendChild(pastBadge); } // Line break before link container.appendChild(document.createElement('br')); container.appendChild(document.createElement('br')); // View event link (opens in new tab) const link = document.createElement('a'); link.className = 'hvac-info-window-event-link'; link.href = event.url; link.target = '_blank'; link.rel = 'noopener'; link.textContent = 'View Event Details'; container.appendChild(link); 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('

Failed to load profile.

'); $loading.hide(); $body.show(); } }, error: function() { $body.html('

Network error. Please try again.

'); $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('Phone: ' + this.escapeHtml(venue.phone)).show(); } else { $modal.find('.hvac-venue-phone').hide(); } // Capacity if (venue.capacity) { $modal.find('.hvac-venue-capacity').html('Capacity: ' + 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(`${this.escapeHtml(item)}`); }); $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(`${this.escapeHtml(item)}`); }); $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(`
  • ${this.escapeHtml(event.title)} ${this.escapeHtml(event.date)}
  • `); }); $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(`

    ${message}

    `); $('.hvac-load-more-wrapper').hide(); return; } // Display trainers (show first batch for compact sidebar, load more on request) const displayCount = Math.min(trainersToShow.length, this.displayedCounts.trainers + this.itemsPerPage); this.displayedCounts.trainers = displayCount; for (let i = 0; i < displayCount; i++) { $grid.append(this.createTrainerCard(trainersToShow[i])); } // Show load more if there are more trainers if (trainersToShow.length > displayCount) { $('.hvac-load-more-wrapper').show(); } else { $('.hvac-load-more-wrapper').hide(); } }, /** * Create trainer card HTML */ createTrainerCard: function(trainer) { const imageHtml = trainer.image ? `${this.escapeHtml(trainer.name)}` : '
    '; const certHtml = trainer.certifications && trainer.certifications.length > 0 ? trainer.certifications.map(cert => `${this.escapeHtml(cert)}`).join('') : ''; return `
    ${imageHtml}
    ${this.escapeHtml(trainer.name)}
    ${this.escapeHtml(trainer.city)}, ${this.escapeHtml(trainer.state)}
    ${certHtml}
    `; }, /** * Update counts display (legacy method - now uses updateAllCounts) * @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) { // Update all tab counts this.updateAllCounts(); }, /** * 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); } }); // Venue card click $(document).on('click', '.hvac-venue-card', function() { const venueId = $(this).data('venue-id'); if (venueId) { self.openVenueModal(venueId); } }); // Event card click - open in new tab $(document).on('click', '.hvac-event-card', function() { const eventUrl = $(this).data('event-url'); if (eventUrl) { window.open(eventUrl, '_blank', 'noopener'); } }); // 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, #hvac-show-events').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 items for the active tab */ loadMoreTrainers: function() { switch (this.activeTab) { case 'trainers': this.loadMoreForTab('trainers', this.visibleTrainers.length > 0 ? this.visibleTrainers : this.trainers, '#hvac-trainer-grid', '.hvac-trainer-card'); break; case 'venues': this.loadMoreForTab('venues', this.visibleVenues.length > 0 ? this.visibleVenues : this.venues, '#hvac-venue-grid', '.hvac-venue-card'); break; case 'events': this.loadMoreForTab('events', this.visibleEvents.length > 0 ? this.visibleEvents : this.events, '#hvac-event-grid', '.hvac-event-card'); break; } }, /** * Load more items for a specific tab */ loadMoreForTab: function(tabType, items, gridSelector, cardSelector) { const $grid = $(gridSelector); const currentCount = $grid.find(cardSelector).length; for (let i = currentCount; i < currentCount + this.itemsPerPage && i < items.length; i++) { switch (tabType) { case 'trainers': $grid.append(this.createTrainerCard(items[i])); break; case 'venues': $grid.append(this.createVenueCard(items[i])); break; case 'events': $grid.append(this.createEventCard(items[i])); break; } } this.displayedCounts[tabType] = $grid.find(cardSelector).length; if ($grid.find(cardSelector).length >= items.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 lists with visible map viewport */ syncSidebarWithViewport: function() { if (!this.map) { 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); }); // Filter venues to those visible in current viewport this.visibleVenues = this.venues.filter(venue => { if (!venue.lat || !venue.lng) { return false; } const position = new google.maps.LatLng(venue.lat, venue.lng); return bounds.contains(position); }); // Filter events to those visible in current viewport this.visibleEvents = this.events.filter(event => { if (!event.lat || !event.lng) { return false; } const position = new google.maps.LatLng(event.lat, event.lng); return bounds.contains(position); }); // Reset displayed counts when viewport changes this.displayedCounts = { trainers: 0, venues: 0, events: 0 }; // Update all counts this.updateAllCounts(); // Render the active tab's list this.renderActiveTabList(); }, /** * Show loading state */ showLoading: function() { $('#hvac-trainer-grid').html('
    Loading trainers...
    '); }, /** * 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(`

    ${this.escapeHtml(message)}

    `); }, /** * Initialize tab navigation */ initTabs: function() { const self = this; // Tab click handlers $(document).on('click', '.hvac-tab', function() { const tab = $(this).data('tab'); self.switchTab(tab); }); // Keyboard navigation for tabs $(document).on('keydown', '.hvac-tab', function(e) { const $tabs = $('.hvac-tab'); const currentIndex = $tabs.index(this); let newIndex = currentIndex; if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { newIndex = currentIndex > 0 ? currentIndex - 1 : $tabs.length - 1; e.preventDefault(); } else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { newIndex = currentIndex < $tabs.length - 1 ? currentIndex + 1 : 0; e.preventDefault(); } else if (e.key === 'Home') { newIndex = 0; e.preventDefault(); } else if (e.key === 'End') { newIndex = $tabs.length - 1; e.preventDefault(); } if (newIndex !== currentIndex) { $tabs.eq(newIndex).focus().click(); } }); // Info button handler $(document).on('click', '#hvac-info-btn', function() { $('#hvac-info-modal').show(); }); // Info modal close $('#hvac-info-modal').on('click', '.hvac-modal-close, .hvac-modal-overlay', function() { $('#hvac-info-modal').hide(); }); }, /** * Switch to a different tab */ switchTab: function(tab) { if (tab === this.activeTab) return; this.activeTab = tab; // Update tab buttons $('.hvac-tab').removeClass('active').attr('aria-selected', 'false'); $(`.hvac-tab[data-tab="${tab}"]`).addClass('active').attr('aria-selected', 'true'); // Update panels $('.hvac-tab-panel').removeClass('active').attr('hidden', ''); $(`#hvac-panel-${tab}`).addClass('active').removeAttr('hidden'); // Update search placeholder this.updateSearchPlaceholder(tab); // Reset displayed count for current tab this.displayedCounts[tab] = 0; // Render active tab list this.renderActiveTabList(); }, /** * Update search placeholder based on active tab */ updateSearchPlaceholder: function(tab) { const placeholders = { trainers: 'Search trainers...', venues: 'Search venues...', events: 'Search events...' }; $('#hvac-training-search').attr('placeholder', placeholders[tab] || 'Search...'); }, /** * Render the active tab's list */ renderActiveTabList: function() { switch (this.activeTab) { case 'trainers': this.updateTrainerGrid(); break; case 'venues': this.updateVenueGrid(); break; case 'events': this.updateEventGrid(); break; } }, /** * Update venue grid */ updateVenueGrid: function() { const $grid = $('#hvac-venue-grid'); $grid.empty(); const venuesToShow = this.visibleVenues.length > 0 || this.map ? this.visibleVenues : this.venues; if (venuesToShow.length === 0) { const message = this.venues.length > 0 ? 'No venues visible in this area. Zoom out or pan the map to see more.' : 'No venues found matching your criteria.'; $grid.html(`

    ${message}

    `); $('.hvac-load-more-wrapper').hide(); return; } const displayCount = Math.min(venuesToShow.length, this.displayedCounts.venues + this.itemsPerPage); this.displayedCounts.venues = displayCount; for (let i = 0; i < displayCount; i++) { $grid.append(this.createVenueCard(venuesToShow[i])); } if (venuesToShow.length > displayCount) { $('.hvac-load-more-wrapper').show(); } else { $('.hvac-load-more-wrapper').hide(); } }, /** * Create venue card HTML */ createVenueCard: function(venue) { const location = [venue.city, venue.state].filter(Boolean).join(', '); const eventsText = venue.upcoming_events > 0 ? `${venue.upcoming_events} upcoming event${venue.upcoming_events > 1 ? 's' : ''}` : 'No upcoming events'; return `
    ${this.escapeHtml(venue.name)}
    ${this.escapeHtml(location)}
    ${eventsText}
    `; }, /** * Update event grid */ updateEventGrid: function() { const $grid = $('#hvac-event-grid'); $grid.empty(); const eventsToShow = this.visibleEvents.length > 0 || this.map ? this.visibleEvents : this.events; if (eventsToShow.length === 0) { const message = this.events.length > 0 ? 'No events visible in this area. Zoom out or pan the map to see more.' : 'No events found matching your criteria.'; $grid.html(`

    ${message}

    `); $('.hvac-load-more-wrapper').hide(); return; } const displayCount = Math.min(eventsToShow.length, this.displayedCounts.events + this.itemsPerPage); this.displayedCounts.events = displayCount; for (let i = 0; i < displayCount; i++) { $grid.append(this.createEventCard(eventsToShow[i])); } if (eventsToShow.length > displayCount) { $('.hvac-load-more-wrapper').show(); } else { $('.hvac-load-more-wrapper').hide(); } }, /** * Create event card HTML */ createEventCard: function(event) { // Parse date for month/day display const dateObj = new Date(event.start_date); const month = dateObj.toLocaleString('en-US', { month: 'short' }).toUpperCase(); const day = dateObj.getDate(); const venueText = event.venue_name || ''; const costClass = (event.cost === 'Free' || event.cost.toLowerCase() === 'free') ? 'hvac-free' : ''; const pastClass = event.is_past ? 'hvac-event-past' : ''; const pastBadge = event.is_past ? 'Past' : ''; return `
    ${month} ${day}
    ${this.escapeHtml(event.title)}
    ${venueText ? 'at ' + this.escapeHtml(venueText) : ''}
    ${this.escapeHtml(event.cost)} ${pastBadge}
    `; }, /** * Update all tab counts */ updateAllCounts: function() { const trainerCount = this.visibleTrainers.length || this.trainers.length; const venueCount = this.visibleVenues.length || this.venues.length; const eventCount = this.visibleEvents.length || this.events.length; $('[data-count="trainers"]').text(trainerCount); $('[data-count="venues"]').text(venueCount); $('[data-count="events"]').text(eventCount); }, /** * 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);