mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-24 23:11:08 -05:00
Add secret management guide, client-side performance monitoring, and search accessibility enhancements
- Introduced a comprehensive Secret Management Guide detailing best practices, secret classification, development setup, production management, rotation procedures, and emergency protocols. - Implemented a client-side performance monitoring script to track various metrics including page load performance, paint metrics, layout shifts, and memory usage. - Enhanced search accessibility with keyboard navigation support for search results, ensuring compliance with WCAG standards and improving user experience.
This commit is contained in:
@@ -1,33 +1,336 @@
|
||||
// Lightbox Functionality
|
||||
/**
|
||||
* ThrillWiki Performance Optimization Module
|
||||
*
|
||||
* Features:
|
||||
* - Advanced lazy loading with Intersection Observer
|
||||
* - Image preloading and progressive loading
|
||||
* - Dynamic module loading (code splitting)
|
||||
* - Memory leak prevention utilities
|
||||
* - Event listener cleanup management
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// Event Manager - Memory Leak Prevention
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* EventManager class for preventing memory leaks from orphaned event listeners.
|
||||
* Use this for dynamic elements that may be removed from the DOM.
|
||||
*/
|
||||
class EventManager {
|
||||
constructor() {
|
||||
this.listeners = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an event listener with automatic cleanup tracking
|
||||
* @param {Element} element - The DOM element
|
||||
* @param {string} event - Event type (e.g., 'click')
|
||||
* @param {Function} handler - Event handler function
|
||||
* @param {Object} options - Event listener options
|
||||
*/
|
||||
addEventListener(element, event, handler, options = {}) {
|
||||
if (!element) return;
|
||||
|
||||
if (!this.listeners.has(element)) {
|
||||
this.listeners.set(element, []);
|
||||
}
|
||||
this.listeners.get(element).push({ event, handler, options });
|
||||
element.addEventListener(event, handler, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all tracked event listeners for an element
|
||||
* @param {Element} element - The DOM element to clean up
|
||||
*/
|
||||
removeListeners(element) {
|
||||
if (!this.listeners.has(element)) return;
|
||||
|
||||
const handlers = this.listeners.get(element);
|
||||
handlers.forEach(({ event, handler, options }) => {
|
||||
element.removeEventListener(event, handler, options);
|
||||
});
|
||||
this.listeners.delete(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all tracked event listeners
|
||||
*/
|
||||
cleanup() {
|
||||
this.listeners.forEach((handlers, element) => {
|
||||
handlers.forEach(({ event, handler, options }) => {
|
||||
element.removeEventListener(event, handler, options);
|
||||
});
|
||||
});
|
||||
this.listeners.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Global event manager instance
|
||||
window.ThrillWikiEventManager = new EventManager();
|
||||
|
||||
// =============================================================================
|
||||
// Advanced Lazy Loading with Intersection Observer
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* LazyLoader class for efficient image lazy loading using Intersection Observer.
|
||||
* Supports progressive loading with blur-up placeholder technique.
|
||||
*/
|
||||
class LazyLoader {
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
rootMargin: options.rootMargin || '50px 0px',
|
||||
threshold: options.threshold || 0.01,
|
||||
loadingClass: options.loadingClass || 'lazy-loading',
|
||||
loadedClass: options.loadedClass || 'lazy-loaded',
|
||||
errorClass: options.errorClass || 'lazy-error',
|
||||
};
|
||||
|
||||
this.observer = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Check for Intersection Observer support
|
||||
if (!('IntersectionObserver' in window)) {
|
||||
// Fallback: load all images immediately
|
||||
this.loadAllImages();
|
||||
return;
|
||||
}
|
||||
|
||||
this.observer = new IntersectionObserver((entries, observer) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
this.loadImage(entry.target);
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, {
|
||||
rootMargin: this.options.rootMargin,
|
||||
threshold: this.options.threshold,
|
||||
});
|
||||
|
||||
// Observe all lazy images
|
||||
this.observeImages();
|
||||
}
|
||||
|
||||
observeImages() {
|
||||
const lazyImages = document.querySelectorAll('img[data-src], img.lazy');
|
||||
lazyImages.forEach(img => {
|
||||
if (this.observer) {
|
||||
this.observer.observe(img);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadImage(img) {
|
||||
const src = img.dataset.src || img.getAttribute('data-src');
|
||||
const srcset = img.dataset.srcset || img.getAttribute('data-srcset');
|
||||
|
||||
if (!src && !srcset) return;
|
||||
|
||||
img.classList.add(this.options.loadingClass);
|
||||
|
||||
// Create a temporary image to preload
|
||||
const tempImage = new Image();
|
||||
|
||||
tempImage.onload = () => {
|
||||
if (src) img.src = src;
|
||||
if (srcset) img.srcset = srcset;
|
||||
|
||||
img.classList.remove(this.options.loadingClass);
|
||||
img.classList.add(this.options.loadedClass);
|
||||
img.classList.remove('lazy');
|
||||
|
||||
// Trigger custom event for tracking
|
||||
img.dispatchEvent(new CustomEvent('lazyloaded', { bubbles: true }));
|
||||
};
|
||||
|
||||
tempImage.onerror = () => {
|
||||
img.classList.remove(this.options.loadingClass);
|
||||
img.classList.add(this.options.errorClass);
|
||||
console.warn('Failed to load image:', src);
|
||||
};
|
||||
|
||||
// Start loading
|
||||
tempImage.src = src || '';
|
||||
if (srcset) tempImage.srcset = srcset;
|
||||
}
|
||||
|
||||
loadAllImages() {
|
||||
const lazyImages = document.querySelectorAll('img[data-src], img.lazy');
|
||||
lazyImages.forEach(img => this.loadImage(img));
|
||||
}
|
||||
|
||||
// Add new images to observation (for dynamically added content)
|
||||
observe(img) {
|
||||
if (this.observer) {
|
||||
this.observer.observe(img);
|
||||
} else {
|
||||
this.loadImage(img);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.observer) {
|
||||
this.observer.disconnect();
|
||||
this.observer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Dynamic Module Loading (Code Splitting)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* ModuleLoader for lazy loading JavaScript modules only when needed.
|
||||
* Reduces initial bundle size and improves page load time.
|
||||
*/
|
||||
class ModuleLoader {
|
||||
constructor() {
|
||||
this.loadedModules = new Set();
|
||||
this.loadingPromises = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically load a JavaScript module
|
||||
* @param {string} modulePath - Path to the module
|
||||
* @returns {Promise} - Resolves when module is loaded
|
||||
*/
|
||||
async load(modulePath) {
|
||||
// Return cached if already loaded
|
||||
if (this.loadedModules.has(modulePath)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Return existing promise if currently loading
|
||||
if (this.loadingPromises.has(modulePath)) {
|
||||
return this.loadingPromises.get(modulePath);
|
||||
}
|
||||
|
||||
// Create loading promise
|
||||
const loadPromise = new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = modulePath;
|
||||
script.async = true;
|
||||
|
||||
script.onload = () => {
|
||||
this.loadedModules.add(modulePath);
|
||||
this.loadingPromises.delete(modulePath);
|
||||
resolve();
|
||||
};
|
||||
|
||||
script.onerror = () => {
|
||||
this.loadingPromises.delete(modulePath);
|
||||
reject(new Error(`Failed to load module: ${modulePath}`));
|
||||
};
|
||||
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
|
||||
this.loadingPromises.set(modulePath, loadPromise);
|
||||
return loadPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditionally load a module if an element exists
|
||||
* @param {string} selector - CSS selector to check
|
||||
* @param {string} modulePath - Path to the module to load
|
||||
*/
|
||||
async loadIfNeeded(selector, modulePath) {
|
||||
if (document.querySelector(selector)) {
|
||||
await this.load(modulePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global module loader instance
|
||||
window.ThrillWikiModuleLoader = new ModuleLoader();
|
||||
|
||||
// =============================================================================
|
||||
// Lightbox Functionality (Alpine.js Component)
|
||||
// =============================================================================
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('lightbox', () => ({
|
||||
isOpen: false,
|
||||
imgSrc: '',
|
||||
imgAlt: '',
|
||||
|
||||
|
||||
open(event) {
|
||||
const img = event.target.tagName === 'IMG' ? event.target : event.target.querySelector('img');
|
||||
if (img) {
|
||||
this.imgSrc = img.src;
|
||||
// Use full-size image if available via data-full-src
|
||||
this.imgSrc = img.dataset.fullSrc || img.src;
|
||||
this.imgAlt = img.alt;
|
||||
this.isOpen = true;
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
close() {
|
||||
this.isOpen = false;
|
||||
this.imgSrc = '';
|
||||
this.imgAlt = '';
|
||||
document.body.style.overflow = '';
|
||||
},
|
||||
|
||||
// Cleanup method to prevent memory leaks
|
||||
destroy() {
|
||||
this.close();
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
// Add lazy loading to all images
|
||||
// =============================================================================
|
||||
// Initialization
|
||||
// =============================================================================
|
||||
|
||||
// Global lazy loader instance
|
||||
let lazyLoader = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Initialize lazy loader
|
||||
lazyLoader = new LazyLoader();
|
||||
window.ThrillWikiLazyLoader = lazyLoader;
|
||||
|
||||
// Add native lazy loading to images that don't have it
|
||||
const images = document.querySelectorAll('img:not([loading])');
|
||||
images.forEach(img => {
|
||||
img.setAttribute('loading', 'lazy');
|
||||
// Set eager for above-the-fold images (first 3 visible images)
|
||||
const rect = img.getBoundingClientRect();
|
||||
const isAboveFold = rect.top < window.innerHeight && rect.top >= 0;
|
||||
img.setAttribute('loading', isAboveFold ? 'eager' : 'lazy');
|
||||
});
|
||||
|
||||
// Handle HTMX content updates
|
||||
document.body.addEventListener('htmx:afterSwap', (event) => {
|
||||
// Re-observe any new lazy images after HTMX updates
|
||||
if (lazyLoader) {
|
||||
const newImages = event.detail.target.querySelectorAll('img[data-src], img.lazy');
|
||||
newImages.forEach(img => lazyLoader.observe(img));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Cleanup on page unload
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (window.ThrillWikiEventManager) {
|
||||
window.ThrillWikiEventManager.cleanup();
|
||||
}
|
||||
if (lazyLoader) {
|
||||
lazyLoader.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Export for use in other modules
|
||||
// =============================================================================
|
||||
|
||||
window.ThrillWikiOptimization = {
|
||||
EventManager,
|
||||
LazyLoader,
|
||||
ModuleLoader,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user