This commit introduces a complete announcement management system for HVAC trainers with enterprise-grade security, performance optimization, and email notifications. ## Core Features - Custom post type for trainer announcements with categories and tags - Role-based permissions (master trainers can create/edit, all trainers can read) - AJAX-powered admin interface with real-time updates - Modal popup viewing for announcements on frontend - Automated email notifications when announcements are published - Google Drive integration for training resources ## Security Enhancements - Fixed critical capability mapping bug preventing proper permission checks - Added content disclosure protection for draft/private announcements - Fixed XSS vulnerabilities with proper output escaping and sanitization - Implemented permission checks on all AJAX endpoints - Added rate limiting to prevent abuse (30 requests/minute) - Email validation before sending notifications ## Performance Optimizations - Implemented intelligent caching for user queries (5-minute TTL) - Added cache versioning for announcement lists (2-minute TTL) - Automatic cache invalidation on content changes - Batch email processing to prevent timeouts (50 emails per batch) - Retry mechanism for failed email sends (max 3 attempts) ## Technical Implementation - Singleton pattern for all manager classes - WordPress coding standards compliance - Proper nonce verification on all AJAX requests - Comprehensive error handling and logging - Mobile-responsive UI with smooth animations - WCAG accessibility compliance ## Components Added - 6 PHP classes for modular architecture - 2 page templates (master announcements, trainer resources) - Admin and frontend JavaScript with jQuery integration - Comprehensive CSS for both admin and frontend - Email notification system with HTML templates - Complete documentation and implementation plans This system provides a secure, scalable foundation for trainer communications while following WordPress best practices and maintaining high code quality. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
231 lines
No EOL
7.5 KiB
JavaScript
231 lines
No EOL
7.5 KiB
JavaScript
/**
|
|
* HVAC Announcements View Handler
|
|
* Handles modal popup for viewing announcements
|
|
*
|
|
* @package HVAC_Community_Events
|
|
*/
|
|
|
|
jQuery(document).ready(function($) {
|
|
'use strict';
|
|
|
|
// Cache DOM elements
|
|
var $modal = $('#announcement-modal');
|
|
var $modalContent = $modal.find('.modal-body');
|
|
var $modalClose = $modal.find('.modal-close');
|
|
var isLoading = false;
|
|
|
|
// Handle announcement link clicks
|
|
$(document).on('click', '.announcement-link', function(e) {
|
|
e.preventDefault();
|
|
|
|
if (isLoading) {
|
|
return;
|
|
}
|
|
|
|
var announcementId = $(this).data('id');
|
|
|
|
if (!announcementId) {
|
|
console.error('No announcement ID found');
|
|
return;
|
|
}
|
|
|
|
openAnnouncementModal(announcementId);
|
|
});
|
|
|
|
// Handle modal close button
|
|
$modalClose.on('click', function() {
|
|
closeModal();
|
|
});
|
|
|
|
// Close modal when clicking outside
|
|
$modal.on('click', function(e) {
|
|
if (e.target === this) {
|
|
closeModal();
|
|
}
|
|
});
|
|
|
|
// Close modal with ESC key
|
|
$(document).on('keydown', function(e) {
|
|
if (e.key === 'Escape' && $modal.is(':visible')) {
|
|
closeModal();
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Open announcement in modal
|
|
*/
|
|
function openAnnouncementModal(announcementId) {
|
|
isLoading = true;
|
|
|
|
// Show modal with loading state
|
|
$modalContent.html('<div class="modal-loading"><span class="spinner is-active"></span><p>Loading announcement...</p></div>');
|
|
$modal.fadeIn(300);
|
|
|
|
// Prevent body scroll
|
|
$('body').addClass('modal-open');
|
|
|
|
// Make AJAX request to get announcement content
|
|
$.ajax({
|
|
url: hvac_ajax.ajax_url,
|
|
type: 'POST',
|
|
data: {
|
|
action: 'hvac_view_announcement',
|
|
id: announcementId,
|
|
nonce: hvac_ajax.nonce
|
|
},
|
|
success: function(response) {
|
|
if (response.success && response.data.content) {
|
|
$modalContent.html(response.data.content);
|
|
|
|
// Focus on modal for accessibility
|
|
$modal.attr('aria-hidden', 'false');
|
|
$modalContent.focus();
|
|
} else {
|
|
var errorMsg = response.data || 'Failed to load announcement';
|
|
$modalContent.html('<div class="modal-error"><p>' + errorMsg + '</p></div>');
|
|
}
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error('AJAX error:', error);
|
|
$modalContent.html('<div class="modal-error"><p>Error loading announcement. Please try again.</p></div>');
|
|
},
|
|
complete: function() {
|
|
isLoading = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Close modal
|
|
*/
|
|
function closeModal() {
|
|
$modal.fadeOut(300, function() {
|
|
$modalContent.empty();
|
|
$('body').removeClass('modal-open');
|
|
$modal.attr('aria-hidden', 'true');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle Load More button for timeline
|
|
*/
|
|
$(document).on('click', '.load-more-announcements', function(e) {
|
|
e.preventDefault();
|
|
|
|
var $button = $(this);
|
|
var currentPage = parseInt($button.data('page'));
|
|
var maxPages = parseInt($button.data('max'));
|
|
|
|
if (currentPage > maxPages) {
|
|
return;
|
|
}
|
|
|
|
$button.prop('disabled', true).text('Loading...');
|
|
|
|
$.ajax({
|
|
url: hvac_ajax.ajax_url,
|
|
type: 'POST',
|
|
data: {
|
|
action: 'hvac_get_announcements',
|
|
page: currentPage,
|
|
per_page: 10,
|
|
status: 'publish',
|
|
nonce: hvac_ajax.nonce
|
|
},
|
|
success: function(response) {
|
|
if (response.success && response.data.announcements) {
|
|
var announcements = response.data.announcements;
|
|
var $timeline = $('.timeline-wrapper');
|
|
|
|
// Append new announcements to timeline
|
|
announcements.forEach(function(announcement) {
|
|
var html = buildAnnouncementItem(announcement);
|
|
$timeline.append(html);
|
|
});
|
|
|
|
// Update button
|
|
if (currentPage >= maxPages) {
|
|
$button.parent().remove();
|
|
} else {
|
|
$button.data('page', currentPage + 1);
|
|
$button.prop('disabled', false).text('Load More Announcements');
|
|
}
|
|
}
|
|
},
|
|
error: function() {
|
|
$button.prop('disabled', false).text('Load More Announcements');
|
|
alert('Error loading more announcements. Please try again.');
|
|
}
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Build announcement item HTML
|
|
*/
|
|
function buildAnnouncementItem(announcement) {
|
|
var html = '<article class="timeline-item">';
|
|
html += '<div class="timeline-marker"></div>';
|
|
html += '<div class="timeline-content">';
|
|
html += '<header class="timeline-header">';
|
|
html += '<h3 class="timeline-title">';
|
|
// Ensure ID is numeric to prevent attribute injection
|
|
html += '<a href="#" class="announcement-link" data-id="' + parseInt(announcement.id, 10) + '">';
|
|
html += escapeHtml(announcement.title);
|
|
html += '</a>';
|
|
html += '</h3>';
|
|
html += '<div class="timeline-meta">';
|
|
html += '<span class="timeline-date">' + formatDate(announcement.date) + '</span>';
|
|
html += '<span class="timeline-author">' + escapeHtml(announcement.author) + '</span>';
|
|
html += '</div>';
|
|
html += '</header>';
|
|
|
|
if (announcement.featured_image) {
|
|
html += '<div class="timeline-thumbnail">';
|
|
// Safely build image element
|
|
var imgSrc = String(announcement.featured_image || '').replace(/"/g, '"');
|
|
html += '<img src="' + imgSrc + '" alt="">';
|
|
html += '</div>';
|
|
}
|
|
|
|
if (announcement.excerpt) {
|
|
// Excerpt is pre-sanitized server-side with wp_kses_post, safe to insert as HTML
|
|
html += '<div class="timeline-excerpt">' + announcement.excerpt + '</div>';
|
|
}
|
|
|
|
if (announcement.categories && announcement.categories.length > 0) {
|
|
html += '<div class="timeline-categories">';
|
|
announcement.categories.forEach(function(category) {
|
|
html += '<span class="category-badge">' + escapeHtml(category) + '</span>';
|
|
});
|
|
html += '</div>';
|
|
}
|
|
|
|
html += '</div>';
|
|
html += '</article>';
|
|
|
|
return html;
|
|
}
|
|
|
|
/**
|
|
* Format date string
|
|
*/
|
|
function formatDate(dateString) {
|
|
var date = new Date(dateString);
|
|
var options = { year: 'numeric', month: 'long', day: 'numeric' };
|
|
return date.toLocaleDateString('en-US', options);
|
|
}
|
|
|
|
/**
|
|
* Escape HTML for security
|
|
*/
|
|
function escapeHtml(text) {
|
|
var map = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
};
|
|
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
|
|
}
|
|
}); |