feat(find-training): Add tabbed interface for Trainers, Venues, and Events

Replace single trainer list with a tabbed sidebar interface:
- Three tabs with dynamic counts for each category
- Venue cards with icon, name, location, and upcoming events count
- Event cards with date badge, title, venue, and cost
- Visibility toggles moved from map overlay to sidebar header
- Context-aware search placeholder based on active tab
- Client-side filtering for instant search results
- Info button and modal with usage instructions and map legend
- Keyboard navigation for tabs (arrow keys)
- All lists sync with map viewport on pan/zoom

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
ben 2026-02-01 19:36:17 -04:00
parent 5c15b27935
commit 17dd3c9bdb
4 changed files with 1430 additions and 95 deletions

View file

@ -17,7 +17,9 @@
--hvac-primary-dark: #009688;
--hvac-secondary: #164B60;
--hvac-secondary-dark: #1a5a73;
--hvac-venue-color: #f5a623;
--hvac-venue-color: #89c92e;
--hvac-trainer-color: #f0f7e8;
--hvac-event-color: #0ebaa6;
--hvac-text: #333;
--hvac-text-muted: #666;
--hvac-border: #e0e0e0;
@ -633,13 +635,18 @@ body .hvac-find-training-page {
}
.hvac-legend-trainer {
background: var(--hvac-primary);
background: var(--hvac-trainer-color);
border: 2px solid #5a8a1a;
}
.hvac-legend-venue {
background: var(--hvac-venue-color);
}
.hvac-legend-event {
background: var(--hvac-event-color);
}
/* Map Toggles Overlay */
.hvac-map-toggles {
position: absolute;
@ -1057,6 +1064,103 @@ body .hvac-find-training-page {
margin-bottom: 10px;
}
/* Event Info Window */
.hvac-info-window-event {
padding: 12px;
max-width: 280px;
}
.hvac-info-window-event .hvac-info-window-title {
font-weight: 600;
color: var(--hvac-secondary);
margin-bottom: 6px;
font-size: 15px;
}
.hvac-info-window-date {
color: var(--hvac-text);
font-size: 13px;
margin-bottom: 4px;
}
.hvac-info-window-date .dashicons {
font-size: 14px;
width: 14px;
height: 14px;
vertical-align: middle;
margin-right: 4px;
color: var(--hvac-event-color);
}
.hvac-info-window-venue-name {
color: var(--hvac-text-muted);
font-size: 13px;
margin-bottom: 8px;
}
.hvac-info-window-cost {
display: inline-block;
padding: 3px 8px;
background: #e8f5f4;
color: #00736a;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
margin-bottom: 10px;
}
.hvac-info-window-cost.hvac-free {
background: #d1fae5;
color: #065f46;
}
.hvac-info-window-past-badge {
display: inline-block;
padding: 3px 8px;
background: #f3f4f6;
color: #6b7280;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
margin-left: 6px;
}
.hvac-info-window-event-link {
display: inline-block;
padding: 8px 16px;
background: var(--hvac-event-color);
color: #fff;
border: none;
border-radius: 4px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
text-decoration: none;
}
.hvac-info-window-event-link:hover {
background: #0ca696;
color: #fff;
}
/* Filter checkbox for Include Past Events */
.hvac-filter-checkbox {
display: flex;
align-items: center;
gap: 6px;
padding: 10px 0;
font-size: 14px;
color: var(--hvac-text);
cursor: pointer;
white-space: nowrap;
}
.hvac-filter-checkbox input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: var(--hvac-event-color);
}
/* ==========================================================================
Location Error Message
========================================================================== */
@ -1126,7 +1230,7 @@ body .hvac-find-training-page {
/* Collapsible sidebar */
.hvac-sidebar.collapsed {
max-height: 52px;
max-height: 80px;
overflow: hidden;
}
@ -1156,12 +1260,35 @@ body .hvac-find-training-page {
font-size: 12px;
}
.hvac-map-toggles {
top: 8px;
left: 8px;
padding: 6px 10px;
/* Tabs on tablet */
.hvac-sidebar-header {
flex-direction: row;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.hvac-sidebar-tabs {
flex: 1;
min-width: 0;
margin: 0;
padding: 0;
}
.hvac-tab {
padding: 8px 4px;
font-size: 12px;
}
.hvac-visibility-toggles {
gap: 8px;
padding: 4px 0;
}
.hvac-toggle-dot {
width: 14px;
height: 14px;
}
}
/* ==========================================================================
@ -1218,30 +1345,66 @@ body .hvac-find-training-page {
font-size: 13px;
}
.hvac-info-btn {
width: 32px;
height: 32px;
}
/* Sidebar adjustments */
.hvac-sidebar-header {
padding: 12px 14px;
padding: 10px 12px;
}
.hvac-sidebar-content {
padding: 12px;
}
.hvac-trainer-card {
/* Tab adjustments */
.hvac-sidebar-tabs {
margin: 0 -12px;
padding: 0 12px;
}
.hvac-tab {
padding: 6px 2px;
font-size: 11px;
}
.hvac-visibility-toggles {
display: none;
}
.hvac-trainer-card,
.hvac-venue-card,
.hvac-event-card {
padding: 12px;
}
.hvac-trainer-card-image {
width: 48px;
height: 48px;
.hvac-trainer-card-image,
.hvac-venue-card-icon,
.hvac-event-card-date {
width: 44px;
height: 44px;
}
.hvac-trainer-card-name {
font-size: 14px;
.hvac-trainer-card-name,
.hvac-venue-card-name,
.hvac-event-card-title {
font-size: 13px;
}
.hvac-trainer-card-location {
font-size: 12px;
.hvac-trainer-card-location,
.hvac-venue-card-location,
.hvac-event-card-venue {
font-size: 11px;
}
.hvac-event-card-month {
font-size: 9px;
}
.hvac-event-card-day {
font-size: 16px;
}
/* Modal adjustments */
@ -1477,6 +1640,448 @@ body .hvac-find-training-page {
}
}
/* ==========================================================================
Sidebar Tabs
========================================================================== */
.hvac-sidebar-header {
flex-direction: column;
align-items: stretch;
gap: 10px;
}
.hvac-sidebar-tabs {
display: flex;
gap: 0;
border-bottom: 2px solid var(--hvac-border);
margin: 0 -16px;
padding: 0 16px;
}
.hvac-tab {
flex: 1;
padding: 10px 8px;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
font-size: 13px;
font-weight: 500;
color: var(--hvac-text-muted);
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.hvac-tab:hover {
color: var(--hvac-secondary);
background: #f5f5f5;
}
.hvac-tab.active {
color: var(--hvac-primary);
border-bottom-color: var(--hvac-primary);
}
.hvac-tab:focus {
outline: none;
box-shadow: inset 0 0 0 2px rgba(0, 179, 164, 0.3);
}
.hvac-tab [data-count] {
font-weight: 600;
}
/* Visibility Toggles */
.hvac-visibility-toggles {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 0;
}
.hvac-visibility-toggle {
display: flex;
align-items: center;
cursor: pointer;
}
.hvac-visibility-toggle input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.hvac-toggle-dot {
width: 18px;
height: 18px;
border-radius: 50%;
border: 2px solid;
transition: all 0.2s;
}
.hvac-toggle-trainer {
background: var(--hvac-trainer-color);
border-color: #5a8a1a;
}
.hvac-toggle-venue {
background: var(--hvac-venue-color);
border-color: #6fa024;
}
.hvac-toggle-event {
background: var(--hvac-event-color);
border-color: #0a9a8a;
}
.hvac-visibility-toggle input:not(:checked) + .hvac-toggle-dot {
background: #f5f5f5;
border-color: #ccc;
}
.hvac-visibility-toggle:hover .hvac-toggle-dot {
transform: scale(1.1);
}
/* Tab Panels */
.hvac-tab-panel {
display: none;
}
.hvac-tab-panel.active {
display: block;
}
.hvac-item-list {
display: flex;
flex-direction: column;
gap: 12px;
}
/* ==========================================================================
Venue Cards
========================================================================== */
.hvac-venue-card {
background: #fff;
border: 1px solid var(--hvac-border);
border-radius: 8px;
padding: 14px;
cursor: pointer;
transition: border-color 0.2s, box-shadow 0.2s;
display: flex;
gap: 12px;
}
.hvac-venue-card:hover {
border-color: var(--hvac-venue-color);
box-shadow: 0 2px 8px rgba(137, 201, 46, 0.15);
}
.hvac-venue-card-icon {
width: 48px;
height: 48px;
flex-shrink: 0;
background: #f0f9e8;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
}
.hvac-venue-card-icon .dashicons {
font-size: 24px;
width: 24px;
height: 24px;
color: var(--hvac-venue-color);
}
.hvac-venue-card-info {
flex: 1;
min-width: 0;
}
.hvac-venue-card-name {
font-weight: 600;
color: var(--hvac-secondary);
margin-bottom: 3px;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.hvac-venue-card-location {
color: var(--hvac-text-muted);
font-size: 12px;
margin-bottom: 4px;
}
.hvac-venue-card-events {
font-size: 12px;
color: var(--hvac-venue-color);
font-weight: 500;
}
/* ==========================================================================
Event Cards
========================================================================== */
.hvac-event-card {
background: #fff;
border: 1px solid var(--hvac-border);
border-radius: 8px;
padding: 14px;
cursor: pointer;
transition: border-color 0.2s, box-shadow 0.2s;
display: flex;
gap: 12px;
}
.hvac-event-card:hover {
border-color: var(--hvac-event-color);
box-shadow: 0 2px 8px rgba(14, 186, 166, 0.15);
}
.hvac-event-card.hvac-event-past {
opacity: 0.7;
}
.hvac-event-card-date {
width: 48px;
flex-shrink: 0;
background: var(--hvac-event-color);
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 6px;
color: #fff;
}
.hvac-event-card.hvac-event-past .hvac-event-card-date {
background: #9ca3af;
}
.hvac-event-card-month {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.hvac-event-card-day {
font-size: 18px;
font-weight: 700;
line-height: 1;
}
.hvac-event-card-info {
flex: 1;
min-width: 0;
}
.hvac-event-card-title {
font-weight: 600;
color: var(--hvac-secondary);
margin-bottom: 3px;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.hvac-event-card-venue {
color: var(--hvac-text-muted);
font-size: 12px;
margin-bottom: 4px;
}
.hvac-event-card-meta {
display: flex;
align-items: center;
gap: 8px;
}
.hvac-event-card-cost {
font-size: 12px;
font-weight: 600;
color: var(--hvac-primary);
}
.hvac-event-card-cost.hvac-free {
color: #059669;
}
.hvac-event-card-past-badge {
font-size: 10px;
padding: 2px 6px;
background: #f3f4f6;
color: #6b7280;
border-radius: 3px;
font-weight: 500;
}
/* ==========================================================================
Info Button & Modal
========================================================================== */
.hvac-info-btn {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
background: #f5f5f5;
border: 1px solid var(--hvac-border);
border-radius: 50%;
cursor: pointer;
transition: all 0.2s;
flex-shrink: 0;
}
.hvac-info-btn:hover {
background: var(--hvac-primary);
border-color: var(--hvac-primary);
color: #fff;
}
.hvac-info-btn .dashicons {
font-size: 18px;
width: 18px;
height: 18px;
color: var(--hvac-text-muted);
}
.hvac-info-btn:hover .dashicons {
color: #fff;
}
/* Info Modal Content */
.hvac-info-modal-content {
max-width: 560px;
}
.hvac-info-modal-header {
padding: 24px 24px 0;
position: relative;
}
.hvac-info-modal-header h2 {
color: var(--hvac-secondary);
font-size: 1.5rem;
margin: 0;
padding-right: 40px;
}
.hvac-info-modal-body {
padding: 24px;
}
.hvac-info-section {
margin-bottom: 24px;
}
.hvac-info-section:last-child {
margin-bottom: 0;
}
.hvac-info-section h3 {
font-size: 1rem;
font-weight: 600;
color: var(--hvac-secondary);
margin: 0 0 12px;
}
.hvac-info-section p {
font-size: 14px;
line-height: 1.6;
color: var(--hvac-text);
margin: 0;
}
.hvac-info-list {
list-style: none;
padding: 0;
margin: 0;
}
.hvac-info-list li {
font-size: 14px;
line-height: 1.5;
color: var(--hvac-text);
padding: 8px 0;
border-bottom: 1px solid #f3f4f6;
}
.hvac-info-list li:last-child {
border-bottom: none;
}
.hvac-info-list li strong {
color: var(--hvac-secondary);
}
/* Info Modal Legend */
.hvac-info-legend {
display: flex;
flex-direction: column;
gap: 12px;
}
.hvac-info-legend-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 10px;
background: #f9fafb;
border-radius: 8px;
}
.hvac-info-legend-item .hvac-legend-marker {
margin-top: 4px;
flex-shrink: 0;
}
.hvac-info-legend-item strong {
display: block;
font-size: 14px;
color: var(--hvac-secondary);
margin-bottom: 2px;
}
.hvac-info-legend-item p {
font-size: 13px;
color: var(--hvac-text-muted);
margin: 0;
}
/* ==========================================================================
Empty States
========================================================================== */
.hvac-empty-state {
text-align: center;
padding: 40px 20px;
color: var(--hvac-text-muted);
}
.hvac-empty-state .dashicons {
font-size: 48px;
width: 48px;
height: 48px;
color: #ddd;
margin-bottom: 12px;
}
.hvac-empty-state p {
font-size: 14px;
margin: 0;
}
/* ==========================================================================
Print Styles
========================================================================== */

View file

@ -25,7 +25,8 @@
state: '',
certification: '',
training_format: '',
search: ''
search: '',
include_past: false
},
// User location (if obtained)
@ -45,15 +46,22 @@
bindEvents: function() {
const self = this;
// Search input with debounce
// Search input with debounce - client-side filtering for instant results
$('#hvac-training-search').on('input', function() {
clearTimeout(self.searchTimer);
const value = $(this).val();
const value = $(this).val().toLowerCase().trim();
self.searchTimer = setTimeout(function() {
self.activeFilters.search = value;
self.applyFilters();
}, 300);
// For empty search or server-side filters, use AJAX
if (!value || self.hasActiveServerFilters()) {
self.applyFilters();
} else {
// Client-side filtering for instant results
self.filterActiveTabList(value);
}
}, 150); // Faster for client-side
});
// State filter
@ -77,6 +85,22 @@
self.updateActiveFiltersDisplay();
});
// Include past events checkbox
$('#hvac-include-past').on('change', function() {
self.activeFilters.include_past = $(this).is(':checked');
self.applyFilters();
self.updateActiveFiltersDisplay();
});
// Mobile include past events checkbox
$('#hvac-include-past-mobile').on('change', function() {
const checked = $(this).is(':checked');
$('#hvac-include-past').prop('checked', checked);
self.activeFilters.include_past = checked;
self.applyFilters();
self.updateActiveFiltersDisplay();
});
// Near Me button
$('#hvac-near-me-btn').on('click', function() {
self.handleNearMeClick($(this));
@ -114,7 +138,9 @@
training_format: this.activeFilters.training_format,
search: this.activeFilters.search,
show_trainers: $('#hvac-show-trainers').is(':checked'),
show_venues: $('#hvac-show-venues').is(':checked')
show_venues: $('#hvac-show-venues').is(':checked'),
show_events: $('#hvac-show-events').is(':checked'),
include_past: this.activeFilters.include_past
};
// Add user location if available
@ -134,10 +160,25 @@
// Update map data
HVACTrainingMap.trainers = response.data.trainers || [];
HVACTrainingMap.venues = response.data.venues || [];
HVACTrainingMap.visibleTrainers = HVACTrainingMap.trainers.slice(); // Reset to all
HVACTrainingMap.events = response.data.events || [];
// Reset visible arrays to all items
HVACTrainingMap.visibleTrainers = HVACTrainingMap.trainers.slice();
HVACTrainingMap.visibleVenues = HVACTrainingMap.venues.slice();
HVACTrainingMap.visibleEvents = HVACTrainingMap.events.slice();
// Reset displayed counts
HVACTrainingMap.displayedCounts = { trainers: 0, venues: 0, events: 0 };
// Update map markers
HVACTrainingMap.updateMarkers();
HVACTrainingMap.updateCounts(HVACTrainingMap.trainers.length);
HVACTrainingMap.updateTrainerGrid();
// Update all counts
HVACTrainingMap.updateAllCounts();
// Render the active tab
HVACTrainingMap.renderActiveTabList();
// Note: syncSidebarWithViewport will be called by map 'idle' event
}
},
@ -235,7 +276,8 @@
state: '',
certification: '',
training_format: '',
search: ''
search: '',
include_past: false
};
// Reset user location
@ -246,6 +288,8 @@
$('#hvac-filter-certification').val('');
$('#hvac-filter-format').val('');
$('#hvac-training-search').val('');
$('#hvac-include-past').prop('checked', false);
$('#hvac-include-past-mobile').prop('checked', false);
// Reset Near Me button
$('#hvac-near-me-btn')
@ -295,6 +339,11 @@
.html('<span class="dashicons dashicons-location-alt"></span> Near Me')
.prop('disabled', false);
break;
case 'include_past':
this.activeFilters.include_past = false;
$('#hvac-include-past').prop('checked', false);
$('#hvac-include-past-mobile').prop('checked', false);
break;
}
this.applyFilters();
@ -333,6 +382,11 @@
this.addActiveFilter('location', 'Near Me');
}
// Include past events filter
if (this.activeFilters.include_past) {
this.addActiveFilter('include_past', 'Including Past Events');
}
this.updateClearButtonVisibility();
},
@ -358,6 +412,7 @@
this.activeFilters.certification ||
this.activeFilters.training_format ||
this.activeFilters.search ||
this.activeFilters.include_past ||
this.userLocation;
if (hasFilters) {
@ -367,6 +422,97 @@
}
},
/**
* Check if any server-side filters are active
*/
hasActiveServerFilters: function() {
return this.activeFilters.state ||
this.activeFilters.certification ||
this.activeFilters.training_format ||
this.activeFilters.include_past ||
this.userLocation;
},
/**
* Filter the active tab's list client-side for instant results
*/
filterActiveTabList: function(searchTerm) {
const activeTab = HVACTrainingMap.activeTab;
let items, filterFn;
switch (activeTab) {
case 'trainers':
items = HVACTrainingMap.visibleTrainers.length > 0
? HVACTrainingMap.trainers
: HVACTrainingMap.trainers;
filterFn = (trainer) => {
const searchFields = [
trainer.name,
trainer.city,
trainer.state,
trainer.company,
...(trainer.certifications || [])
].filter(Boolean).join(' ').toLowerCase();
return searchFields.includes(searchTerm);
};
break;
case 'venues':
items = HVACTrainingMap.venues;
filterFn = (venue) => {
const searchFields = [
venue.name,
venue.city,
venue.state,
venue.address
].filter(Boolean).join(' ').toLowerCase();
return searchFields.includes(searchTerm);
};
break;
case 'events':
items = HVACTrainingMap.events;
filterFn = (event) => {
const searchFields = [
event.title,
event.venue_name,
event.venue_city,
event.venue_state
].filter(Boolean).join(' ').toLowerCase();
return searchFields.includes(searchTerm);
};
break;
default:
return;
}
// Apply filter
const filteredItems = searchTerm ? items.filter(filterFn) : items;
// Update the visible items for the active tab
switch (activeTab) {
case 'trainers':
HVACTrainingMap.visibleTrainers = filteredItems;
HVACTrainingMap.displayedCounts.trainers = 0;
HVACTrainingMap.updateTrainerGrid();
break;
case 'venues':
HVACTrainingMap.visibleVenues = filteredItems;
HVACTrainingMap.displayedCounts.venues = 0;
HVACTrainingMap.updateVenueGrid();
break;
case 'events':
HVACTrainingMap.visibleEvents = filteredItems;
HVACTrainingMap.displayedCounts.events = 0;
HVACTrainingMap.updateEventGrid();
break;
}
// Update all counts
HVACTrainingMap.updateAllCounts();
},
/**
* Escape HTML for safe output
*/

View file

@ -20,6 +20,7 @@
// Marker collections
trainerMarkers: [],
venueMarkers: [],
eventMarkers: [],
// MarkerClusterer instance
markerClusterer: null,
@ -30,9 +31,23 @@
// Current data
trainers: [],
venues: [],
events: [],
// Visible trainers (filtered by map bounds)
// 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: {
@ -51,6 +66,8 @@
// 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;
@ -60,6 +77,8 @@
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;
@ -85,6 +104,9 @@
// Bind events
this.bindEvents();
// Initialize tabs
this.initTabs();
// Initialize responsive features
this.handleWindowResize();
this.initSidebarToggle();
@ -106,8 +128,16 @@
success: function(response) {
if (response.success && response.data) {
self.trainers = response.data.trainers || [];
self.updateTrainerGrid(self.trainers);
self.updateCounts(self.trainers.length, 0);
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() {
@ -204,13 +234,21 @@
if (response.success) {
self.trainers = response.data.trainers || [];
self.venues = response.data.venues || [];
self.visibleTrainers = self.trainers.slice(); // Initially all trainers are "visible"
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.updateCounts(self.trainers.length);
self.updateTrainerGrid();
self.updateAllCounts();
self.renderActiveTabList();
// Note: syncSidebarWithViewport will be called by map 'idle' event
// to filter trainers to current viewport
// to filter items to current viewport
} else {
self.showMapError(response.data?.message || 'Failed to load data');
}
@ -234,6 +272,7 @@
// 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) {
@ -253,6 +292,15 @@
});
}
// 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();
@ -324,6 +372,38 @@
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
*/
@ -336,12 +416,12 @@
};
}
// SVG circle marker (teal)
// SVG circle marker (light green with dark outline)
return {
path: google.maps.SymbolPath.CIRCLE,
fillColor: '#00b3a4',
fillColor: '#f0f7e8',
fillOpacity: 1,
strokeColor: '#ffffff',
strokeColor: '#5a8a1a',
strokeWeight: 2,
scale: 10
};
@ -359,10 +439,10 @@
};
}
// SVG marker (orange)
// SVG marker (green for mQ Approved)
return {
path: google.maps.SymbolPath.BACKWARD_CLOSED_ARROW,
fillColor: '#f5a623',
fillColor: '#89c92e',
fillOpacity: 1,
strokeColor: '#ffffff',
strokeWeight: 2,
@ -370,6 +450,29 @@
};
},
/**
* 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
*/
@ -380,7 +483,7 @@
}
// Combine all markers
const allMarkers = [...this.trainerMarkers, ...this.venueMarkers];
const allMarkers = [...this.trainerMarkers, ...this.venueMarkers, ...this.eventMarkers];
if (allMarkers.length === 0) {
return;
@ -410,6 +513,10 @@
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();
@ -420,7 +527,7 @@
* Fit map bounds to show all markers
*/
fitBounds: function() {
const allMarkers = [...this.trainerMarkers, ...this.venueMarkers];
const allMarkers = [...this.trainerMarkers, ...this.venueMarkers, ...this.eventMarkers];
if (allMarkers.length === 0) {
// Reset to default view
@ -528,6 +635,87 @@
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
*/
@ -779,21 +967,21 @@
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>');
$grid.html(`<div class="hvac-empty-state"><span class="dashicons dashicons-businessperson"></span><p>${message}</p></div>`);
$('.hvac-load-more-wrapper').hide();
return;
}
// Display trainers (show first 6 for compact sidebar, load more on request)
const displayCount = Math.min(trainersToShow.length, 6);
// 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++) {
const trainer = trainersToShow[i];
$grid.append(this.createTrainerCard(trainer));
$grid.append(this.createTrainerCard(trainersToShow[i]));
}
// Show load more if there are more trainers
if (trainersToShow.length > 6) {
if (trainersToShow.length > displayCount) {
$('.hvac-load-more-wrapper').show();
} else {
$('.hvac-load-more-wrapper').hide();
@ -825,20 +1013,13 @@
},
/**
* Update counts display
* 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) {
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);
}
// Update all tab counts
this.updateAllCounts();
},
/**
@ -855,6 +1036,22 @@
}
});
// 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();
@ -868,7 +1065,7 @@
});
// Marker toggles
$('#hvac-show-trainers, #hvac-show-venues').on('change', function() {
$('#hvac-show-trainers, #hvac-show-venues, #hvac-show-events').on('change', function() {
self.updateMarkers();
});
@ -939,23 +1136,46 @@
},
/**
* Load more trainers
* Load more items for the active tab
*/
loadMoreTrainers: function() {
const $grid = $('#hvac-trainer-grid');
const currentCount = $grid.find('.hvac-trainer-card').length;
const loadMore = 6; // Load 6 more at a time for sidebar
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;
}
},
// Use visible trainers if available (viewport sync), otherwise use all trainers
const trainersToShow = this.visibleTrainers.length > 0 || this.map
? this.visibleTrainers
: this.trainers;
/**
* 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 + loadMore && i < trainersToShow.length; i++) {
$grid.append(this.createTrainerCard(trainersToShow[i]));
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;
}
}
if ($grid.find('.hvac-trainer-card').length >= trainersToShow.length) {
this.displayedCounts[tabType] = $grid.find(cardSelector).length;
if ($grid.find(cardSelector).length >= items.length) {
$('.hvac-load-more-wrapper').hide();
}
},
@ -1033,10 +1253,10 @@
},
/**
* Sync sidebar trainer list with visible map viewport
* Sync sidebar lists with visible map viewport
*/
syncSidebarWithViewport: function() {
if (!this.map || this.trainers.length === 0) {
if (!this.map) {
return;
}
@ -1055,11 +1275,32 @@
return bounds.contains(position);
});
// Update the sidebar grid with visible trainers
this.updateTrainerGrid();
// 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);
});
// Update count to show visible vs total
this.updateCounts(this.visibleTrainers.length, this.trainers.length);
// 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();
},
/**
@ -1089,6 +1330,245 @@
`);
},
/**
* 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(`<div class="hvac-empty-state"><span class="dashicons dashicons-building"></span><p>${message}</p></div>`);
$('.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 `
<div class="hvac-venue-card" data-venue-id="${venue.id}">
<div class="hvac-venue-card-icon">
<span class="dashicons dashicons-building"></span>
</div>
<div class="hvac-venue-card-info">
<div class="hvac-venue-card-name">${this.escapeHtml(venue.name)}</div>
<div class="hvac-venue-card-location">${this.escapeHtml(location)}</div>
<div class="hvac-venue-card-events">${eventsText}</div>
</div>
</div>
`;
},
/**
* 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(`<div class="hvac-empty-state"><span class="dashicons dashicons-calendar-alt"></span><p>${message}</p></div>`);
$('.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 ? '<span class="hvac-event-card-past-badge">Past</span>' : '';
return `
<div class="hvac-event-card ${pastClass}" data-event-url="${this.escapeHtml(event.url)}">
<div class="hvac-event-card-date">
<span class="hvac-event-card-month">${month}</span>
<span class="hvac-event-card-day">${day}</span>
</div>
<div class="hvac-event-card-info">
<div class="hvac-event-card-title">${this.escapeHtml(event.title)}</div>
<div class="hvac-event-card-venue">${venueText ? 'at ' + this.escapeHtml(venueText) : ''}</div>
<div class="hvac-event-card-meta">
<span class="hvac-event-card-cost ${costClass}">${this.escapeHtml(event.cost)}</span>
${pastBadge}
</div>
</div>
</div>
`;
},
/**
* 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
*/

View file

@ -33,6 +33,11 @@ $api_key_configured = $find_training->is_api_key_configured();
<input type="text" id="hvac-training-search" placeholder="Search trainers..." aria-label="Search trainers">
</div>
<!-- Info Button -->
<button type="button" id="hvac-info-btn" class="hvac-info-btn" aria-label="How to use this page">
<span class="dashicons dashicons-info-outline" aria-hidden="true"></span>
</button>
<!-- Filter Dropdowns (hidden on mobile, shown in panel) -->
<div class="hvac-filter-dropdowns">
<select id="hvac-filter-state" class="hvac-filter-select" aria-label="Filter by state">
@ -63,6 +68,12 @@ $api_key_configured = $find_training->is_api_key_configured();
<span class="hvac-btn-text">Near Me</span>
</button>
<!-- Include Past Events Checkbox -->
<label class="hvac-filter-checkbox">
<input type="checkbox" id="hvac-include-past">
<span>Include Past</span>
</label>
<!-- Clear Filters -->
<button type="button" class="hvac-clear-filters" style="display: none;">
Clear
@ -110,36 +121,80 @@ $api_key_configured = $find_training->is_api_key_configured();
<?php endforeach; ?>
</select>
</div>
<div class="hvac-mobile-filter-group">
<label class="hvac-filter-checkbox">
<input type="checkbox" id="hvac-include-past-mobile">
<span>Include Past Events</span>
</label>
</div>
</div>
</div>
<!-- Main Content: Sidebar + Map -->
<div class="hvac-map-layout">
<!-- Left Sidebar -->
<aside class="hvac-sidebar" role="region" aria-label="Trainer directory">
<aside class="hvac-sidebar" role="region" aria-label="Training directory">
<div class="hvac-sidebar-header">
<span class="hvac-results-summary" aria-live="polite">
<span id="hvac-trainer-count">0</span> trainers
</span>
<!-- Tab Navigation -->
<div class="hvac-sidebar-tabs" role="tablist" aria-label="Browse by category">
<button role="tab" class="hvac-tab active" data-tab="trainers" aria-selected="true" aria-controls="hvac-panel-trainers">
Trainers (<span data-count="trainers">0</span>)
</button>
<button role="tab" class="hvac-tab" data-tab="venues" aria-selected="false" aria-controls="hvac-panel-venues">
Venues (<span data-count="venues">0</span>)
</button>
<button role="tab" class="hvac-tab" data-tab="events" aria-selected="false" aria-controls="hvac-panel-events">
Events (<span data-count="events">0</span>)
</button>
</div>
<!-- Visibility Toggles (moved from map overlay) -->
<div class="hvac-visibility-toggles">
<label class="hvac-visibility-toggle" title="Show trainers on map">
<input type="checkbox" id="hvac-show-trainers" checked>
<span class="hvac-toggle-dot hvac-toggle-trainer"></span>
</label>
<label class="hvac-visibility-toggle" title="Show venues on map">
<input type="checkbox" id="hvac-show-venues" checked>
<span class="hvac-toggle-dot hvac-toggle-venue"></span>
</label>
<label class="hvac-visibility-toggle" title="Show events on map">
<input type="checkbox" id="hvac-show-events" checked>
<span class="hvac-toggle-dot hvac-toggle-event"></span>
</label>
</div>
<!-- Mobile collapse toggle -->
<button type="button"
class="hvac-sidebar-toggle"
aria-expanded="true"
aria-controls="hvac-sidebar-content"
aria-label="Toggle trainer list">
aria-label="Toggle directory list">
<span class="dashicons dashicons-arrow-down-alt2" aria-hidden="true"></span>
</button>
</div>
<div id="hvac-sidebar-content" class="hvac-sidebar-content">
<!-- Trainer List (scrollable) -->
<div id="hvac-trainer-grid" class="hvac-trainer-list">
<div class="hvac-grid-loading">
<span class="dashicons dashicons-update-alt hvac-spin" aria-hidden="true"></span>
Loading trainers...
<!-- Trainers Panel -->
<div role="tabpanel" id="hvac-panel-trainers" class="hvac-tab-panel active" aria-labelledby="tab-trainers">
<div id="hvac-trainer-grid" class="hvac-item-list">
<div class="hvac-grid-loading">
<span class="dashicons dashicons-update-alt hvac-spin" aria-hidden="true"></span>
Loading trainers...
</div>
</div>
</div>
<!-- Venues Panel -->
<div role="tabpanel" id="hvac-panel-venues" class="hvac-tab-panel" aria-labelledby="tab-venues" hidden>
<div id="hvac-venue-grid" class="hvac-item-list"></div>
</div>
<!-- Events Panel -->
<div role="tabpanel" id="hvac-panel-events" class="hvac-tab-panel" aria-labelledby="tab-events" hidden>
<div id="hvac-event-grid" class="hvac-item-list"></div>
</div>
<!-- Load More Button -->
<div class="hvac-load-more-wrapper" style="display: none;">
<button type="button" id="hvac-load-more" class="hvac-btn-secondary">
@ -184,19 +239,12 @@ $api_key_configured = $find_training->is_api_key_configured();
<span class="hvac-legend-marker hvac-legend-venue" aria-hidden="true"></span>
<span>Venue</span>
</div>
<div class="hvac-legend-item">
<span class="hvac-legend-marker hvac-legend-event" aria-hidden="true"></span>
<span>Event</span>
</div>
</div>
<!-- Map Toggles Overlay -->
<div class="hvac-map-toggles">
<label class="hvac-toggle-compact">
<input type="checkbox" id="hvac-show-trainers" checked>
<span class="hvac-toggle-label">Trainers</span>
</label>
<label class="hvac-toggle-compact">
<input type="checkbox" id="hvac-show-venues" checked>
<span class="hvac-toggle-label">Venues</span>
</label>
</div>
</div>
</div>
</div>
@ -302,4 +350,60 @@ $api_key_configured = $find_training->is_api_key_configured();
</div>
</div>
<!-- Info Modal -->
<div id="hvac-info-modal" class="hvac-training-modal" style="display: none;" role="dialog" aria-modal="true" aria-labelledby="info-modal-title">
<div class="hvac-modal-overlay"></div>
<div class="hvac-modal-content hvac-info-modal-content">
<div class="hvac-info-modal-header">
<h2 id="info-modal-title">Find Training Near You</h2>
<button class="hvac-modal-close" aria-label="Close modal">&times;</button>
</div>
<div class="hvac-info-modal-body">
<section class="hvac-info-section">
<h3>What is Upskill HVAC?</h3>
<p>Upskill HVAC is a community of certified trainers and training facilities dedicated to advancing skills in the HVAC industry. Our platform connects technicians with professional training opportunities across the country.</p>
</section>
<section class="hvac-info-section">
<h3>How to Use This Page</h3>
<ul class="hvac-info-list">
<li><strong>Browse by Category:</strong> Use the tabs above the list to switch between Trainers, Venues, and Events</li>
<li><strong>Search:</strong> Type in the search bar to filter results within the current tab</li>
<li><strong>Filter:</strong> Use the dropdown filters to narrow by state, certification, or format</li>
<li><strong>Near Me:</strong> Click the "Near Me" button to find training options close to your location</li>
<li><strong>Map Markers:</strong> Click on any marker to see details, or hover to preview</li>
<li><strong>Toggle Visibility:</strong> Use the colored dots in the header to show/hide marker types on the map</li>
</ul>
</section>
<section class="hvac-info-section">
<h3>Map Legend</h3>
<div class="hvac-info-legend">
<div class="hvac-info-legend-item">
<span class="hvac-legend-marker hvac-legend-trainer"></span>
<div>
<strong>Trainer</strong>
<p>Certified HVAC trainers who conduct training sessions</p>
</div>
</div>
<div class="hvac-info-legend-item">
<span class="hvac-legend-marker hvac-legend-venue"></span>
<div>
<strong>Training Lab</strong>
<p>measureQuick Approved facilities with professional equipment</p>
</div>
</div>
<div class="hvac-info-legend-item">
<span class="hvac-legend-marker hvac-legend-event"></span>
<div>
<strong>Event</strong>
<p>Scheduled training sessions you can register for</p>
</div>
</div>
</div>
</section>
</div>
</div>
</div>
<?php get_footer(); ?>