upskill-event-manager/assets/js/hvac-announcements-view.js
Ben c20b461e7d feat: Implement secure Trainer Announcements system with comprehensive features
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>
2025-08-20 13:34:15 -03:00

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, '&quot;');
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 = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
}
});