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:
parent
5c15b27935
commit
17dd3c9bdb
4 changed files with 1430 additions and 95 deletions
|
|
@ -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
|
||||
========================================================================== */
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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">×</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(); ?>
|
||||
|
|
|
|||
Loading…
Reference in a new issue