more web improvements
This commit is contained in:
parent
08ed1660c9
commit
4836ca5948
3 changed files with 345 additions and 542 deletions
213
dist/css/styles.css
vendored
213
dist/css/styles.css
vendored
|
|
@ -1301,50 +1301,72 @@ 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;
|
||||||
gap: var(--space-2);
|
justify-content: center;
|
||||||
padding: var(--space-4);
|
gap: var(--space-2);
|
||||||
background: var(--bg-primary);
|
padding: var(--space-4) var(--space-3);
|
||||||
border: 1px solid var(--border-primary);
|
background: var(--bg-primary);
|
||||||
border-radius: var(--radius-lg);
|
border: 1px solid var(--border-primary);
|
||||||
text-decoration: none;
|
border-radius: var(--radius-lg);
|
||||||
transition: all var(--transition-fast);
|
text-decoration: none;
|
||||||
|
transition: var(--transition-all);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 200px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.referral-card:hover {
|
.referral-card:hover {
|
||||||
border-color: var(--primary);
|
border-color: var(--primary);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-name {
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: var(--text-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-badge {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--primary);
|
||||||
|
background: rgb(99 102 241 / 0.1);
|
||||||
|
padding: var(--space-1) var(--space-2);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-name {
|
.service-name {
|
||||||
|
|
@ -1352,14 +1374,6 @@ select:focus {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.referral-badge {
|
|
||||||
font-size: var(--text-xs);
|
|
||||||
color: var(--primary);
|
|
||||||
background: rgb(99 102 241 / 0.1);
|
|
||||||
padding: var(--space-1) var(--space-2);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-grid {
|
.status-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
407
dist/index.html
vendored
407
dist/index.html
vendored
File diff suppressed because one or more lines are too long
247
dist/js/app.js
vendored
247
dist/js/app.js
vendored
|
|
@ -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 = {
|
|
||||||
'&': '&',
|
|
||||||
'<': '<',
|
|
||||||
'>': '>',
|
|
||||||
'"': '"',
|
|
||||||
"'": '''
|
|
||||||
};
|
|
||||||
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 = {
|
|
||||||
'&': '&',
|
|
||||||
'<': '<',
|
|
||||||
'>': '>',
|
|
||||||
'"': '"',
|
|
||||||
"'": '''
|
|
||||||
};
|
|
||||||
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 = {
|
|
||||||
'&': '&',
|
|
||||||
'<': '<',
|
|
||||||
'>': '>',
|
|
||||||
'"': '"',
|
|
||||||
"'": '''
|
|
||||||
};
|
|
||||||
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();
|
||||||
});
|
});
|
||||||
Loading…
Reference in a new issue