/** * Debrid Services Comparison - Modern Refactored Version * Optimized for performance, maintainability, and modern web standards * @version 2.0.0 */ /* ============================================================================ CONFIGURATION & CONSTANTS ============================================================================ */ const CONFIG = Object.freeze({ API: { FILE_HOSTS: ['./json/file-hosts-optimized.json', './json/file-hosts.json'], ADULT_HOSTS: ['./json/adult-hosts-optimized.json', './json/adult-hosts.json'], TIMEOUT: 8000, RETRY_ATTEMPTS: 2 }, PERFORMANCE: { DEBOUNCE_DELAY: 250, THROTTLE_DELAY: 100, RENDER_CHUNK_SIZE: 40, RENDER_IDLE_TIMEOUT: 160, SCROLL_THRESHOLD: 300 }, INTERSECTION: { THRESHOLD: 0.1, ROOT_MARGIN: '-40% 0px -50% 0px' }, ANIMATION: { DURATION: 250, EASING: 'cubic-bezier(0.4, 0, 0.2, 1)' } }); /* ============================================================================ UTILITIES MODULE ============================================================================ */ const Utils = (() => { // Performance-optimized debounce with WeakMap cache const debounceCache = new WeakMap(); const debounce = (func, wait = CONFIG.PERFORMANCE.DEBOUNCE_DELAY) => { if (debounceCache.has(func)) return debounceCache.get(func); let timeoutId; const debounced = function executeDebouncedFunction(...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => func.apply(this, args), wait); }; debounced.cancel = () => clearTimeout(timeoutId); debounceCache.set(func, debounced); return debounced; }; // RAF-based throttle for smooth animations const throttle = (func, limit = CONFIG.PERFORMANCE.THROTTLE_DELAY) => { let waiting = false; let lastArgs = null; return function executeThrottledFunction(...args) { if (!waiting) { func.apply(this, args); waiting = true; requestAnimationFrame(() => { setTimeout(() => { waiting = false; if (lastArgs) { func.apply(this, lastArgs); lastArgs = null; } }, limit); }); } else { lastArgs = args; } }; }; // Lazy fetch with timeout and abort controller const fetchWithTimeout = async (url, options = {}) => { const { timeout = CONFIG.API.TIMEOUT, ...fetchOptions } = options; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { ...fetchOptions, signal: controller.signal }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return response; } finally { clearTimeout(timeoutId); } }; // Cascade fetch with multiple fallback URLs const fetchWithFallback = async (urls, options = {}) => { const errors = []; for (const url of urls) { try { const response = await fetchWithTimeout(url, options); return await response.json(); } catch (error) { errors.push({ url, error: error.message }); continue; } } throw new Error(`All fetch attempts failed: ${JSON.stringify(errors)}`); }; // Efficient DOM element creation with attributes const createElement = (tag, attributes = {}, children = []) => { const element = document.createElement(tag); Object.entries(attributes).forEach(([key, value]) => { if (key === 'className') { element.className = value; } else if (key === 'dataset') { Object.assign(element.dataset, value); } else if (key.startsWith('on') && typeof value === 'function') { element.addEventListener(key.slice(2).toLowerCase(), value); } else { element.setAttribute(key, value); } }); children.forEach(child => { if (typeof child === 'string') { element.appendChild(document.createTextNode(child)); } else if (child instanceof Node) { element.appendChild(child); } }); return element; }; // Schedule work during idle time const scheduleIdleWork = (callback, options = {}) => { if ('requestIdleCallback' in window) { return requestIdleCallback(callback, { timeout: CONFIG.PERFORMANCE.RENDER_IDLE_TIMEOUT, ...options }); } return requestAnimationFrame(callback); }; // Cancel scheduled idle work const cancelIdleWork = (id) => { if ('cancelIdleCallback' in window) { cancelIdleCallback(id); } else { cancelAnimationFrame(id); } }; return Object.freeze({ debounce, throttle, fetchWithTimeout, fetchWithFallback, createElement, scheduleIdleWork, cancelIdleWork }); })(); /* ============================================================================ STATE MANAGEMENT ============================================================================ */ class StateManager { #state = {}; #listeners = new Map(); constructor(initialState = {}) { this.#state = { ...initialState }; } get(key) { return key ? this.#state[key] : { ...this.#state }; } set(key, value) { const oldValue = this.#state[key]; this.#state[key] = value; if (this.#listeners.has(key)) { this.#listeners.get(key).forEach(callback => { callback(value, oldValue); }); } return this; } update(updates) { Object.entries(updates).forEach(([key, value]) => { this.set(key, value); }); return this; } subscribe(key, callback) { if (!this.#listeners.has(key)) { this.#listeners.set(key, new Set()); } this.#listeners.get(key).add(callback); return () => this.#listeners.get(key)?.delete(callback); } reset() { this.#state = {}; this.#listeners.clear(); } } /* ============================================================================ LOADING MANAGER ============================================================================ */ class LoadingManager { #loaders = new Map(); #template = null; constructor() { this.#template = this.#createTemplate(); } #createTemplate() { const template = document.createElement('template'); template.innerHTML = `

`; return template; } show(target, text = 'Loading...') { const element = typeof target === 'string' ? document.querySelector(target) : target; if (!element) return null; const loaderId = `loader-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; const loader = this.#template.content.cloneNode(true).firstElementChild; loader.dataset.loaderId = loaderId; loader.querySelector('.loading-text').textContent = text; // Ensure parent has positioning context if (getComputedStyle(element).position === 'static') { element.style.position = 'relative'; } element.appendChild(loader); this.#loaders.set(loaderId, { element: loader, parent: element }); // Trigger animation requestAnimationFrame(() => { loader.classList.add('loading-overlay--visible'); }); return loaderId; } hide(loaderId) { const loaderData = this.#loaders.get(loaderId); if (!loaderData) return; const { element } = loaderData; element.classList.remove('loading-overlay--visible'); element.classList.add('loading-overlay--hiding'); setTimeout(() => { element.remove(); this.#loaders.delete(loaderId); }, CONFIG.ANIMATION.DURATION); } hideAll() { this.#loaders.forEach((_, loaderId) => this.hide(loaderId)); } } /* ============================================================================ DATA TRANSFORMER ============================================================================ */ class DataTransformer { static transformIndexedData(data) { if (!data?.services || !data?.supported) { return { isIndexed: false, data }; } const { services, supported } = data; const transformed = {}; Object.entries(supported).forEach(([host, indices]) => { transformed[host] = Object.fromEntries( services.map((service, idx) => [ service, indices.includes(idx) ? '✅' : '❌' ]) ); }); return { isIndexed: true, data: transformed, services }; } static extractServices(data) { const firstHost = Object.values(data)[0]; return firstHost ? Object.keys(firstHost) : []; } } /* ============================================================================ TABLE MANAGER ============================================================================ */ class TableManager { #elements = {}; #state = null; #renderToken = null; #idleCallbackId = null; constructor(containerId, searchInputId, clearIconId, options = {}) { this.#elements = { container: document.getElementById(containerId), searchInput: document.getElementById(searchInputId), clearIcon: document.getElementById(clearIconId) }; this.options = { sortable: true, chunkSize: CONFIG.PERFORMANCE.RENDER_CHUNK_SIZE, ...options }; this.#state = new StateManager({ currentData: {}, filteredData: {}, sort: { column: null, direction: 'asc' }, services: [], columns: [] }); this.#init(); } #init() { if (!this.#elements.container) return; // Bind event handlers if (this.#elements.searchInput) { const debouncedSearch = Utils.debounce(() => this.#performSearch()); this.#elements.searchInput.addEventListener('input', debouncedSearch); this.#elements.searchInput.addEventListener('keydown', (e) => { if (e.key === 'Escape') this.#clearSearch(); }); } if (this.#elements.clearIcon) { this.#elements.clearIcon.addEventListener('click', () => this.#clearSearch()); } } generateTable(rawData) { if (!this.#elements.container || !rawData) { this.#showEmptyState('No data available'); return; } // Transform data if needed const { data, services } = DataTransformer.transformIndexedData(rawData); if (!Object.keys(data).length) { this.#showEmptyState('No data available'); return; } // Update state this.#state.update({ currentData: data, filteredData: { ...data }, services: services || DataTransformer.extractServices(data), columns: DataTransformer.extractServices(data) }); this.#renderTable(); } #renderTable() { const { columns } = this.#state.get(); const tableId = this.#elements.container.id.replace('-container', ''); // Create table structure const table = Utils.createElement('table', { id: tableId, className: 'enhanced-table', role: 'table', 'aria-label': 'Service Comparison Table' }); // Create header const thead = Utils.createElement('thead'); thead.innerHTML = this.#generateHeaderHTML(columns); table.appendChild(thead); // Create body const tbody = Utils.createElement('tbody'); table.appendChild(tbody); // Create wrapper const wrapper = Utils.createElement('div', { className: 'table-wrapper' }, [table]); // Replace container content this.#elements.container.innerHTML = ''; this.#elements.container.appendChild(wrapper); // Attach event handlers this.#attachTableEvents(); // Render rows this.#renderTableBody(); } #generateHeaderHTML(columns) { const generateHeaderCell = (label, dataColumn) => ` ${label} `; const columnHeaders = columns .map(col => generateHeaderCell(col, col)) .join(''); return ` ${generateHeaderCell('Service Name', 'service')} ${columnHeaders} `; } #renderTableBody() { const table = this.#elements.container?.querySelector('table'); const tbody = table?.querySelector('tbody'); if (!tbody) return; const { filteredData, columns } = this.#state.get(); const entries = Object.entries(filteredData); // Cancel any pending render if (this.#idleCallbackId) { Utils.cancelIdleWork(this.#idleCallbackId); } // Generate unique render token const token = Symbol('render'); this.#renderToken = token; // Clear tbody tbody.textContent = ''; if (!entries.length) { this.#renderEmptyRow(tbody, columns.length + 1); return; } // Render in chunks this.#renderChunked(tbody, entries, columns, token); } #renderChunked(tbody, entries, columns, token) { let index = 0; const chunkSize = this.options.chunkSize; const renderNextChunk = () => { // Check if render was cancelled if (this.#renderToken !== token) return; const fragment = document.createDocumentFragment(); const limit = Math.min(entries.length, index + chunkSize); for (; index < limit; index++) { const [host, hostData] = entries[index]; const row = this.#createTableRow(host, hostData, columns); fragment.appendChild(row); } tbody.appendChild(fragment); // Continue rendering if more entries exist if (index < entries.length) { this.#idleCallbackId = Utils.scheduleIdleWork(renderNextChunk); } }; this.#idleCallbackId = Utils.scheduleIdleWork(renderNextChunk); } #createTableRow(host, hostData, columns) { const row = Utils.createElement('tr', { role: 'row', dataset: { host: host.toLowerCase() } }); // Service name cell const serviceCell = this.#createServiceCell(host); row.appendChild(serviceCell); // Status cells columns.forEach(column => { const statusCell = this.#createStatusCell(hostData[column]); row.appendChild(statusCell); }); return row; } #createServiceCell(serviceName) { const cell = Utils.createElement('td', { className: 'service-cell', role: 'gridcell' }); const serviceInfo = Utils.createElement('div', { className: 'service-info' }); const nameSpan = Utils.createElement('span', { className: 'service-name' }, [serviceName]); serviceInfo.appendChild(nameSpan); cell.appendChild(serviceInfo); return cell; } #createStatusCell(status) { const isSupported = status === '✅'; const cell = Utils.createElement('td', { className: 'status-cell', role: 'gridcell', dataset: { status: status || '', supported: isSupported ? 'true' : 'false' } }); const indicator = Utils.createElement('span', { className: 'status-indicator', 'aria-hidden': 'true', dataset: { supported: isSupported ? 'true' : 'false' } }, [isSupported ? '✅' : '❌']); const srText = Utils.createElement('span', { className: 'visually-hidden' }, [isSupported ? 'Supported' : 'Not supported']); cell.appendChild(indicator); cell.appendChild(srText); return cell; } #renderEmptyRow(tbody, colSpan) { const row = Utils.createElement('tr'); const cell = Utils.createElement('td', { className: 'empty-state-row', colSpan }, ['No services match your current filters.']); row.appendChild(cell); tbody.appendChild(row); } #attachTableEvents() { if (!this.options.sortable) return; const thead = this.#elements.container.querySelector('thead'); if (!thead) return; thead.addEventListener('click', (e) => this.#handleSort(e)); thead.addEventListener('keydown', (e) => { const sortableHeader = e.target.closest('.sortable'); if (sortableHeader && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); this.#handleSort({ target: sortableHeader }); } }); } #handleSort(event) { const header = event.target.closest('.sortable'); if (!header) return; const column = header.dataset.column; const { sort, currentData } = this.#state.get(); // Determine sort direction const direction = sort.column === column && sort.direction === 'asc' ? 'desc' : 'asc'; // Update state this.#state.set('sort', { column, direction }); // Update ARIA attributes const allHeaders = this.#elements.container.querySelectorAll('[role="columnheader"]'); allHeaders.forEach(h => h.setAttribute('aria-sort', 'none')); header.setAttribute('aria-sort', direction === 'asc' ? 'ascending' : 'descending'); // Sort data const sortedData = this.#sortData(currentData, column, direction); this.#state.set('filteredData', sortedData); // Re-render this.#renderTableBody(); } #sortData(data, column, direction) { const entries = Object.entries(data); entries.sort(([hostA, dataA], [hostB, dataB]) => { let valueA, valueB; if (column === 'service') { valueA = hostA.toLowerCase(); valueB = hostB.toLowerCase(); } else { valueA = dataA[column] === '✅' ? 1 : 0; valueB = dataB[column] === '✅' ? 1 : 0; } if (valueA < valueB) return direction === 'asc' ? -1 : 1; if (valueA > valueB) return direction === 'asc' ? 1 : -1; return 0; }); return Object.fromEntries(entries); } #performSearch() { const searchTerm = (this.#elements.searchInput?.value || '').toLowerCase().trim(); const { currentData } = this.#state.get(); if (searchTerm) { const filtered = Object.fromEntries( Object.entries(currentData).filter(([host]) => host.toLowerCase().includes(searchTerm) ) ); this.#state.set('filteredData', filtered); if (this.#elements.clearIcon) { this.#elements.clearIcon.style.display = 'block'; } this.#updateSearchResults(searchTerm, Object.keys(filtered).length, Object.keys(currentData).length); } else { this.#clearSearch(); } this.#renderTableBody(); } #clearSearch() { if (this.#elements.searchInput) { this.#elements.searchInput.value = ''; } const { currentData } = this.#state.get(); this.#state.set('filteredData', { ...currentData }); if (this.#elements.clearIcon) { this.#elements.clearIcon.style.display = 'none'; } this.#updateSearchResults('', 0, 0); this.#renderTableBody(); } #updateSearchResults(searchTerm, filteredCount, totalCount) { if (!this.#elements.container) return; let resultsElement = this.#elements.container.querySelector('.search-results'); if (searchTerm) { if (!resultsElement) { resultsElement = Utils.createElement('div', { className: 'search-results', role: 'status', 'aria-live': 'polite' }); this.#elements.container.insertBefore(resultsElement, this.#elements.container.firstChild); } resultsElement.textContent = `Showing ${filteredCount} of ${totalCount} services`; resultsElement.style.display = 'block'; } else if (resultsElement) { resultsElement.style.display = 'none'; } } #showEmptyState(message) { if (!this.#elements.container) return; const emptyState = Utils.createElement('div', { className: 'empty-state' }); const emptyMessage = Utils.createElement('p', {}, [message]); emptyState.appendChild(emptyMessage); this.#elements.container.innerHTML = ''; this.#elements.container.appendChild(emptyState); } } /* ============================================================================ COMPARISON MANAGER ============================================================================ */ class ComparisonManager { #elements = {}; #data = null; #services = []; constructor(containerId, select1Id, select2Id, data) { this.#elements = { container: document.getElementById(containerId), select1: document.getElementById(select1Id), select2: document.getElementById(select2Id) }; const transformed = DataTransformer.transformIndexedData(data); this.#data = transformed.data; this.#services = transformed.services || DataTransformer.extractServices(transformed.data); this.#init(); } #init() { if (!this.#elements.select1 || !this.#elements.select2) return; this.#elements.select1.addEventListener('change', () => this.#handleCompare()); this.#elements.select2.addEventListener('change', () => this.#handleCompare()); } #handleCompare() { const service1 = this.#elements.select1.value; const service2 = this.#elements.select2.value; if (!service1 || !service2) { this.#showEmptyState(); return; } if (service1 === service2) { this.#showSameProviderWarning(); return; } this.#renderComparison(service1, service2); } #renderComparison(service1, service2) { const hosts = Object.keys(this.#data); const stats = this.#calculateStats(service1, service2, hosts); const table = this.#createComparisonTable(service1, service2, hosts); const header = this.#createComparisonHeader(service1, service2, stats); this.#elements.container.innerHTML = ''; this.#elements.container.appendChild(header); this.#elements.container.appendChild(table); this.#elements.container.style.display = 'block'; } #createComparisonHeader(service1, service2, stats) { const header = Utils.createElement('div', { className: 'comparison-header' }); const title = Utils.createElement('h3', {}, [`Comparing ${service1} vs ${service2}`]); header.appendChild(title); const statsDiv = Utils.createElement('div', { className: 'comparison-stats' }); const createStatItem = (label, value) => { const stat = Utils.createElement('div', { className: 'stat' }); const statLabel = Utils.createElement('span', { className: 'stat-label' }, [label]); const statValue = Utils.createElement('span', { className: 'stat-value' }, [String(value)]); stat.appendChild(statLabel); stat.appendChild(statValue); return stat; }; statsDiv.appendChild(createStatItem('Shared', stats.shared)); statsDiv.appendChild(createStatItem(`${service1} Only`, stats.service1Only)); statsDiv.appendChild(createStatItem(`${service2} Only`, stats.service2Only)); header.appendChild(statsDiv); return header; } #createComparisonTable(service1, service2, hosts) { const wrapper = Utils.createElement('div', { className: 'table-wrapper' }); const table = Utils.createElement('table', { className: 'comparison-table' }); // Header const thead = Utils.createElement('thead'); thead.innerHTML = ` Host ${service1} ${service2} Status `; table.appendChild(thead); // Body const tbody = Utils.createElement('tbody'); hosts.forEach(host => { const row = this.#createComparisonRow(host, service1, service2); tbody.appendChild(row); }); table.appendChild(tbody); wrapper.appendChild(table); return wrapper; } #createComparisonRow(host, service1, service2) { const hostData = this.#data[host]; const supported1 = hostData[service1] === '✅'; const supported2 = hostData[service2] === '✅'; let statusClass = 'neither-supported'; let statusText = 'Neither'; if (supported1 && supported2) { statusClass = 'both-supported'; statusText = 'Both'; } else if (supported1) { statusClass = 'service1-only'; statusText = `${service1} only`; } else if (supported2) { statusClass = 'service2-only'; statusText = `${service2} only`; } const row = Utils.createElement('tr', { className: `comparison-row ${statusClass}`, dataset: { status: statusClass } }); // Host name const hostCell = Utils.createElement('td', { className: 'service-name' }, [host]); row.appendChild(hostCell); // Service 1 status const status1Cell = this.#createStatusCell(supported1); row.appendChild(status1Cell); // Service 2 status const status2Cell = this.#createStatusCell(supported2); row.appendChild(status2Cell); // Status text const statusTextCell = Utils.createElement('td', { className: 'status-text' }); const badge = Utils.createElement('span', { className: `status-badge ${statusClass}` }, [statusText]); statusTextCell.appendChild(badge); row.appendChild(statusTextCell); return row; } #createStatusCell(isSupported) { const cell = Utils.createElement('td', { className: `support-status ${isSupported ? 'supported' : 'not-supported'}`, dataset: { supported: isSupported ? 'true' : 'false' } }); const indicator = Utils.createElement('span', { className: 'status-indicator', 'aria-hidden': 'true', dataset: { supported: isSupported ? 'true' : 'false' } }, [isSupported ? '✅' : '❌']); const srText = Utils.createElement('span', { className: 'visually-hidden' }, [isSupported ? 'Supported' : 'Not supported']); cell.appendChild(indicator); cell.appendChild(srText); return cell; } #calculateStats(service1, service2, hosts) { let shared = 0; let service1Only = 0; let service2Only = 0; hosts.forEach(host => { const hostData = this.#data[host]; const supported1 = hostData[service1] === '✅'; const supported2 = hostData[service2] === '✅'; if (supported1 && supported2) shared++; else if (supported1) service1Only++; else if (supported2) service2Only++; }); return { shared, service1Only, service2Only }; } #showEmptyState() { this.#elements.container.innerHTML = `

Select Two Services to Compare

Choose services from the dropdowns above to see a detailed comparison.

`; this.#elements.container.style.display = 'block'; } #showSameProviderWarning() { this.#elements.container.innerHTML = `
⚠️

Same Service Selected

Please select two different services to compare

`; this.#elements.container.style.display = 'block'; } } /* ============================================================================ UI FEATURES ============================================================================ */ const UIFeatures = (() => { // Intersection Observer for scroll animations const setupScrollAnimations = () => { if (!('IntersectionObserver' in window)) return; const elements = document.querySelectorAll('.section, .status-card, .referral-card'); if (!elements.length) return; const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('animate-in'); observer.unobserve(entry.target); } }); }, { threshold: CONFIG.INTERSECTION.THRESHOLD, rootMargin: '0px 0px -50px 0px' }); elements.forEach(el => observer.observe(el)); }; // Header elevation on scroll const setupHeaderElevation = () => { const header = document.querySelector('.site-header'); if (!header) return; const updateHeader = () => { header.classList.toggle('is-scrolled', window.scrollY > 8); }; updateHeader(); const throttledUpdate = Utils.throttle(updateHeader); window.addEventListener('scroll', throttledUpdate, { passive: true }); }; // Back to top button const setupBackToTop = () => { const btn = document.getElementById('backToTop'); if (!btn) return; const updateVisibility = () => { btn.classList.toggle('visible', window.pageYOffset > CONFIG.PERFORMANCE.SCROLL_THRESHOLD); }; const throttledUpdate = Utils.throttle(updateVisibility, 150); window.addEventListener('scroll', throttledUpdate, { passive: true }); btn.addEventListener('click', () => { window.scrollTo({ top: 0, behavior: 'smooth' }); }); }; // Navigation highlight const setupNavHighlight = () => { if (!('IntersectionObserver' in window)) return; const links = Array.from(document.querySelectorAll('.header-nav .nav-link[href^="#"]')); if (!links.length) return; const sections = links .map(link => document.querySelector(link.getAttribute('href'))) .filter(Boolean); const linkMap = new Map( links.map(link => [link.getAttribute('href'), link]) ); const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { const id = `#${entry.target.id}`; const link = linkMap.get(id); if (link && entry.isIntersecting) { links.forEach(l => l.removeAttribute('aria-current')); link.setAttribute('aria-current', 'page'); } }); }, { threshold: CONFIG.INTERSECTION.THRESHOLD, rootMargin: CONFIG.INTERSECTION.ROOT_MARGIN }); sections.forEach(section => observer.observe(section)); }; // URL-based comparison const setupURLComparison = () => { const params = new URLSearchParams(window.location.search); const provider1 = params.get('compare'); const provider2 = params.get('with'); if (provider1 && provider2) { requestAnimationFrame(() => { const select1 = document.getElementById('provider1'); const select2 = document.getElementById('provider2'); if (select1 && select2) { select1.value = provider1; select2.value = provider2; select1.dispatchEvent(new Event('change')); } }); } }; // Online/offline detection const setupOfflineDetection = () => { const showStatus = (isOnline) => { console.log(isOnline ? '✅ Connection restored' : '⚠️ You are offline'); }; window.addEventListener('online', () => showStatus(true)); window.addEventListener('offline', () => showStatus(false)); }; return Object.freeze({ setupScrollAnimations, setupHeaderElevation, setupBackToTop, setupNavHighlight, setupURLComparison, setupOfflineDetection }); })(); /* ============================================================================ PERFORMANCE MONITOR ============================================================================ */ class PerformanceMonitor { #marks = new Map(); constructor() { this.mark('start'); } mark(name) { this.#marks.set(name, performance.now()); } measure(name, startMark = 'start') { const start = this.#marks.get(startMark) || 0; const end = performance.now(); return end - start; } log(name, startMark = 'start') { const duration = this.measure(name, startMark); console.log(`⏱️ ${name}: ${duration.toFixed(2)}ms`); } } /* ============================================================================ APPLICATION BOOTSTRAP ============================================================================ */ class App { #loadingManager = null; #performanceMonitor = null; constructor() { this.#loadingManager = new LoadingManager(); this.#performanceMonitor = new PerformanceMonitor(); } async init() { try { this.#performanceMonitor.mark('dom-ready'); // Initialize tables const fileHostsTable = new TableManager( 'file-hosts-table-container', 'host-search-input', 'clear-host-search' ); const adultHostsTable = new TableManager( 'adult-hosts-table-container', 'adult-host-search-input', 'clear-adult-host-search' ); // Show loaders const loaders = [ this.#loadingManager.show('#file-hosts-table-container', 'Loading file hosts...'), this.#loadingManager.show('#adult-hosts-table-container', 'Loading adult hosts...') ]; // Fetch data const dataPromises = [ Utils.fetchWithFallback(CONFIG.API.FILE_HOSTS), Utils.fetchWithFallback(CONFIG.API.ADULT_HOSTS) ]; const results = await Promise.allSettled(dataPromises); // Process results results.forEach((result, index) => { this.#loadingManager.hide(loaders[index]); if (result.status === 'fulfilled') { if (index === 0) { fileHostsTable.generateTable(result.value); new ComparisonManager('compare-table-container', 'provider1', 'provider2', result.value); } else { adultHostsTable.generateTable(result.value); } } else { console.error(`Failed to load dataset ${index}:`, result.reason); const containerId = index === 0 ? '#file-hosts-table-container' : '#adult-hosts-table-container'; const container = document.querySelector(containerId); if (container) { container.innerHTML = `

Failed to load data. Please check your connection and refresh.

`; } } }); // Setup UI features UIFeatures.setupScrollAnimations(); UIFeatures.setupHeaderElevation(); UIFeatures.setupBackToTop(); UIFeatures.setupNavHighlight(); UIFeatures.setupURLComparison(); UIFeatures.setupOfflineDetection(); this.#performanceMonitor.mark('fully-loaded'); this.#performanceMonitor.log('Application initialized', 'dom-ready'); } catch (error) { console.error('❌ Critical application error:', error); this.#loadingManager.hideAll(); } } } /* ============================================================================ GLOBAL ERROR HANDLING ============================================================================ */ window.addEventListener('error', (event) => { console.error('❌ Global error:', event.error || event.message); }, { passive: true }); window.addEventListener('unhandledrejection', (event) => { console.error('❌ Unhandled promise rejection:', event.reason); }); /* ============================================================================ SERVICE WORKER REGISTRATION ============================================================================ */ if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker .register('/sw.js') .then(() => console.log('✅ Service worker registered')) .catch((error) => console.warn('⚠️ Service worker registration failed:', error)); }); } /* ============================================================================ INITIALIZE APPLICATION ============================================================================ */ document.addEventListener('DOMContentLoaded', () => { const app = new App(); app.init(); });