feat(find-training): Add viewport sync and marker hover interactions

- Add viewport sync: sidebar shows only trainers visible in map area
- Add mouseover event on markers showing info window on hover
- Set optimized:false on markers for reliable hover events
- Add legacy URL redirects (/find-a-trainer → /find-training)
- Remove deprecated find-a-trainer page from Page Manager
- Update Status.md with session changes
- Bump version to 2.2.4

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
ben 2026-02-01 11:42:45 -04:00
parent 21c908af81
commit 19147d978e
6 changed files with 301 additions and 49 deletions

View file

@ -1,14 +1,57 @@
# HVAC Community Events - Project Status # HVAC Community Events - Project Status
**Last Updated:** February 1, 2026 **Last Updated:** February 1, 2026
**Current Session:** Find Training Page Implementation - Complete **Current Session:** Find Training Page Enhancements - Complete
**Version:** 2.2.0 (Ready for Staging Deployment) **Version:** 2.2.4 (Deployed to Production)
--- ---
## 🎯 CURRENT SESSION - FIND TRAINING PAGE IMPLEMENTATION (Jan 31 - Feb 1, 2026) ## 🎯 CURRENT SESSION - FIND TRAINING PAGE ENHANCEMENTS (Feb 1, 2026)
### Status: ✅ **COMPLETE - Ready for Staging Deployment & E2E Testing** ### Status: ✅ **COMPLETE - Deployed to Production**
**Objective:** Improve Find Training page UX with viewport sync and marker hover interactions.
### Changes Made
1. ✅ **Viewport Sync** - Sidebar now shows only trainers visible in current map area
- Added `visibleTrainers` array to track filtered trainers
- Added `syncSidebarWithViewport()` method filtering by map bounds
- Map `idle` event triggers sync on pan/zoom
- Count shows "X of Y trainers" when zoomed in
2. ✅ **Marker Hover Interaction** - Info window appears on hover
- Added `mouseover` event listener to trainer/venue markers
- Set `optimized: false` on markers for reliable hover events
- Hover shows info window preview with "View Profile" button
- Click on "View Profile" opens full modal with contact form
3. ✅ **Legacy URL Redirects**
- `/find-a-trainer/``/find-training/` (301 redirect)
- `/find-trainer/``/find-training/` (301 redirect)
- Removed old page from Page Manager
### Files Modified
| File | Change |
|------|--------|
| `assets/js/find-training-map.js` | Added viewport sync, hover events, optimized:false |
| `assets/js/find-training-filters.js` | Updated filter handler for visibleTrainers |
| `includes/class-hvac-route-manager.php` | Added legacy URL redirects |
| `includes/class-hvac-page-manager.php` | Removed find-a-trainer page definition |
| `includes/class-hvac-plugin.php` | Version bumped to 2.2.4 |
### Verified Behavior
- ✅ Hover over marker → Info window appears immediately
- ✅ Click "View Profile" → Full modal with trainer details + contact form
- ✅ Pan/zoom map → Sidebar updates to show visible trainers only
- ✅ Legacy URLs redirect to new page
---
## 📋 PREVIOUS SESSION - FIND TRAINING PAGE IMPLEMENTATION (Jan 31 - Feb 1, 2026)
### Status: ✅ **COMPLETE - Deployed to Production**
**Objective:** Replace the buggy MapGeo-based `/find-a-trainer` page with a new `/find-training` page built from scratch using Google Maps JavaScript API. **Objective:** Replace the buggy MapGeo-based `/find-a-trainer` page with a new `/find-training` page built from scratch using Google Maps JavaScript API.
@ -69,13 +112,12 @@ Ran comprehensive code review using GPT-5, Gemini 3, and Zen MCP tools. Found an
- ✅ Auto-geocoding for new venues - ✅ Auto-geocoding for new venues
- ✅ Rate-limited batch geocoding for existing venues - ✅ Rate-limited batch geocoding for existing venues
### Next Steps ### Deployment Status
1. ⏳ Deploy to staging: `./scripts/deploy.sh staging` - ✅ Deployed to staging
2. ⏳ Run E2E tests on Find Training page - ✅ Map loads with markers and clustering
3. ⏳ Verify map loads with markers - ✅ Filters working (state, certification, format)
4. ⏳ Test filters and geolocation - ✅ Contact form functional
5. ⏳ Verify contact form sends email - ✅ Deployed to production
6. ⏳ Deploy to production after validation
--- ---

View file

@ -36,6 +36,7 @@
*/ */
init: function() { init: function() {
this.bindEvents(); this.bindEvents();
this.initMobileFilterToggle();
}, },
/** /**
@ -133,12 +134,11 @@
// Update map data // Update map data
HVACTrainingMap.trainers = response.data.trainers || []; HVACTrainingMap.trainers = response.data.trainers || [];
HVACTrainingMap.venues = response.data.venues || []; HVACTrainingMap.venues = response.data.venues || [];
HVACTrainingMap.visibleTrainers = HVACTrainingMap.trainers.slice(); // Reset to all
HVACTrainingMap.updateMarkers(); HVACTrainingMap.updateMarkers();
HVACTrainingMap.updateCounts( HVACTrainingMap.updateCounts(HVACTrainingMap.trainers.length);
response.data.total_trainers,
response.data.total_venues
);
HVACTrainingMap.updateTrainerGrid(); HVACTrainingMap.updateTrainerGrid();
// Note: syncSidebarWithViewport will be called by map 'idle' event
} }
}, },
complete: function() { complete: function() {
@ -375,6 +375,66 @@
const div = document.createElement('div'); const div = document.createElement('div');
div.textContent = text; div.textContent = text;
return div.innerHTML; return div.innerHTML;
},
/**
* Initialize mobile filter toggle
*/
initMobileFilterToggle: function() {
const self = this;
// Mobile filter panel toggle
$(document).on('click', '.hvac-mobile-filter-toggle', function() {
const $toggle = $(this);
const $panel = $('#hvac-mobile-filter-panel');
const isExpanded = $toggle.attr('aria-expanded') === 'true';
if (isExpanded) {
$panel.attr('hidden', '');
$toggle.attr('aria-expanded', 'false');
} else {
$panel.removeAttr('hidden');
$toggle.attr('aria-expanded', 'true');
}
});
// Sync mobile filter selects with desktop selects
$('#hvac-filter-state-mobile').on('change', function() {
const value = $(this).val();
$('#hvac-filter-state').val(value);
self.activeFilters.state = value;
self.applyFilters();
self.updateActiveFiltersDisplay();
});
$('#hvac-filter-certification-mobile').on('change', function() {
const value = $(this).val();
$('#hvac-filter-certification').val(value);
self.activeFilters.certification = value;
self.applyFilters();
self.updateActiveFiltersDisplay();
});
$('#hvac-filter-format-mobile').on('change', function() {
const value = $(this).val();
$('#hvac-filter-format').val(value);
self.activeFilters.training_format = value;
self.applyFilters();
self.updateActiveFiltersDisplay();
});
// Also sync desktop to mobile when desktop changes
$('#hvac-filter-state').on('change', function() {
$('#hvac-filter-state-mobile').val($(this).val());
});
$('#hvac-filter-certification').on('change', function() {
$('#hvac-filter-certification-mobile').val($(this).val());
});
$('#hvac-filter-format').on('change', function() {
$('#hvac-filter-format-mobile').val($(this).val());
});
} }
}; };

View file

@ -31,6 +31,9 @@
trainers: [], trainers: [],
venues: [], venues: [],
// Visible trainers (filtered by map bounds)
visibleTrainers: [],
// Configuration // Configuration
config: { config: {
mapElementId: 'hvac-training-map', mapElementId: 'hvac-training-map',
@ -45,21 +48,29 @@
init: function() { init: function() {
const self = this; 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 // Check if Google Maps is loaded
if (typeof google === 'undefined' || typeof google.maps === 'undefined') { if (typeof google === 'undefined' || typeof google.maps === 'undefined') {
console.error('Google Maps API not loaded'); console.error('Google Maps API not loaded');
this.showMapError('Google Maps failed to load. Please refresh the page.'); this.showMapError('Google Maps failed to load. Please refresh the page.');
// Still load trainer directory data
this.loadTrainerDirectory();
return; return;
} }
// Override config with localized data // Override config with localized data
if (typeof hvacFindTraining !== 'undefined') { if (hvacFindTraining.map_center) {
if (hvacFindTraining.map_center) { this.config.defaultCenter = hvacFindTraining.map_center;
this.config.defaultCenter = hvacFindTraining.map_center; }
} if (hvacFindTraining.default_zoom) {
if (hvacFindTraining.default_zoom) { this.config.defaultZoom = parseInt(hvacFindTraining.default_zoom);
this.config.defaultZoom = parseInt(hvacFindTraining.default_zoom);
}
} }
// Create the map // Create the map
@ -73,6 +84,36 @@
// Bind events // Bind events
this.bindEvents(); 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();
}
});
}, },
/** /**
@ -110,6 +151,12 @@
this.map.addListener('click', () => { this.map.addListener('click', () => {
this.infoWindow.close(); this.infoWindow.close();
}); });
// Sync sidebar with map viewport on pan/zoom
const self = this;
this.map.addListener('idle', function() {
self.syncSidebarWithViewport();
});
}, },
/** /**
@ -157,10 +204,13 @@
if (response.success) { if (response.success) {
self.trainers = response.data.trainers || []; self.trainers = response.data.trainers || [];
self.venues = response.data.venues || []; self.venues = response.data.venues || [];
self.visibleTrainers = self.trainers.slice(); // Initially all trainers are "visible"
self.updateMarkers(); self.updateMarkers();
self.updateCounts(response.data.total_trainers, response.data.total_venues); self.updateCounts(self.trainers.length);
self.updateTrainerGrid(); self.updateTrainerGrid();
// Note: syncSidebarWithViewport will be called by map 'idle' event
// to filter trainers to current viewport
} else { } else {
self.showMapError(response.data?.message || 'Failed to load data'); self.showMapError(response.data?.message || 'Failed to load data');
} }
@ -222,14 +272,19 @@
map: this.map, map: this.map,
title: trainer.name, title: trainer.name,
icon: this.getTrainerIcon(), icon: this.getTrainerIcon(),
optimized: true optimized: false // Required for reliable hover events
}); });
// Store trainer data on marker // Store trainer data on marker
marker.trainerData = trainer; marker.trainerData = trainer;
marker.markerType = 'trainer'; marker.markerType = 'trainer';
// Add click listener // 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() { marker.addListener('click', function() {
self.showTrainerInfoWindow(this); self.showTrainerInfoWindow(this);
}); });
@ -249,14 +304,19 @@
map: this.map, map: this.map,
title: venue.name, title: venue.name,
icon: this.getVenueIcon(), icon: this.getVenueIcon(),
optimized: true optimized: false // Required for reliable hover events
}); });
// Store venue data on marker // Store venue data on marker
marker.venueData = venue; marker.venueData = venue;
marker.markerType = 'venue'; marker.markerType = 'venue';
// Add click listener // 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() { marker.addListener('click', function() {
self.showVenueInfoWindow(this); self.showVenueInfoWindow(this);
}); });
@ -585,21 +645,30 @@
const $grid = $('#hvac-trainer-grid'); const $grid = $('#hvac-trainer-grid');
$grid.empty(); $grid.empty();
if (this.trainers.length === 0) { // Use visible trainers if available (viewport sync), otherwise use all trainers
$grid.html('<div class="hvac-no-results"><p>No trainers found matching your criteria.</p></div>'); 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; return;
} }
// Display trainers (show first 12, load more on request) // Display trainers (show first 6 for compact sidebar, load more on request)
const displayCount = Math.min(this.trainers.length, 12); const displayCount = Math.min(trainersToShow.length, 6);
for (let i = 0; i < displayCount; i++) { for (let i = 0; i < displayCount; i++) {
const trainer = this.trainers[i]; const trainer = trainersToShow[i];
$grid.append(this.createTrainerCard(trainer)); $grid.append(this.createTrainerCard(trainer));
} }
// Show load more if there are more trainers // Show load more if there are more trainers
if (this.trainers.length > 12) { if (trainersToShow.length > 6) {
$('.hvac-load-more-wrapper').show(); $('.hvac-load-more-wrapper').show();
} else { } else {
$('.hvac-load-more-wrapper').hide(); $('.hvac-load-more-wrapper').hide();
@ -632,10 +701,19 @@
/** /**
* Update counts display * 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(trainers, venues) { updateCounts: function(visible, total) {
$('#hvac-trainer-count').text(trainers || 0); const $count = $('#hvac-trainer-count');
$('#hvac-venue-count').text(venues || 0);
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);
}
}, },
/** /**
@ -741,17 +819,64 @@
loadMoreTrainers: function() { loadMoreTrainers: function() {
const $grid = $('#hvac-trainer-grid'); const $grid = $('#hvac-trainer-grid');
const currentCount = $grid.find('.hvac-trainer-card').length; const currentCount = $grid.find('.hvac-trainer-card').length;
const loadMore = 12; const loadMore = 6; // Load 6 more at a time for sidebar
for (let i = currentCount; i < currentCount + loadMore && i < this.trainers.length; i++) { // Use visible trainers if available (viewport sync), otherwise use all trainers
$grid.append(this.createTrainerCard(this.trainers[i])); 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 >= this.trainers.length) { if ($grid.find('.hvac-trainer-card').length >= trainersToShow.length) {
$('.hvac-load-more-wrapper').hide(); $('.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 * Get user's location
*/ */
@ -782,6 +907,36 @@
this.map.setZoom(zoom || 10); 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 * Show loading state
*/ */

View file

@ -31,13 +31,7 @@ class HVAC_Page_Manager {
'parent' => null, 'parent' => null,
'capability' => null 'capability' => null
], ],
'find-a-trainer' => [ // Note: find-a-trainer removed - redirects to find-training (see HVAC_Route_Manager)
'title' => 'Find a Trainer',
'template' => 'page-find-trainer.php',
'public' => true,
'parent' => null,
'capability' => null
],
'find-training' => [ 'find-training' => [
'title' => 'Find Training', 'title' => 'Find Training',
'template' => 'page-find-training.php', 'template' => 'page-find-training.php',

View file

@ -115,7 +115,7 @@ final class HVAC_Plugin {
define('HVAC_PLUGIN_VERSION', '2.0.0'); define('HVAC_PLUGIN_VERSION', '2.0.0');
} }
if (!defined('HVAC_VERSION')) { if (!defined('HVAC_VERSION')) {
define('HVAC_VERSION', '2.1.7'); define('HVAC_VERSION', '2.2.4');
} }
if (!defined('HVAC_PLUGIN_FILE')) { if (!defined('HVAC_PLUGIN_FILE')) {
define('HVAC_PLUGIN_FILE', dirname(__DIR__) . '/hvac-community-events.php'); define('HVAC_PLUGIN_FILE', dirname(__DIR__) . '/hvac-community-events.php');

View file

@ -81,7 +81,8 @@ class HVAC_Route_Manager {
'communication-templates' => 'trainer/communication-templates', 'communication-templates' => 'trainer/communication-templates',
'communication-schedules' => 'trainer/communication-schedules', 'communication-schedules' => 'trainer/communication-schedules',
'trainer-registration' => 'trainer/registration', 'trainer-registration' => 'trainer/registration',
'find-trainer' => 'find-a-trainer', // Fix E2E testing URL mismatch 'find-trainer' => 'find-training', // Legacy URL redirect
'find-a-trainer' => 'find-training', // Old page redirect to new Google Maps page
); );
// Parent pages that redirect to dashboards // Parent pages that redirect to dashboards