diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 8e31fa70..6f967924 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -72,7 +72,9 @@
"Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp post create --post_type=page --post_title=''Native Event Test'' --post_name=''native-event-test'' --post_status=publish --meta_input=''{\"\"_wp_page_template\"\":\"\"page-native-event-test.php\"\"}'' --format=ids\")",
"Bash(SSHPASS=\"uSCO6f1y\" sshpass -e scp -o StrictHostKeyChecking=no /home/ben/dev/upskill-event-manager/templates/page-native-event-test.php roodev@146.190.76.204:/home/974670.cloudwaysapps.com/uberrxmprk/public_html/wp-content/plugins/hvac-community-events/templates/)",
"Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp post get 6394 --format=table\")",
- "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp post meta list 6394 --format=table\")"
+ "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp post meta list 6394 --format=table\")",
+ "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp post get 6395 --format=table\")",
+ "Bash(SSHPASS=\"uSCO6f1y\" sshpass -e ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp post meta list 6395 --format=table\")"
],
"deny": [],
"ask": [],
diff --git a/assets/js/hvac-ajax-optimizer.js b/assets/js/hvac-ajax-optimizer.js
new file mode 100644
index 00000000..27ab8129
--- /dev/null
+++ b/assets/js/hvac-ajax-optimizer.js
@@ -0,0 +1,678 @@
+/**
+ * HVAC AJAX Optimizer
+ *
+ * Provides optimized AJAX handling with debouncing, caching, and rate limiting
+ * for the native WordPress event management system.
+ *
+ * @package HVAC_Community_Events
+ * @since 3.0.0
+ */
+
+(function($) {
+ 'use strict';
+
+ // Configuration from localized script
+ const config = window.hvacAjax || {};
+ const ajaxUrl = config.ajaxurl;
+ const nonce = config.nonce;
+ const debounceDelay = config.debounceDelay || 300;
+ const cacheEnabled = config.cacheEnabled !== false;
+ const rateLimits = config.rateLimits || {};
+
+ // Local cache for AJAX responses
+ const cache = new Map();
+
+ // Rate limiting tracking
+ const rateLimitTracking = new Map();
+
+ /**
+ * HVAC AJAX Optimizer Class
+ */
+ class HVACAjaxOptimizer {
+ constructor() {
+ this.debounceTimers = new Map();
+ this.pendingRequests = new Map();
+ this.init();
+ }
+
+ /**
+ * Initialize the optimizer
+ */
+ init() {
+ this.bindEvents();
+ this.warmCache();
+ }
+
+ /**
+ * Bind event handlers
+ */
+ bindEvents() {
+ // Venue search with debouncing
+ $(document).on('input', '.hvac-venue-search', (e) => {
+ this.debouncedVenueSearch($(e.target));
+ });
+
+ // Organizer search with debouncing
+ $(document).on('input', '.hvac-organizer-search', (e) => {
+ this.debouncedOrganizerSearch($(e.target));
+ });
+
+ // Form validation on change
+ $(document).on('change input', '.hvac-event-form input, .hvac-event-form select, .hvac-event-form textarea', (e) => {
+ this.debouncedFormValidation($(e.target).closest('form'));
+ });
+
+ // Auto-save draft
+ $(document).on('change', '.hvac-event-form input, .hvac-event-form select, .hvac-event-form textarea', (e) => {
+ this.debouncedSaveDraft($(e.target).closest('form'));
+ });
+
+ // Image upload with progress
+ $(document).on('change', '.hvac-featured-image', (e) => {
+ this.handleImageUpload($(e.target));
+ });
+
+ // Cache management (admin only)
+ $(document).on('click', '.hvac-clear-cache', () => {
+ this.clearCache();
+ });
+
+ $(document).on('click', '.hvac-warm-cache', () => {
+ this.warmCache();
+ });
+ }
+
+ /**
+ * Debounced venue search
+ */
+ debouncedVenueSearch($input) {
+ this.debounce('venue-search', () => {
+ this.searchVenues($input);
+ }, debounceDelay);
+ }
+
+ /**
+ * Debounced organizer search
+ */
+ debouncedOrganizerSearch($input) {
+ this.debounce('organizer-search', () => {
+ this.searchOrganizers($input);
+ }, debounceDelay);
+ }
+
+ /**
+ * Debounced form validation
+ */
+ debouncedFormValidation($form) {
+ this.debounce('form-validation', () => {
+ this.validateForm($form);
+ }, debounceDelay);
+ }
+
+ /**
+ * Debounced draft saving
+ */
+ debouncedSaveDraft($form) {
+ this.debounce('save-draft', () => {
+ this.saveDraft($form);
+ }, 2000); // 2 second delay for auto-save
+ }
+
+ /**
+ * Generic debounce function
+ */
+ debounce(key, func, delay) {
+ if (this.debounceTimers.has(key)) {
+ clearTimeout(this.debounceTimers.get(key));
+ }
+
+ const timer = setTimeout(() => {
+ func();
+ this.debounceTimers.delete(key);
+ }, delay);
+
+ this.debounceTimers.set(key, timer);
+ }
+
+ /**
+ * Check rate limits before making requests
+ */
+ checkRateLimit(action) {
+ if (!rateLimits[action]) {
+ return true;
+ }
+
+ const now = Date.now();
+ const windowStart = now - 60000; // 1 minute window
+ const limit = rateLimits[action];
+
+ if (!rateLimitTracking.has(action)) {
+ rateLimitTracking.set(action, []);
+ }
+
+ const requests = rateLimitTracking.get(action);
+
+ // Remove old requests outside the window
+ const recentRequests = requests.filter(timestamp => timestamp > windowStart);
+
+ if (recentRequests.length >= limit) {
+ this.showError('Rate limit exceeded. Please slow down your requests.');
+ return false;
+ }
+
+ // Add current request
+ recentRequests.push(now);
+ rateLimitTracking.set(action, recentRequests);
+
+ return true;
+ }
+
+ /**
+ * Make optimized AJAX request
+ */
+ async makeRequest(action, data, options = {}) {
+ const {
+ method = 'GET',
+ useCache = cacheEnabled,
+ showLoader = true,
+ timeout = 10000
+ } = options;
+
+ // Check rate limits
+ if (!this.checkRateLimit(action)) {
+ return Promise.reject(new Error('Rate limit exceeded'));
+ }
+
+ // Generate cache key
+ const cacheKey = this.generateCacheKey(action, data);
+
+ // Check cache first for GET requests
+ if (method === 'GET' && useCache && cache.has(cacheKey)) {
+ const cachedData = cache.get(cacheKey);
+ if (Date.now() - cachedData.timestamp < 300000) { // 5 minute cache
+ return Promise.resolve(cachedData.data);
+ } else {
+ cache.delete(cacheKey);
+ }
+ }
+
+ // Cancel any pending identical request
+ if (this.pendingRequests.has(cacheKey)) {
+ this.pendingRequests.get(cacheKey).abort();
+ }
+
+ // Show loader
+ if (showLoader) {
+ this.showLoader(action);
+ }
+
+ // Prepare request data
+ const requestData = {
+ action: action,
+ nonce: nonce,
+ ...data
+ };
+
+ // Create AJAX request
+ const xhr = $.ajax({
+ url: ajaxUrl,
+ type: method,
+ data: requestData,
+ timeout: timeout,
+ dataType: 'json'
+ });
+
+ // Track pending request
+ this.pendingRequests.set(cacheKey, xhr);
+
+ try {
+ const response = await xhr;
+
+ // Remove from pending requests
+ this.pendingRequests.delete(cacheKey);
+
+ // Hide loader
+ if (showLoader) {
+ this.hideLoader(action);
+ }
+
+ if (response.success) {
+ // Cache successful GET responses
+ if (method === 'GET' && useCache) {
+ cache.set(cacheKey, {
+ data: response,
+ timestamp: Date.now()
+ });
+ }
+
+ return response.data;
+ } else {
+ throw new Error(response.data?.message || 'Request failed');
+ }
+ } catch (error) {
+ // Remove from pending requests
+ this.pendingRequests.delete(cacheKey);
+
+ // Hide loader
+ if (showLoader) {
+ this.hideLoader(action);
+ }
+
+ if (error.statusText !== 'abort') {
+ this.showError(error.message || 'Request failed');
+ }
+
+ throw error;
+ }
+ }
+
+ /**
+ * Generate cache key from action and data
+ */
+ generateCacheKey(action, data) {
+ return `${action}_${JSON.stringify(data)}`;
+ }
+
+ /**
+ * Search venues with AJAX
+ */
+ async searchVenues($input) {
+ const query = $input.val().trim();
+
+ if (query.length < 2) {
+ this.hideVenueResults();
+ return;
+ }
+
+ try {
+ const data = await this.makeRequest('hvac_search_venues', { q: query });
+ this.displayVenueResults(data.venues, $input);
+ } catch (error) {
+ console.error('Venue search failed:', error);
+ }
+ }
+
+ /**
+ * Search organizers with AJAX
+ */
+ async searchOrganizers($input) {
+ const query = $input.val().trim();
+
+ if (query.length < 2) {
+ this.hideOrganizerResults();
+ return;
+ }
+
+ try {
+ const data = await this.makeRequest('hvac_search_organizers', { q: query });
+ this.displayOrganizerResults(data.organizers, $input);
+ } catch (error) {
+ console.error('Organizer search failed:', error);
+ }
+ }
+
+ /**
+ * Validate form with AJAX
+ */
+ async validateForm($form) {
+ const formData = this.serializeForm($form);
+
+ try {
+ const data = await this.makeRequest('hvac_validate_form',
+ { form_data: formData },
+ { method: 'POST', showLoader: false }
+ );
+
+ this.displayValidationResults(data, $form);
+ } catch (error) {
+ console.error('Form validation failed:', error);
+ }
+ }
+
+ /**
+ * Save draft with AJAX
+ */
+ async saveDraft($form) {
+ const formData = this.serializeForm($form);
+
+ try {
+ const data = await this.makeRequest('hvac_save_draft',
+ { form_data: formData },
+ { method: 'POST', showLoader: false, useCache: false }
+ );
+
+ this.showSuccess('Draft saved automatically');
+
+ // Store draft ID for later use
+ $form.data('draft-id', data.draft_id);
+ } catch (error) {
+ console.error('Draft save failed:', error);
+ }
+ }
+
+ /**
+ * Handle image upload
+ */
+ async handleImageUpload($input) {
+ const file = $input[0].files[0];
+
+ if (!file) {
+ return;
+ }
+
+ // Validate file type
+ const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
+ if (!allowedTypes.includes(file.type)) {
+ this.showError('Please select a valid image file (JPG, PNG, or WebP)');
+ $input.val('');
+ return;
+ }
+
+ // Validate file size (5MB limit)
+ if (file.size > 5 * 1024 * 1024) {
+ this.showError('Image file must be smaller than 5MB');
+ $input.val('');
+ return;
+ }
+
+ const formData = new FormData();
+ formData.append('action', 'hvac_upload_image');
+ formData.append('nonce', nonce);
+ formData.append('image', file);
+
+ // Show upload progress
+ const $progress = this.showUploadProgress($input);
+
+ try {
+ const response = await $.ajax({
+ url: ajaxUrl,
+ type: 'POST',
+ data: formData,
+ processData: false,
+ contentType: false,
+ xhr: () => {
+ const xhr = new window.XMLHttpRequest();
+ xhr.upload.addEventListener('progress', (e) => {
+ if (e.lengthComputable) {
+ const percentComplete = (e.loaded / e.total) * 100;
+ $progress.find('.progress-bar').css('width', percentComplete + '%');
+ }
+ });
+ return xhr;
+ }
+ });
+
+ this.hideUploadProgress($progress);
+
+ if (response.success) {
+ this.displayUploadedImage(response.data, $input);
+ this.showSuccess('Image uploaded successfully');
+ } else {
+ throw new Error(response.data?.message || 'Upload failed');
+ }
+ } catch (error) {
+ this.hideUploadProgress($progress);
+ this.showError(error.message || 'Image upload failed');
+ $input.val('');
+ }
+ }
+
+ /**
+ * Warm cache with frequently used data
+ */
+ async warmCache() {
+ try {
+ // Load timezone list
+ await this.makeRequest('hvac_get_timezone_list', {});
+
+ console.log('Cache warmed successfully');
+ } catch (error) {
+ console.error('Cache warming failed:', error);
+ }
+ }
+
+ /**
+ * Clear all caches
+ */
+ async clearCache() {
+ cache.clear();
+ rateLimitTracking.clear();
+
+ try {
+ await this.makeRequest('hvac_clear_cache', { cache_type: 'all' }, { method: 'POST' });
+ this.showSuccess('Cache cleared successfully');
+ } catch (error) {
+ console.error('Cache clear failed:', error);
+ }
+ }
+
+ /**
+ * Serialize form data to object
+ */
+ serializeForm($form) {
+ const formData = {};
+ $form.find('input, select, textarea').each(function() {
+ const $field = $(this);
+ const name = $field.attr('name');
+
+ if (name && !$field.is(':disabled')) {
+ if ($field.is(':checkbox')) {
+ if ($field.is(':checked')) {
+ formData[name] = $field.val();
+ }
+ } else if ($field.is(':radio')) {
+ if ($field.is(':checked')) {
+ formData[name] = $field.val();
+ }
+ } else {
+ formData[name] = $field.val();
+ }
+ }
+ });
+
+ return formData;
+ }
+
+ /**
+ * Display venue search results
+ */
+ displayVenueResults(venues, $input) {
+ let $results = $input.siblings('.venue-results');
+
+ if ($results.length === 0) {
+ $results = $('
');
+ $input.after($results);
+ }
+
+ if (venues.length === 0) {
+ $results.html('No venues found
');
+ return;
+ }
+
+ const html = venues.map(venue => `
+
+ ${venue.title}
+ ${venue.address}, ${venue.city}, ${venue.state}
+
+ `).join('');
+
+ $results.html(html);
+
+ // Handle venue selection
+ $results.find('.venue-item').on('click', function() {
+ const venueId = $(this).data('venue-id');
+ const venueName = $(this).find('strong').text();
+ $input.val(venueName);
+ $input.data('venue-id', venueId);
+ $results.hide();
+ });
+ }
+
+ /**
+ * Hide venue results
+ */
+ hideVenueResults() {
+ $('.venue-results').hide();
+ }
+
+ /**
+ * Display organizer search results
+ */
+ displayOrganizerResults(organizers, $input) {
+ let $results = $input.siblings('.organizer-results');
+
+ if ($results.length === 0) {
+ $results = $('');
+ $input.after($results);
+ }
+
+ if (organizers.length === 0) {
+ $results.html('No organizers found
');
+ return;
+ }
+
+ const html = organizers.map(organizer => `
+
+ ${organizer.title}
+ ${organizer.email}
+
+ `).join('');
+
+ $results.html(html);
+
+ // Handle organizer selection
+ $results.find('.organizer-item').on('click', function() {
+ const organizerId = $(this).data('organizer-id');
+ const organizerName = $(this).find('strong').text();
+ $input.val(organizerName);
+ $input.data('organizer-id', organizerId);
+ $results.hide();
+ });
+ }
+
+ /**
+ * Hide organizer results
+ */
+ hideOrganizerResults() {
+ $('.organizer-results').hide();
+ }
+
+ /**
+ * Display validation results
+ */
+ displayValidationResults(data, $form) {
+ // Clear existing errors
+ $form.find('.field-error').remove();
+ $form.find('.error').removeClass('error');
+
+ if (!data.valid && data.errors) {
+ Object.keys(data.errors).forEach(fieldName => {
+ const $field = $form.find(`[name="${fieldName}"]`);
+ if ($field.length) {
+ $field.addClass('error');
+ $field.after(`${data.errors[fieldName]}`);
+ }
+ });
+ }
+ }
+
+ /**
+ * Show upload progress
+ */
+ showUploadProgress($input) {
+ const $progress = $('');
+ $input.after($progress);
+ return $progress;
+ }
+
+ /**
+ * Hide upload progress
+ */
+ hideUploadProgress($progress) {
+ $progress.remove();
+ }
+
+ /**
+ * Display uploaded image
+ */
+ displayUploadedImage(imageData, $input) {
+ let $preview = $input.siblings('.image-preview');
+
+ if ($preview.length === 0) {
+ $preview = $('');
+ $input.after($preview);
+ }
+
+ $preview.html(`
+
+
+ `);
+ }
+
+ /**
+ * Show loader
+ */
+ showLoader(action) {
+ const $loader = $(`Loading...
`);
+ $('body').append($loader);
+ }
+
+ /**
+ * Hide loader
+ */
+ hideLoader(action) {
+ $(`.hvac-loader[data-action="${action}"]`).remove();
+ }
+
+ /**
+ * Show success message
+ */
+ showSuccess(message) {
+ this.showNotification(message, 'success');
+ }
+
+ /**
+ * Show error message
+ */
+ showError(message) {
+ this.showNotification(message, 'error');
+ }
+
+ /**
+ * Show notification
+ */
+ showNotification(message, type) {
+ const $notification = $(`
+
+ ${message}
+
+
+ `);
+
+ $('body').append($notification);
+
+ // Auto-hide after 5 seconds
+ setTimeout(() => {
+ $notification.fadeOut(() => $notification.remove());
+ }, 5000);
+
+ // Manual close
+ $notification.find('.close').on('click', () => {
+ $notification.fadeOut(() => $notification.remove());
+ });
+ }
+ }
+
+ // Initialize when DOM is ready
+ $(document).ready(() => {
+ window.hvacAjaxOptimizer = new HVACAjaxOptimizer();
+ });
+
+ // Hide results when clicking outside
+ $(document).on('click', (e) => {
+ if (!$(e.target).closest('.ajax-results').length && !$(e.target).hasClass('hvac-venue-search') && !$(e.target).hasClass('hvac-organizer-search')) {
+ $('.ajax-results').hide();
+ }
+ });
+
+})(jQuery);
\ No newline at end of file
diff --git a/includes/class-hvac-ajax-optimizer.php b/includes/class-hvac-ajax-optimizer.php
new file mode 100644
index 00000000..cb744aaf
--- /dev/null
+++ b/includes/class-hvac-ajax-optimizer.php
@@ -0,0 +1,524 @@
+ 30,
+ 'hvac_search_organizers' => 30,
+ 'hvac_validate_form' => 60,
+ 'hvac_save_draft' => 20,
+ 'hvac_upload_image' => 10,
+ ];
+
+ /**
+ * Constructor
+ */
+ private function __construct() {
+ $this->cache = HVAC_Event_Cache::instance();
+ $this->init_hooks();
+ }
+
+ /**
+ * Initialize WordPress hooks
+ */
+ private function init_hooks(): void {
+ // Optimized AJAX endpoints
+ add_action('wp_ajax_hvac_search_venues', [$this, 'ajax_search_venues']);
+ add_action('wp_ajax_hvac_search_organizers', [$this, 'ajax_search_organizers']);
+ add_action('wp_ajax_hvac_validate_form', [$this, 'ajax_validate_form']);
+ add_action('wp_ajax_hvac_save_draft', [$this, 'ajax_save_draft']);
+ add_action('wp_ajax_hvac_upload_image', [$this, 'ajax_upload_image']);
+ add_action('wp_ajax_hvac_get_timezone_list', [$this, 'ajax_get_timezone_list']);
+
+ // Admin-only endpoints
+ add_action('wp_ajax_hvac_cache_stats', [$this, 'ajax_cache_stats']);
+
+ // Enqueue optimized AJAX scripts
+ add_action('wp_enqueue_scripts', [$this, 'enqueue_ajax_scripts']);
+ }
+
+ /**
+ * Enqueue optimized AJAX scripts
+ */
+ public function enqueue_ajax_scripts(): void {
+ // Only load on event-related pages
+ if (!$this->should_load_ajax_scripts()) {
+ return;
+ }
+
+ wp_enqueue_script(
+ 'hvac-ajax-optimizer',
+ HVAC_PLUGIN_URL . 'assets/js/hvac-ajax-optimizer.js',
+ ['jquery'],
+ HVAC_VERSION,
+ true
+ );
+
+ // Localize script with AJAX configuration
+ wp_localize_script('hvac-ajax-optimizer', 'hvacAjax', [
+ 'ajaxurl' => admin_url('admin-ajax.php'),
+ 'nonce' => wp_create_nonce('hvac_ajax_nonce'),
+ 'debounceDelay' => 300,
+ 'cacheEnabled' => true,
+ 'rateLimits' => $this->ajax_rate_limits,
+ ]);
+ }
+
+ /**
+ * Check if AJAX scripts should be loaded
+ *
+ * @return bool
+ */
+ private function should_load_ajax_scripts(): bool {
+ global $post;
+
+ // Load on event form pages
+ if (is_page() && $post) {
+ $template = get_page_template_slug($post->ID);
+ if (strpos($template, 'event') !== false) {
+ return true;
+ }
+ }
+
+ // Load on trainer/master trainer dashboards
+ if (is_user_logged_in()) {
+ $user = wp_get_current_user();
+ if (in_array('hvac_trainer', $user->roles) || in_array('hvac_master_trainer', $user->roles)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Apply rate limiting to AJAX requests
+ *
+ * @param string $action AJAX action name
+ * @return bool True if request is allowed, false if rate limited
+ */
+ private function apply_rate_limiting(string $action): bool {
+ if (!isset($this->ajax_rate_limits[$action])) {
+ return true;
+ }
+
+ $limit = $this->ajax_rate_limits[$action];
+ $user_id = get_current_user_id();
+ $client_ip = $this->get_client_ip();
+
+ // Create rate limit key based on user ID or IP
+ $rate_key = $user_id > 0 ? "user_{$user_id}_{$action}" : "ip_{$client_ip}_{$action}";
+
+ $current_time = time();
+ $window_start = $current_time - 60; // 1 minute window
+
+ // Get existing rate limit data
+ $rate_data = get_transient("hvac_rate_limit_{$rate_key}") ?: [];
+
+ // Remove old entries outside the time window
+ $rate_data = array_filter($rate_data, fn($timestamp) => $timestamp > $window_start);
+
+ // Check if limit exceeded
+ if (count($rate_data) >= $limit) {
+ return false;
+ }
+
+ // Add current request timestamp
+ $rate_data[] = $current_time;
+
+ // Store updated rate limit data
+ set_transient("hvac_rate_limit_{$rate_key}", $rate_data, 120);
+
+ return true;
+ }
+
+ /**
+ * Get client IP address
+ *
+ * @return string
+ */
+ private function get_client_ip(): string {
+ $ip_keys = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP', 'REMOTE_ADDR'];
+
+ foreach ($ip_keys as $key) {
+ if (!empty($_SERVER[$key])) {
+ $ip = sanitize_text_field($_SERVER[$key]);
+ // Handle comma-separated IPs (from proxies)
+ if (strpos($ip, ',') !== false) {
+ $ip = trim(explode(',', $ip)[0]);
+ }
+ if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
+ return $ip;
+ }
+ }
+ }
+
+ return sanitize_text_field($_SERVER['REMOTE_ADDR'] ?? '');
+ }
+
+ /**
+ * AJAX handler for venue search with caching
+ */
+ public function ajax_search_venues(): void {
+ // Security and rate limiting
+ if (!wp_verify_nonce($_GET['nonce'] ?? '', 'hvac_ajax_nonce')) {
+ wp_send_json_error(['message' => 'Security check failed']);
+ return;
+ }
+
+ if (!$this->apply_rate_limiting('hvac_search_venues')) {
+ wp_send_json_error(['message' => 'Rate limit exceeded']);
+ return;
+ }
+
+ $search_query = sanitize_text_field($_GET['q'] ?? '');
+ if (empty($search_query) || strlen($search_query) < 2) {
+ wp_send_json_error(['message' => 'Search query too short']);
+ return;
+ }
+
+ // Check cache first
+ $cache_key = md5($search_query . '_' . get_current_user_id());
+ $cached_results = $this->cache->get_venue_search($cache_key);
+
+ if ($cached_results !== false) {
+ wp_send_json_success([
+ 'venues' => $cached_results,
+ 'cached' => true
+ ]);
+ return;
+ }
+
+ // Search venues
+ $venues = $this->search_venues($search_query);
+
+ // Cache results
+ $this->cache->cache_venue_search($cache_key, $venues);
+
+ wp_send_json_success([
+ 'venues' => $venues,
+ 'cached' => false
+ ]);
+ }
+
+ /**
+ * AJAX handler for organizer search with caching
+ */
+ public function ajax_search_organizers(): void {
+ // Security and rate limiting
+ if (!wp_verify_nonce($_GET['nonce'] ?? '', 'hvac_ajax_nonce')) {
+ wp_send_json_error(['message' => 'Security check failed']);
+ return;
+ }
+
+ if (!$this->apply_rate_limiting('hvac_search_organizers')) {
+ wp_send_json_error(['message' => 'Rate limit exceeded']);
+ return;
+ }
+
+ $search_query = sanitize_text_field($_GET['q'] ?? '');
+ if (empty($search_query) || strlen($search_query) < 2) {
+ wp_send_json_error(['message' => 'Search query too short']);
+ return;
+ }
+
+ // Search organizers
+ $organizers = $this->search_organizers($search_query);
+
+ wp_send_json_success(['organizers' => $organizers]);
+ }
+
+ /**
+ * AJAX handler for form validation
+ */
+ public function ajax_validate_form(): void {
+ // Security and rate limiting
+ if (!wp_verify_nonce($_POST['nonce'] ?? '', 'hvac_ajax_nonce')) {
+ wp_send_json_error(['message' => 'Security check failed']);
+ return;
+ }
+
+ if (!$this->apply_rate_limiting('hvac_validate_form')) {
+ wp_send_json_error(['message' => 'Rate limit exceeded']);
+ return;
+ }
+
+ $form_data = $_POST['form_data'] ?? [];
+
+ // Create form builder for validation
+ $form_builder = new HVAC_Event_Form_Builder('hvac_ajax_validation');
+ $form_builder->create_event_form();
+
+ // Validate the form data
+ $errors = $form_builder->validate($form_data);
+
+ wp_send_json_success([
+ 'valid' => empty($errors),
+ 'errors' => $errors
+ ]);
+ }
+
+ /**
+ * AJAX handler for saving draft events
+ */
+ public function ajax_save_draft(): void {
+ // Security and rate limiting
+ if (!wp_verify_nonce($_POST['nonce'] ?? '', 'hvac_ajax_nonce')) {
+ wp_send_json_error(['message' => 'Security check failed']);
+ return;
+ }
+
+ if (!$this->apply_rate_limiting('hvac_save_draft')) {
+ wp_send_json_error(['message' => 'Rate limit exceeded']);
+ return;
+ }
+
+ // Check user permissions
+ if (!is_user_logged_in()) {
+ wp_send_json_error(['message' => 'Authentication required']);
+ return;
+ }
+
+ $form_data = $_POST['form_data'] ?? [];
+
+ // Generate unique form ID for this user session
+ $form_id = 'draft_' . get_current_user_id() . '_' . time();
+
+ // Cache the form data
+ $cached = $this->cache->cache_form_data($form_id, $form_data);
+
+ if ($cached) {
+ wp_send_json_success([
+ 'message' => 'Draft saved successfully',
+ 'draft_id' => $form_id
+ ]);
+ } else {
+ wp_send_json_error(['message' => 'Failed to save draft']);
+ }
+ }
+
+ /**
+ * AJAX handler for image upload
+ */
+ public function ajax_upload_image(): void {
+ // Security and rate limiting
+ if (!wp_verify_nonce($_POST['nonce'] ?? '', 'hvac_ajax_nonce')) {
+ wp_send_json_error(['message' => 'Security check failed']);
+ return;
+ }
+
+ if (!$this->apply_rate_limiting('hvac_upload_image')) {
+ wp_send_json_error(['message' => 'Rate limit exceeded']);
+ return;
+ }
+
+ // Check user permissions
+ if (!current_user_can('upload_files')) {
+ wp_send_json_error(['message' => 'Insufficient permissions']);
+ return;
+ }
+
+ if (!isset($_FILES['image'])) {
+ wp_send_json_error(['message' => 'No image uploaded']);
+ return;
+ }
+
+ // Handle file upload
+ require_once ABSPATH . 'wp-admin/includes/image.php';
+ require_once ABSPATH . 'wp-admin/includes/file.php';
+ require_once ABSPATH . 'wp-admin/includes/media.php';
+
+ $attachment_id = media_handle_upload('image', 0);
+
+ if (is_wp_error($attachment_id)) {
+ wp_send_json_error(['message' => $attachment_id->get_error_message()]);
+ return;
+ }
+
+ $attachment_url = wp_get_attachment_url($attachment_id);
+ $attachment_meta = wp_get_attachment_metadata($attachment_id);
+
+ wp_send_json_success([
+ 'attachment_id' => $attachment_id,
+ 'url' => $attachment_url,
+ 'meta' => $attachment_meta
+ ]);
+ }
+
+ /**
+ * AJAX handler for timezone list (cached)
+ */
+ public function ajax_get_timezone_list(): void {
+ // Security check
+ if (!wp_verify_nonce($_GET['nonce'] ?? '', 'hvac_ajax_nonce')) {
+ wp_send_json_error(['message' => 'Security check failed']);
+ return;
+ }
+
+ // Try cache first
+ $timezone_list = $this->cache->get_timezone_list();
+
+ if ($timezone_list === false) {
+ // Generate timezone list
+ $zones = wp_timezone_choice('UTC');
+ $timezone_list = [];
+
+ if (preg_match_all('/