diff --git a/assets/css/find-training-map.css b/assets/css/find-training-map.css index 7549737e..fa9496bd 100644 --- a/assets/css/find-training-map.css +++ b/assets/css/find-training-map.css @@ -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 ========================================================================== */ diff --git a/assets/js/find-training-filters.js b/assets/js/find-training-filters.js index a24c5a6f..6b3f9961 100644 --- a/assets/js/find-training-filters.js +++ b/assets/js/find-training-filters.js @@ -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(' 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 */ diff --git a/assets/js/find-training-map.js b/assets/js/find-training-map.js index 76848415..70be8088 100644 --- a/assets/js/find-training-map.js +++ b/assets/js/find-training-map.js @@ -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('
' + message + '
${message}
${message}
${message}