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:
parent
21c908af81
commit
19147d978e
6 changed files with 301 additions and 49 deletions
64
Status.md
64
Status.md
|
|
@ -1,14 +1,57 @@
|
|||
# HVAC Community Events - Project Status
|
||||
|
||||
**Last Updated:** February 1, 2026
|
||||
**Current Session:** Find Training Page Implementation - Complete
|
||||
**Version:** 2.2.0 (Ready for Staging Deployment)
|
||||
**Current Session:** Find Training Page Enhancements - Complete
|
||||
**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.
|
||||
|
||||
|
|
@ -69,13 +112,12 @@ Ran comprehensive code review using GPT-5, Gemini 3, and Zen MCP tools. Found an
|
|||
- ✅ Auto-geocoding for new venues
|
||||
- ✅ Rate-limited batch geocoding for existing venues
|
||||
|
||||
### Next Steps
|
||||
1. ⏳ Deploy to staging: `./scripts/deploy.sh staging`
|
||||
2. ⏳ Run E2E tests on Find Training page
|
||||
3. ⏳ Verify map loads with markers
|
||||
4. ⏳ Test filters and geolocation
|
||||
5. ⏳ Verify contact form sends email
|
||||
6. ⏳ Deploy to production after validation
|
||||
### Deployment Status
|
||||
- ✅ Deployed to staging
|
||||
- ✅ Map loads with markers and clustering
|
||||
- ✅ Filters working (state, certification, format)
|
||||
- ✅ Contact form functional
|
||||
- ✅ Deployed to production
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@
|
|||
*/
|
||||
init: function() {
|
||||
this.bindEvents();
|
||||
this.initMobileFilterToggle();
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -133,12 +134,11 @@
|
|||
// Update map data
|
||||
HVACTrainingMap.trainers = response.data.trainers || [];
|
||||
HVACTrainingMap.venues = response.data.venues || [];
|
||||
HVACTrainingMap.visibleTrainers = HVACTrainingMap.trainers.slice(); // Reset to all
|
||||
HVACTrainingMap.updateMarkers();
|
||||
HVACTrainingMap.updateCounts(
|
||||
response.data.total_trainers,
|
||||
response.data.total_venues
|
||||
);
|
||||
HVACTrainingMap.updateCounts(HVACTrainingMap.trainers.length);
|
||||
HVACTrainingMap.updateTrainerGrid();
|
||||
// Note: syncSidebarWithViewport will be called by map 'idle' event
|
||||
}
|
||||
},
|
||||
complete: function() {
|
||||
|
|
@ -375,6 +375,66 @@
|
|||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
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());
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@
|
|||
trainers: [],
|
||||
venues: [],
|
||||
|
||||
// Visible trainers (filtered by map bounds)
|
||||
visibleTrainers: [],
|
||||
|
||||
// Configuration
|
||||
config: {
|
||||
mapElementId: 'hvac-training-map',
|
||||
|
|
@ -45,21 +48,29 @@
|
|||
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 (typeof hvacFindTraining !== 'undefined') {
|
||||
if (hvacFindTraining.map_center) {
|
||||
this.config.defaultCenter = hvacFindTraining.map_center;
|
||||
}
|
||||
if (hvacFindTraining.default_zoom) {
|
||||
this.config.defaultZoom = parseInt(hvacFindTraining.default_zoom);
|
||||
}
|
||||
if (hvacFindTraining.map_center) {
|
||||
this.config.defaultCenter = hvacFindTraining.map_center;
|
||||
}
|
||||
if (hvacFindTraining.default_zoom) {
|
||||
this.config.defaultZoom = parseInt(hvacFindTraining.default_zoom);
|
||||
}
|
||||
|
||||
// Create the map
|
||||
|
|
@ -73,6 +84,36 @@
|
|||
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -110,6 +151,12 @@
|
|||
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();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -157,10 +204,13 @@
|
|||
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(response.data.total_trainers, response.data.total_venues);
|
||||
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');
|
||||
}
|
||||
|
|
@ -222,14 +272,19 @@
|
|||
map: this.map,
|
||||
title: trainer.name,
|
||||
icon: this.getTrainerIcon(),
|
||||
optimized: true
|
||||
optimized: false // Required for reliable hover events
|
||||
});
|
||||
|
||||
// Store trainer data on marker
|
||||
marker.trainerData = 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() {
|
||||
self.showTrainerInfoWindow(this);
|
||||
});
|
||||
|
|
@ -249,14 +304,19 @@
|
|||
map: this.map,
|
||||
title: venue.name,
|
||||
icon: this.getVenueIcon(),
|
||||
optimized: true
|
||||
optimized: false // Required for reliable hover events
|
||||
});
|
||||
|
||||
// Store venue data on marker
|
||||
marker.venueData = 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() {
|
||||
self.showVenueInfoWindow(this);
|
||||
});
|
||||
|
|
@ -585,21 +645,30 @@
|
|||
const $grid = $('#hvac-trainer-grid');
|
||||
$grid.empty();
|
||||
|
||||
if (this.trainers.length === 0) {
|
||||
$grid.html('<div class="hvac-no-results"><p>No trainers found matching your criteria.</p></div>');
|
||||
// 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 12, load more on request)
|
||||
const displayCount = Math.min(this.trainers.length, 12);
|
||||
// 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 = this.trainers[i];
|
||||
const trainer = trainersToShow[i];
|
||||
$grid.append(this.createTrainerCard(trainer));
|
||||
}
|
||||
|
||||
// Show load more if there are more trainers
|
||||
if (this.trainers.length > 12) {
|
||||
if (trainersToShow.length > 6) {
|
||||
$('.hvac-load-more-wrapper').show();
|
||||
} else {
|
||||
$('.hvac-load-more-wrapper').hide();
|
||||
|
|
@ -632,10 +701,19 @@
|
|||
|
||||
/**
|
||||
* 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) {
|
||||
$('#hvac-trainer-count').text(trainers || 0);
|
||||
$('#hvac-venue-count').text(venues || 0);
|
||||
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);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -741,17 +819,64 @@
|
|||
loadMoreTrainers: function() {
|
||||
const $grid = $('#hvac-trainer-grid');
|
||||
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++) {
|
||||
$grid.append(this.createTrainerCard(this.trainers[i]));
|
||||
// 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 >= this.trainers.length) {
|
||||
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
|
||||
*/
|
||||
|
|
@ -782,6 +907,36 @@
|
|||
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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -31,13 +31,7 @@ class HVAC_Page_Manager {
|
|||
'parent' => null,
|
||||
'capability' => null
|
||||
],
|
||||
'find-a-trainer' => [
|
||||
'title' => 'Find a Trainer',
|
||||
'template' => 'page-find-trainer.php',
|
||||
'public' => true,
|
||||
'parent' => null,
|
||||
'capability' => null
|
||||
],
|
||||
// Note: find-a-trainer removed - redirects to find-training (see HVAC_Route_Manager)
|
||||
'find-training' => [
|
||||
'title' => 'Find Training',
|
||||
'template' => 'page-find-training.php',
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ final class HVAC_Plugin {
|
|||
define('HVAC_PLUGIN_VERSION', '2.0.0');
|
||||
}
|
||||
if (!defined('HVAC_VERSION')) {
|
||||
define('HVAC_VERSION', '2.1.7');
|
||||
define('HVAC_VERSION', '2.2.4');
|
||||
}
|
||||
if (!defined('HVAC_PLUGIN_FILE')) {
|
||||
define('HVAC_PLUGIN_FILE', dirname(__DIR__) . '/hvac-community-events.php');
|
||||
|
|
|
|||
|
|
@ -81,7 +81,8 @@ class HVAC_Route_Manager {
|
|||
'communication-templates' => 'trainer/communication-templates',
|
||||
'communication-schedules' => 'trainer/communication-schedules',
|
||||
'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
|
||||
|
|
|
|||
Loading…
Reference in a new issue