## Major Enhancements ### 🏗️ Architecture & Infrastructure - Implement comprehensive Docker testing infrastructure with hermetic environment - Add Forgejo Actions CI/CD pipeline for automated deployments - Create Page Object Model (POM) testing architecture reducing test duplication by 90% - Establish security-first development patterns with input validation and output escaping ### 🧪 Testing Framework Modernization - Migrate 146+ tests from 80 duplicate files to centralized architecture - Add comprehensive E2E test suites for all user roles and workflows - Implement WordPress error detection with automatic site health monitoring - Create robust browser lifecycle management with proper cleanup ### 📚 Documentation & Guides - Add comprehensive development best practices guide - Create detailed administrator setup documentation - Establish user guides for trainers and master trainers - Document security incident reports and migration guides ### 🔧 Core Plugin Features - Enhance trainer profile management with certification system - Improve find trainer functionality with advanced filtering - Strengthen master trainer area with content management - Add comprehensive venue and organizer management ### 🛡️ Security & Reliability - Implement security-first patterns throughout codebase - Add comprehensive input validation and output escaping - Create secure credential management system - Establish proper WordPress role-based access control ### 🎯 WordPress Integration - Strengthen singleton pattern implementation across all classes - Enhance template hierarchy with proper WordPress integration - Improve page manager with hierarchical URL structure - Add comprehensive shortcode and menu system ### 🔍 Developer Experience - Add extensive debugging and troubleshooting tools - Create comprehensive test data seeding scripts - Implement proper error handling and logging - Establish consistent code patterns and standards ### 📊 Performance & Optimization - Optimize database queries and caching strategies - Improve asset loading and script management - Enhance template rendering performance - Streamline user experience across all interfaces 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
964 lines
No EOL
39 KiB
JavaScript
964 lines
No EOL
39 KiB
JavaScript
/**
|
||
* Find a Trainer Page JavaScript
|
||
* Handles filtering, modals, and AJAX interactions
|
||
*
|
||
* @package HVAC_Plugin
|
||
* @since 1.0.0
|
||
*/
|
||
|
||
(function($) {
|
||
'use strict';
|
||
|
||
// Cache DOM elements
|
||
let $filterModal, $trainerModal, $contactForm;
|
||
let currentFilter = '';
|
||
let activeFilters = {};
|
||
let currentPage = 1;
|
||
let isLoading = false;
|
||
|
||
// Initialize on document ready
|
||
$(document).ready(function() {
|
||
initializeElements();
|
||
bindEvents();
|
||
|
||
// Handle direct profile URL access
|
||
handleDirectProfileAccess();
|
||
|
||
// Enable MapGeo interaction handling (only if not showing direct profile)
|
||
if (!hvac_find_trainer.show_direct_profile) {
|
||
preventMapGeoSidebarContent();
|
||
interceptMapGeoMarkers();
|
||
|
||
// Additional MapGeo integration after map loads
|
||
setTimeout(function() {
|
||
initializeMapGeoEvents();
|
||
}, 2000); // Give MapGeo time to initialize
|
||
}
|
||
});
|
||
|
||
/**
|
||
* Initialize cached elements
|
||
*/
|
||
function initializeElements() {
|
||
$filterModal = $('#hvac-filter-modal');
|
||
$trainerModal = $('#hvac-trainer-modal');
|
||
$contactForm = $('#hvac-contact-form');
|
||
|
||
// CRITICAL: Ensure modals are hidden on initialization
|
||
if ($filterModal.length) {
|
||
$filterModal.removeClass('modal-active active show').css({
|
||
'display': 'none',
|
||
'visibility': 'hidden',
|
||
'opacity': '0'
|
||
});
|
||
}
|
||
if ($trainerModal.length) {
|
||
$trainerModal.css('display', 'none');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Prevent MapGeo from displaying content in its sidebar
|
||
* This ensures trainer cards only appear in our Container 5
|
||
*/
|
||
function preventMapGeoSidebarContent() {
|
||
// Remove any MapGeo sidebar content immediately
|
||
$('.igm_content_right_1_3').remove();
|
||
$('.igm_content_gutter').remove();
|
||
|
||
// Watch for any dynamic content injection from MapGeo
|
||
const observer = new MutationObserver(function(mutations) {
|
||
mutations.forEach(function(mutation) {
|
||
if (mutation.addedNodes.length) {
|
||
mutation.addedNodes.forEach(function(node) {
|
||
if (node.nodeType === 1) { // Element node
|
||
// Remove any MapGeo sidebar that gets added
|
||
if ($(node).hasClass('igm_content_right_1_3') ||
|
||
$(node).hasClass('igm_content_gutter')) {
|
||
$(node).remove();
|
||
}
|
||
// Also check children
|
||
$(node).find('.igm_content_right_1_3, .igm_content_gutter').remove();
|
||
}
|
||
});
|
||
}
|
||
});
|
||
});
|
||
|
||
// Observe the map section for changes
|
||
const mapSection = document.querySelector('.hvac-map-section');
|
||
if (mapSection) {
|
||
observer.observe(mapSection, {
|
||
childList: true,
|
||
subtree: true
|
||
});
|
||
}
|
||
|
||
// Also observe the entire page for MapGeo injections
|
||
observer.observe(document.body, {
|
||
childList: true,
|
||
subtree: true
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Initialize MapGeo-specific event handlers after map loads
|
||
*/
|
||
function initializeMapGeoEvents() {
|
||
console.log('Initializing MapGeo events...');
|
||
|
||
// Create the main MapGeo handler function
|
||
// This replaces the early version created in the page template
|
||
window.hvacMainShowTrainerModal = function(data) {
|
||
console.log('MapGeo custom action triggered with data:', data);
|
||
|
||
// Method 1: Use profile_id if available (most reliable)
|
||
let profileId = null;
|
||
if (data && data.profile_id && data.profile_id.trim() !== '') {
|
||
profileId = data.profile_id.trim();
|
||
} else if (data && data.id && data.id.toString().startsWith('trainer_')) {
|
||
// Extract profile ID from marker ID format "trainer_123"
|
||
profileId = data.id.replace('trainer_', '');
|
||
} else if (data && data.name && data.name.match(/^\d+$/)) {
|
||
// Check if name field actually contains the profile ID (common case)
|
||
profileId = data.name;
|
||
} else if (data && data.id && data.id.match(/^\d+$/)) {
|
||
// Check if id field contains just the profile ID number
|
||
profileId = data.id;
|
||
}
|
||
|
||
console.log('Extracted profile ID:', profileId);
|
||
|
||
if (profileId) {
|
||
// Find trainer card by profile ID (most reliable method)
|
||
const $matchingCard = $('.hvac-trainer-card[data-profile-id="' + profileId + '"]');
|
||
|
||
if ($matchingCard.length > 0 && !$matchingCard.hasClass('hvac-champion-card')) {
|
||
console.log('Found matching trainer card by profile ID:', profileId);
|
||
|
||
// Extract trainer data from the card
|
||
const trainerData = {
|
||
profile_id: profileId,
|
||
name: $matchingCard.find('.hvac-trainer-name a, .hvac-trainer-name .hvac-champion-name').text().trim(),
|
||
city: $matchingCard.find('.hvac-trainer-location').text().split(',')[0],
|
||
state: $matchingCard.find('.hvac-trainer-location').text().split(',')[1]?.trim(),
|
||
certification_type: $matchingCard.find('.hvac-trainer-certification').text(), // Legacy compatibility
|
||
certifications: [],
|
||
profile_image: $matchingCard.find('.hvac-trainer-image img:not(.hvac-mq-badge)').attr('src') || '',
|
||
business_type: 'Independent Contractor', // Mock data
|
||
event_count: parseInt($matchingCard.data('event-count')) || 0,
|
||
training_formats: 'In-Person, Virtual',
|
||
training_locations: 'On-site, Remote',
|
||
upcoming_events: []
|
||
};
|
||
|
||
// Extract certifications from card badges
|
||
$matchingCard.find('.hvac-trainer-cert-badge').each(function() {
|
||
const certText = $(this).text().trim();
|
||
if (certText && certText !== 'HVAC Trainer') {
|
||
trainerData.certifications.push({
|
||
type: certText,
|
||
status: $(this).hasClass('hvac-cert-legacy') ? 'legacy' : 'active'
|
||
});
|
||
}
|
||
});
|
||
|
||
// Show the trainer modal
|
||
showTrainerModal(trainerData);
|
||
return; // Successfully handled
|
||
} else if ($matchingCard.length > 0 && $matchingCard.hasClass('hvac-champion-card')) {
|
||
console.log('Clicked marker is for a Champion, not showing modal');
|
||
return; // Champions don't get modals
|
||
} else {
|
||
console.warn('No trainer card found for profile ID:', profileId);
|
||
}
|
||
}
|
||
|
||
// Fallback Method 2: Try to extract trainer name and match
|
||
let trainerName = null;
|
||
|
||
// Try various name fields
|
||
if (data && data.name && data.name.trim() !== '+' && !data.name.match(/^\d+$/)) {
|
||
trainerName = data.name.trim();
|
||
} else if (data && data.title && data.title.trim() !== '+') {
|
||
trainerName = data.title.trim();
|
||
}
|
||
|
||
// Try content field
|
||
if (!trainerName && data && data.content) {
|
||
console.log('Trying to extract trainer from content:', data.content);
|
||
const tempDiv = document.createElement('div');
|
||
tempDiv.innerHTML = data.content;
|
||
const nameElements = tempDiv.querySelectorAll('h4, strong, .trainer-name, [class*="name"]');
|
||
if (nameElements.length > 0) {
|
||
trainerName = nameElements[0].textContent.trim();
|
||
}
|
||
}
|
||
|
||
// Try tooltipContent
|
||
if (!trainerName && data && data.tooltipContent) {
|
||
const tempDiv = document.createElement('div');
|
||
tempDiv.innerHTML = data.tooltipContent;
|
||
const strongElements = tempDiv.querySelectorAll('strong');
|
||
if (strongElements.length > 0) {
|
||
trainerName = strongElements[0].textContent.trim();
|
||
}
|
||
}
|
||
|
||
console.log('Extracted trainer name (fallback):', trainerName);
|
||
|
||
if (trainerName && trainerName !== '+') {
|
||
// Try to find matching trainer by name
|
||
let $matchingCard = $('.hvac-trainer-card').filter(function() {
|
||
const cardName = $(this).find('.hvac-trainer-name a, .hvac-trainer-name .hvac-champion-name').text().trim();
|
||
return cardName === trainerName;
|
||
});
|
||
|
||
// If exact match not found, try partial matching
|
||
if ($matchingCard.length === 0) {
|
||
$matchingCard = $('.hvac-trainer-card').filter(function() {
|
||
const cardName = $(this).find('.hvac-trainer-name a, .hvac-trainer-name .hvac-champion-name').text().trim();
|
||
return cardName.toLowerCase().includes(trainerName.toLowerCase()) ||
|
||
trainerName.toLowerCase().includes(cardName.toLowerCase());
|
||
});
|
||
}
|
||
|
||
if ($matchingCard.length > 0 && !$matchingCard.hasClass('hvac-champion-card')) {
|
||
console.log('Found matching trainer card by name:', trainerName);
|
||
|
||
// Extract trainer data from the card
|
||
const trainerData = {
|
||
profile_id: $matchingCard.data('profile-id'),
|
||
name: $matchingCard.find('.hvac-trainer-name a, .hvac-trainer-name .hvac-champion-name').text().trim(),
|
||
city: $matchingCard.find('.hvac-trainer-location').text().split(',')[0],
|
||
state: $matchingCard.find('.hvac-trainer-location').text().split(',')[1]?.trim(),
|
||
certification_type: $matchingCard.find('.hvac-trainer-certification').text(), // Legacy compatibility
|
||
certifications: [],
|
||
profile_image: $matchingCard.find('.hvac-trainer-image img:not(.hvac-mq-badge)').attr('src') || '',
|
||
business_type: 'Independent Contractor', // Mock data
|
||
event_count: parseInt($matchingCard.data('event-count')) || 0,
|
||
training_formats: 'In-Person, Virtual',
|
||
training_locations: 'On-site, Remote',
|
||
upcoming_events: []
|
||
};
|
||
|
||
// Extract certifications from card badges
|
||
$matchingCard.find('.hvac-trainer-cert-badge').each(function() {
|
||
const certText = $(this).text().trim();
|
||
if (certText && certText !== 'HVAC Trainer') {
|
||
trainerData.certifications.push({
|
||
type: certText,
|
||
status: $(this).hasClass('hvac-cert-legacy') ? 'legacy' : 'active'
|
||
});
|
||
}
|
||
});
|
||
|
||
// Show the trainer modal
|
||
showTrainerModal(trainerData);
|
||
} else if ($matchingCard.length > 0 && $matchingCard.hasClass('hvac-champion-card')) {
|
||
console.log('Matched trainer is a Champion, not showing modal');
|
||
} else {
|
||
console.warn('No matching trainer found for name:', trainerName);
|
||
console.log('Available trainers:',
|
||
$('.hvac-trainer-card .hvac-trainer-name a, .hvac-trainer-card .hvac-trainer-name .hvac-champion-name').map(function() {
|
||
return $(this).text().trim();
|
||
}).get()
|
||
);
|
||
}
|
||
} else {
|
||
console.warn('Could not extract valid trainer identifier from MapGeo data:', data);
|
||
console.log('Available data properties:', Object.keys(data || {}));
|
||
console.log('Available profile IDs on page:',
|
||
$('.hvac-trainer-card').map(function() {
|
||
return $(this).data('profile-id');
|
||
}).get()
|
||
);
|
||
}
|
||
};
|
||
|
||
// Replace the early function with the main one
|
||
window.hvacShowTrainerModal = window.hvacMainShowTrainerModal;
|
||
|
||
// Process any queued calls from before the main script loaded
|
||
if (window.hvacPendingModalCalls && window.hvacPendingModalCalls.length > 0) {
|
||
console.log('Processing', window.hvacPendingModalCalls.length, 'queued MapGeo calls');
|
||
window.hvacPendingModalCalls.forEach(function(data) {
|
||
window.hvacMainShowTrainerModal(data);
|
||
});
|
||
window.hvacPendingModalCalls = []; // Clear the queue
|
||
}
|
||
|
||
console.log('MapGeo custom action function created: window.hvacShowTrainerModal');
|
||
}
|
||
|
||
|
||
/**
|
||
* Prevent MapGeo from showing content in sidebar (if needed)
|
||
*/
|
||
function interceptMapGeoMarkers() {
|
||
// This function now primarily handles preventing MapGeo sidebar content
|
||
// The actual marker clicks are handled via the MapGeo custom action: window.hvacShowTrainerModal
|
||
|
||
// Handle any legacy view profile links if they exist in tooltips/popups
|
||
$(document).on('click', '.hvac-view-profile, .hvac-marker-popup button', function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
const profileId = $(this).data('profile-id');
|
||
if (profileId) {
|
||
// Find the corresponding trainer data from the cards
|
||
const $trainerCard = $('.hvac-trainer-card[data-profile-id="' + profileId + '"]');
|
||
|
||
if ($trainerCard.length > 0 && !$trainerCard.hasClass('hvac-champion-card')) {
|
||
// Get trainer name and trigger the MapGeo custom action
|
||
const trainerName = $trainerCard.find('.hvac-trainer-name a, .hvac-trainer-name .hvac-champion-name').text().trim();
|
||
// Trigger the same function MapGeo would call
|
||
if (window.hvacShowTrainerModal) {
|
||
window.hvacShowTrainerModal({ name: trainerName });
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Bind all event handlers
|
||
*/
|
||
function bindEvents() {
|
||
// Filter button clicks - handle both class variations
|
||
$('.hvac-filter-btn, .hvac-filter-button').on('click', handleFilterClick);
|
||
|
||
// Filter modal apply
|
||
$('.hvac-filter-apply').on('click', applyFilters);
|
||
|
||
// Clear all filters button
|
||
$('.hvac-clear-filters').on('click', clearAllFilters);
|
||
|
||
// Trainer profile clicks - using event delegation
|
||
$(document).on('click', '.hvac-open-profile', handleProfileClick);
|
||
|
||
// Modal close buttons and backdrop clicks
|
||
$('.hvac-modal-close').on('click', closeModals);
|
||
|
||
// Click on modal backdrop to close
|
||
$filterModal.on('click', function(e) {
|
||
if ($(e.target).is('#hvac-filter-modal')) {
|
||
closeModals();
|
||
}
|
||
});
|
||
|
||
$trainerModal.on('click', function(e) {
|
||
if ($(e.target).is('#hvac-trainer-modal')) {
|
||
closeModals();
|
||
}
|
||
});
|
||
|
||
// Escape key to close modals
|
||
$(document).on('keydown', function(e) {
|
||
if (e.key === 'Escape') {
|
||
closeModals();
|
||
}
|
||
});
|
||
|
||
// Search input
|
||
$('.hvac-search-input').on('input', debounce(handleSearch, 500));
|
||
|
||
// Contact form submission (both modal and direct forms)
|
||
$contactForm.on('submit', handleContactSubmit);
|
||
$(document).on('submit', '#hvac-direct-contact-form', handleContactSubmit);
|
||
|
||
// Pagination clicks
|
||
$(document).on('click', '.hvac-pagination a, .hvac-page-link', handlePagination);
|
||
|
||
// Active filter removal
|
||
$(document).on('click', '.hvac-active-filter button', removeActiveFilter);
|
||
}
|
||
|
||
/**
|
||
* Handle filter button click
|
||
*/
|
||
function handleFilterClick(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
currentFilter = $(this).data('filter');
|
||
|
||
// Load real filter options via AJAX
|
||
loadFilterOptions(currentFilter);
|
||
}
|
||
|
||
/**
|
||
* Load filter options via AJAX
|
||
*/
|
||
function loadFilterOptions(filterType) {
|
||
if (isLoading) return;
|
||
|
||
isLoading = true;
|
||
|
||
// Show loading state for filter button
|
||
$(`.hvac-filter-btn[data-filter="${filterType}"]`).addClass('loading');
|
||
|
||
$.post(hvac_find_trainer.ajax_url, {
|
||
action: 'hvac_get_filter_options',
|
||
filter_type: filterType,
|
||
nonce: hvac_find_trainer.nonce
|
||
})
|
||
.done(function(response) {
|
||
if (response.success && response.data.options) {
|
||
// Convert the different response formats to standard format
|
||
let options = [];
|
||
|
||
if (filterType === 'business_type') {
|
||
// Business types have {value, label, count} format
|
||
options = response.data.options;
|
||
} else {
|
||
// States and other simple arrays need to be converted to {value, label} format
|
||
options = response.data.options.map(function(option) {
|
||
if (typeof option === 'string') {
|
||
return {value: option, label: option};
|
||
}
|
||
return option;
|
||
});
|
||
}
|
||
|
||
showFilterModal({options: options});
|
||
} else {
|
||
console.error('Failed to load filter options:', response);
|
||
// Fallback to empty options
|
||
showFilterModal({options: []});
|
||
}
|
||
})
|
||
.fail(function(xhr, status, error) {
|
||
console.error('AJAX error loading filter options:', status, error);
|
||
// Fallback to empty options
|
||
showFilterModal({options: []});
|
||
})
|
||
.always(function() {
|
||
isLoading = false;
|
||
$(`.hvac-filter-btn[data-filter="${filterType}"]`).removeClass('loading');
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Get mock filter options (kept as fallback)
|
||
*/
|
||
function getMockFilterOptions(filterType) {
|
||
const options = {
|
||
state: [
|
||
{value: 'Alabama', label: 'Alabama'},
|
||
{value: 'Alaska', label: 'Alaska'},
|
||
{value: 'Arizona', label: 'Arizona'},
|
||
{value: 'Arkansas', label: 'Arkansas'},
|
||
{value: 'California', label: 'California'},
|
||
{value: 'Colorado', label: 'Colorado'},
|
||
{value: 'Florida', label: 'Florida'},
|
||
{value: 'Georgia', label: 'Georgia'},
|
||
{value: 'Illinois', label: 'Illinois'},
|
||
{value: 'Michigan', label: 'Michigan'},
|
||
{value: 'Minnesota', label: 'Minnesota'},
|
||
{value: 'Ohio', label: 'Ohio'},
|
||
{value: 'Texas', label: 'Texas'},
|
||
{value: 'Wisconsin', label: 'Wisconsin'}
|
||
],
|
||
business_type: [
|
||
{value: 'Independent Contractor', label: 'Independent Contractor'},
|
||
{value: 'Small Business', label: 'Small Business'},
|
||
{value: 'Corporation', label: 'Corporation'},
|
||
{value: 'Non-Profit', label: 'Non-Profit'}
|
||
],
|
||
training_format: [
|
||
{value: 'In-Person', label: 'In-Person'},
|
||
{value: 'Virtual', label: 'Virtual'},
|
||
{value: 'Hybrid', label: 'Hybrid'},
|
||
{value: 'Self-Paced', label: 'Self-Paced'}
|
||
],
|
||
training_resources: [
|
||
{value: 'Video Tutorials', label: 'Video Tutorials'},
|
||
{value: 'Written Guides', label: 'Written Guides'},
|
||
{value: 'Hands-On Training', label: 'Hands-On Training'},
|
||
{value: 'Certification Programs', label: 'Certification Programs'}
|
||
]
|
||
};
|
||
|
||
return {
|
||
options: options[filterType] || []
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Show filter modal with options
|
||
*/
|
||
function showFilterModal(data) {
|
||
const $modalTitle = $filterModal.find('.hvac-filter-modal-title');
|
||
const $modalOptions = $filterModal.find('.hvac-filter-options');
|
||
|
||
// Set title
|
||
let title = currentFilter.replace(/_/g, ' ');
|
||
title = title.charAt(0).toUpperCase() + title.slice(1);
|
||
$modalTitle.text(title);
|
||
|
||
// Build options HTML
|
||
let optionsHtml = '';
|
||
const currentValues = activeFilters[currentFilter] || [];
|
||
|
||
data.options.forEach(function(option) {
|
||
const checked = currentValues.includes(option.value) ? 'checked' : '';
|
||
optionsHtml += `
|
||
<div class="hvac-filter-option">
|
||
<input type="checkbox" id="filter_${option.value.replace(/\s+/g, '_')}" value="${option.value}" ${checked}>
|
||
<label for="filter_${option.value.replace(/\s+/g, '_')}">${option.label}</label>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
$modalOptions.html(optionsHtml);
|
||
// Show modal with proper CSS class and inline style overrides
|
||
$filterModal.addClass('modal-active');
|
||
|
||
// Force styles with higher specificity by setting them directly on the element
|
||
$filterModal[0].style.setProperty('display', 'flex', 'important');
|
||
$filterModal[0].style.setProperty('visibility', 'visible', 'important');
|
||
$filterModal[0].style.setProperty('opacity', '1', 'important');
|
||
}
|
||
|
||
/**
|
||
* Apply selected filters
|
||
*/
|
||
function applyFilters() {
|
||
const selectedValues = [];
|
||
|
||
$filterModal.find('.hvac-filter-option input:checked').each(function() {
|
||
selectedValues.push($(this).val());
|
||
});
|
||
|
||
if (selectedValues.length > 0) {
|
||
activeFilters[currentFilter] = selectedValues;
|
||
} else {
|
||
delete activeFilters[currentFilter];
|
||
}
|
||
|
||
updateActiveFiltersDisplay();
|
||
updateClearButtonVisibility();
|
||
currentPage = 1;
|
||
loadFilteredTrainers();
|
||
closeModals();
|
||
}
|
||
|
||
/**
|
||
* Update active filters display
|
||
*/
|
||
function updateActiveFiltersDisplay() {
|
||
const $container = $('.hvac-active-filters');
|
||
let html = '';
|
||
|
||
for (const [filter, values] of Object.entries(activeFilters)) {
|
||
values.forEach(function(value) {
|
||
html += `
|
||
<div class="hvac-active-filter" data-filter="${filter}" data-value="${value}">
|
||
${value}
|
||
<button type="button" aria-label="Remove filter">×</button>
|
||
</div>
|
||
`;
|
||
});
|
||
}
|
||
|
||
$container.html(html);
|
||
}
|
||
|
||
/**
|
||
* Remove active filter
|
||
*/
|
||
function removeActiveFilter(e) {
|
||
e.preventDefault();
|
||
const $filter = $(this).parent();
|
||
const filter = $filter.data('filter');
|
||
const value = $filter.data('value');
|
||
|
||
if (activeFilters[filter]) {
|
||
activeFilters[filter] = activeFilters[filter].filter(v => v !== value);
|
||
if (activeFilters[filter].length === 0) {
|
||
delete activeFilters[filter];
|
||
}
|
||
}
|
||
|
||
updateActiveFiltersDisplay();
|
||
updateClearButtonVisibility();
|
||
currentPage = 1;
|
||
loadFilteredTrainers();
|
||
}
|
||
|
||
/**
|
||
* Handle trainer profile click
|
||
*/
|
||
function handleProfileClick(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
|
||
const $card = $(this).closest('.hvac-trainer-card');
|
||
|
||
// Don't allow clicks on Champion cards
|
||
if ($card.hasClass('hvac-champion-card')) {
|
||
return false;
|
||
}
|
||
|
||
const profileId = $(this).data('profile-id');
|
||
|
||
// Get trainer data from the card
|
||
const trainerData = {
|
||
profile_id: profileId,
|
||
name: $card.find('.hvac-trainer-name a').text(),
|
||
city: $card.find('.hvac-trainer-location').text().split(',')[0],
|
||
state: $card.find('.hvac-trainer-location').text().split(',')[1]?.trim(),
|
||
certification_type: $card.find('.hvac-trainer-certification').text(), // Legacy field for compatibility
|
||
certifications: [], // Will be populated from card badges
|
||
profile_image: $card.find('.hvac-trainer-image img:not(.hvac-mq-badge)').attr('src') || '',
|
||
business_type: 'Independent Contractor', // Mock data
|
||
event_count: parseInt($card.data('event-count')) || 0, // Real event count from data attribute
|
||
training_formats: 'In-Person, Virtual',
|
||
training_locations: 'On-site, Remote',
|
||
upcoming_events: [] // Mock empty events
|
||
};
|
||
|
||
// Extract certifications from card badges
|
||
$card.find('.hvac-trainer-cert-badge').each(function() {
|
||
const certText = $(this).text().trim();
|
||
if (certText && certText !== 'HVAC Trainer') {
|
||
trainerData.certifications.push({
|
||
type: certText,
|
||
status: $(this).hasClass('hvac-cert-legacy') ? 'legacy' : 'active'
|
||
});
|
||
}
|
||
});
|
||
|
||
showTrainerModal(trainerData);
|
||
}
|
||
|
||
/**
|
||
* Show trainer profile modal
|
||
* Made global so MapGeo can access it
|
||
*/
|
||
function showTrainerModal(trainer) {
|
||
// Update modal title
|
||
$trainerModal.find('.hvac-modal-title').text(trainer.name);
|
||
|
||
// Update profile image
|
||
const $imgContainer = $trainerModal.find('.hvac-modal-image');
|
||
let imageHtml = '';
|
||
|
||
if (trainer.profile_image) {
|
||
imageHtml = `<img src="${trainer.profile_image}" alt="${trainer.name}">`;
|
||
} else {
|
||
imageHtml = '<div class="hvac-trainer-avatar"><span class="dashicons dashicons-businessperson"></span></div>';
|
||
}
|
||
|
||
// Add mQ badge overlay for certified trainers
|
||
let hasTrainerCert = false;
|
||
if (trainer.certifications && trainer.certifications.length > 0) {
|
||
// Check if any certification is a trainer certification
|
||
hasTrainerCert = trainer.certifications.some(cert =>
|
||
cert.type.toLowerCase().includes('trainer') ||
|
||
cert.type === 'measureQuick Certified Trainer'
|
||
);
|
||
} else if (trainer.certification_type === 'Certified measureQuick Trainer' ||
|
||
trainer.certification_type === 'measureQuick Certified Trainer') {
|
||
// Fallback for legacy single certification
|
||
hasTrainerCert = true;
|
||
}
|
||
|
||
if (hasTrainerCert) {
|
||
imageHtml += '<div class="hvac-mq-badge-overlay"><img src="/wp-content/uploads/2025/08/mQ-Certified-trainer.png" alt="measureQuick Certified Trainer" class="hvac-mq-badge"></div>';
|
||
}
|
||
|
||
$imgContainer.html(imageHtml);
|
||
|
||
// Update profile info
|
||
$trainerModal.find('.hvac-modal-location').text(`${trainer.city}, ${trainer.state}`);
|
||
|
||
// Update certifications section - handle both single and multiple certifications
|
||
const $certContainer = $trainerModal.find('.hvac-modal-certification-badges');
|
||
let certHtml = '';
|
||
|
||
if (trainer.certifications && trainer.certifications.length > 0) {
|
||
// Show multiple certifications as badges
|
||
trainer.certifications.forEach(function(cert) {
|
||
const badgeClass = cert.type.toLowerCase()
|
||
.replace('measurequick certified ', '')
|
||
.replace(/\s+/g, '-');
|
||
const legacyClass = cert.status === 'legacy' ? ' hvac-cert-legacy' : '';
|
||
|
||
certHtml += `<span class="hvac-trainer-cert-badge hvac-cert-${badgeClass}${legacyClass}">${cert.type}</span>`;
|
||
});
|
||
} else if (trainer.certification_type && trainer.certification_type !== 'HVAC Trainer') {
|
||
// Fallback to legacy single certification
|
||
const badgeClass = trainer.certification_type.toLowerCase()
|
||
.replace('measurequick certified ', '')
|
||
.replace(/\s+/g, '-');
|
||
certHtml = `<span class="hvac-trainer-cert-badge hvac-cert-${badgeClass}">${trainer.certification_type}</span>`;
|
||
} else {
|
||
// Default fallback
|
||
certHtml = '<span class="hvac-trainer-cert-badge hvac-cert-default">HVAC Trainer</span>';
|
||
}
|
||
|
||
$certContainer.html(certHtml);
|
||
|
||
$trainerModal.find('.hvac-modal-business').text(trainer.business_type || '');
|
||
$trainerModal.find('.hvac-modal-events span').text(trainer.event_count || 0);
|
||
|
||
// Update training details
|
||
$trainerModal.find('.hvac-training-formats').text(trainer.training_formats || 'Various');
|
||
$trainerModal.find('.hvac-training-locations').text(trainer.training_locations || 'On-site');
|
||
|
||
// Show loading state for events
|
||
$trainerModal.find('.hvac-events-list').html('<li>Loading upcoming events...</li>');
|
||
|
||
// Set hidden fields for contact form
|
||
$contactForm.find('input[name="trainer_id"]').val(trainer.user_id || '');
|
||
$contactForm.find('input[name="trainer_profile_id"]').val(trainer.profile_id);
|
||
|
||
// Reset contact form
|
||
$contactForm[0].reset();
|
||
$('.hvac-form-message').hide();
|
||
|
||
// Show modal
|
||
$trainerModal.fadeIn(300);
|
||
|
||
// Fetch upcoming events via AJAX
|
||
fetchUpcomingEvents(trainer.profile_id);
|
||
}
|
||
|
||
/**
|
||
* Fetch upcoming events for a trainer via AJAX
|
||
*/
|
||
function fetchUpcomingEvents(profileId) {
|
||
if (!profileId) {
|
||
$trainerModal.find('.hvac-events-list').html('<li>No upcoming events scheduled</li>');
|
||
return;
|
||
}
|
||
|
||
$.post(hvac_find_trainer.ajax_url, {
|
||
action: 'hvac_get_trainer_upcoming_events',
|
||
nonce: hvac_find_trainer.nonce,
|
||
profile_id: profileId
|
||
}, function(response) {
|
||
if (response.success && response.data.events) {
|
||
let eventsHtml = '';
|
||
if (response.data.events.length > 0) {
|
||
response.data.events.forEach(function(event) {
|
||
eventsHtml += `<li><a href="${event.url}" target="_blank">${event.title}</a> - ${event.date}</li>`;
|
||
});
|
||
} else {
|
||
eventsHtml = '<li>No upcoming events scheduled</li>';
|
||
}
|
||
$trainerModal.find('.hvac-events-list').html(eventsHtml);
|
||
} else {
|
||
$trainerModal.find('.hvac-events-list').html('<li>No upcoming events scheduled</li>');
|
||
}
|
||
}).fail(function() {
|
||
$trainerModal.find('.hvac-events-list').html('<li>Unable to load events</li>');
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Handle contact form submission
|
||
*/
|
||
function handleContactSubmit(e) {
|
||
e.preventDefault();
|
||
|
||
const $form = $(this);
|
||
const $submitBtn = $form.find('.hvac-form-submit');
|
||
const $successMsg = $form.find('.hvac-form-success');
|
||
const $errorMsg = $form.find('.hvac-form-error');
|
||
const originalText = $submitBtn.text();
|
||
|
||
$submitBtn.text('Sending...').prop('disabled', true);
|
||
|
||
// For now, just show success message
|
||
setTimeout(function() {
|
||
$successMsg.show();
|
||
$errorMsg.hide();
|
||
$form[0].reset();
|
||
$submitBtn.text(originalText).prop('disabled', false);
|
||
|
||
// Hide success message after 5 seconds
|
||
setTimeout(function() {
|
||
$successMsg.fadeOut();
|
||
}, 5000);
|
||
}, 1000);
|
||
}
|
||
|
||
/**
|
||
* Handle search input
|
||
*/
|
||
function handleSearch() {
|
||
const searchTerm = $('.hvac-search-input').val();
|
||
|
||
if (isLoading) return;
|
||
|
||
updateClearButtonVisibility();
|
||
currentPage = 1;
|
||
loadFilteredTrainers();
|
||
}
|
||
|
||
/**
|
||
* Handle pagination
|
||
*/
|
||
function handlePagination(e) {
|
||
e.preventDefault();
|
||
currentPage = $(this).data('page');
|
||
loadFilteredTrainers();
|
||
|
||
// Scroll to top of trainer grid
|
||
$('html, body').animate({
|
||
scrollTop: $('.hvac-trainer-directory-container').offset().top - 100
|
||
}, 500);
|
||
}
|
||
|
||
/**
|
||
* Load filtered trainers via AJAX
|
||
*/
|
||
function loadFilteredTrainers() {
|
||
if (isLoading) return;
|
||
|
||
isLoading = true;
|
||
const $container = $('.hvac-trainer-grid');
|
||
let $pagination = $('.hvac-pagination');
|
||
|
||
// Show loading state
|
||
$container.addClass('hvac-loading');
|
||
|
||
// Prepare data
|
||
const data = {
|
||
action: 'hvac_filter_trainers',
|
||
nonce: hvac_find_trainer.nonce,
|
||
page: currentPage,
|
||
search: $('.hvac-search-input').val(),
|
||
// Flatten the activeFilters for PHP processing
|
||
...activeFilters
|
||
};
|
||
|
||
// Make AJAX request
|
||
$.post(hvac_find_trainer.ajax_url, data, function(response) {
|
||
if (response.success) {
|
||
// Our PHP returns an array of trainer card HTML
|
||
if (response.data.trainers && response.data.trainers.length > 0) {
|
||
const trainersHtml = response.data.trainers.join('');
|
||
$container.html(trainersHtml);
|
||
} else {
|
||
$container.html('<div class="hvac-no-results"><p>No trainers found matching your criteria. Please try adjusting your filters.</p></div>');
|
||
}
|
||
|
||
// Update count display if exists
|
||
if (response.data.count !== undefined) {
|
||
$('.hvac-trainer-count').text(response.data.count + ' trainers found');
|
||
}
|
||
|
||
// Simple pagination logic - show/hide existing pagination based on results
|
||
if (response.data.count > 12) { // Assuming 12 per page
|
||
if ($pagination.length > 0) {
|
||
$pagination.show();
|
||
}
|
||
} else {
|
||
if ($pagination.length > 0) {
|
||
$pagination.hide();
|
||
}
|
||
}
|
||
} else {
|
||
console.error('Failed to load trainers:', response);
|
||
$container.html('<div class="hvac-no-results"><p>Error loading trainers. Please try again.</p></div>');
|
||
}
|
||
}).fail(function(xhr) {
|
||
console.error('AJAX error:', xhr);
|
||
}).always(function() {
|
||
isLoading = false;
|
||
$container.removeClass('hvac-loading');
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Close all modals
|
||
*/
|
||
function closeModals() {
|
||
// Remove the modal-active class and force hide styles
|
||
$filterModal.removeClass('modal-active');
|
||
|
||
// Force hide styles with !important
|
||
$filterModal[0].style.setProperty('display', 'none', 'important');
|
||
$filterModal[0].style.setProperty('visibility', 'hidden', 'important');
|
||
$filterModal[0].style.setProperty('opacity', '0', 'important');
|
||
|
||
$trainerModal.fadeOut(300);
|
||
}
|
||
|
||
/**
|
||
* Debounce helper function
|
||
*/
|
||
function debounce(func, wait) {
|
||
let timeout;
|
||
return function executedFunction(...args) {
|
||
const later = () => {
|
||
clearTimeout(timeout);
|
||
func(...args);
|
||
};
|
||
clearTimeout(timeout);
|
||
timeout = setTimeout(later, wait);
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Clear all filters
|
||
*/
|
||
function clearAllFilters() {
|
||
activeFilters = {};
|
||
$('.hvac-search-input').val('');
|
||
updateActiveFiltersDisplay();
|
||
updateClearButtonVisibility();
|
||
currentPage = 1;
|
||
loadFilteredTrainers();
|
||
}
|
||
|
||
/**
|
||
* Update clear button visibility
|
||
*/
|
||
function updateClearButtonVisibility() {
|
||
const hasFilters = Object.keys(activeFilters).length > 0;
|
||
const hasSearch = $('.hvac-search-input').val().trim() !== '';
|
||
|
||
if (hasFilters || hasSearch) {
|
||
$('.hvac-clear-filters').show();
|
||
} else {
|
||
$('.hvac-clear-filters').hide();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Handle direct profile URL access
|
||
* When someone accesses /find-a-trainer/profile/{id}, show the profile and handle interactions
|
||
*/
|
||
function handleDirectProfileAccess() {
|
||
// Check if we're showing a direct profile
|
||
if (hvac_find_trainer.show_direct_profile && hvac_find_trainer.direct_profile_id) {
|
||
console.log('Direct profile access detected for profile ID:', hvac_find_trainer.direct_profile_id);
|
||
|
||
// Update page title in browser
|
||
if (document.title.includes('Find a Trainer')) {
|
||
document.title = document.title.replace('Find a Trainer', 'Trainer Profile');
|
||
}
|
||
|
||
// Bind contact trainer button
|
||
$(document).on('click', '.hvac-contact-trainer-btn', function(e) {
|
||
e.preventDefault();
|
||
const profileId = $(this).data('profile-id');
|
||
showTrainerModal(profileId);
|
||
});
|
||
|
||
// Update URL without page reload for clean sharing
|
||
const currentUrl = window.location.href;
|
||
if (currentUrl.includes('/profile/') && window.history && window.history.replaceState) {
|
||
const cleanUrl = currentUrl.split('?')[0]; // Remove any query parameters
|
||
window.history.replaceState({}, document.title, cleanUrl);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Expose showTrainerModal globally for MapGeo integration
|
||
window.showTrainerModal = showTrainerModal;
|
||
|
||
})(jQuery); |