more web improvements

This commit is contained in:
fynks 2025-08-18 19:55:42 +05:00
parent 08ed1660c9
commit 4836ca5948
3 changed files with 345 additions and 542 deletions

159
dist/css/styles.css vendored
View file

@ -1301,44 +1301,51 @@ select:focus {
flex-shrink: 0; flex-shrink: 0;
margin-top: 2px; margin-top: 2px;
} }
/* Referral Section */
.referral-section { .referral-section {
margin-top: var(--space-16); margin-block: var(--space-16);
padding: var(--space-8); padding: var(--space-8);
background: var(--bg-secondary); background: var(--bg-secondary);
border-radius: var(--radius-xl); border-radius: var(--radius-xl);
border: 1px solid var(--border-primary); border: 1px solid var(--border-primary);
text-align: center;
} }
.referral-section h3 { .referral-header h3 {
margin: 0 0 var(--space-2); margin: 0 0 var(--space-2);
text-align: center;
color: var(--text-primary); color: var(--text-primary);
font-size: var(--text-2xl);
font-weight: var(--font-bold);
} }
.referral-section p { .referral-header p {
text-align: center; margin: 0 auto var(--space-6);
color: var(--text-secondary); color: var(--text-secondary);
margin-bottom: var(--space-6); max-width: var(--content-max-width);
} }
.referral-grid { .referral-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: var(--space-4); gap: var(--space-4);
justify-items: center;
} }
.referral-card { .referral-card {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center;
gap: var(--space-2); gap: var(--space-2);
padding: var(--space-4); padding: var(--space-4) var(--space-3);
background: var(--bg-primary); background: var(--bg-primary);
border: 1px solid var(--border-primary); border: 1px solid var(--border-primary);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
text-decoration: none; text-decoration: none;
transition: all var(--transition-fast); transition: var(--transition-all);
width: 100%;
max-width: 200px;
box-shadow: var(--shadow-sm);
} }
.referral-card:hover { .referral-card:hover {
@ -1348,8 +1355,9 @@ select:focus {
} }
.service-name { .service-name {
font-weight: var(--font-medium); font-weight: var(--font-semibold);
color: var(--text-primary); color: var(--text-primary);
font-size: var(--text-base);
} }
.referral-badge { .referral-badge {
@ -1358,6 +1366,12 @@ select:focus {
background: rgb(99 102 241 / 0.1); background: rgb(99 102 241 / 0.1);
padding: var(--space-1) var(--space-2); padding: var(--space-1) var(--space-2);
border-radius: var(--radius-md); border-radius: var(--radius-md);
font-weight: var(--font-medium);
}
.service-name {
font-weight: var(--font-medium);
color: var(--text-primary);
} }
.status-grid { .status-grid {
@ -1436,103 +1450,6 @@ select:focus {
} }
} }
/* Notifications */
.notification-container {
position: fixed;
top: var(--space-6);
right: var(--space-6);
z-index: var(--z-tooltip);
max-width: 400px;
width: 100%;
}
.notification {
background: var(--bg-primary);
border: 1px solid var(--border-primary);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-xl);
margin-bottom: var(--space-3);
opacity: 0;
transform: translateX(100%);
transition: all var(--transition-normal);
}
.notification--visible {
opacity: 1;
transform: translateX(0);
}
.notification--dismissing {
opacity: 0;
transform: translateX(100%);
}
.notification__content {
display: flex;
align-items: flex-start;
gap: var(--space-3);
padding: var(--space-4);
}
.notification__icon {
flex-shrink: 0;
margin-top: 2px;
}
.notification__message {
flex: 1;
font-size: var(--text-sm);
color: var(--text-primary);
}
.notification__close {
background: none;
border: none;
color: var(--text-tertiary);
cursor: pointer;
padding: var(--space-1);
border-radius: var(--radius-md);
transition: all var(--transition-fast);
flex-shrink: 0;
}
.notification__close:hover {
background: var(--bg-secondary);
color: var(--text-primary);
}
.notification--success {
border-color: var(--success);
}
.notification--success .notification__icon {
color: var(--success);
}
.notification--error {
border-color: var(--error);
}
.notification--error .notification__icon {
color: var(--error);
}
.notification--warning {
border-color: var(--warning);
}
.notification--warning .notification__icon {
color: var(--warning);
}
.notification--info {
border-color: var(--info);
}
.notification--info .notification__icon {
color: var(--info);
}
/* Footer */ /* Footer */
.site-footer { .site-footer {
background: var(--bg-secondary); background: var(--bg-secondary);
@ -1736,9 +1653,6 @@ select:focus {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.referral-grid {
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.footer-content { .footer-content {
grid-template-columns: 1fr; grid-template-columns: 1fr;
@ -1808,24 +1722,6 @@ select:focus {
font-size: var(--text-xs); font-size: var(--text-xs);
} }
.notification-container {
left: var(--space-4);
right: var(--space-4);
max-width: none;
}
.notification {
transform: translateY(-100%);
}
.notification--visible {
transform: translateY(0);
}
.notification--dismissing {
transform: translateY(-100%);
}
th, th,
td { td {
padding: var(--space-3) var(--space-4); padding: var(--space-3) var(--space-4);
@ -1928,7 +1824,6 @@ select:focus {
.site-footer, .site-footer,
.back-to-top, .back-to-top,
.dark-mode-toggle, .dark-mode-toggle,
.notification-container,
.comparison-filters, .comparison-filters,
.comparison-actions, .comparison-actions,
#close-compare { #close-compare {
@ -1971,10 +1866,6 @@ select:focus {
html { html {
scroll-behavior: auto; scroll-behavior: auto;
} }
.notification {
transition: none;
}
} }
/* High Contrast */ /* High Contrast */

91
dist/index.html vendored
View file

@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -61,11 +62,14 @@
<a href="#compare" class="nav-link">Compare</a> <a href="#compare" class="nav-link">Compare</a>
</nav> </nav>
<button id="darkModeToggle" class="dark-mode-toggle" aria-label="Toggle dark mode" type="button"> <button id="darkModeToggle" class="dark-mode-toggle" aria-label="Toggle dark mode" type="button">
<svg class="sun-icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"> <svg class="sun-icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<circle cx="12" cy="12" r="5" /> <circle cx="12" cy="12" r="5" />
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/> <path
d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
</svg> </svg>
<svg class="moon-icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"> <svg class="moon-icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" /> <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
</svg> </svg>
</button> </button>
@ -82,7 +86,8 @@
Compare Premium <span class="gradient-text">Debrid Services</span> Compare Premium <span class="gradient-text">Debrid Services</span>
</h1> </h1>
<p class="hero-description"> <p class="hero-description">
Find the perfect premium download service for your needs. Compare pricing, features, and supported hosts across all major debrid providers. Find the perfect premium download service for your needs. Compare pricing, features, and
supported hosts across all major debrid providers.
</p> </p>
<div class="hero-actions"> <div class="hero-actions">
<a href="#pricing" class="btn btn-primary">View Pricing</a> <a href="#pricing" class="btn btn-primary">View Pricing</a>
@ -146,31 +151,39 @@
</div> </div>
<div class="pricing-note"> <div class="pricing-note">
<div class="alert alert-info" role="alert"> <div class="alert alert-info" role="alert">
<svg class="alert-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"> <svg class="alert-icon" width="20" height="20" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" aria-hidden="true">
<circle cx="12" cy="12" r="10" /> <circle cx="12" cy="12" r="10" />
<path d="M12 16v-4M12 8h.01" /> <path d="M12 16v-4M12 8h.01" />
</svg> </svg>
<div> <div>
<strong>Important:</strong> Pricing can change frequently. Always verify current pricing on official websites before purchasing. <strong>Important:</strong> Pricing can change frequently. Always verify current
pricing on official websites before purchasing.
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Referral Links --> <!-- Referral Section -->
<div class="referral-section"> <div class="referral-section">
<div class="referral-header">
<h3>Support This Project</h3> <h3>Support This Project</h3>
<p>Consider using our referral links - it helps maintain this project at no extra cost to you!</p> <p>Consider using our referral links - it helps maintain this project at no extra cost to
you!</p>
</div>
<div class="referral-grid"> <div class="referral-grid">
<a href="https://alldebrid.com/?uid=3wvya&lang=en" class="referral-card" rel="noopener sponsored"> <a href="https://alldebrid.com/?uid=3wvya&lang=en" class="referral-card"
rel="noopener sponsored">
<span class="service-name">AllDebrid</span> <span class="service-name">AllDebrid</span>
<span class="referral-badge">Referral Link</span> <span class="referral-badge">Referral Link</span>
</a> </a>
<a href="http://real-debrid.com/?id=10990901" class="referral-card" rel="noopener sponsored"> <a href="http://real-debrid.com/?id=10990901" class="referral-card"
rel="noopener sponsored">
<span class="service-name">Real-Debrid</span> <span class="service-name">Real-Debrid</span>
<span class="referral-badge">Referral Link</span> <span class="referral-badge">Referral Link</span>
</a> </a>
<a href="https://torbox.app/subscription?referral=47eece72-46b8-483b-8b7a-79d6c16dedf2" class="referral-card" rel="noopener sponsored"> <a href="https://torbox.app/subscription?referral=47eece72-46b8-483b-8b7a-79d6c16dedf2"
class="referral-card" rel="noopener sponsored">
<span class="service-name">TorBox</span> <span class="service-name">TorBox</span>
<span class="referral-badge">Referral Link</span> <span class="referral-badge">Referral Link</span>
</a> </a>
@ -182,34 +195,38 @@
<span class="service-name">Debrid-Link</span> <span class="service-name">Debrid-Link</span>
<span class="referral-badge">Referral Link</span> <span class="referral-badge">Referral Link</span>
</a> </a>
<a href="https://www.deepbrid.com/aff/go/upward1971" class="referral-card" rel="noopener sponsored"> <a href="https://www.deepbrid.com/aff/go/upward1971" class="referral-card"
rel="noopener sponsored">
<span class="service-name">DeepBird</span> <span class="service-name">DeepBird</span>
<span class="referral-badge">Referral Link</span> <span class="referral-badge">Referral Link</span>
</a> </a>
</div> </div>
</div> </div>
</div>
</section>
<!-- File Hosts Section --> <!-- File Hosts Section -->
<section id="file-hosts" class="section"> <section id="file-hosts" class="section">
<div class="container"> <div class="container">
<div class="section-header"> <div class="section-header">
<h2 class="section-title">Supported File Hosts</h2> <h2 class="section-title">Supported File Hosts</h2>
<p class="section-description">Browse and search through 300+ supported file hosting services</p> <p class="section-description">Browse and search through 300+ supported file hosting
services
</p>
</div> </div>
<div class="search-section"> <div class="search-section">
<div class="search-container enhanced"> <div class="search-container enhanced">
<svg class="search-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"> <svg class="search-icon" width="20" height="20" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" aria-hidden="true">
<circle cx="11" cy="11" r="8" /> <circle cx="11" cy="11" r="8" />
<path d="M21 21l-4.35-4.35" /> <path d="M21 21l-4.35-4.35" />
</svg> </svg>
<input type="search" id="host-search-input" class="search-input" <input type="search" id="host-search-input" class="search-input"
placeholder="Search file hosts (e.g., Mega, Rapidgator...)" placeholder="Search file hosts (e.g., Mega, Rapidgator...)"
aria-label="Search file hosts" autocomplete="off" /> aria-label="Search file hosts" autocomplete="off" />
<button id="clear-host-search" class="clear-icon" aria-label="Clear search" type="button"> <button id="clear-host-search" class="clear-icon" aria-label="Clear search"
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"> type="button">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" aria-hidden="true">
<line x1="18" y1="6" x2="6" y2="18" /> <line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" /> <line x1="6" y1="6" x2="18" y2="18" />
</svg> </svg>
@ -217,7 +234,8 @@
</div> </div>
</div> </div>
<div id="file-hosts-table-container" class="data-table-container" role="region" aria-live="polite"> <div id="file-hosts-table-container" class="data-table-container" role="region"
aria-live="polite">
<div class="loading-state"> <div class="loading-state">
<div class="loading-spinner" aria-hidden="true"></div> <div class="loading-spinner" aria-hidden="true"></div>
<p>Loading file hosts...</p> <p>Loading file hosts...</p>
@ -231,7 +249,8 @@
<div class="container"> <div class="container">
<div class="section-header"> <div class="section-header">
<h2 class="section-title">Compare Services</h2> <h2 class="section-title">Compare Services</h2>
<p class="section-description">Select two services to see a detailed side-by-side comparison</p> <p class="section-description">Select two services to see a detailed side-by-side
comparison</p>
</div> </div>
<div class="compare-controls"> <div class="compare-controls">
@ -254,7 +273,8 @@
</div> </div>
</div> </div>
<div id="compare-table-container" class="data-table-container comparison-table" role="region" aria-live="polite"> <div id="compare-table-container" class="data-table-container comparison-table"
role="region" aria-live="polite">
<div class="empty-state"> <div class="empty-state">
<div class="empty-state-icon" aria-hidden="true">⚖️</div> <div class="empty-state-icon" aria-hidden="true">⚖️</div>
<h3>Ready to Compare</h3> <h3>Ready to Compare</h3>
@ -274,15 +294,18 @@
<div class="search-section"> <div class="search-section">
<div class="search-container enhanced"> <div class="search-container enhanced">
<svg class="search-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"> <svg class="search-icon" width="20" height="20" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" aria-hidden="true">
<circle cx="11" cy="11" r="8" /> <circle cx="11" cy="11" r="8" />
<path d="M21 21l-4.35-4.35" /> <path d="M21 21l-4.35-4.35" />
</svg> </svg>
<input type="search" id="adult-host-search-input" class="search-input" <input type="search" id="adult-host-search-input" class="search-input"
placeholder="Search adult hosts..." placeholder="Search adult hosts..." aria-label="Search adult hosts"
aria-label="Search adult hosts" autocomplete="off" /> autocomplete="off" />
<button id="clear-adult-host-search" class="clear-icon" aria-label="Clear search" type="button"> <button id="clear-adult-host-search" class="clear-icon" aria-label="Clear search"
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"> type="button">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" aria-hidden="true">
<line x1="18" y1="6" x2="6" y2="18" /> <line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" /> <line x1="6" y1="6" x2="18" y2="18" />
</svg> </svg>
@ -290,7 +313,8 @@
</div> </div>
</div> </div>
<div id="adult-hosts-table-container" class="data-table-container" role="region" aria-live="polite"> <div id="adult-hosts-table-container" class="data-table-container" role="region"
aria-live="polite">
<div class="loading-state"> <div class="loading-state">
<div class="loading-spinner" aria-hidden="true"></div> <div class="loading-spinner" aria-hidden="true"></div>
<p>Loading adult hosts...</p> <p>Loading adult hosts...</p>
@ -350,7 +374,8 @@
</div> </div>
<p>View current status and host list</p> <p>View current status and host list</p>
</a> </a>
<a href="https://www.mega-debrid.eu/index.php?page=hebergeurs" class="status-card" rel="noopener"> <a href="https://www.mega-debrid.eu/index.php?page=hebergeurs" class="status-card"
rel="noopener">
<div class="status-header"> <div class="status-header">
<span class="service-name">Mega-Debrid</span> <span class="service-name">Mega-Debrid</span>
<span class="service-status-indicator online" aria-label="Online"></span> <span class="service-status-indicator online" aria-label="Online"></span>
@ -379,8 +404,10 @@
<div class="footer-section"> <div class="footer-section">
<h4>Resources</h4> <h4>Resources</h4>
<ul> <ul>
<li><a href="https://github.com/debridmediamanager/awesome-debrid" rel="noopener">Awesome Debrid</a></li> <li><a href="https://github.com/debridmediamanager/awesome-debrid" rel="noopener">Awesome
<li><a href="https://github.com/fynks/debrid-services-comparison" rel="noopener">Source Code</a></li> Debrid</a></li>
<li><a href="https://github.com/fynks/debrid-services-comparison" rel="noopener">Source
Code</a></li>
</ul> </ul>
</div> </div>
<div class="footer-section"> <div class="footer-section">
@ -397,7 +424,8 @@
<!-- Back to Top Button --> <!-- Back to Top Button -->
<button id="backToTop" class="back-to-top" aria-label="Back to top" type="button"> <button id="backToTop" class="back-to-top" aria-label="Back to top" type="button">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
aria-hidden="true">
<path d="M18 15l-6-6-6 6" /> <path d="M18 15l-6-6-6 6" />
</svg> </svg>
</button> </button>
@ -470,4 +498,5 @@
} }
</script> </script>
</body> </body>
</html> </html>

247
dist/js/app.js vendored
View file

@ -3,29 +3,31 @@
* Modern, feature-rich JavaScript with performance optimizations and professional UX * Modern, feature-rich JavaScript with performance optimizations and professional UX
*/ */
// Modern utility functions with optimized arrow syntax // Modern utility functions
const Utils = { const Utils = {
// Enhanced debounce with immediate option // Enhanced debounce with immediate option
debounce: (func, delay = 300, immediate = false) => { debounce: (func, delay = 300, immediate = false) => {
let timeoutId; let timeout;
return (...args) => { return function executedFunction(...args) {
const later = () => { const later = () => {
timeoutId = null; timeout = null;
if (!immediate) func(...args); if (!immediate) func.apply(this, args);
}; };
const callNow = immediate && !timeoutId; const callNow = immediate && !timeout;
clearTimeout(timeoutId); clearTimeout(timeout);
timeoutId = setTimeout(later, delay); timeout = setTimeout(later, delay);
if (callNow) func(...args); if (callNow) func.apply(this, args);
}; };
}, },
// Throttle function for scroll events // Throttle function for scroll events
throttle: (func, limit) => { throttle: (func, limit) => {
let inThrottle; let inThrottle;
return function(...args) { return function() {
const args = arguments;
const context = this;
if (!inThrottle) { if (!inThrottle) {
func.apply(this, args); func.apply(context, args);
inThrottle = true; inThrottle = true;
setTimeout(() => inThrottle = false, limit); setTimeout(() => inThrottle = false, limit);
} }
@ -34,10 +36,9 @@ const Utils = {
// Animate elements with Intersection Observer // Animate elements with Intersection Observer
animateOnScroll: (elements, options = {}) => { animateOnScroll: (elements, options = {}) => {
const observerOptions = { const defaultOptions = {
threshold: 0.1, threshold: 0.1,
rootMargin: '0px 0px -50px 0px', rootMargin: '0px 0px -50px 0px'
...options
}; };
const observer = new IntersectionObserver((entries) => { const observer = new IntersectionObserver((entries) => {
@ -47,108 +48,23 @@ const Utils = {
observer.unobserve(entry.target); observer.unobserve(entry.target);
} }
}); });
}, observerOptions); }, { ...defaultOptions, ...options });
elements.forEach(el => observer.observe(el)); elements.forEach(el => observer.observe(el));
} }
}; };
// Enhanced notification system with optimized DOM handling // Enhanced loading manager
class NotificationManager {
constructor() {
this.container = this.createContainer();
this.notifications = new Map();
}
createContainer() {
const container = document.createElement('div');
container.className = 'notification-container';
container.setAttribute('aria-live', 'polite');
document.body.appendChild(container);
return container;
}
show(message, type = 'info', duration = 4000) {
const id = Date.now().toString();
const notification = document.createElement('div');
notification.className = `notification notification--${type}`;
notification.setAttribute('role', 'alert');
notification.innerHTML = `
<div class="notification__content">
<svg class="notification__icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
${this.getIcon(type)}
</svg>
<span class="notification__message">${message}</span>
<button class="notification__close" aria-label="Close notification">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
`;
this.container.appendChild(notification);
this.notifications.set(id, notification);
// Auto dismiss
const timeoutId = setTimeout(() => this.dismiss(id), duration);
notification.dataset.timeoutId = timeoutId;
// Manual dismiss
notification.querySelector('.notification__close').addEventListener('click', () => {
this.dismiss(id);
});
// Animate in
requestAnimationFrame(() => {
notification.classList.add('notification--visible');
});
return id;
}
getIcon(type) {
const icons = {
success: '<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22,4 12,14.01 9,11.01"></polyline>',
error: '<circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line>',
warning: '<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line>',
info: '<circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"></path>'
};
return icons[type] || icons.info;
}
dismiss(id) {
const notification = this.notifications.get(id);
if (!notification) return;
notification.classList.add('notification--dismissing');
this.notifications.delete(id);
// Clear auto-dismiss timeout
const timeoutId = notification.dataset.timeoutId;
if (timeoutId) clearTimeout(parseInt(timeoutId, 10));
setTimeout(() => {
if (notification.parentNode) {
this.container.removeChild(notification);
}
}, 300);
}
}
// Enhanced loading manager with optimized DOM handling
class LoadingManager { class LoadingManager {
constructor() { constructor() {
this.activeLoaders = new Map(); this.activeLoaders = new Set();
} }
show(target, text = 'Loading...') { show(target, text = 'Loading...') {
const loaderId = Date.now().toString(); const loaderId = Date.now().toString();
const loader = document.createElement('div'); const loader = document.createElement('div');
loader.className = 'loading-overlay'; loader.className = 'loading-overlay';
loader.dataset.loaderId = loaderId; loader.setAttribute('data-loader-id', loaderId);
loader.innerHTML = ` loader.innerHTML = `
<div class="loading-content"> <div class="loading-content">
<div class="loading-spinner"> <div class="loading-spinner">
@ -160,10 +76,13 @@ class LoadingManager {
</div> </div>
`; `;
const targetElement = typeof target === 'string' ? document.querySelector(target) : target; if (typeof target === 'string') {
targetElement.style.position = 'relative'; target = document.querySelector(target);
targetElement.appendChild(loader); }
this.activeLoaders.set(loaderId, { element: loader, target: targetElement });
target.style.position = 'relative';
target.appendChild(loader);
this.activeLoaders.add(loaderId);
requestAnimationFrame(() => { requestAnimationFrame(() => {
loader.classList.add('loading-overlay--visible'); loader.classList.add('loading-overlay--visible');
@ -173,34 +92,39 @@ class LoadingManager {
} }
hide(target, loaderId) { hide(target, loaderId) {
const targetElement = typeof target === 'string' ? document.querySelector(target) : target; if (typeof target === 'string') {
const loader = targetElement.querySelector(`[data-loader-id="${loaderId}"]`); target = document.querySelector(target);
}
const loader = target.querySelector(`[data-loader-id="${loaderId}"]`);
if (loader) { if (loader) {
loader.classList.add('loading-overlay--hiding'); loader.classList.add('loading-overlay--hiding');
this.activeLoaders.delete(loaderId); this.activeLoaders.delete(loaderId);
setTimeout(() => { setTimeout(() => {
if (loader.parentNode) { if (loader.parentNode) {
targetElement.removeChild(loader); target.removeChild(loader);
} }
}, 300); }, 300);
} }
} }
hideAll() { hideAll() {
this.activeLoaders.forEach(({ element, target }, id) => { this.activeLoaders.forEach(id => {
this.hide(target, id); const loader = document.querySelector(`[data-loader-id="${id}"]`);
if (loader) {
this.hide(loader.parentNode, id);
}
}); });
} }
} }
// Enhanced table manager with optimized rendering // Enhanced table manager with advanced features
class TableManager { class TableManager {
constructor(containerId, searchInputId, clearIconId, options = {}) { constructor(containerId, searchInputId, clearIconId, options = {}) {
this.container = document.getElementById(containerId); this.container = document.querySelector(`#${containerId}`);
this.searchInput = document.getElementById(searchInputId); this.searchInput = document.querySelector(`#${searchInputId}`);
this.clearIcon = document.getElementById(clearIconId); this.clearIcon = document.querySelector(`#${clearIconId}`);
this.options = { this.options = {
sortable: true, sortable: true,
filterable: true, filterable: true,
@ -269,7 +193,7 @@ class TableManager {
<tr data-host="${host.toLowerCase()}" role="row"> <tr data-host="${host.toLowerCase()}" role="row">
<td class="service-cell" role="gridcell"> <td class="service-cell" role="gridcell">
<div class="service-info"> <div class="service-info">
<span class="service-name">${this._escapeHtml(host)}</span> <span class="service-name">${host}</span>
</div> </div>
</td> </td>
${providers.map(provider => ` ${providers.map(provider => `
@ -287,17 +211,6 @@ class TableManager {
`).join(''); `).join('');
} }
_escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '<',
'>': '>',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, m => map[m]);
}
_setupSearchFunctionality() { _setupSearchFunctionality() {
const handleSearch = Utils.debounce(() => { const handleSearch = Utils.debounce(() => {
const searchTerm = this.searchInput.value.toLowerCase().trim(); const searchTerm = this.searchInput.value.toLowerCase().trim();
@ -375,7 +288,7 @@ class TableManager {
} }
_handleSort(header) { _handleSort(header) {
const column = header.dataset.column; const column = header.getAttribute('data-column');
const currentDirection = this.currentSort.column === column ? this.currentSort.direction : 'asc'; const currentDirection = this.currentSort.column === column ? this.currentSort.direction : 'asc';
const newDirection = currentDirection === 'asc' ? 'desc' : 'asc'; const newDirection = currentDirection === 'asc' ? 'desc' : 'asc';
@ -460,12 +373,12 @@ class TableManager {
} }
} }
// Enhanced comparison manager with optimized rendering // Enhanced comparison manager
class ComparisonManager { class ComparisonManager {
constructor(containerId, select1Id, select2Id, data) { constructor(containerId, select1Id, select2Id, data) {
this.container = document.getElementById(containerId); this.container = document.querySelector(`#${containerId}`);
this.select1 = document.getElementById(select1Id); this.select1 = document.querySelector(`#${select1Id}`);
this.select2 = document.getElementById(select2Id); this.select2 = document.querySelector(`#${select2Id}`);
this.data = data; this.data = data;
this.isComparing = false; this.isComparing = false;
@ -524,18 +437,18 @@ class ComparisonManager {
this.container.innerHTML = ` this.container.innerHTML = `
<div class="comparison-header"> <div class="comparison-header">
<h3>Comparing ${this._escapeHtml(provider1)} vs ${this._escapeHtml(provider2)}</h3> <h3>Comparing ${provider1} vs ${provider2}</h3>
<div class="comparison-stats"> <div class="comparison-stats">
<div class="stat"> <div class="stat">
<span class="stat-label">Shared Support</span> <span class="stat-label">Shared Support</span>
<span class="stat-value">${comparisonData.shared}</span> <span class="stat-value">${comparisonData.shared}</span>
</div> </div>
<div class="stat"> <div class="stat">
<span class="stat-label">${this._escapeHtml(provider1)} Only</span> <span class="stat-label">${provider1} Only</span>
<span class="stat-value">${comparisonData.provider1Only}</span> <span class="stat-value">${comparisonData.provider1Only}</span>
</div> </div>
<div class="stat"> <div class="stat">
<span class="stat-label">${this._escapeHtml(provider2)} Only</span> <span class="stat-label">${provider2} Only</span>
<span class="stat-value">${comparisonData.provider2Only}</span> <span class="stat-value">${comparisonData.provider2Only}</span>
</div> </div>
</div> </div>
@ -569,8 +482,8 @@ class ComparisonManager {
<thead> <thead>
<tr> <tr>
<th>Service Name</th> <th>Service Name</th>
<th class="provider-header ${provider1.toLowerCase()}">${this._escapeHtml(provider1)}</th> <th class="provider-header ${provider1.toLowerCase()}">${provider1}</th>
<th class="provider-header ${provider2.toLowerCase()}">${this._escapeHtml(provider2)}</th> <th class="provider-header ${provider2.toLowerCase()}">${provider2}</th>
<th>Status</th> <th>Status</th>
</tr> </tr>
</thead> </thead>
@ -614,7 +527,7 @@ class ComparisonManager {
return ` return `
<tr class="comparison-row ${statusClass}" data-status="${statusClass}"> <tr class="comparison-row ${statusClass}" data-status="${statusClass}">
<td class="service-name">${this._escapeHtml(host)}</td> <td class="service-name">${host}</td>
<td class="support-status ${support1 ? 'supported' : 'not-supported'}"> <td class="support-status ${support1 ? 'supported' : 'not-supported'}">
<span class="status-indicator" aria-label="${support1 ? 'Supported' : 'Not supported'}"> <span class="status-indicator" aria-label="${support1 ? 'Supported' : 'Not supported'}">
${support1 ? ${support1 ?
@ -632,24 +545,13 @@ class ComparisonManager {
</span> </span>
</td> </td>
<td class="status-text"> <td class="status-text">
<span class="status-badge ${statusClass}">${this._escapeHtml(statusText)}</span> <span class="status-badge ${statusClass}">${statusText}</span>
</td> </td>
</tr> </tr>
`; `;
}).join(''); }).join('');
} }
_escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '<',
'>': '>',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, m => map[m]);
}
_calculateComparisonStats(provider1, provider2) { _calculateComparisonStats(provider1, provider2) {
let shared = 0; let shared = 0;
let provider1Only = 0; let provider1Only = 0;
@ -685,7 +587,7 @@ class ComparisonManager {
const rows = this.container.querySelectorAll('.comparison-row'); const rows = this.container.querySelectorAll('.comparison-row');
rows.forEach(row => { rows.forEach(row => {
const status = row.dataset.status; const status = row.getAttribute('data-status');
let show = true; let show = true;
switch (filter) { switch (filter) {
@ -776,10 +678,10 @@ class ComparisonManager {
} }
} }
// Enhanced pricing manager with optimized rendering // Enhanced pricing manager
class PricingManager { class PricingManager {
constructor(containerId) { constructor(containerId) {
this.container = document.getElementById(`${containerId}-table-container`); this.container = document.querySelector(`#${containerId}-table-container`);
this.currentData = null; this.currentData = null;
} }
@ -801,7 +703,7 @@ class PricingManager {
${services.map(service => ` ${services.map(service => `
<th class="service-column"> <th class="service-column">
<div class="service-header"> <div class="service-header">
<span class="service-name">${this._escapeHtml(service)}</span> <span class="service-name">${service}</span>
</div> </div>
</th> </th>
`).join('')} `).join('')}
@ -811,7 +713,7 @@ class PricingManager {
${data.plans.map((plan, index) => ` ${data.plans.map((plan, index) => `
<tr class="pricing-row ${index % 2 === 0 ? 'even' : 'odd'}"> <tr class="pricing-row ${index % 2 === 0 ? 'even' : 'odd'}">
<td class="plan-name"> <td class="plan-name">
<strong>${this._escapeHtml(plan.name)}</strong> <strong>${plan.name}</strong>
</td> </td>
${services.map(service => { ${services.map(service => {
const price = plan[service]; const price = plan[service];
@ -820,7 +722,7 @@ class PricingManager {
return ` return `
<td class="price-cell ${isNumeric ? 'numeric-price' : 'text-price'}"> <td class="price-cell ${isNumeric ? 'numeric-price' : 'text-price'}">
<div class="price-content"> <div class="price-content">
<span class="price-value">${this._escapeHtml(price)}</span> <span class="price-value">${price}</span>
${isNumeric ? '<span class="price-period">/month</span>' : ''} ${isNumeric ? '<span class="price-period">/month</span>' : ''}
</div> </div>
</td> </td>
@ -836,17 +738,6 @@ class PricingManager {
this._setupPricingFeatures(); this._setupPricingFeatures();
} }
_escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '<',
'>': '>',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, m => map[m]);
}
_setupPricingFeatures() { _setupPricingFeatures() {
// Add hover effects for better UX // Add hover effects for better UX
const priceCells = this.container.querySelectorAll('.price-cell'); const priceCells = this.container.querySelectorAll('.price-cell');
@ -874,7 +765,7 @@ class PricingManager {
} }
} }
// Performance monitor with optimized metrics collection // Performance monitor
class PerformanceMonitor { class PerformanceMonitor {
constructor() { constructor() {
this.metrics = { this.metrics = {
@ -904,11 +795,10 @@ class PerformanceMonitor {
} }
// Global instances // Global instances
const notificationManager = new NotificationManager();
const loadingManager = new LoadingManager(); const loadingManager = new LoadingManager();
const performanceMonitor = new PerformanceMonitor(); const performanceMonitor = new PerformanceMonitor();
// Enhanced application initialization with optimized async handling // Enhanced application initialization
document.addEventListener('DOMContentLoaded', async () => { document.addEventListener('DOMContentLoaded', async () => {
performanceMonitor.markDOMReady(); performanceMonitor.markDOMReady();
@ -963,7 +853,6 @@ document.addEventListener('DOMContentLoaded', async () => {
new ComparisonManager('compare-table-container', 'provider1', 'provider2', fileHostsData.value); new ComparisonManager('compare-table-container', 'provider1', 'provider2', fileHostsData.value);
} else { } else {
loadingManager.hide('#file-hosts-table-container', loaderId1); loadingManager.hide('#file-hosts-table-container', loaderId1);
notificationManager.show('Failed to load file hosts data', 'error');
console.error('File hosts error:', fileHostsData.reason); console.error('File hosts error:', fileHostsData.reason);
} }
@ -972,7 +861,6 @@ document.addEventListener('DOMContentLoaded', async () => {
adultHostsTable.generateTable(adultHostsData.value); adultHostsTable.generateTable(adultHostsData.value);
} else { } else {
loadingManager.hide('#adult-hosts-table-container', loaderId2); loadingManager.hide('#adult-hosts-table-container', loaderId2);
notificationManager.show('Failed to load adult hosts data', 'error');
console.error('Adult hosts error:', adultHostsData.reason); console.error('Adult hosts error:', adultHostsData.reason);
} }
@ -981,7 +869,6 @@ document.addEventListener('DOMContentLoaded', async () => {
pricingManager.generatePricingTable(pricingData.value); pricingManager.generatePricingTable(pricingData.value);
} else { } else {
loadingManager.hide('#pricing-table-container', loaderId3); loadingManager.hide('#pricing-table-container', loaderId3);
notificationManager.show('Failed to load pricing data', 'error');
console.error('Pricing error:', pricingData.reason); console.error('Pricing error:', pricingData.reason);
} }
@ -995,11 +882,9 @@ document.addEventListener('DOMContentLoaded', async () => {
setupOfflineDetection(); setupOfflineDetection();
performanceMonitor.markFullyLoaded(); performanceMonitor.markFullyLoaded();
notificationManager.show('Application loaded successfully', 'success', 2000);
} catch (error) { } catch (error) {
console.error('Critical error initializing application:', error); console.error('Critical error initializing application:', error);
notificationManager.show('Failed to initialize application', 'error');
loadingManager.hideAll(); loadingManager.hideAll();
} }
}); });
@ -1012,8 +897,8 @@ function setupURLComparison() {
if (provider1 && provider2) { if (provider1 && provider2) {
setTimeout(() => { setTimeout(() => {
const select1 = document.getElementById('provider1'); const select1 = document.querySelector('#provider1');
const select2 = document.getElementById('provider2'); const select2 = document.querySelector('#provider2');
if (select1 && select2) { if (select1 && select2) {
select1.value = provider1; select1.value = provider1;
@ -1033,22 +918,20 @@ function setupScrollAnimations() {
// Offline detection // Offline detection
function setupOfflineDetection() { function setupOfflineDetection() {
window.addEventListener('online', () => { window.addEventListener('online', () => {
notificationManager.show('Connection restored', 'success', 3000); console.log('Connection restored');
}); });
window.addEventListener('offline', () => { window.addEventListener('offline', () => {
notificationManager.show('You are offline. Some features may not work.', 'warning', 5000); console.log('You are offline. Some features may not work.');
}); });
} }
// Enhanced error boundary // Enhanced error boundary
window.addEventListener('error', (event) => { window.addEventListener('error', (event) => {
console.error('Global error:', event.error); console.error('Global error:', event.error);
notificationManager.show('An unexpected error occurred', 'error');
}); });
window.addEventListener('unhandledrejection', (event) => { window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason); console.error('Unhandled promise rejection:', event.reason);
notificationManager.show('A network error occurred', 'error');
event.preventDefault(); event.preventDefault();
}); });